import {describe, it, expect, vi, beforeEach} from 'vitest';
import {OAuthHandler} from './oauth-handler.js';
import type {OAuthCredentials, OAuthTokens} from '../oauth.js';
const {mockGetToken, mockGetProfile} = vi.hoisted(() => ({
mockGetToken: vi.fn(),
mockGetProfile: vi.fn(),
}));
vi.mock('googleapis', () => {
return {
google: {
auth: {
OAuth2: class {
setCredentials = vi.fn();
generateAuthUrl = vi.fn().mockReturnValue('https://accounts.google.com/o/oauth2/auth?...');
getToken = mockGetToken;
},
},
gmail: vi.fn().mockReturnValue({
users: {
getProfile: mockGetProfile,
},
}),
},
};
});
describe('OAuthHandler', () => {
const mockCredentials: OAuthCredentials = {
client_id: 'test-client-id',
client_secret: 'test-client-secret',
redirect_uri: 'http://localhost:3000/callback',
};
const mockTokens: OAuthTokens = {
access_token: 'test-access-token',
refresh_token: 'test-refresh-token',
scope: 'https://www.googleapis.com/auth/gmail.readonly',
token_type: 'Bearer',
expiry_date: Date.now() + 3600000,
};
let handler: OAuthHandler;
beforeEach(() => {
vi.clearAllMocks();
handler = new OAuthHandler(mockCredentials);
mockGetToken.mockResolvedValue({tokens: {access_token: 'new-token'}});
mockGetProfile.mockResolvedValue({data: {emailAddress: 'test@example.com'}});
});
describe('generateAuthUrl', () => {
it('generates OAuth URL with state parameter', () => {
const url = handler.generateAuthUrl('test-state');
expect(url).toContain('accounts.google.com');
});
});
describe('exchangeCodeForTokens', () => {
it('exchanges authorization code for tokens', async () => {
const result = await handler.exchangeCodeForTokens('auth-code');
expect(result.access_token).toBe('new-token');
expect(mockGetToken).toHaveBeenCalledWith('auth-code');
});
it('propagates error when getToken fails', async () => {
mockGetToken.mockRejectedValueOnce(new Error('Invalid authorization code'));
await expect(handler.exchangeCodeForTokens('bad-code')).rejects.toThrow('Invalid authorization code');
});
});
describe('getUserInfo', () => {
it('returns email from Gmail profile', async () => {
const result = await handler.getUserInfo(mockTokens);
expect(result.email).toBe('test@example.com');
});
it('throws error when emailAddress is null', async () => {
mockGetProfile.mockResolvedValueOnce({data: {emailAddress: null}});
await expect(handler.getUserInfo(mockTokens)).rejects.toThrow('Gmail profile missing email address');
});
it('throws error when emailAddress is undefined', async () => {
mockGetProfile.mockResolvedValueOnce({data: {}});
await expect(handler.getUserInfo(mockTokens)).rejects.toThrow('Gmail profile missing email address');
});
it('propagates error when getProfile fails', async () => {
mockGetProfile.mockRejectedValueOnce(new Error('Gmail API error'));
await expect(handler.getUserInfo(mockTokens)).rejects.toThrow('Gmail API error');
});
});
describe('createOAuth2Client', () => {
it('creates client without tokens', () => {
const client = handler.createOAuth2Client();
expect(client).toBeDefined();
});
it('creates client with tokens', () => {
const client = handler.createOAuth2Client(mockTokens);
expect(client).toBeDefined();
expect(client.setCredentials).toHaveBeenCalledWith(mockTokens);
});
});
});