@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.
- package/LICENSE +90 -0
- package/README.md +30 -0
- package/package.json +7 -2
- package/jest.config.js +0 -15
- package/jest.setup.ts +0 -6
- package/src/adapters/database-columns.ts +0 -165
- package/src/adapters/express.adapter.ts +0 -385
- package/src/adapters/fastify.adapter.ts +0 -416
- package/src/adapters/index.ts +0 -16
- package/src/adapters/storage.factory.ts +0 -143
- package/src/bootstrap.ts +0 -374
- package/src/dto/auth-challenge.dto.ts +0 -231
- package/src/dto/auth-response.dto.ts +0 -253
- package/src/dto/challenge-response.dto.ts +0 -234
- package/src/dto/change-password-request.dto.ts +0 -50
- package/src/dto/change-password-response.dto.ts +0 -29
- package/src/dto/change-password.dto.ts +0 -57
- package/src/dto/error-response.dto.ts +0 -136
- package/src/dto/get-available-methods.dto.ts +0 -55
- package/src/dto/get-challenge-data-response.dto.ts +0 -28
- package/src/dto/get-challenge-data.dto.ts +0 -69
- package/src/dto/get-client-info.dto.ts +0 -104
- package/src/dto/get-device-token-response.dto.ts +0 -25
- package/src/dto/get-events-by-type.dto.ts +0 -76
- package/src/dto/get-ip-address-response.dto.ts +0 -24
- package/src/dto/get-mfa-status.dto.ts +0 -94
- package/src/dto/get-risk-assessment-history.dto.ts +0 -39
- package/src/dto/get-session-id-response.dto.ts +0 -25
- package/src/dto/get-setup-data-response.dto.ts +0 -31
- package/src/dto/get-setup-data.dto.ts +0 -75
- package/src/dto/get-suspicious-activity.dto.ts +0 -42
- package/src/dto/get-user-agent-response.dto.ts +0 -23
- package/src/dto/get-user-auth-history.dto.ts +0 -95
- package/src/dto/get-user-by-email.dto.ts +0 -61
- package/src/dto/get-user-by-id.dto.ts +0 -46
- package/src/dto/get-user-devices.dto.ts +0 -53
- package/src/dto/get-user-response.dto.ts +0 -17
- package/src/dto/has-provider.dto.ts +0 -56
- package/src/dto/index.ts +0 -57
- package/src/dto/is-trusted-device-response.dto.ts +0 -34
- package/src/dto/list-providers-response.dto.ts +0 -23
- package/src/dto/login.dto.ts +0 -95
- package/src/dto/logout-all-response.dto.ts +0 -24
- package/src/dto/logout-all.dto.ts +0 -65
- package/src/dto/logout-response.dto.ts +0 -25
- package/src/dto/logout.dto.ts +0 -64
- package/src/dto/refresh-token.dto.ts +0 -36
- package/src/dto/remove-devices.dto.ts +0 -85
- package/src/dto/resend-code-response.dto.ts +0 -32
- package/src/dto/resend-code.dto.ts +0 -51
- package/src/dto/reset-password.dto.ts +0 -115
- package/src/dto/respond-challenge.dto.ts +0 -272
- package/src/dto/set-mfa-exemption.dto.ts +0 -112
- package/src/dto/set-must-change-password-response.dto.ts +0 -27
- package/src/dto/set-must-change-password.dto.ts +0 -46
- package/src/dto/set-preferred-method.dto.ts +0 -80
- package/src/dto/setup-mfa.dto.ts +0 -98
- package/src/dto/signup.dto.ts +0 -174
- package/src/dto/social-auth.dto.ts +0 -422
- package/src/dto/trust-device-response.dto.ts +0 -30
- package/src/dto/trust-device.dto.ts +0 -9
- package/src/dto/update-user-attributes-request.dto.ts +0 -51
- package/src/dto/user-response.dto.ts +0 -138
- package/src/dto/user-update.dto.ts +0 -222
- package/src/dto/verify-email.dto.ts +0 -313
- package/src/dto/verify-mfa-code.dto.ts +0 -103
- package/src/dto/verify-phone-by-sub.dto.ts +0 -78
- package/src/dto/verify-phone.dto.ts +0 -245
- package/src/entities/auth-audit.entity.ts +0 -232
- package/src/entities/challenge-session.entity.ts +0 -116
- package/src/entities/index.ts +0 -29
- package/src/entities/login-attempt.entity.ts +0 -64
- package/src/entities/mfa-device.entity.ts +0 -151
- package/src/entities/rate-limit.entity.ts +0 -44
- package/src/entities/session.entity.ts +0 -180
- package/src/entities/social-account.entity.ts +0 -96
- package/src/entities/storage-lock.entity.ts +0 -39
- package/src/entities/trusted-device.entity.ts +0 -112
- package/src/entities/user.entity.ts +0 -243
- package/src/entities/verification-token.entity.ts +0 -141
- package/src/enums/auth-audit-event-type.enum.ts +0 -360
- package/src/enums/error-codes.enum.ts +0 -420
- package/src/enums/mfa-method.enum.ts +0 -97
- package/src/enums/risk-factor.enum.ts +0 -111
- package/src/exceptions/nauth.exception.ts +0 -231
- package/src/handlers/auth.handler.ts +0 -260
- package/src/handlers/client-info.handler.ts +0 -101
- package/src/handlers/csrf.handler.ts +0 -156
- package/src/handlers/token-delivery.handler.ts +0 -118
- package/src/index.ts +0 -118
- package/src/interfaces/client-info.interface.ts +0 -85
- package/src/interfaces/config.interface.ts +0 -2135
- package/src/interfaces/entities.interface.ts +0 -226
- package/src/interfaces/index.ts +0 -15
- package/src/interfaces/logger.interface.ts +0 -283
- package/src/interfaces/mfa-provider.interface.ts +0 -154
- package/src/interfaces/oauth.interface.ts +0 -148
- package/src/interfaces/provider.interface.ts +0 -47
- package/src/interfaces/social-auth-provider.interface.ts +0 -131
- package/src/interfaces/storage-adapter.interface.ts +0 -82
- package/src/interfaces/template.interface.ts +0 -510
- package/src/interfaces/token-verifier.interface.ts +0 -110
- package/src/internal.ts +0 -178
- package/src/platform/interfaces.ts +0 -299
- package/src/schemas/auth-config.schema.ts +0 -646
- package/src/services/adaptive-mfa-decision.service.spec.ts +0 -1058
- package/src/services/adaptive-mfa-decision.service.ts +0 -457
- package/src/services/auth-audit.service.spec.ts +0 -675
- package/src/services/auth-audit.service.ts +0 -558
- package/src/services/auth-challenge-helper.service.spec.ts +0 -3227
- package/src/services/auth-challenge-helper.service.ts +0 -825
- package/src/services/auth-flow-context-builder.service.ts +0 -520
- package/src/services/auth-flow-rules.ts +0 -202
- package/src/services/auth-flow-state-definitions.ts +0 -190
- package/src/services/auth-flow-state-machine.service.ts +0 -207
- package/src/services/auth-flow-state-machine.types.ts +0 -316
- package/src/services/auth.service.spec.ts +0 -4195
- package/src/services/auth.service.ts +0 -3727
- package/src/services/challenge.service.spec.ts +0 -1363
- package/src/services/challenge.service.ts +0 -696
- package/src/services/client-info.service.spec.ts +0 -572
- package/src/services/client-info.service.ts +0 -374
- package/src/services/csrf.service.ts +0 -54
- package/src/services/email-verification.service.spec.ts +0 -1229
- package/src/services/email-verification.service.ts +0 -578
- package/src/services/geo-location.service.spec.ts +0 -603
- package/src/services/geo-location.service.ts +0 -599
- package/src/services/index.ts +0 -13
- package/src/services/jwt.service.spec.ts +0 -882
- package/src/services/jwt.service.ts +0 -621
- package/src/services/mfa-base.service.spec.ts +0 -246
- package/src/services/mfa-base.service.ts +0 -611
- package/src/services/mfa.service.spec.ts +0 -693
- package/src/services/mfa.service.ts +0 -960
- package/src/services/password.service.spec.ts +0 -166
- package/src/services/password.service.ts +0 -309
- package/src/services/phone-verification.service.spec.ts +0 -1120
- package/src/services/phone-verification.service.ts +0 -751
- package/src/services/risk-detection.service.spec.ts +0 -1292
- package/src/services/risk-detection.service.ts +0 -1012
- package/src/services/risk-scoring.service.spec.ts +0 -204
- package/src/services/risk-scoring.service.ts +0 -131
- package/src/services/session.service.spec.ts +0 -1293
- package/src/services/session.service.ts +0 -803
- package/src/services/social-account.service.spec.ts +0 -725
- package/src/services/social-auth-base.service.spec.ts +0 -418
- package/src/services/social-auth-base.service.ts +0 -581
- package/src/services/social-auth.service.spec.ts +0 -238
- package/src/services/social-auth.service.ts +0 -436
- package/src/services/social-provider-registry.service.spec.ts +0 -238
- package/src/services/social-provider-registry.service.ts +0 -122
- package/src/services/trusted-device.service.spec.ts +0 -505
- package/src/services/trusted-device.service.ts +0 -339
- package/src/storage/account-lockout-storage.service.spec.ts +0 -310
- package/src/storage/account-lockout-storage.service.ts +0 -89
- package/src/storage/index.ts +0 -3
- package/src/storage/memory-storage.adapter.ts +0 -443
- package/src/storage/rate-limit-storage.service.spec.ts +0 -247
- package/src/storage/rate-limit-storage.service.ts +0 -38
- package/src/templates/html-template.engine.spec.ts +0 -161
- package/src/templates/html-template.engine.ts +0 -688
- package/src/templates/index.ts +0 -7
- package/src/utils/common-passwords.spec.ts +0 -230
- package/src/utils/common-passwords.ts +0 -170
- package/src/utils/context-storage.ts +0 -188
- package/src/utils/cookie-names.util.ts +0 -67
- package/src/utils/cookies.util.ts +0 -94
- package/src/utils/index.ts +0 -12
- package/src/utils/ip-extractor.spec.ts +0 -330
- package/src/utils/ip-extractor.ts +0 -220
- package/src/utils/nauth-logger.spec.ts +0 -388
- package/src/utils/nauth-logger.ts +0 -215
- package/src/utils/pii-redactor.spec.ts +0 -130
- package/src/utils/pii-redactor.ts +0 -288
- package/src/utils/setup/get-repositories.ts +0 -140
- package/src/utils/setup/init-services.ts +0 -422
- package/src/utils/setup/init-social.ts +0 -189
- package/src/utils/setup/init-storage.ts +0 -94
- package/src/utils/setup/register-mfa.ts +0 -165
- package/src/utils/setup/run-nauth-migrations.ts +0 -61
- package/src/utils/token-delivery-policy.ts +0 -38
- package/src/validators/template.validator.ts +0 -219
- package/tsconfig.json +0 -37
- 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
|
-
});
|