import {describe, it, expect, vi, beforeEach} from 'vitest';
import {Request, Response} from 'express';
import {handleCallback} from './routes.js';
import {getRedirectUrl, InvalidStateError} from './oauth-bridge.js';
import type {GmailOAuthServerProvider} from './gmail/oauth-provider.js';
vi.mock('./oauth-bridge.js', async (importOriginal) => {
const original = await importOriginal<typeof import('./oauth-bridge.js')>();
return {
...original,
getRedirectUrl: vi.fn(),
};
});
function createMockRequest(query: Record<string, string> = {}): Partial<Request> {
return {query};
}
function createMockResponse(): {
_status: number;
_body: string;
_redirectUrl: string;
status: (code: number) => unknown;
send: (body: string) => unknown;
redirect: (url: string) => unknown;
} {
const res = {
_status: 0,
_body: '',
_redirectUrl: '',
status(code: number) {
this._status = code;
return this;
},
send(body: string) {
this._body = body;
return this;
},
redirect(url: string) {
this._status = 302;
this._redirectUrl = url;
return this;
},
};
return res;
}
function createMockProvider(): GmailOAuthServerProvider {
return {} as GmailOAuthServerProvider;
}
describe('handleCallback', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('returns 400 when code is missing', async () => {
const req = createMockRequest({state: 'test-state'});
const res = createMockResponse();
await handleCallback(req as Request, res as unknown as Response, createMockProvider());
expect(res._status).toBe(400);
expect(res._body).toContain('Authentication Failed');
expect(res._body).toContain('Missing authorization code or state parameter');
});
it('returns 400 when state is missing', async () => {
const req = createMockRequest({code: 'test-code'});
const res = createMockResponse();
await handleCallback(req as Request, res as unknown as Response, createMockProvider());
expect(res._status).toBe(400);
expect(res._body).toContain('Missing authorization code or state parameter');
});
it('returns 400 on InvalidStateError', async () => {
vi.mocked(getRedirectUrl).mockRejectedValue(new InvalidStateError());
const req = createMockRequest({code: 'test-code', state: 'invalid-state'});
const res = createMockResponse();
await handleCallback(req as Request, res as unknown as Response, createMockProvider());
expect(res._status).toBe(400);
expect(res._body).toContain('Invalid state parameter format');
});
it('returns 400 on generic error', async () => {
vi.mocked(getRedirectUrl).mockRejectedValue(new Error('Token exchange failed'));
const req = createMockRequest({code: 'test-code', state: 'test-state'});
const res = createMockResponse();
await handleCallback(req as Request, res as unknown as Response, createMockProvider());
expect(res._status).toBe(400);
expect(res._body).toContain('Failed to complete authentication');
});
it('redirects on success', async () => {
vi.mocked(getRedirectUrl).mockResolvedValue('https://client.example.com/callback?code=auth-code');
const req = createMockRequest({code: 'google-code', state: 'request-id'});
const res = createMockResponse();
await handleCallback(req as Request, res as unknown as Response, createMockProvider());
expect(res._status).toBe(302);
expect(res._redirectUrl).toBe('https://client.example.com/callback?code=auth-code');
});
});