@nauth-toolkit/core 0.1.0 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. package/LICENSE +90 -0
  2. package/README.md +30 -0
  3. package/package.json +7 -2
  4. package/jest.config.js +0 -15
  5. package/jest.setup.ts +0 -6
  6. package/src/adapters/database-columns.ts +0 -165
  7. package/src/adapters/express.adapter.ts +0 -385
  8. package/src/adapters/fastify.adapter.ts +0 -416
  9. package/src/adapters/index.ts +0 -16
  10. package/src/adapters/storage.factory.ts +0 -143
  11. package/src/bootstrap.ts +0 -374
  12. package/src/dto/auth-challenge.dto.ts +0 -231
  13. package/src/dto/auth-response.dto.ts +0 -253
  14. package/src/dto/challenge-response.dto.ts +0 -234
  15. package/src/dto/change-password-request.dto.ts +0 -50
  16. package/src/dto/change-password-response.dto.ts +0 -29
  17. package/src/dto/change-password.dto.ts +0 -57
  18. package/src/dto/error-response.dto.ts +0 -136
  19. package/src/dto/get-available-methods.dto.ts +0 -55
  20. package/src/dto/get-challenge-data-response.dto.ts +0 -28
  21. package/src/dto/get-challenge-data.dto.ts +0 -69
  22. package/src/dto/get-client-info.dto.ts +0 -104
  23. package/src/dto/get-device-token-response.dto.ts +0 -25
  24. package/src/dto/get-events-by-type.dto.ts +0 -76
  25. package/src/dto/get-ip-address-response.dto.ts +0 -24
  26. package/src/dto/get-mfa-status.dto.ts +0 -94
  27. package/src/dto/get-risk-assessment-history.dto.ts +0 -39
  28. package/src/dto/get-session-id-response.dto.ts +0 -25
  29. package/src/dto/get-setup-data-response.dto.ts +0 -31
  30. package/src/dto/get-setup-data.dto.ts +0 -75
  31. package/src/dto/get-suspicious-activity.dto.ts +0 -42
  32. package/src/dto/get-user-agent-response.dto.ts +0 -23
  33. package/src/dto/get-user-auth-history.dto.ts +0 -95
  34. package/src/dto/get-user-by-email.dto.ts +0 -61
  35. package/src/dto/get-user-by-id.dto.ts +0 -46
  36. package/src/dto/get-user-devices.dto.ts +0 -53
  37. package/src/dto/get-user-response.dto.ts +0 -17
  38. package/src/dto/has-provider.dto.ts +0 -56
  39. package/src/dto/index.ts +0 -57
  40. package/src/dto/is-trusted-device-response.dto.ts +0 -34
  41. package/src/dto/list-providers-response.dto.ts +0 -23
  42. package/src/dto/login.dto.ts +0 -95
  43. package/src/dto/logout-all-response.dto.ts +0 -24
  44. package/src/dto/logout-all.dto.ts +0 -65
  45. package/src/dto/logout-response.dto.ts +0 -25
  46. package/src/dto/logout.dto.ts +0 -64
  47. package/src/dto/refresh-token.dto.ts +0 -36
  48. package/src/dto/remove-devices.dto.ts +0 -85
  49. package/src/dto/resend-code-response.dto.ts +0 -32
  50. package/src/dto/resend-code.dto.ts +0 -51
  51. package/src/dto/reset-password.dto.ts +0 -115
  52. package/src/dto/respond-challenge.dto.ts +0 -272
  53. package/src/dto/set-mfa-exemption.dto.ts +0 -112
  54. package/src/dto/set-must-change-password-response.dto.ts +0 -27
  55. package/src/dto/set-must-change-password.dto.ts +0 -46
  56. package/src/dto/set-preferred-method.dto.ts +0 -80
  57. package/src/dto/setup-mfa.dto.ts +0 -98
  58. package/src/dto/signup.dto.ts +0 -174
  59. package/src/dto/social-auth.dto.ts +0 -422
  60. package/src/dto/trust-device-response.dto.ts +0 -30
  61. package/src/dto/trust-device.dto.ts +0 -9
  62. package/src/dto/update-user-attributes-request.dto.ts +0 -51
  63. package/src/dto/user-response.dto.ts +0 -138
  64. package/src/dto/user-update.dto.ts +0 -222
  65. package/src/dto/verify-email.dto.ts +0 -313
  66. package/src/dto/verify-mfa-code.dto.ts +0 -103
  67. package/src/dto/verify-phone-by-sub.dto.ts +0 -78
  68. package/src/dto/verify-phone.dto.ts +0 -245
  69. package/src/entities/auth-audit.entity.ts +0 -232
  70. package/src/entities/challenge-session.entity.ts +0 -116
  71. package/src/entities/index.ts +0 -29
  72. package/src/entities/login-attempt.entity.ts +0 -64
  73. package/src/entities/mfa-device.entity.ts +0 -151
  74. package/src/entities/rate-limit.entity.ts +0 -44
  75. package/src/entities/session.entity.ts +0 -180
  76. package/src/entities/social-account.entity.ts +0 -96
  77. package/src/entities/storage-lock.entity.ts +0 -39
  78. package/src/entities/trusted-device.entity.ts +0 -112
  79. package/src/entities/user.entity.ts +0 -243
  80. package/src/entities/verification-token.entity.ts +0 -141
  81. package/src/enums/auth-audit-event-type.enum.ts +0 -360
  82. package/src/enums/error-codes.enum.ts +0 -420
  83. package/src/enums/mfa-method.enum.ts +0 -97
  84. package/src/enums/risk-factor.enum.ts +0 -111
  85. package/src/exceptions/nauth.exception.ts +0 -231
  86. package/src/handlers/auth.handler.ts +0 -260
  87. package/src/handlers/client-info.handler.ts +0 -101
  88. package/src/handlers/csrf.handler.ts +0 -156
  89. package/src/handlers/token-delivery.handler.ts +0 -118
  90. package/src/index.ts +0 -118
  91. package/src/interfaces/client-info.interface.ts +0 -85
  92. package/src/interfaces/config.interface.ts +0 -2135
  93. package/src/interfaces/entities.interface.ts +0 -226
  94. package/src/interfaces/index.ts +0 -15
  95. package/src/interfaces/logger.interface.ts +0 -283
  96. package/src/interfaces/mfa-provider.interface.ts +0 -154
  97. package/src/interfaces/oauth.interface.ts +0 -148
  98. package/src/interfaces/provider.interface.ts +0 -47
  99. package/src/interfaces/social-auth-provider.interface.ts +0 -131
  100. package/src/interfaces/storage-adapter.interface.ts +0 -82
  101. package/src/interfaces/template.interface.ts +0 -510
  102. package/src/interfaces/token-verifier.interface.ts +0 -110
  103. package/src/internal.ts +0 -178
  104. package/src/platform/interfaces.ts +0 -299
  105. package/src/schemas/auth-config.schema.ts +0 -646
  106. package/src/services/adaptive-mfa-decision.service.spec.ts +0 -1058
  107. package/src/services/adaptive-mfa-decision.service.ts +0 -457
  108. package/src/services/auth-audit.service.spec.ts +0 -675
  109. package/src/services/auth-audit.service.ts +0 -558
  110. package/src/services/auth-challenge-helper.service.spec.ts +0 -3227
  111. package/src/services/auth-challenge-helper.service.ts +0 -825
  112. package/src/services/auth-flow-context-builder.service.ts +0 -520
  113. package/src/services/auth-flow-rules.ts +0 -202
  114. package/src/services/auth-flow-state-definitions.ts +0 -190
  115. package/src/services/auth-flow-state-machine.service.ts +0 -207
  116. package/src/services/auth-flow-state-machine.types.ts +0 -316
  117. package/src/services/auth.service.spec.ts +0 -4195
  118. package/src/services/auth.service.ts +0 -3727
  119. package/src/services/challenge.service.spec.ts +0 -1363
  120. package/src/services/challenge.service.ts +0 -696
  121. package/src/services/client-info.service.spec.ts +0 -572
  122. package/src/services/client-info.service.ts +0 -374
  123. package/src/services/csrf.service.ts +0 -54
  124. package/src/services/email-verification.service.spec.ts +0 -1229
  125. package/src/services/email-verification.service.ts +0 -578
  126. package/src/services/geo-location.service.spec.ts +0 -603
  127. package/src/services/geo-location.service.ts +0 -599
  128. package/src/services/index.ts +0 -13
  129. package/src/services/jwt.service.spec.ts +0 -882
  130. package/src/services/jwt.service.ts +0 -621
  131. package/src/services/mfa-base.service.spec.ts +0 -246
  132. package/src/services/mfa-base.service.ts +0 -611
  133. package/src/services/mfa.service.spec.ts +0 -693
  134. package/src/services/mfa.service.ts +0 -960
  135. package/src/services/password.service.spec.ts +0 -166
  136. package/src/services/password.service.ts +0 -309
  137. package/src/services/phone-verification.service.spec.ts +0 -1120
  138. package/src/services/phone-verification.service.ts +0 -751
  139. package/src/services/risk-detection.service.spec.ts +0 -1292
  140. package/src/services/risk-detection.service.ts +0 -1012
  141. package/src/services/risk-scoring.service.spec.ts +0 -204
  142. package/src/services/risk-scoring.service.ts +0 -131
  143. package/src/services/session.service.spec.ts +0 -1293
  144. package/src/services/session.service.ts +0 -803
  145. package/src/services/social-account.service.spec.ts +0 -725
  146. package/src/services/social-auth-base.service.spec.ts +0 -418
  147. package/src/services/social-auth-base.service.ts +0 -581
  148. package/src/services/social-auth.service.spec.ts +0 -238
  149. package/src/services/social-auth.service.ts +0 -436
  150. package/src/services/social-provider-registry.service.spec.ts +0 -238
  151. package/src/services/social-provider-registry.service.ts +0 -122
  152. package/src/services/trusted-device.service.spec.ts +0 -505
  153. package/src/services/trusted-device.service.ts +0 -339
  154. package/src/storage/account-lockout-storage.service.spec.ts +0 -310
  155. package/src/storage/account-lockout-storage.service.ts +0 -89
  156. package/src/storage/index.ts +0 -3
  157. package/src/storage/memory-storage.adapter.ts +0 -443
  158. package/src/storage/rate-limit-storage.service.spec.ts +0 -247
  159. package/src/storage/rate-limit-storage.service.ts +0 -38
  160. package/src/templates/html-template.engine.spec.ts +0 -161
  161. package/src/templates/html-template.engine.ts +0 -688
  162. package/src/templates/index.ts +0 -7
  163. package/src/utils/common-passwords.spec.ts +0 -230
  164. package/src/utils/common-passwords.ts +0 -170
  165. package/src/utils/context-storage.ts +0 -188
  166. package/src/utils/cookie-names.util.ts +0 -67
  167. package/src/utils/cookies.util.ts +0 -94
  168. package/src/utils/index.ts +0 -12
  169. package/src/utils/ip-extractor.spec.ts +0 -330
  170. package/src/utils/ip-extractor.ts +0 -220
  171. package/src/utils/nauth-logger.spec.ts +0 -388
  172. package/src/utils/nauth-logger.ts +0 -215
  173. package/src/utils/pii-redactor.spec.ts +0 -130
  174. package/src/utils/pii-redactor.ts +0 -288
  175. package/src/utils/setup/get-repositories.ts +0 -140
  176. package/src/utils/setup/init-services.ts +0 -422
  177. package/src/utils/setup/init-social.ts +0 -189
  178. package/src/utils/setup/init-storage.ts +0 -94
  179. package/src/utils/setup/register-mfa.ts +0 -165
  180. package/src/utils/setup/run-nauth-migrations.ts +0 -61
  181. package/src/utils/token-delivery-policy.ts +0 -38
  182. package/src/validators/template.validator.ts +0 -219
  183. package/tsconfig.json +0 -37
  184. package/tsconfig.lint.json +0 -6
@@ -1,725 +0,0 @@
1
- import { Repository } from 'typeorm';
2
- import { SocialAuthService } from './social-auth.service';
3
- import { SocialProviderRegistry } from './social-provider-registry.service';
4
- import { NAuthException } from '../exceptions/nauth.exception';
5
- import { AuthService } from './auth.service';
6
- import { IUser, ISocialAccount } from '../interfaces/entities.interface';
7
- import { BaseUser, BaseSocialAccount } from '../entities';
8
- import { AuthAuditService } from './auth-audit.service';
9
- import { NAuthLogger } from '../utils/nauth-logger';
10
- import { AuthErrorCode } from '../enums/error-codes.enum';
11
- import { AuthAuditEventType } from '../enums/auth-audit-event-type.enum';
12
- import { ClientInfoService } from './client-info.service';
13
-
14
- /**
15
- * Social Auth Service Unit Tests
16
- *
17
- * Platform-agnostic: Uses direct instantiation, no NestJS dependencies.
18
- *
19
- * Tests social authentication and account management functionality including:
20
- * - OAuth authentication flows
21
- * - Listing linked accounts
22
- * - Unlinking accounts
23
- * - Password management for social-only users
24
- * - Internal methods for social auth provider integration
25
- */
26
- describe('SocialAuthService', () => {
27
- let service: SocialAuthService;
28
- let mockProviderRegistry: jest.Mocked<SocialProviderRegistry>;
29
- let mockUserRepository: jest.Mocked<Repository<BaseUser>>;
30
- let mockSocialAccountRepository: jest.Mocked<Repository<BaseSocialAccount>>;
31
- let mockAuthService: jest.Mocked<AuthService>;
32
- let mockClientInfoService: jest.Mocked<ClientInfoService>;
33
- let mockLogger: jest.Mocked<NAuthLogger>;
34
- let mockAuditService: jest.Mocked<AuthAuditService>;
35
-
36
- const mockUser: IUser = {
37
- id: 1,
38
- sub: 'user-123',
39
- email: 'user@example.com',
40
- username: 'testuser',
41
- phone: null,
42
- firstName: 'John',
43
- lastName: 'Doe',
44
- passwordHash: null, // Social-only user
45
- passwordChangedAt: null,
46
- passwordHistory: null,
47
- isEmailVerified: true,
48
- isPhoneVerified: false,
49
- isActive: true,
50
- mustChangePassword: false,
51
- isLocked: false,
52
- lockReason: null,
53
- lockedAt: null,
54
- lockedUntil: null,
55
- failedLoginAttempts: 0,
56
- lastFailedLoginAt: null,
57
- lastLoginAt: null,
58
- lastLoginIp: null,
59
- hasSocialAuth: true,
60
- socialProviders: ['google'],
61
- mfaEnabled: false,
62
- mfaMethods: null,
63
- preferredMfaMethod: null,
64
- backupCodes: null,
65
- metadata: null,
66
- createdAt: new Date(),
67
- updatedAt: new Date(),
68
- deletedAt: null,
69
- };
70
-
71
- const mockSocialAccount: ISocialAccount = {
72
- id: 1,
73
- userId: 1,
74
- provider: 'google',
75
- providerId: 'google-123',
76
- providerEmail: 'user@gmail.com',
77
- linkedAt: new Date(),
78
- lastUsedAt: new Date(),
79
- metadata: { raw: 'data' },
80
- createdAt: new Date(),
81
- updatedAt: new Date(),
82
- } as ISocialAccount;
83
-
84
- const mockUserWithPassword: IUser = {
85
- ...mockUser,
86
- passwordHash: 'hashed-password',
87
- };
88
-
89
- beforeEach(() => {
90
- mockUserRepository = {
91
- findOne: jest.fn(),
92
- update: jest.fn().mockResolvedValue({ affected: 1 } as any),
93
- } as any;
94
-
95
- mockSocialAccountRepository = {
96
- findOne: jest.fn(),
97
- find: jest.fn(),
98
- remove: jest.fn(),
99
- save: jest.fn(),
100
- create: jest.fn(),
101
- update: jest.fn().mockResolvedValue({ affected: 1 } as any),
102
- } as any;
103
-
104
- mockAuthService = {
105
- changePassword: jest.fn().mockResolvedValue({ success: true }),
106
- } as any;
107
-
108
- mockLogger = {
109
- log: jest.fn(),
110
- error: jest.fn(),
111
- warn: jest.fn(),
112
- debug: jest.fn(),
113
- verbose: jest.fn(),
114
- } as any;
115
-
116
- mockAuditService = {
117
- recordEvent: jest.fn().mockResolvedValue(null),
118
- } as any;
119
-
120
- mockClientInfoService = {
121
- get: jest.fn().mockReturnValue({
122
- ipAddress: '1.2.3.4',
123
- userAgent: 'test-agent',
124
- }),
125
- getIpAddress: jest.fn().mockReturnValue('1.2.3.4'),
126
- getUserAgent: jest.fn().mockReturnValue('test-agent'),
127
- getDeviceToken: jest.fn().mockReturnValue(undefined),
128
- getDeviceId: jest.fn().mockReturnValue(undefined),
129
- } as any;
130
-
131
- // Instantiate service directly
132
- mockProviderRegistry = {
133
- getProvider: jest.fn(),
134
- registerProvider: jest.fn(),
135
- hasProvider: jest.fn(),
136
- listProviders: jest.fn(),
137
- } as any;
138
-
139
- service = new SocialAuthService(
140
- mockProviderRegistry,
141
- mockUserRepository,
142
- mockSocialAccountRepository,
143
- mockAuthService,
144
- mockLogger,
145
- mockAuditService,
146
- );
147
- });
148
-
149
- afterEach(() => {
150
- jest.clearAllMocks();
151
- });
152
-
153
- // ============================================================================
154
- // Service Initialization
155
- // ============================================================================
156
-
157
- it('should be defined', () => {
158
- expect(service).toBeDefined();
159
- });
160
-
161
- // ============================================================================
162
- // getLinkedAccounts
163
- // ============================================================================
164
-
165
- describe('getLinkedAccounts', () => {
166
- it('should return linked social accounts for user', async () => {
167
- const mockAccounts = [mockSocialAccount];
168
- mockUserRepository.findOne.mockResolvedValue(mockUser as any);
169
- mockSocialAccountRepository.find.mockResolvedValue(mockAccounts as any);
170
-
171
- const result = await service.getLinkedAccounts('user-123');
172
-
173
- expect(result).toEqual({
174
- accounts: [
175
- {
176
- provider: 'google',
177
- providerEmail: 'user@gmail.com',
178
- linkedAt: mockSocialAccount.linkedAt,
179
- lastUsedAt: mockSocialAccount.lastUsedAt || undefined,
180
- },
181
- ],
182
- });
183
- expect(mockUserRepository.findOne).toHaveBeenCalledWith({
184
- where: { sub: 'user-123' } as any,
185
- });
186
- expect(mockSocialAccountRepository.find).toHaveBeenCalledWith({
187
- where: { userId: 1 } as any,
188
- order: { linkedAt: 'DESC' } as any,
189
- });
190
- });
191
-
192
- it('should throw NAuthException when user not found', async () => {
193
- mockUserRepository.findOne.mockResolvedValue(null);
194
-
195
- try {
196
- await service.getLinkedAccounts('nonexistent-user');
197
- fail('Should have thrown NAuthException');
198
- } catch (error: any) {
199
- expect(error).toBeInstanceOf(NAuthException);
200
- expect(error.code).toBe(AuthErrorCode.NOT_FOUND);
201
- }
202
- });
203
-
204
- it('should return empty accounts array when user has no social accounts', async () => {
205
- mockUserRepository.findOne.mockResolvedValue(mockUser as any);
206
- mockSocialAccountRepository.find.mockResolvedValue([]);
207
-
208
- const result = await service.getLinkedAccounts('user-123');
209
-
210
- expect(result).toEqual({ accounts: [] });
211
- });
212
-
213
- it('should handle accounts without providerEmail', async () => {
214
- const accountWithoutEmail = { ...mockSocialAccount, providerEmail: null };
215
- mockUserRepository.findOne.mockResolvedValue(mockUser as any);
216
- mockSocialAccountRepository.find.mockResolvedValue([accountWithoutEmail] as any);
217
-
218
- const result = await service.getLinkedAccounts('user-123');
219
-
220
- expect(result.accounts[0].providerEmail).toBeUndefined();
221
- });
222
-
223
- it('should handle accounts without lastUsedAt', async () => {
224
- const accountWithoutLastUsed = { ...mockSocialAccount, lastUsedAt: null };
225
- mockUserRepository.findOne.mockResolvedValue(mockUser as any);
226
- mockSocialAccountRepository.find.mockResolvedValue([accountWithoutLastUsed] as any);
227
-
228
- const result = await service.getLinkedAccounts('user-123');
229
-
230
- expect(result.accounts[0].lastUsedAt).toBeUndefined();
231
- });
232
-
233
- it('should return multiple accounts sorted by linkedAt DESC', async () => {
234
- const account1 = { ...mockSocialAccount, id: 1, provider: 'google', linkedAt: new Date('2024-01-01') };
235
- const account2 = { ...mockSocialAccount, id: 2, provider: 'apple', linkedAt: new Date('2024-02-01') };
236
- mockUserRepository.findOne.mockResolvedValue(mockUser as any);
237
- mockSocialAccountRepository.find.mockResolvedValue([account2, account1] as any); // Repository returns sorted
238
-
239
- const result = await service.getLinkedAccounts('user-123');
240
-
241
- expect(result.accounts.length).toBe(2);
242
- expect(result.accounts[0].provider).toBe('apple'); // Most recent first
243
- expect(result.accounts[1].provider).toBe('google');
244
- });
245
- });
246
-
247
- // ============================================================================
248
- // unlinkSocialAccount
249
- // ============================================================================
250
-
251
- describe('unlinkSocialAccount', () => {
252
- it('should unlink social account from user', async () => {
253
- mockUserRepository.findOne.mockResolvedValue(mockUser as any);
254
- mockSocialAccountRepository.findOne.mockResolvedValue(mockSocialAccount as any);
255
- mockSocialAccountRepository.remove.mockResolvedValue(mockSocialAccount as any);
256
- mockSocialAccountRepository.find.mockResolvedValue([]); // No accounts left after unlink
257
-
258
- const result = await service.unlinkSocialAccount('user-123', 'google');
259
-
260
- expect(result).toEqual({ message: 'google account unlinked successfully' });
261
- expect(mockSocialAccountRepository.remove).toHaveBeenCalledWith(mockSocialAccount);
262
- expect(mockAuditService.recordEvent).toHaveBeenCalledWith(
263
- (expect as any).objectContaining({
264
- userId: 1,
265
- eventType: AuthAuditEventType.SOCIAL_ACCOUNT_UNLINKED,
266
- eventStatus: 'INFO',
267
- authMethod: 'google',
268
- }),
269
- );
270
- });
271
-
272
- it('should throw NAuthException when user not found', async () => {
273
- mockUserRepository.findOne.mockResolvedValue(null);
274
-
275
- try {
276
- await service.unlinkSocialAccount('nonexistent-user', 'google');
277
- fail('Should have thrown NAuthException');
278
- } catch (error: any) {
279
- expect(error).toBeInstanceOf(NAuthException);
280
- expect(error.code).toBe(AuthErrorCode.NOT_FOUND);
281
- }
282
- });
283
-
284
- it('should throw NAuthException when social account not found', async () => {
285
- mockUserRepository.findOne.mockResolvedValue(mockUser as any);
286
- mockSocialAccountRepository.findOne.mockResolvedValue(null);
287
-
288
- try {
289
- await service.unlinkSocialAccount('user-123', 'google');
290
- fail('Should have thrown NAuthException');
291
- } catch (error: any) {
292
- expect(error).toBeInstanceOf(NAuthException);
293
- expect(error.code).toBe(AuthErrorCode.SOCIAL_ACCOUNT_NOT_FOUND);
294
- expect(error.message).toContain('google account is not linked');
295
- }
296
- expect(mockSocialAccountRepository.remove).not.toHaveBeenCalled();
297
- });
298
-
299
- it('should update user social flags after unlinking', async () => {
300
- mockUserRepository.findOne.mockResolvedValue(mockUser as any);
301
- mockSocialAccountRepository.findOne.mockResolvedValue(mockSocialAccount as any);
302
- mockSocialAccountRepository.remove.mockResolvedValue(mockSocialAccount as any);
303
- mockSocialAccountRepository.find.mockResolvedValue([]); // No accounts left
304
-
305
- await service.unlinkSocialAccount('user-123', 'google');
306
-
307
- expect(mockUserRepository.update).toHaveBeenCalledWith(1, {
308
- hasSocialAuth: false,
309
- socialProviders: null,
310
- });
311
- });
312
-
313
- it('should update user social flags when other accounts remain', async () => {
314
- const appleAccount = { ...mockSocialAccount, id: 2, provider: 'apple' };
315
- mockUserRepository.findOne.mockResolvedValue(mockUser as any);
316
- mockSocialAccountRepository.findOne.mockResolvedValue(mockSocialAccount as any);
317
- mockSocialAccountRepository.remove.mockResolvedValue(mockSocialAccount as any);
318
- mockSocialAccountRepository.find.mockResolvedValue([appleAccount] as any); // Apple account remains
319
-
320
- await service.unlinkSocialAccount('user-123', 'google');
321
-
322
- expect(mockUserRepository.update).toHaveBeenCalledWith(1, {
323
- hasSocialAuth: true,
324
- socialProviders: ['apple'],
325
- });
326
- });
327
-
328
- it('should handle unlink for different providers', async () => {
329
- const appleAccount = { ...mockSocialAccount, id: 2, provider: 'apple', providerId: 'apple-456' };
330
- mockUserRepository.findOne.mockResolvedValue(mockUser as any);
331
- mockSocialAccountRepository.findOne.mockResolvedValue(appleAccount as any);
332
- mockSocialAccountRepository.remove.mockResolvedValue(appleAccount as any);
333
- mockSocialAccountRepository.find.mockResolvedValue([]);
334
-
335
- const result = await service.unlinkSocialAccount('user-123', 'apple');
336
-
337
- expect(result.message).toContain('apple account unlinked successfully');
338
- expect(mockAuditService.recordEvent).toHaveBeenCalledWith(
339
- (expect as any).objectContaining({
340
- authMethod: 'apple',
341
- }),
342
- );
343
- });
344
-
345
- it('should handle audit logging errors gracefully', async () => {
346
- mockUserRepository.findOne.mockResolvedValue(mockUser as any);
347
- mockSocialAccountRepository.findOne.mockResolvedValue(mockSocialAccount as any);
348
- mockSocialAccountRepository.remove.mockResolvedValue(mockSocialAccount as any);
349
- mockSocialAccountRepository.find.mockResolvedValue([]);
350
- mockAuditService.recordEvent.mockRejectedValue(new Error('Audit error'));
351
-
352
- const result = await service.unlinkSocialAccount('user-123', 'google');
353
-
354
- expect(result.message).toBeDefined(); // Should still unlink
355
- expect(mockLogger.error).toHaveBeenCalled();
356
- });
357
-
358
- it('should handle database errors during unlinking', async () => {
359
- mockUserRepository.findOne.mockResolvedValue(mockUser as any);
360
- mockSocialAccountRepository.findOne.mockResolvedValue(mockSocialAccount as any);
361
- mockSocialAccountRepository.remove.mockRejectedValue(new Error('Database error'));
362
-
363
- try {
364
- await service.unlinkSocialAccount('user-123', 'google');
365
- fail('Should have thrown Error');
366
- } catch (error) {
367
- expect(error).toBeInstanceOf(Error);
368
- expect((error as Error).message).toBe('Database error');
369
- }
370
- });
371
-
372
- it('should include providerEmail in audit metadata when available', async () => {
373
- mockUserRepository.findOne.mockResolvedValue(mockUser as any);
374
- mockSocialAccountRepository.findOne.mockResolvedValue(mockSocialAccount as any);
375
- mockSocialAccountRepository.remove.mockResolvedValue(mockSocialAccount as any);
376
- mockSocialAccountRepository.find.mockResolvedValue([]);
377
-
378
- await service.unlinkSocialAccount('user-123', 'google');
379
-
380
- expect(mockAuditService.recordEvent).toHaveBeenCalledWith(
381
- (expect as any).objectContaining({
382
- metadata: {
383
- provider: 'google',
384
- providerEmail: 'user@gmail.com',
385
- },
386
- }),
387
- );
388
- });
389
-
390
- it('should include null providerEmail in audit metadata when not available', async () => {
391
- const accountWithoutEmail = { ...mockSocialAccount, providerEmail: null };
392
- mockUserRepository.findOne.mockResolvedValue(mockUser as any);
393
- mockSocialAccountRepository.findOne.mockResolvedValue(accountWithoutEmail as any);
394
- mockSocialAccountRepository.remove.mockResolvedValue(accountWithoutEmail as any);
395
- mockSocialAccountRepository.find.mockResolvedValue([]);
396
-
397
- await service.unlinkSocialAccount('user-123', 'google');
398
-
399
- expect(mockAuditService.recordEvent).toHaveBeenCalledWith(
400
- (expect as any).objectContaining({
401
- metadata: {
402
- provider: 'google',
403
- providerEmail: null,
404
- },
405
- }),
406
- );
407
- });
408
- });
409
-
410
- // ============================================================================
411
- // canSetPassword
412
- // ============================================================================
413
-
414
- describe('canSetPassword', () => {
415
- it('should return true for social-only user (no password hash)', async () => {
416
- mockUserRepository.findOne.mockResolvedValue(mockUser as any);
417
-
418
- const result = await service.canSetPassword('user-123');
419
-
420
- expect(result).toBe(true);
421
- });
422
-
423
- it('should return false for user with password', async () => {
424
- mockUserRepository.findOne.mockResolvedValue(mockUserWithPassword as any);
425
-
426
- const result = await service.canSetPassword('user-123');
427
-
428
- expect(result).toBe(false);
429
- });
430
-
431
- it('should return false when user not found', async () => {
432
- mockUserRepository.findOne.mockResolvedValue(null);
433
-
434
- const result = await service.canSetPassword('nonexistent-user');
435
-
436
- expect(result).toBe(false);
437
- });
438
- });
439
-
440
- // ============================================================================
441
- // setPasswordForSocialUser
442
- // ============================================================================
443
-
444
- describe('setPasswordForSocialUser', () => {
445
- it('should set password for social-only user', async () => {
446
- mockUserRepository.findOne.mockResolvedValue(mockUser as any);
447
- mockAuthService.changePassword.mockResolvedValue({ success: true });
448
-
449
- const result = await service.setPasswordForSocialUser('user-123', 'newpassword');
450
-
451
- expect(result).toEqual({ message: 'Password set successfully' });
452
- expect(mockAuthService.changePassword).toHaveBeenCalledWith('user-123', {
453
- newPassword: 'newpassword',
454
- } as any);
455
- });
456
-
457
- it('should throw NAuthException when user not found', async () => {
458
- mockUserRepository.findOne.mockResolvedValue(null);
459
-
460
- try {
461
- await service.setPasswordForSocialUser('nonexistent-user', 'newpassword');
462
- fail('Should have thrown NAuthException');
463
- } catch (error: any) {
464
- expect(error).toBeInstanceOf(NAuthException);
465
- expect(error.code).toBe(AuthErrorCode.NOT_FOUND);
466
- }
467
- });
468
-
469
- it('should throw NAuthException when user already has password', async () => {
470
- mockUserRepository.findOne.mockResolvedValue(mockUserWithPassword as any);
471
-
472
- try {
473
- await service.setPasswordForSocialUser('user-123', 'newpassword');
474
- fail('Should have thrown NAuthException');
475
- } catch (error: any) {
476
- expect(error).toBeInstanceOf(NAuthException);
477
- expect(error.code).toBe(AuthErrorCode.VALIDATION_FAILED);
478
- expect(error.message).toContain('already has a password');
479
- }
480
- expect(mockAuthService.changePassword).not.toHaveBeenCalled();
481
- });
482
-
483
- it('should handle database errors during password setting', async () => {
484
- mockUserRepository.findOne.mockResolvedValue(mockUser as any);
485
- mockAuthService.changePassword.mockRejectedValue(new Error('Database error'));
486
-
487
- try {
488
- await service.setPasswordForSocialUser('user-123', 'newpassword');
489
- fail('Should have thrown Error');
490
- } catch (error) {
491
- expect(error).toBeInstanceOf(Error);
492
- expect((error as Error).message).toBe('Database error');
493
- }
494
- });
495
- });
496
-
497
- // ============================================================================
498
- // updateUserSocialFlags (tested through public methods)
499
- // ============================================================================
500
-
501
- describe('updateUserSocialFlags', () => {
502
- it('should update user flags when social accounts exist', async () => {
503
- mockSocialAccountRepository.find.mockResolvedValue([mockSocialAccount] as any);
504
-
505
- // Access private method through type assertion
506
- await (service as any).updateUserSocialFlags(1);
507
-
508
- expect(mockUserRepository.update).toHaveBeenCalledWith(1, {
509
- hasSocialAuth: true,
510
- socialProviders: ['google'],
511
- });
512
- });
513
-
514
- it('should update user flags when no social accounts remain', async () => {
515
- mockSocialAccountRepository.find.mockResolvedValue([]);
516
-
517
- await (service as any).updateUserSocialFlags(1);
518
-
519
- expect(mockUserRepository.update).toHaveBeenCalledWith(1, {
520
- hasSocialAuth: false,
521
- socialProviders: null,
522
- });
523
- });
524
-
525
- it('should handle multiple social providers', async () => {
526
- const accountsWithMultipleProviders = [
527
- { ...mockSocialAccount, provider: 'google' },
528
- { ...mockSocialAccount, provider: 'apple', id: 2 },
529
- ];
530
- mockSocialAccountRepository.find.mockResolvedValue(accountsWithMultipleProviders as any);
531
-
532
- await (service as any).updateUserSocialFlags(1);
533
-
534
- expect(mockUserRepository.update).toHaveBeenCalledWith(1, {
535
- hasSocialAuth: true,
536
- socialProviders: ['google', 'apple'],
537
- });
538
- });
539
-
540
- it('should handle null user id in social account', async () => {
541
- const accountWithNullUserId = { ...mockSocialAccount, userId: null, provider: 'google' };
542
- mockSocialAccountRepository.find.mockResolvedValue([accountWithNullUserId] as any);
543
-
544
- await (service as any).updateUserSocialFlags(1);
545
-
546
- // Should still update with the providers found
547
- expect(mockUserRepository.update).toHaveBeenCalledWith(1, {
548
- hasSocialAuth: true,
549
- socialProviders: ['google'],
550
- });
551
- });
552
-
553
- it('should handle empty social accounts array', async () => {
554
- mockSocialAccountRepository.find.mockResolvedValue([]);
555
-
556
- await (service as any).updateUserSocialFlags(1);
557
-
558
- expect(mockUserRepository.update).toHaveBeenCalledWith(1, {
559
- hasSocialAuth: false,
560
- socialProviders: null,
561
- });
562
- });
563
- });
564
-
565
- // ============================================================================
566
- // Internal Methods (for social auth provider integration)
567
- // ============================================================================
568
-
569
- describe('findSocialAccountByProvider', () => {
570
- it('should find social account by provider and provider ID', async () => {
571
- mockSocialAccountRepository.findOne.mockResolvedValue(mockSocialAccount as any);
572
-
573
- const result = await service.findSocialAccountByProvider('google', 'google-123');
574
-
575
- expect(result).toEqual(mockSocialAccount);
576
- expect(mockSocialAccountRepository.findOne).toHaveBeenCalledWith({
577
- where: { provider: 'google', providerId: 'google-123' } as any,
578
- relations: ['user'],
579
- });
580
- });
581
-
582
- it('should return null when account not found', async () => {
583
- mockSocialAccountRepository.findOne.mockResolvedValue(null);
584
-
585
- const result = await service.findSocialAccountByProvider('google', 'nonexistent-id');
586
-
587
- expect(result).toBeNull();
588
- });
589
- });
590
-
591
- describe('findSocialAccountByUser', () => {
592
- it('should find social account by user ID and provider', async () => {
593
- mockSocialAccountRepository.findOne.mockResolvedValue(mockSocialAccount as any);
594
-
595
- const result = await service.findSocialAccountByUser(1, 'google');
596
-
597
- expect(result).toEqual(mockSocialAccount);
598
- expect(mockSocialAccountRepository.findOne).toHaveBeenCalledWith({
599
- where: { userId: 1, provider: 'google' } as any,
600
- });
601
- });
602
-
603
- it('should return null when account not found', async () => {
604
- mockSocialAccountRepository.findOne.mockResolvedValue(null);
605
-
606
- const result = await service.findSocialAccountByUser(1, 'nonexistent-provider');
607
-
608
- expect(result).toBeNull();
609
- });
610
- });
611
-
612
- describe('createOrUpdateSocialAccount', () => {
613
- it('should create new social account when not exists', async () => {
614
- mockSocialAccountRepository.findOne.mockResolvedValue(null);
615
- const createdAccount = { ...mockSocialAccount };
616
- mockSocialAccountRepository.create.mockReturnValue(createdAccount as any);
617
- mockSocialAccountRepository.save.mockResolvedValue(createdAccount as any);
618
- mockSocialAccountRepository.find.mockResolvedValue([createdAccount] as any);
619
-
620
- await service.createOrUpdateSocialAccount(1, 'google', 'google-123', 'user@gmail.com', { raw: 'data' });
621
-
622
- expect(mockSocialAccountRepository.create).toHaveBeenCalledWith({
623
- userId: 1,
624
- provider: 'google',
625
- providerId: 'google-123',
626
- providerEmail: 'user@gmail.com',
627
- linkedAt: (expect as any).any(Date),
628
- lastUsedAt: (expect as any).any(Date),
629
- metadata: { raw: 'data' },
630
- });
631
- expect(mockSocialAccountRepository.save).toHaveBeenCalled();
632
- expect(mockUserRepository.update).toHaveBeenCalled(); // Flags updated
633
- });
634
-
635
- it('should update existing social account', async () => {
636
- const existingAccount = { ...mockSocialAccount };
637
- mockSocialAccountRepository.findOne.mockResolvedValue(existingAccount as any);
638
- mockSocialAccountRepository.save.mockResolvedValue(existingAccount as any);
639
- mockSocialAccountRepository.find.mockResolvedValue([existingAccount] as any);
640
-
641
- await service.createOrUpdateSocialAccount(1, 'google', 'google-123', 'updated@gmail.com', { new: 'data' });
642
-
643
- expect(existingAccount.providerEmail).toBe('updated@gmail.com');
644
- expect(existingAccount.lastUsedAt).toBeInstanceOf(Date);
645
- expect(existingAccount.metadata).toEqual({ new: 'data' });
646
- expect(mockSocialAccountRepository.save).toHaveBeenCalledWith(existingAccount);
647
- expect(mockUserRepository.update).toHaveBeenCalled(); // Flags updated
648
- });
649
-
650
- it('should handle null providerEmail', async () => {
651
- mockSocialAccountRepository.findOne.mockResolvedValue(null);
652
- const createdAccount = { ...mockSocialAccount };
653
- mockSocialAccountRepository.create.mockReturnValue(createdAccount as any);
654
- mockSocialAccountRepository.save.mockResolvedValue(createdAccount as any);
655
- mockSocialAccountRepository.find.mockResolvedValue([createdAccount] as any);
656
-
657
- await service.createOrUpdateSocialAccount(1, 'google', 'google-123', null, null);
658
-
659
- expect(mockSocialAccountRepository.create).toHaveBeenCalledWith(
660
- (expect as any).objectContaining({
661
- providerEmail: null,
662
- metadata: null,
663
- }),
664
- );
665
- });
666
-
667
- it('should handle undefined providerEmail', async () => {
668
- mockSocialAccountRepository.findOne.mockResolvedValue(null);
669
- const createdAccount = { ...mockSocialAccount };
670
- mockSocialAccountRepository.create.mockReturnValue(createdAccount as any);
671
- mockSocialAccountRepository.save.mockResolvedValue(createdAccount as any);
672
- mockSocialAccountRepository.find.mockResolvedValue([createdAccount] as any);
673
-
674
- await service.createOrUpdateSocialAccount(1, 'google', 'google-123', undefined, undefined);
675
-
676
- expect(mockSocialAccountRepository.create).toHaveBeenCalledWith(
677
- (expect as any).objectContaining({
678
- providerEmail: null,
679
- metadata: null,
680
- }),
681
- );
682
- });
683
-
684
- it('should update lastUsedAt when updating existing account', async () => {
685
- const existingAccount = {
686
- ...mockSocialAccount,
687
- lastUsedAt: new Date('2024-01-01'),
688
- };
689
- mockSocialAccountRepository.findOne.mockResolvedValue(existingAccount as any);
690
- mockSocialAccountRepository.save.mockResolvedValue(existingAccount as any);
691
- mockSocialAccountRepository.find.mockResolvedValue([existingAccount] as any);
692
-
693
- const beforeUpdate = existingAccount.lastUsedAt.getTime();
694
- await service.createOrUpdateSocialAccount(1, 'google', 'google-123', 'user@gmail.com', null);
695
-
696
- // lastUsedAt should be updated to current time
697
- expect(existingAccount.lastUsedAt.getTime()).toBeGreaterThan(beforeUpdate);
698
- });
699
- });
700
-
701
- // ============================================================================
702
- // Service Without Optional Dependencies
703
- // ============================================================================
704
-
705
- describe('Service without optional dependencies', () => {
706
- it('should work without audit service', async () => {
707
- const serviceWithoutAudit = new SocialAuthService(
708
- mockProviderRegistry,
709
- mockUserRepository,
710
- mockSocialAccountRepository,
711
- mockAuthService,
712
- mockLogger,
713
- undefined, // No audit service
714
- );
715
-
716
- mockUserRepository.findOne.mockResolvedValue(mockUser as any);
717
- mockSocialAccountRepository.find.mockResolvedValue([mockSocialAccount] as any);
718
-
719
- const result = await serviceWithoutAudit.getLinkedAccounts('user-123');
720
-
721
- // Should not throw error
722
- expect(result).toBeDefined();
723
- });
724
- });
725
- });