236 lines
6.6 KiB
TypeScript
236 lines
6.6 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||
|
||
// Set required env vars before any imports
|
||
process.env.ROOT_ADMIN_USERNAME = 'admin';
|
||
process.env.ROOT_ADMIN_PASSWORD = 'admin-password';
|
||
|
||
const { mockPool, mockClient } = vi.hoisted(() => ({
|
||
mockPool: {
|
||
connect: vi.fn(),
|
||
},
|
||
mockClient: {
|
||
query: vi.fn(),
|
||
release: vi.fn(),
|
||
},
|
||
}));
|
||
|
||
vi.mock('../../src/infra/database/database.manager', () => ({
|
||
DatabaseManager: {
|
||
getInstance: () => ({
|
||
getPool: () => mockPool,
|
||
}),
|
||
},
|
||
}));
|
||
|
||
vi.mock('../../src/utils/logger', () => ({
|
||
default: {
|
||
info: vi.fn(),
|
||
error: vi.fn(),
|
||
warn: vi.fn(),
|
||
debug: vi.fn(),
|
||
},
|
||
}));
|
||
|
||
vi.mock('bcryptjs', () => ({
|
||
default: {
|
||
hash: vi.fn().mockResolvedValue('hashed-password'),
|
||
},
|
||
}));
|
||
|
||
vi.mock('../../src/infra/security/token.manager', () => ({
|
||
TokenManager: {
|
||
getInstance: () => ({
|
||
generateAccessToken: vi.fn().mockReturnValue('test-access-token'),
|
||
}),
|
||
},
|
||
}));
|
||
|
||
vi.mock('../../src/services/auth/auth-config.service', () => ({
|
||
AuthConfigService: {
|
||
getInstance: () => ({
|
||
getAuthConfig: vi.fn().mockResolvedValue({
|
||
requireEmailVerification: true,
|
||
verifyEmailMethod: 'code',
|
||
passwordMinLength: 8,
|
||
passwordRequireUppercase: false,
|
||
passwordRequireLowercase: false,
|
||
passwordRequireNumbers: false,
|
||
passwordRequireSymbols: false,
|
||
}),
|
||
validateRedirectUrl: vi.fn().mockResolvedValue(true),
|
||
}),
|
||
},
|
||
}));
|
||
|
||
vi.mock('../../src/services/auth/auth-otp.service', () => ({
|
||
AuthOTPService: {
|
||
getInstance: () => ({
|
||
generateOTP: vi.fn().mockResolvedValue('123456'),
|
||
}),
|
||
},
|
||
OTPPurpose: { VERIFY_EMAIL: 'VERIFY_EMAIL' },
|
||
OTPType: { CODE: 'CODE' },
|
||
}));
|
||
|
||
vi.mock('../../src/services/auth/oauth-config.service', () => ({
|
||
OAuthConfigService: {
|
||
getInstance: () => ({}),
|
||
},
|
||
}));
|
||
|
||
vi.mock('../../src/services/auth/custom-oauth-config.service', () => ({
|
||
CustomOAuthConfigService: {
|
||
getInstance: () => ({}),
|
||
},
|
||
}));
|
||
|
||
vi.mock('../../src/services/email/email.service', () => ({
|
||
EmailService: {
|
||
getInstance: () => ({
|
||
sendMail: vi.fn().mockResolvedValue(undefined),
|
||
}),
|
||
},
|
||
}));
|
||
|
||
// Mock all OAuth providers the constructor initializes
|
||
const mockOAuthProvider = { getInstance: () => ({}) };
|
||
vi.mock('../../src/providers/oauth/google.oauth.provider', () => ({
|
||
GoogleOAuthProvider: mockOAuthProvider,
|
||
}));
|
||
vi.mock('../../src/providers/oauth/github.oauth.provider', () => ({
|
||
GitHubOAuthProvider: mockOAuthProvider,
|
||
}));
|
||
vi.mock('../../src/providers/oauth/discord.oauth.provider', () => ({
|
||
DiscordOAuthProvider: mockOAuthProvider,
|
||
}));
|
||
vi.mock('../../src/providers/oauth/facebook.oauth.provider', () => ({
|
||
FacebookOAuthProvider: mockOAuthProvider,
|
||
}));
|
||
vi.mock('../../src/providers/oauth/microsoft.oauth.provider', () => ({
|
||
MicrosoftOAuthProvider: mockOAuthProvider,
|
||
}));
|
||
vi.mock('../../src/providers/oauth/x.oauth.provider', () => ({
|
||
XOAuthProvider: mockOAuthProvider,
|
||
}));
|
||
vi.mock('../../src/providers/oauth/apple.oauth.provider', () => ({
|
||
AppleOAuthProvider: mockOAuthProvider,
|
||
}));
|
||
|
||
vi.mock('../../src/infra/config/app.config', () => {
|
||
const c = {
|
||
app: {
|
||
jwtSecret: 'test-secret',
|
||
name: 'test',
|
||
},
|
||
cloud: {
|
||
projectId: null,
|
||
},
|
||
auth: {
|
||
rootAdminUsername: 'admin@test.com',
|
||
rootAdminPassword: 'admin-password',
|
||
},
|
||
};
|
||
return {
|
||
config: c,
|
||
appConfig: c,
|
||
getApiBaseUrl: () => 'http://localhost:3000',
|
||
};
|
||
});
|
||
|
||
import { AuthService } from '../../src/services/auth/auth.service';
|
||
|
||
describe('AuthService.register – autoConfirm', () => {
|
||
let authService: AuthService;
|
||
|
||
beforeEach(() => {
|
||
vi.clearAllMocks();
|
||
mockPool.connect.mockResolvedValue(mockClient);
|
||
mockClient.query.mockResolvedValue({ rows: [] });
|
||
// Reset singleton to get fresh instance
|
||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||
(AuthService as any).instance = undefined;
|
||
authService = AuthService.getInstance();
|
||
// Mock getUserById to return a user record after INSERT
|
||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||
vi.spyOn(authService as any, 'getUserById').mockResolvedValue({
|
||
id: 'test-user-id',
|
||
email: 'test@example.com',
|
||
profile: { name: 'Test' },
|
||
email_verified: false,
|
||
created_at: new Date(),
|
||
updated_at: new Date(),
|
||
metadata: null,
|
||
is_anonymous: false,
|
||
});
|
||
});
|
||
|
||
it('sets email_verified=true when autoConfirm=true and isAdminCreation=true', async () => {
|
||
const result = await authService.register(
|
||
'test@example.com',
|
||
'password123',
|
||
'Test',
|
||
undefined,
|
||
{ isAdminCreation: true, autoConfirm: true }
|
||
);
|
||
|
||
// Verify INSERT was called with email_verified=true (5th param)
|
||
const insertCall = mockClient.query.mock.calls.find(
|
||
(call: unknown[]) => typeof call[0] === 'string' && call[0].includes('INSERT INTO auth.users')
|
||
);
|
||
expect(insertCall).toBeDefined();
|
||
expect(insertCall![1][4]).toBe(true);
|
||
|
||
// Should not require email verification
|
||
expect(result.requireEmailVerification).toBe(false);
|
||
});
|
||
|
||
it('sets email_verified=false when autoConfirm=false and isAdminCreation=true', async () => {
|
||
const result = await authService.register(
|
||
'test@example.com',
|
||
'password123',
|
||
'Test',
|
||
undefined,
|
||
{ isAdminCreation: true, autoConfirm: false }
|
||
);
|
||
|
||
const insertCall = mockClient.query.mock.calls.find(
|
||
(call: unknown[]) => typeof call[0] === 'string' && call[0].includes('INSERT INTO auth.users')
|
||
);
|
||
expect(insertCall).toBeDefined();
|
||
expect(insertCall![1][4]).toBe(false);
|
||
|
||
expect(result.requireEmailVerification).toBe(true);
|
||
});
|
||
|
||
it('ignores autoConfirm=true when isAdminCreation is false', async () => {
|
||
await authService.register('test@example.com', 'password123', 'Test', undefined, {
|
||
isAdminCreation: false,
|
||
autoConfirm: true,
|
||
});
|
||
|
||
const insertCall = mockClient.query.mock.calls.find(
|
||
(call: unknown[]) => typeof call[0] === 'string' && call[0].includes('INSERT INTO auth.users')
|
||
);
|
||
expect(insertCall).toBeDefined();
|
||
expect(insertCall![1][4]).toBe(false);
|
||
});
|
||
|
||
it('preserves existing behavior when autoConfirm is omitted', async () => {
|
||
const result = await authService.register(
|
||
'test@example.com',
|
||
'password123',
|
||
'Test',
|
||
undefined,
|
||
{ isAdminCreation: true }
|
||
);
|
||
|
||
const insertCall = mockClient.query.mock.calls.find(
|
||
(call: unknown[]) => typeof call[0] === 'string' && call[0].includes('INSERT INTO auth.users')
|
||
);
|
||
expect(insertCall).toBeDefined();
|
||
expect(insertCall![1][4]).toBe(false);
|
||
|
||
expect(result.requireEmailVerification).toBe(true);
|
||
});
|
||
});
|