Migración desde workspace-v2/projects/template-saas/apps/backend Este repositorio es parte del estándar multi-repo v2 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1395 lines
42 KiB
TypeScript
1395 lines
42 KiB
TypeScript
import { Test, TestingModule } from '@nestjs/testing';
|
|
import { BadRequestException } from '@nestjs/common';
|
|
import { ConfigService } from '@nestjs/config';
|
|
import { OAuthController } from '../controllers/oauth.controller';
|
|
import {
|
|
OAuthService,
|
|
OAuthProfile,
|
|
OAuthTokens,
|
|
OAuthUrlResponse,
|
|
OAuthConnectionResponse,
|
|
} from '../services/oauth.service';
|
|
import { OAuthProvider } from '../entities/oauth-provider.enum';
|
|
import { RequestUser } from '../strategies/jwt.strategy';
|
|
|
|
describe('OAuthController', () => {
|
|
let controller: OAuthController;
|
|
let oauthService: jest.Mocked<OAuthService>;
|
|
|
|
const mockTenantId = '550e8400-e29b-41d4-a716-446655440001';
|
|
const mockUserId = '550e8400-e29b-41d4-a716-446655440000';
|
|
|
|
const mockRequestUser: RequestUser = {
|
|
id: mockUserId,
|
|
email: 'test@example.com',
|
|
tenant_id: mockTenantId,
|
|
};
|
|
|
|
const mockOAuthProfile: OAuthProfile = {
|
|
id: 'oauth-provider-user-id-123',
|
|
email: 'oauth-user@example.com',
|
|
name: 'OAuth User',
|
|
avatar_url: 'https://example.com/avatar.jpg',
|
|
raw_data: { provider_specific: 'data' },
|
|
};
|
|
|
|
const mockOAuthTokens: OAuthTokens = {
|
|
access_token: 'oauth_access_token_abc123',
|
|
refresh_token: 'oauth_refresh_token_xyz789',
|
|
expires_at: new Date(Date.now() + 3600000), // 1 hour from now
|
|
scopes: ['email', 'profile'],
|
|
};
|
|
|
|
const mockUser = {
|
|
id: mockUserId,
|
|
tenant_id: mockTenantId,
|
|
email: 'oauth-user@example.com',
|
|
first_name: 'OAuth',
|
|
last_name: 'User',
|
|
status: 'active',
|
|
email_verified: true,
|
|
};
|
|
|
|
const mockAuthResponse = {
|
|
user: mockUser,
|
|
accessToken: 'jwt_access_token',
|
|
refreshToken: 'jwt_refresh_token',
|
|
};
|
|
|
|
const mockRequest = {
|
|
ip: '127.0.0.1',
|
|
headers: {
|
|
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)',
|
|
'x-tenant-id': mockTenantId,
|
|
},
|
|
};
|
|
|
|
const mockOAuthUrlResponse: OAuthUrlResponse = {
|
|
url: 'https://accounts.google.com/o/oauth2/v2/auth?...',
|
|
state: 'encoded_state_string',
|
|
};
|
|
|
|
const mockOAuthConnection: OAuthConnectionResponse = {
|
|
id: '550e8400-e29b-41d4-a716-446655440002',
|
|
provider: OAuthProvider.GOOGLE,
|
|
provider_email: 'oauth-user@example.com',
|
|
provider_name: 'OAuth User',
|
|
provider_avatar_url: 'https://example.com/avatar.jpg',
|
|
created_at: new Date(),
|
|
last_used_at: new Date(),
|
|
};
|
|
|
|
beforeEach(async () => {
|
|
const mockOAuthService = {
|
|
getAuthorizationUrl: jest.fn(),
|
|
handleOAuthLogin: jest.fn(),
|
|
getConnections: jest.fn(),
|
|
disconnectProvider: jest.fn(),
|
|
linkProvider: jest.fn(),
|
|
};
|
|
|
|
const mockConfigService = {
|
|
get: jest.fn((key: string) => {
|
|
const config: Record<string, string> = {
|
|
'app.frontendUrl': 'http://localhost:3000',
|
|
};
|
|
return config[key];
|
|
}),
|
|
};
|
|
|
|
const module: TestingModule = await Test.createTestingModule({
|
|
controllers: [OAuthController],
|
|
providers: [
|
|
{ provide: OAuthService, useValue: mockOAuthService },
|
|
{ provide: ConfigService, useValue: mockConfigService },
|
|
],
|
|
}).compile();
|
|
|
|
controller = module.get<OAuthController>(OAuthController);
|
|
oauthService = module.get(OAuthService);
|
|
});
|
|
|
|
afterEach(() => {
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
// ==================== getAuthorizationUrl Tests ====================
|
|
|
|
describe('getAuthorizationUrl', () => {
|
|
describe('success cases', () => {
|
|
it('should return authorization URL for Google provider', () => {
|
|
const googleUrl: OAuthUrlResponse = {
|
|
url: 'https://accounts.google.com/o/oauth2/v2/auth?client_id=...',
|
|
state: 'google_state_123',
|
|
};
|
|
oauthService.getAuthorizationUrl.mockReturnValue(googleUrl);
|
|
|
|
const result = controller.getAuthorizationUrl('google', mockTenantId);
|
|
|
|
expect(result).toEqual(googleUrl);
|
|
expect(oauthService.getAuthorizationUrl).toHaveBeenCalledWith(
|
|
OAuthProvider.GOOGLE,
|
|
mockTenantId,
|
|
);
|
|
});
|
|
|
|
it('should return authorization URL for Microsoft provider', () => {
|
|
const microsoftUrl: OAuthUrlResponse = {
|
|
url: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize?...',
|
|
state: 'microsoft_state_456',
|
|
};
|
|
oauthService.getAuthorizationUrl.mockReturnValue(microsoftUrl);
|
|
|
|
const result = controller.getAuthorizationUrl('microsoft', mockTenantId);
|
|
|
|
expect(result).toEqual(microsoftUrl);
|
|
expect(oauthService.getAuthorizationUrl).toHaveBeenCalledWith(
|
|
OAuthProvider.MICROSOFT,
|
|
mockTenantId,
|
|
);
|
|
});
|
|
|
|
it('should return authorization URL for GitHub provider', () => {
|
|
const githubUrl: OAuthUrlResponse = {
|
|
url: 'https://github.com/login/oauth/authorize?...',
|
|
state: 'github_state_789',
|
|
};
|
|
oauthService.getAuthorizationUrl.mockReturnValue(githubUrl);
|
|
|
|
const result = controller.getAuthorizationUrl('github', mockTenantId);
|
|
|
|
expect(result).toEqual(githubUrl);
|
|
expect(oauthService.getAuthorizationUrl).toHaveBeenCalledWith(
|
|
OAuthProvider.GITHUB,
|
|
mockTenantId,
|
|
);
|
|
});
|
|
|
|
it('should return authorization URL for Apple provider', () => {
|
|
const appleUrl: OAuthUrlResponse = {
|
|
url: 'https://appleid.apple.com/auth/authorize?...',
|
|
state: 'apple_state_abc',
|
|
};
|
|
oauthService.getAuthorizationUrl.mockReturnValue(appleUrl);
|
|
|
|
const result = controller.getAuthorizationUrl('apple', mockTenantId);
|
|
|
|
expect(result).toEqual(appleUrl);
|
|
expect(oauthService.getAuthorizationUrl).toHaveBeenCalledWith(
|
|
OAuthProvider.APPLE,
|
|
mockTenantId,
|
|
);
|
|
});
|
|
|
|
it('should handle uppercase provider names', () => {
|
|
oauthService.getAuthorizationUrl.mockReturnValue(mockOAuthUrlResponse);
|
|
|
|
const result = controller.getAuthorizationUrl('GOOGLE', mockTenantId);
|
|
|
|
expect(result).toEqual(mockOAuthUrlResponse);
|
|
expect(oauthService.getAuthorizationUrl).toHaveBeenCalledWith(
|
|
OAuthProvider.GOOGLE,
|
|
mockTenantId,
|
|
);
|
|
});
|
|
|
|
it('should handle mixed case provider names', () => {
|
|
oauthService.getAuthorizationUrl.mockReturnValue(mockOAuthUrlResponse);
|
|
|
|
const result = controller.getAuthorizationUrl('GoOgLe', mockTenantId);
|
|
|
|
expect(result).toEqual(mockOAuthUrlResponse);
|
|
expect(oauthService.getAuthorizationUrl).toHaveBeenCalledWith(
|
|
OAuthProvider.GOOGLE,
|
|
mockTenantId,
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('error cases', () => {
|
|
it('should throw BadRequestException when tenant ID is missing', () => {
|
|
expect(() =>
|
|
controller.getAuthorizationUrl('google', undefined as any),
|
|
).toThrow(BadRequestException);
|
|
expect(() =>
|
|
controller.getAuthorizationUrl('google', undefined as any),
|
|
).toThrow('Tenant ID es requerido');
|
|
});
|
|
|
|
it('should throw BadRequestException when tenant ID is empty string', () => {
|
|
expect(() => controller.getAuthorizationUrl('google', '')).toThrow(
|
|
BadRequestException,
|
|
);
|
|
});
|
|
|
|
it('should throw BadRequestException for invalid provider', () => {
|
|
expect(() =>
|
|
controller.getAuthorizationUrl('facebook', mockTenantId),
|
|
).toThrow(BadRequestException);
|
|
expect(() =>
|
|
controller.getAuthorizationUrl('facebook', mockTenantId),
|
|
).toThrow(/Proveedor OAuth no válido: facebook/);
|
|
});
|
|
|
|
it('should throw BadRequestException for unknown provider', () => {
|
|
expect(() =>
|
|
controller.getAuthorizationUrl('linkedin', mockTenantId),
|
|
).toThrow(BadRequestException);
|
|
});
|
|
|
|
it('should include supported providers in error message', () => {
|
|
try {
|
|
controller.getAuthorizationUrl('invalid-provider', mockTenantId);
|
|
fail('Expected BadRequestException');
|
|
} catch (error) {
|
|
expect(error.message).toContain('google');
|
|
expect(error.message).toContain('microsoft');
|
|
expect(error.message).toContain('github');
|
|
expect(error.message).toContain('apple');
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
// ==================== handleCallback Tests ====================
|
|
|
|
describe('handleCallback', () => {
|
|
describe('success cases', () => {
|
|
it('should handle Google OAuth callback successfully', async () => {
|
|
oauthService.handleOAuthLogin.mockResolvedValue(mockAuthResponse);
|
|
|
|
const body = {
|
|
code: 'authorization_code_123',
|
|
state: 'state_token',
|
|
profile: mockOAuthProfile,
|
|
tokens: mockOAuthTokens,
|
|
};
|
|
|
|
const result = await controller.handleCallback(
|
|
'google',
|
|
body,
|
|
mockTenantId,
|
|
mockRequest as any,
|
|
);
|
|
|
|
expect(result).toEqual(mockAuthResponse);
|
|
expect(oauthService.handleOAuthLogin).toHaveBeenCalledWith(
|
|
OAuthProvider.GOOGLE,
|
|
mockOAuthProfile,
|
|
mockOAuthTokens,
|
|
mockTenantId,
|
|
mockRequest,
|
|
);
|
|
});
|
|
|
|
it('should handle Microsoft OAuth callback successfully', async () => {
|
|
oauthService.handleOAuthLogin.mockResolvedValue(mockAuthResponse);
|
|
|
|
const body = {
|
|
code: 'ms_auth_code',
|
|
profile: { ...mockOAuthProfile, email: 'user@outlook.com' },
|
|
tokens: mockOAuthTokens,
|
|
};
|
|
|
|
const result = await controller.handleCallback(
|
|
'microsoft',
|
|
body,
|
|
mockTenantId,
|
|
mockRequest as any,
|
|
);
|
|
|
|
expect(result).toEqual(mockAuthResponse);
|
|
expect(oauthService.handleOAuthLogin).toHaveBeenCalledWith(
|
|
OAuthProvider.MICROSOFT,
|
|
expect.objectContaining({ email: 'user@outlook.com' }),
|
|
mockOAuthTokens,
|
|
mockTenantId,
|
|
mockRequest,
|
|
);
|
|
});
|
|
|
|
it('should handle GitHub OAuth callback successfully', async () => {
|
|
oauthService.handleOAuthLogin.mockResolvedValue(mockAuthResponse);
|
|
|
|
const body = {
|
|
code: 'github_code',
|
|
profile: { ...mockOAuthProfile, email: 'dev@github.com' },
|
|
tokens: { ...mockOAuthTokens, refresh_token: undefined }, // GitHub doesn't return refresh token
|
|
};
|
|
|
|
const result = await controller.handleCallback(
|
|
'github',
|
|
body,
|
|
mockTenantId,
|
|
mockRequest as any,
|
|
);
|
|
|
|
expect(result).toEqual(mockAuthResponse);
|
|
expect(oauthService.handleOAuthLogin).toHaveBeenCalledWith(
|
|
OAuthProvider.GITHUB,
|
|
expect.objectContaining({ email: 'dev@github.com' }),
|
|
expect.objectContaining({ refresh_token: undefined }),
|
|
mockTenantId,
|
|
mockRequest,
|
|
);
|
|
});
|
|
|
|
it('should create new user when OAuth email does not exist', async () => {
|
|
const newUserResponse = {
|
|
user: {
|
|
...mockUser,
|
|
id: 'new-user-id',
|
|
email: 'newuser@oauth.com',
|
|
},
|
|
accessToken: 'new_access_token',
|
|
refreshToken: 'new_refresh_token',
|
|
};
|
|
oauthService.handleOAuthLogin.mockResolvedValue(newUserResponse);
|
|
|
|
const body = {
|
|
code: 'new_user_code',
|
|
profile: { ...mockOAuthProfile, email: 'newuser@oauth.com' },
|
|
tokens: mockOAuthTokens,
|
|
};
|
|
|
|
const result = await controller.handleCallback(
|
|
'google',
|
|
body,
|
|
mockTenantId,
|
|
mockRequest as any,
|
|
);
|
|
|
|
expect(result.user.email).toBe('newuser@oauth.com');
|
|
expect(result).toHaveProperty('accessToken');
|
|
expect(result).toHaveProperty('refreshToken');
|
|
});
|
|
|
|
it('should link OAuth to existing user when email matches', async () => {
|
|
const existingUserResponse = {
|
|
user: mockUser,
|
|
accessToken: 'linked_access_token',
|
|
refreshToken: 'linked_refresh_token',
|
|
};
|
|
oauthService.handleOAuthLogin.mockResolvedValue(existingUserResponse);
|
|
|
|
const body = {
|
|
code: 'existing_user_code',
|
|
profile: mockOAuthProfile,
|
|
tokens: mockOAuthTokens,
|
|
};
|
|
|
|
const result = await controller.handleCallback(
|
|
'google',
|
|
body,
|
|
mockTenantId,
|
|
mockRequest as any,
|
|
);
|
|
|
|
expect(result.user.id).toBe(mockUserId);
|
|
});
|
|
|
|
it('should handle callback with minimal profile data', async () => {
|
|
oauthService.handleOAuthLogin.mockResolvedValue(mockAuthResponse);
|
|
|
|
const minimalProfile: OAuthProfile = {
|
|
id: 'min-oauth-id',
|
|
email: 'minimal@example.com',
|
|
};
|
|
|
|
const body = {
|
|
code: 'minimal_code',
|
|
profile: minimalProfile,
|
|
tokens: mockOAuthTokens,
|
|
};
|
|
|
|
const result = await controller.handleCallback(
|
|
'google',
|
|
body,
|
|
mockTenantId,
|
|
mockRequest as any,
|
|
);
|
|
|
|
expect(result).toEqual(mockAuthResponse);
|
|
expect(oauthService.handleOAuthLogin).toHaveBeenCalledWith(
|
|
OAuthProvider.GOOGLE,
|
|
minimalProfile,
|
|
mockOAuthTokens,
|
|
mockTenantId,
|
|
mockRequest,
|
|
);
|
|
});
|
|
|
|
it('should handle callback with minimal token data', async () => {
|
|
oauthService.handleOAuthLogin.mockResolvedValue(mockAuthResponse);
|
|
|
|
const minimalTokens: OAuthTokens = {
|
|
access_token: 'only_access_token',
|
|
};
|
|
|
|
const body = {
|
|
code: 'minimal_token_code',
|
|
profile: mockOAuthProfile,
|
|
tokens: minimalTokens,
|
|
};
|
|
|
|
const result = await controller.handleCallback(
|
|
'google',
|
|
body,
|
|
mockTenantId,
|
|
mockRequest as any,
|
|
);
|
|
|
|
expect(result).toEqual(mockAuthResponse);
|
|
});
|
|
});
|
|
|
|
describe('error cases', () => {
|
|
it('should throw BadRequestException when tenant ID is missing', async () => {
|
|
const body = {
|
|
code: 'some_code',
|
|
profile: mockOAuthProfile,
|
|
tokens: mockOAuthTokens,
|
|
};
|
|
|
|
await expect(
|
|
controller.handleCallback(
|
|
'google',
|
|
body,
|
|
undefined as any,
|
|
mockRequest as any,
|
|
),
|
|
).rejects.toThrow(BadRequestException);
|
|
await expect(
|
|
controller.handleCallback(
|
|
'google',
|
|
body,
|
|
undefined as any,
|
|
mockRequest as any,
|
|
),
|
|
).rejects.toThrow('Tenant ID es requerido');
|
|
});
|
|
|
|
it('should throw BadRequestException when profile is missing', async () => {
|
|
const body = {
|
|
code: 'some_code',
|
|
tokens: mockOAuthTokens,
|
|
} as any;
|
|
|
|
await expect(
|
|
controller.handleCallback(
|
|
'google',
|
|
body,
|
|
mockTenantId,
|
|
mockRequest as any,
|
|
),
|
|
).rejects.toThrow(BadRequestException);
|
|
await expect(
|
|
controller.handleCallback(
|
|
'google',
|
|
body,
|
|
mockTenantId,
|
|
mockRequest as any,
|
|
),
|
|
).rejects.toThrow('Se requiere profile y tokens del proveedor OAuth');
|
|
});
|
|
|
|
it('should throw BadRequestException when tokens are missing', async () => {
|
|
const body = {
|
|
code: 'some_code',
|
|
profile: mockOAuthProfile,
|
|
} as any;
|
|
|
|
await expect(
|
|
controller.handleCallback(
|
|
'google',
|
|
body,
|
|
mockTenantId,
|
|
mockRequest as any,
|
|
),
|
|
).rejects.toThrow(BadRequestException);
|
|
});
|
|
|
|
it('should throw BadRequestException when both profile and tokens are missing', async () => {
|
|
const body = {
|
|
code: 'some_code',
|
|
} as any;
|
|
|
|
await expect(
|
|
controller.handleCallback(
|
|
'google',
|
|
body,
|
|
mockTenantId,
|
|
mockRequest as any,
|
|
),
|
|
).rejects.toThrow(BadRequestException);
|
|
});
|
|
|
|
it('should throw BadRequestException for invalid provider', async () => {
|
|
const body = {
|
|
code: 'some_code',
|
|
profile: mockOAuthProfile,
|
|
tokens: mockOAuthTokens,
|
|
};
|
|
|
|
await expect(
|
|
controller.handleCallback(
|
|
'facebook',
|
|
body,
|
|
mockTenantId,
|
|
mockRequest as any,
|
|
),
|
|
).rejects.toThrow(BadRequestException);
|
|
});
|
|
|
|
it('should propagate service errors', async () => {
|
|
const serviceError = new Error('OAuth service error');
|
|
oauthService.handleOAuthLogin.mockRejectedValue(serviceError);
|
|
|
|
const body = {
|
|
code: 'error_code',
|
|
profile: mockOAuthProfile,
|
|
tokens: mockOAuthTokens,
|
|
};
|
|
|
|
await expect(
|
|
controller.handleCallback(
|
|
'google',
|
|
body,
|
|
mockTenantId,
|
|
mockRequest as any,
|
|
),
|
|
).rejects.toThrow('OAuth service error');
|
|
});
|
|
});
|
|
});
|
|
|
|
// ==================== getConnections Tests ====================
|
|
|
|
describe('getConnections', () => {
|
|
describe('success cases', () => {
|
|
it('should return list of OAuth connections for user', async () => {
|
|
const connections: OAuthConnectionResponse[] = [
|
|
mockOAuthConnection,
|
|
{
|
|
...mockOAuthConnection,
|
|
id: '550e8400-e29b-41d4-a716-446655440003',
|
|
provider: OAuthProvider.GITHUB,
|
|
provider_email: 'dev@github.com',
|
|
},
|
|
];
|
|
oauthService.getConnections.mockResolvedValue(connections);
|
|
|
|
const result = await controller.getConnections(mockRequestUser);
|
|
|
|
expect(result).toEqual(connections);
|
|
expect(result).toHaveLength(2);
|
|
expect(oauthService.getConnections).toHaveBeenCalledWith(
|
|
mockRequestUser.id,
|
|
mockRequestUser.tenant_id,
|
|
);
|
|
});
|
|
|
|
it('should return empty array when user has no OAuth connections', async () => {
|
|
oauthService.getConnections.mockResolvedValue([]);
|
|
|
|
const result = await controller.getConnections(mockRequestUser);
|
|
|
|
expect(result).toEqual([]);
|
|
expect(result).toHaveLength(0);
|
|
});
|
|
|
|
it('should return single connection when user has one OAuth provider', async () => {
|
|
oauthService.getConnections.mockResolvedValue([mockOAuthConnection]);
|
|
|
|
const result = await controller.getConnections(mockRequestUser);
|
|
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].provider).toBe(OAuthProvider.GOOGLE);
|
|
});
|
|
|
|
it('should return connections with all providers', async () => {
|
|
const allProviderConnections: OAuthConnectionResponse[] = [
|
|
{ ...mockOAuthConnection, provider: OAuthProvider.GOOGLE },
|
|
{ ...mockOAuthConnection, id: 'id-2', provider: OAuthProvider.MICROSOFT },
|
|
{ ...mockOAuthConnection, id: 'id-3', provider: OAuthProvider.GITHUB },
|
|
{ ...mockOAuthConnection, id: 'id-4', provider: OAuthProvider.APPLE },
|
|
];
|
|
oauthService.getConnections.mockResolvedValue(allProviderConnections);
|
|
|
|
const result = await controller.getConnections(mockRequestUser);
|
|
|
|
expect(result).toHaveLength(4);
|
|
expect(result.map((c) => c.provider)).toContain(OAuthProvider.GOOGLE);
|
|
expect(result.map((c) => c.provider)).toContain(OAuthProvider.MICROSOFT);
|
|
expect(result.map((c) => c.provider)).toContain(OAuthProvider.GITHUB);
|
|
expect(result.map((c) => c.provider)).toContain(OAuthProvider.APPLE);
|
|
});
|
|
});
|
|
|
|
describe('error cases', () => {
|
|
it('should propagate service errors', async () => {
|
|
oauthService.getConnections.mockRejectedValue(
|
|
new Error('Database error'),
|
|
);
|
|
|
|
await expect(controller.getConnections(mockRequestUser)).rejects.toThrow(
|
|
'Database error',
|
|
);
|
|
});
|
|
});
|
|
});
|
|
|
|
// ==================== disconnectProvider Tests ====================
|
|
|
|
describe('disconnectProvider', () => {
|
|
describe('success cases', () => {
|
|
it('should disconnect Google provider successfully', async () => {
|
|
const successMessage = {
|
|
message: 'Proveedor google desconectado correctamente',
|
|
};
|
|
oauthService.disconnectProvider.mockResolvedValue(successMessage);
|
|
|
|
const result = await controller.disconnectProvider(
|
|
'google',
|
|
mockRequestUser,
|
|
);
|
|
|
|
expect(result).toEqual(successMessage);
|
|
expect(oauthService.disconnectProvider).toHaveBeenCalledWith(
|
|
mockRequestUser.id,
|
|
mockRequestUser.tenant_id,
|
|
OAuthProvider.GOOGLE,
|
|
);
|
|
});
|
|
|
|
it('should disconnect Microsoft provider successfully', async () => {
|
|
const successMessage = {
|
|
message: 'Proveedor microsoft desconectado correctamente',
|
|
};
|
|
oauthService.disconnectProvider.mockResolvedValue(successMessage);
|
|
|
|
const result = await controller.disconnectProvider(
|
|
'microsoft',
|
|
mockRequestUser,
|
|
);
|
|
|
|
expect(result).toEqual(successMessage);
|
|
expect(oauthService.disconnectProvider).toHaveBeenCalledWith(
|
|
mockRequestUser.id,
|
|
mockRequestUser.tenant_id,
|
|
OAuthProvider.MICROSOFT,
|
|
);
|
|
});
|
|
|
|
it('should disconnect GitHub provider successfully', async () => {
|
|
const successMessage = {
|
|
message: 'Proveedor github desconectado correctamente',
|
|
};
|
|
oauthService.disconnectProvider.mockResolvedValue(successMessage);
|
|
|
|
const result = await controller.disconnectProvider(
|
|
'github',
|
|
mockRequestUser,
|
|
);
|
|
|
|
expect(result).toEqual(successMessage);
|
|
expect(oauthService.disconnectProvider).toHaveBeenCalledWith(
|
|
mockRequestUser.id,
|
|
mockRequestUser.tenant_id,
|
|
OAuthProvider.GITHUB,
|
|
);
|
|
});
|
|
|
|
it('should disconnect Apple provider successfully', async () => {
|
|
const successMessage = {
|
|
message: 'Proveedor apple desconectado correctamente',
|
|
};
|
|
oauthService.disconnectProvider.mockResolvedValue(successMessage);
|
|
|
|
const result = await controller.disconnectProvider(
|
|
'apple',
|
|
mockRequestUser,
|
|
);
|
|
|
|
expect(result).toEqual(successMessage);
|
|
expect(oauthService.disconnectProvider).toHaveBeenCalledWith(
|
|
mockRequestUser.id,
|
|
mockRequestUser.tenant_id,
|
|
OAuthProvider.APPLE,
|
|
);
|
|
});
|
|
|
|
it('should handle uppercase provider names when disconnecting', async () => {
|
|
const successMessage = { message: 'Provider disconnected' };
|
|
oauthService.disconnectProvider.mockResolvedValue(successMessage);
|
|
|
|
await controller.disconnectProvider('GOOGLE', mockRequestUser);
|
|
|
|
expect(oauthService.disconnectProvider).toHaveBeenCalledWith(
|
|
mockRequestUser.id,
|
|
mockRequestUser.tenant_id,
|
|
OAuthProvider.GOOGLE,
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('error cases', () => {
|
|
it('should throw BadRequestException for invalid provider', async () => {
|
|
await expect(
|
|
controller.disconnectProvider('facebook', mockRequestUser),
|
|
).rejects.toThrow(BadRequestException);
|
|
});
|
|
|
|
it('should propagate NotFoundException when connection not found', async () => {
|
|
const notFoundError = new Error('No existe conexion con google');
|
|
oauthService.disconnectProvider.mockRejectedValue(notFoundError);
|
|
|
|
await expect(
|
|
controller.disconnectProvider('google', mockRequestUser),
|
|
).rejects.toThrow('No existe conexion con google');
|
|
});
|
|
|
|
it('should propagate ConflictException when trying to disconnect only auth method', async () => {
|
|
const conflictError = new Error(
|
|
'No puedes desconectar el unico metodo de autenticacion',
|
|
);
|
|
oauthService.disconnectProvider.mockRejectedValue(conflictError);
|
|
|
|
await expect(
|
|
controller.disconnectProvider('google', mockRequestUser),
|
|
).rejects.toThrow('No puedes desconectar el unico metodo de autenticacion');
|
|
});
|
|
});
|
|
});
|
|
|
|
// ==================== linkProvider Tests ====================
|
|
|
|
describe('linkProvider', () => {
|
|
describe('success cases', () => {
|
|
it('should link Google provider to existing account', async () => {
|
|
oauthService.linkProvider.mockResolvedValue(mockOAuthConnection);
|
|
|
|
const body = {
|
|
profile: mockOAuthProfile,
|
|
tokens: mockOAuthTokens,
|
|
};
|
|
|
|
const result = await controller.linkProvider(
|
|
'google',
|
|
body,
|
|
mockRequestUser,
|
|
);
|
|
|
|
expect(result).toEqual(mockOAuthConnection);
|
|
expect(oauthService.linkProvider).toHaveBeenCalledWith(
|
|
mockRequestUser.id,
|
|
mockRequestUser.tenant_id,
|
|
OAuthProvider.GOOGLE,
|
|
mockOAuthProfile,
|
|
mockOAuthTokens,
|
|
);
|
|
});
|
|
|
|
it('should link Microsoft provider to existing account', async () => {
|
|
const microsoftConnection: OAuthConnectionResponse = {
|
|
...mockOAuthConnection,
|
|
provider: OAuthProvider.MICROSOFT,
|
|
provider_email: 'user@outlook.com',
|
|
};
|
|
oauthService.linkProvider.mockResolvedValue(microsoftConnection);
|
|
|
|
const body = {
|
|
profile: { ...mockOAuthProfile, email: 'user@outlook.com' },
|
|
tokens: mockOAuthTokens,
|
|
};
|
|
|
|
const result = await controller.linkProvider(
|
|
'microsoft',
|
|
body,
|
|
mockRequestUser,
|
|
);
|
|
|
|
expect(result.provider).toBe(OAuthProvider.MICROSOFT);
|
|
expect(oauthService.linkProvider).toHaveBeenCalledWith(
|
|
mockRequestUser.id,
|
|
mockRequestUser.tenant_id,
|
|
OAuthProvider.MICROSOFT,
|
|
expect.objectContaining({ email: 'user@outlook.com' }),
|
|
mockOAuthTokens,
|
|
);
|
|
});
|
|
|
|
it('should link GitHub provider to existing account', async () => {
|
|
const githubConnection: OAuthConnectionResponse = {
|
|
...mockOAuthConnection,
|
|
provider: OAuthProvider.GITHUB,
|
|
provider_email: 'dev@github.com',
|
|
};
|
|
oauthService.linkProvider.mockResolvedValue(githubConnection);
|
|
|
|
const body = {
|
|
profile: { ...mockOAuthProfile, email: 'dev@github.com' },
|
|
tokens: mockOAuthTokens,
|
|
};
|
|
|
|
const result = await controller.linkProvider(
|
|
'github',
|
|
body,
|
|
mockRequestUser,
|
|
);
|
|
|
|
expect(result.provider).toBe(OAuthProvider.GITHUB);
|
|
});
|
|
|
|
it('should link Apple provider to existing account', async () => {
|
|
const appleConnection: OAuthConnectionResponse = {
|
|
...mockOAuthConnection,
|
|
provider: OAuthProvider.APPLE,
|
|
provider_email: 'user@privaterelay.appleid.com',
|
|
};
|
|
oauthService.linkProvider.mockResolvedValue(appleConnection);
|
|
|
|
const body = {
|
|
profile: {
|
|
...mockOAuthProfile,
|
|
email: 'user@privaterelay.appleid.com',
|
|
},
|
|
tokens: mockOAuthTokens,
|
|
};
|
|
|
|
const result = await controller.linkProvider(
|
|
'apple',
|
|
body,
|
|
mockRequestUser,
|
|
);
|
|
|
|
expect(result.provider).toBe(OAuthProvider.APPLE);
|
|
});
|
|
|
|
it('should handle link with minimal profile data', async () => {
|
|
oauthService.linkProvider.mockResolvedValue(mockOAuthConnection);
|
|
|
|
const minimalProfile: OAuthProfile = {
|
|
id: 'min-id',
|
|
email: 'minimal@test.com',
|
|
};
|
|
|
|
const body = {
|
|
profile: minimalProfile,
|
|
tokens: mockOAuthTokens,
|
|
};
|
|
|
|
const result = await controller.linkProvider(
|
|
'google',
|
|
body,
|
|
mockRequestUser,
|
|
);
|
|
|
|
expect(result).toEqual(mockOAuthConnection);
|
|
expect(oauthService.linkProvider).toHaveBeenCalledWith(
|
|
mockRequestUser.id,
|
|
mockRequestUser.tenant_id,
|
|
OAuthProvider.GOOGLE,
|
|
minimalProfile,
|
|
mockOAuthTokens,
|
|
);
|
|
});
|
|
|
|
it('should handle uppercase provider names when linking', async () => {
|
|
oauthService.linkProvider.mockResolvedValue(mockOAuthConnection);
|
|
|
|
const body = {
|
|
profile: mockOAuthProfile,
|
|
tokens: mockOAuthTokens,
|
|
};
|
|
|
|
await controller.linkProvider('GOOGLE', body, mockRequestUser);
|
|
|
|
expect(oauthService.linkProvider).toHaveBeenCalledWith(
|
|
mockRequestUser.id,
|
|
mockRequestUser.tenant_id,
|
|
OAuthProvider.GOOGLE,
|
|
mockOAuthProfile,
|
|
mockOAuthTokens,
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('error cases', () => {
|
|
it('should throw BadRequestException when profile is missing', async () => {
|
|
const body = {
|
|
tokens: mockOAuthTokens,
|
|
} as any;
|
|
|
|
await expect(
|
|
controller.linkProvider('google', body, mockRequestUser),
|
|
).rejects.toThrow(BadRequestException);
|
|
await expect(
|
|
controller.linkProvider('google', body, mockRequestUser),
|
|
).rejects.toThrow('Se requiere profile y tokens del proveedor OAuth');
|
|
});
|
|
|
|
it('should throw BadRequestException when tokens are missing', async () => {
|
|
const body = {
|
|
profile: mockOAuthProfile,
|
|
} as any;
|
|
|
|
await expect(
|
|
controller.linkProvider('google', body, mockRequestUser),
|
|
).rejects.toThrow(BadRequestException);
|
|
});
|
|
|
|
it('should throw BadRequestException when both profile and tokens are missing', async () => {
|
|
const body = {} as any;
|
|
|
|
await expect(
|
|
controller.linkProvider('google', body, mockRequestUser),
|
|
).rejects.toThrow(BadRequestException);
|
|
});
|
|
|
|
it('should throw BadRequestException for invalid provider', async () => {
|
|
const body = {
|
|
profile: mockOAuthProfile,
|
|
tokens: mockOAuthTokens,
|
|
};
|
|
|
|
await expect(
|
|
controller.linkProvider('facebook', body, mockRequestUser),
|
|
).rejects.toThrow(BadRequestException);
|
|
});
|
|
|
|
it('should propagate ConflictException when provider already linked to same user', async () => {
|
|
const conflictError = new Error('Ya tienes vinculado google');
|
|
oauthService.linkProvider.mockRejectedValue(conflictError);
|
|
|
|
const body = {
|
|
profile: mockOAuthProfile,
|
|
tokens: mockOAuthTokens,
|
|
};
|
|
|
|
await expect(
|
|
controller.linkProvider('google', body, mockRequestUser),
|
|
).rejects.toThrow('Ya tienes vinculado google');
|
|
});
|
|
|
|
it('should propagate ConflictException when provider account linked to another user', async () => {
|
|
const conflictError = new Error(
|
|
'Esta cuenta de google ya esta vinculada a otro usuario',
|
|
);
|
|
oauthService.linkProvider.mockRejectedValue(conflictError);
|
|
|
|
const body = {
|
|
profile: mockOAuthProfile,
|
|
tokens: mockOAuthTokens,
|
|
};
|
|
|
|
await expect(
|
|
controller.linkProvider('google', body, mockRequestUser),
|
|
).rejects.toThrow('Esta cuenta de google ya esta vinculada a otro usuario');
|
|
});
|
|
});
|
|
});
|
|
|
|
// ==================== validateProvider Private Method Tests ====================
|
|
|
|
describe('validateProvider (through public methods)', () => {
|
|
it('should accept all valid providers', () => {
|
|
oauthService.getAuthorizationUrl.mockReturnValue(mockOAuthUrlResponse);
|
|
|
|
// Test all valid providers through getAuthorizationUrl
|
|
const providers = ['google', 'microsoft', 'github', 'apple'];
|
|
providers.forEach((provider) => {
|
|
expect(() =>
|
|
controller.getAuthorizationUrl(provider, mockTenantId),
|
|
).not.toThrow();
|
|
});
|
|
});
|
|
|
|
it('should reject invalid providers with descriptive error', () => {
|
|
const invalidProviders = [
|
|
'facebook',
|
|
'twitter',
|
|
'linkedin',
|
|
'unknown',
|
|
'',
|
|
' ',
|
|
'null',
|
|
'undefined',
|
|
];
|
|
|
|
invalidProviders.forEach((provider) => {
|
|
expect(() =>
|
|
controller.getAuthorizationUrl(provider, mockTenantId),
|
|
).toThrow(BadRequestException);
|
|
});
|
|
});
|
|
|
|
it('should be case-insensitive for provider validation', () => {
|
|
oauthService.getAuthorizationUrl.mockReturnValue(mockOAuthUrlResponse);
|
|
|
|
const casedProviders = [
|
|
'GOOGLE',
|
|
'Google',
|
|
'gOoGlE',
|
|
'MICROSOFT',
|
|
'Microsoft',
|
|
'GITHUB',
|
|
'GitHub',
|
|
'gitHub',
|
|
'APPLE',
|
|
'Apple',
|
|
];
|
|
|
|
casedProviders.forEach((provider) => {
|
|
expect(() =>
|
|
controller.getAuthorizationUrl(provider, mockTenantId),
|
|
).not.toThrow();
|
|
});
|
|
});
|
|
});
|
|
|
|
// ==================== Integration/Flow Tests ====================
|
|
|
|
describe('OAuth Flow Integration', () => {
|
|
it('should complete full OAuth login flow for new user', async () => {
|
|
// Step 1: Get authorization URL
|
|
oauthService.getAuthorizationUrl.mockReturnValue(mockOAuthUrlResponse);
|
|
const urlResult = controller.getAuthorizationUrl('google', mockTenantId);
|
|
expect(urlResult.url).toContain('accounts.google.com');
|
|
|
|
// Step 2: Handle callback (simulating new user)
|
|
const newUserAuthResponse = {
|
|
user: { ...mockUser, id: 'new-user-uuid' },
|
|
accessToken: 'new_user_access_token',
|
|
refreshToken: 'new_user_refresh_token',
|
|
};
|
|
oauthService.handleOAuthLogin.mockResolvedValue(newUserAuthResponse);
|
|
|
|
const callbackResult = await controller.handleCallback(
|
|
'google',
|
|
{
|
|
code: 'auth_code',
|
|
profile: mockOAuthProfile,
|
|
tokens: mockOAuthTokens,
|
|
},
|
|
mockTenantId,
|
|
mockRequest as any,
|
|
);
|
|
|
|
expect(callbackResult).toHaveProperty('user');
|
|
expect(callbackResult).toHaveProperty('accessToken');
|
|
expect(callbackResult).toHaveProperty('refreshToken');
|
|
});
|
|
|
|
it('should complete full OAuth login flow for existing user', async () => {
|
|
// Step 1: Get authorization URL
|
|
oauthService.getAuthorizationUrl.mockReturnValue(mockOAuthUrlResponse);
|
|
const urlResult = controller.getAuthorizationUrl('google', mockTenantId);
|
|
expect(urlResult).toBeDefined();
|
|
|
|
// Step 2: Handle callback (simulating existing user)
|
|
oauthService.handleOAuthLogin.mockResolvedValue(mockAuthResponse);
|
|
|
|
const callbackResult = await controller.handleCallback(
|
|
'google',
|
|
{
|
|
code: 'auth_code',
|
|
profile: mockOAuthProfile,
|
|
tokens: mockOAuthTokens,
|
|
},
|
|
mockTenantId,
|
|
mockRequest as any,
|
|
);
|
|
|
|
expect(callbackResult.user.id).toBe(mockUserId);
|
|
});
|
|
|
|
it('should allow linking multiple providers to same account', async () => {
|
|
const googleConnection = { ...mockOAuthConnection, provider: OAuthProvider.GOOGLE };
|
|
const githubConnection = { ...mockOAuthConnection, id: 'github-conn-id', provider: OAuthProvider.GITHUB };
|
|
|
|
oauthService.linkProvider
|
|
.mockResolvedValueOnce(googleConnection)
|
|
.mockResolvedValueOnce(githubConnection);
|
|
|
|
// Link Google
|
|
const googleResult = await controller.linkProvider(
|
|
'google',
|
|
{ profile: mockOAuthProfile, tokens: mockOAuthTokens },
|
|
mockRequestUser,
|
|
);
|
|
expect(googleResult.provider).toBe(OAuthProvider.GOOGLE);
|
|
|
|
// Link GitHub
|
|
const githubResult = await controller.linkProvider(
|
|
'github',
|
|
{ profile: { ...mockOAuthProfile, id: 'github-id' }, tokens: mockOAuthTokens },
|
|
mockRequestUser,
|
|
);
|
|
expect(githubResult.provider).toBe(OAuthProvider.GITHUB);
|
|
});
|
|
|
|
it('should show all connections after linking multiple providers', async () => {
|
|
const multipleConnections: OAuthConnectionResponse[] = [
|
|
{ ...mockOAuthConnection, provider: OAuthProvider.GOOGLE },
|
|
{ ...mockOAuthConnection, id: 'ms-conn-id', provider: OAuthProvider.MICROSOFT },
|
|
{ ...mockOAuthConnection, id: 'gh-conn-id', provider: OAuthProvider.GITHUB },
|
|
];
|
|
|
|
oauthService.getConnections.mockResolvedValue(multipleConnections);
|
|
|
|
const connections = await controller.getConnections(mockRequestUser);
|
|
|
|
expect(connections).toHaveLength(3);
|
|
expect(connections.map((c) => c.provider)).toEqual([
|
|
OAuthProvider.GOOGLE,
|
|
OAuthProvider.MICROSOFT,
|
|
OAuthProvider.GITHUB,
|
|
]);
|
|
});
|
|
});
|
|
|
|
// ==================== Session Management Tests ====================
|
|
|
|
describe('Session Management after OAuth', () => {
|
|
it('should return valid JWT tokens after successful OAuth login', async () => {
|
|
oauthService.handleOAuthLogin.mockResolvedValue({
|
|
user: mockUser,
|
|
accessToken: 'valid_jwt_access_token',
|
|
refreshToken: 'valid_jwt_refresh_token',
|
|
});
|
|
|
|
const result = await controller.handleCallback(
|
|
'google',
|
|
{
|
|
code: 'auth_code',
|
|
profile: mockOAuthProfile,
|
|
tokens: mockOAuthTokens,
|
|
},
|
|
mockTenantId,
|
|
mockRequest as any,
|
|
);
|
|
|
|
expect(result.accessToken).toBe('valid_jwt_access_token');
|
|
expect(result.refreshToken).toBe('valid_jwt_refresh_token');
|
|
expect(result.accessToken).not.toBe(mockOAuthTokens.access_token);
|
|
});
|
|
|
|
it('should include user agent in OAuth login request', async () => {
|
|
oauthService.handleOAuthLogin.mockResolvedValue(mockAuthResponse);
|
|
|
|
await controller.handleCallback(
|
|
'google',
|
|
{
|
|
code: 'auth_code',
|
|
profile: mockOAuthProfile,
|
|
tokens: mockOAuthTokens,
|
|
},
|
|
mockTenantId,
|
|
mockRequest as any,
|
|
);
|
|
|
|
expect(oauthService.handleOAuthLogin).toHaveBeenCalledWith(
|
|
expect.any(String),
|
|
expect.any(Object),
|
|
expect.any(Object),
|
|
mockTenantId,
|
|
expect.objectContaining({
|
|
headers: expect.objectContaining({
|
|
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)',
|
|
}),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('should include IP address in OAuth login request', async () => {
|
|
oauthService.handleOAuthLogin.mockResolvedValue(mockAuthResponse);
|
|
|
|
await controller.handleCallback(
|
|
'google',
|
|
{
|
|
code: 'auth_code',
|
|
profile: mockOAuthProfile,
|
|
tokens: mockOAuthTokens,
|
|
},
|
|
mockTenantId,
|
|
mockRequest as any,
|
|
);
|
|
|
|
expect(oauthService.handleOAuthLogin).toHaveBeenCalledWith(
|
|
expect.any(String),
|
|
expect.any(Object),
|
|
expect.any(Object),
|
|
mockTenantId,
|
|
expect.objectContaining({
|
|
ip: '127.0.0.1',
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
|
|
// ==================== Edge Cases ====================
|
|
|
|
describe('Edge Cases', () => {
|
|
it('should handle OAuth profile with special characters in name', async () => {
|
|
oauthService.handleOAuthLogin.mockResolvedValue(mockAuthResponse);
|
|
|
|
const profileWithSpecialChars: OAuthProfile = {
|
|
id: 'special-id',
|
|
email: 'test@example.com',
|
|
name: "O'Connor, Jose Maria",
|
|
avatar_url: 'https://example.com/avatar.jpg',
|
|
};
|
|
|
|
await expect(
|
|
controller.handleCallback(
|
|
'google',
|
|
{
|
|
code: 'auth_code',
|
|
profile: profileWithSpecialChars,
|
|
tokens: mockOAuthTokens,
|
|
},
|
|
mockTenantId,
|
|
mockRequest as any,
|
|
),
|
|
).resolves.not.toThrow();
|
|
});
|
|
|
|
it('should handle OAuth profile with unicode characters', async () => {
|
|
oauthService.handleOAuthLogin.mockResolvedValue(mockAuthResponse);
|
|
|
|
const profileWithUnicode: OAuthProfile = {
|
|
id: 'unicode-id',
|
|
email: 'test@example.com',
|
|
name: 'Juan Carlos',
|
|
};
|
|
|
|
await expect(
|
|
controller.handleCallback(
|
|
'google',
|
|
{
|
|
code: 'auth_code',
|
|
profile: profileWithUnicode,
|
|
tokens: mockOAuthTokens,
|
|
},
|
|
mockTenantId,
|
|
mockRequest as any,
|
|
),
|
|
).resolves.not.toThrow();
|
|
});
|
|
|
|
it('should handle very long OAuth token strings', async () => {
|
|
oauthService.handleOAuthLogin.mockResolvedValue(mockAuthResponse);
|
|
|
|
const longTokens: OAuthTokens = {
|
|
access_token: 'a'.repeat(10000),
|
|
refresh_token: 'r'.repeat(5000),
|
|
};
|
|
|
|
await expect(
|
|
controller.handleCallback(
|
|
'google',
|
|
{
|
|
code: 'auth_code',
|
|
profile: mockOAuthProfile,
|
|
tokens: longTokens,
|
|
},
|
|
mockTenantId,
|
|
mockRequest as any,
|
|
),
|
|
).resolves.not.toThrow();
|
|
});
|
|
|
|
it('should handle empty state in callback body', async () => {
|
|
oauthService.handleOAuthLogin.mockResolvedValue(mockAuthResponse);
|
|
|
|
const result = await controller.handleCallback(
|
|
'google',
|
|
{
|
|
code: 'auth_code',
|
|
state: '',
|
|
profile: mockOAuthProfile,
|
|
tokens: mockOAuthTokens,
|
|
},
|
|
mockTenantId,
|
|
mockRequest as any,
|
|
);
|
|
|
|
expect(result).toEqual(mockAuthResponse);
|
|
});
|
|
|
|
it('should handle null values in OAuth profile optional fields', async () => {
|
|
oauthService.handleOAuthLogin.mockResolvedValue(mockAuthResponse);
|
|
|
|
const profileWithNulls: OAuthProfile = {
|
|
id: 'null-fields-id',
|
|
email: 'test@example.com',
|
|
name: undefined,
|
|
avatar_url: undefined,
|
|
raw_data: undefined,
|
|
};
|
|
|
|
await expect(
|
|
controller.handleCallback(
|
|
'google',
|
|
{
|
|
code: 'auth_code',
|
|
profile: profileWithNulls,
|
|
tokens: mockOAuthTokens,
|
|
},
|
|
mockTenantId,
|
|
mockRequest as any,
|
|
),
|
|
).resolves.not.toThrow();
|
|
});
|
|
|
|
it('should handle request without user-agent header', async () => {
|
|
oauthService.handleOAuthLogin.mockResolvedValue(mockAuthResponse);
|
|
|
|
const requestWithoutUserAgent = {
|
|
ip: '127.0.0.1',
|
|
headers: {
|
|
'x-tenant-id': mockTenantId,
|
|
},
|
|
};
|
|
|
|
await expect(
|
|
controller.handleCallback(
|
|
'google',
|
|
{
|
|
code: 'auth_code',
|
|
profile: mockOAuthProfile,
|
|
tokens: mockOAuthTokens,
|
|
},
|
|
mockTenantId,
|
|
requestWithoutUserAgent as any,
|
|
),
|
|
).resolves.not.toThrow();
|
|
});
|
|
|
|
it('should handle request without IP address', async () => {
|
|
oauthService.handleOAuthLogin.mockResolvedValue(mockAuthResponse);
|
|
|
|
const requestWithoutIp = {
|
|
headers: {
|
|
'user-agent': 'Mozilla/5.0',
|
|
'x-tenant-id': mockTenantId,
|
|
},
|
|
};
|
|
|
|
await expect(
|
|
controller.handleCallback(
|
|
'google',
|
|
{
|
|
code: 'auth_code',
|
|
profile: mockOAuthProfile,
|
|
tokens: mockOAuthTokens,
|
|
},
|
|
mockTenantId,
|
|
requestWithoutIp as any,
|
|
),
|
|
).resolves.not.toThrow();
|
|
});
|
|
});
|
|
});
|