185 lines
6.7 KiB
TypeScript
185 lines
6.7 KiB
TypeScript
/**
|
|
* Tests that the OAuth "unsupported provider" branches in AuthService throw a
|
|
* structured AppError(501, AUTH_UNSUPPORTED_PROVIDER) instead of a raw Error.
|
|
*
|
|
* Issue #1405 — Phase 2: OAuth Error Standardization.
|
|
*/
|
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
import { ERROR_CODES } from '@insforge/shared-schemas';
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Minimal mocks for all AuthService dependencies
|
|
// ---------------------------------------------------------------------------
|
|
vi.mock('../../src/infra/database/database.manager.js', () => ({
|
|
DatabaseManager: {
|
|
getInstance: () => ({ getPool: () => ({ query: vi.fn(), connect: vi.fn() }) }),
|
|
},
|
|
}));
|
|
vi.mock('../../src/infra/security/token.manager.js', () => ({
|
|
TokenManager: { getInstance: () => ({ generateAccessToken: vi.fn(), verifyToken: vi.fn() }) },
|
|
}));
|
|
vi.mock('../../src/services/auth/oauth-config.service.js', () => ({
|
|
OAuthConfigService: { getInstance: () => ({}) },
|
|
}));
|
|
vi.mock('../../src/services/auth/custom-oauth-config.service.js', () => ({
|
|
CustomOAuthConfigService: { getInstance: () => ({}) },
|
|
}));
|
|
vi.mock('../../src/services/auth/auth-config.service.js', () => ({
|
|
AuthConfigService: { getInstance: () => ({}) },
|
|
}));
|
|
vi.mock('../../src/services/auth/auth-otp.service.js', () => ({
|
|
AuthOTPService: { getInstance: () => ({}) },
|
|
OTPPurpose: {},
|
|
OTPType: {},
|
|
}));
|
|
vi.mock('../../src/services/email/smtp-config.service.js', () => ({
|
|
SmtpConfigService: { getInstance: () => ({}) },
|
|
}));
|
|
vi.mock('../../src/services/email/email.service.js', () => ({
|
|
EmailService: { getInstance: () => ({}) },
|
|
}));
|
|
vi.mock('../../src/utils/logger.js', () => ({
|
|
default: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
|
|
}));
|
|
vi.mock('../../src/utils/environment.js', () => ({
|
|
getApiBaseUrl: () => 'http://localhost:3000',
|
|
}));
|
|
|
|
// Mock all OAuth providers — each has generateOAuthUrl and handleCallback
|
|
const makeOAuthProviderMock = () => ({
|
|
getInstance: () => ({
|
|
generateOAuthUrl: vi.fn().mockResolvedValue('https://oauth.example.com'),
|
|
handleCallback: vi.fn().mockResolvedValue({
|
|
provider: 'google',
|
|
providerId: '123',
|
|
email: 'test@test.com',
|
|
userName: 'Test',
|
|
avatarUrl: '',
|
|
identityData: {},
|
|
}),
|
|
handleSharedCallback: vi.fn().mockReturnValue({
|
|
provider: 'google',
|
|
providerId: '123',
|
|
email: 'test@test.com',
|
|
userName: 'Test',
|
|
avatarUrl: '',
|
|
identityData: {},
|
|
}),
|
|
}),
|
|
});
|
|
|
|
vi.mock('../../src/providers/oauth/google.provider.js', () => ({
|
|
GoogleOAuthProvider: makeOAuthProviderMock(),
|
|
}));
|
|
vi.mock('../../src/providers/oauth/github.provider.js', () => ({
|
|
GitHubOAuthProvider: makeOAuthProviderMock(),
|
|
}));
|
|
vi.mock('../../src/providers/oauth/discord.provider.js', () => ({
|
|
DiscordOAuthProvider: makeOAuthProviderMock(),
|
|
}));
|
|
vi.mock('../../src/providers/oauth/linkedin.provider.js', () => ({
|
|
LinkedInOAuthProvider: makeOAuthProviderMock(),
|
|
}));
|
|
vi.mock('../../src/providers/oauth/facebook.provider.js', () => ({
|
|
FacebookOAuthProvider: makeOAuthProviderMock(),
|
|
}));
|
|
vi.mock('../../src/providers/oauth/microsoft.provider.js', () => ({
|
|
MicrosoftOAuthProvider: makeOAuthProviderMock(),
|
|
}));
|
|
vi.mock('../../src/providers/oauth/x.provider.js', () => ({
|
|
XOAuthProvider: makeOAuthProviderMock(),
|
|
}));
|
|
vi.mock('../../src/providers/oauth/apple.provider.js', () => ({
|
|
AppleOAuthProvider: makeOAuthProviderMock(),
|
|
}));
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
async function getAuthService() {
|
|
// Must stub env vars before importing AuthService
|
|
vi.stubEnv('ADMIN_EMAIL', 'admin@test.com');
|
|
vi.stubEnv('ADMIN_PASSWORD', 'password123');
|
|
const { AuthService } = await import('../../src/services/auth/auth.service.js');
|
|
return AuthService.getInstance();
|
|
}
|
|
|
|
describe('AuthService — unsupported OAuth provider branches (Issue #1405 Phase 2)', () => {
|
|
beforeEach(() => {
|
|
vi.resetModules();
|
|
vi.unstubAllEnvs();
|
|
});
|
|
|
|
describe('generateOAuthUrl() default branch', () => {
|
|
it('throws AppError(501, AUTH_UNSUPPORTED_PROVIDER) for unknown provider', async () => {
|
|
const authService = await getAuthService();
|
|
|
|
// Force the TypeScript exhaustive switch to hit its default branch
|
|
await expect(authService.generateOAuthUrl('unknown_provider' as never)).rejects.toMatchObject(
|
|
{
|
|
statusCode: 501,
|
|
code: ERROR_CODES.AUTH_UNSUPPORTED_PROVIDER,
|
|
name: 'AppError',
|
|
}
|
|
);
|
|
});
|
|
|
|
it('error message matches exactly', async () => {
|
|
const authService = await getAuthService();
|
|
await expect(authService.generateOAuthUrl('tiktok' as never)).rejects.toThrow(
|
|
"OAuth provider 'tiktok' is not implemented yet."
|
|
);
|
|
});
|
|
|
|
it('is an AppError (name === AppError), not a plain Error', async () => {
|
|
const authService = await getAuthService();
|
|
let thrown: unknown;
|
|
try {
|
|
await authService.generateOAuthUrl('nonexistent' as never);
|
|
} catch (e) {
|
|
thrown = e;
|
|
}
|
|
// Check name since class identity breaks across module boundaries in Vitest
|
|
expect((thrown as Error).name).toBe('AppError');
|
|
expect((thrown as { statusCode?: number }).statusCode).toBe(501);
|
|
});
|
|
});
|
|
|
|
describe('handleOAuthCallback() default branch', () => {
|
|
it('throws AppError(501, AUTH_UNSUPPORTED_PROVIDER) for unknown provider', async () => {
|
|
const authService = await getAuthService();
|
|
await expect(
|
|
authService.handleOAuthCallback('unknown_provider' as never, { code: 'abc' })
|
|
).rejects.toMatchObject({
|
|
statusCode: 501,
|
|
code: ERROR_CODES.AUTH_UNSUPPORTED_PROVIDER,
|
|
name: 'AppError',
|
|
});
|
|
});
|
|
|
|
it('error message matches exactly', async () => {
|
|
const authService = await getAuthService();
|
|
await expect(
|
|
authService.handleOAuthCallback('unknown_provider' as never, { code: 'abc' })
|
|
).rejects.toThrow("OAuth provider 'unknown_provider' is not implemented yet.");
|
|
});
|
|
});
|
|
|
|
describe('handleSharedCallback() default branch', () => {
|
|
it('throws AppError(501, AUTH_UNSUPPORTED_PROVIDER) for unknown provider', async () => {
|
|
const authService = await getAuthService();
|
|
await expect(authService.handleSharedCallback('unknown' as never, {})).rejects.toMatchObject({
|
|
statusCode: 501,
|
|
code: ERROR_CODES.AUTH_UNSUPPORTED_PROVIDER,
|
|
name: 'AppError',
|
|
});
|
|
});
|
|
|
|
it('error message matches exactly', async () => {
|
|
const authService = await getAuthService();
|
|
await expect(authService.handleSharedCallback('unknown' as never, {})).rejects.toThrow(
|
|
"OAuth provider 'unknown' is not supported for shared callback."
|
|
);
|
|
});
|
|
});
|
|
});
|