// @vitest-environment node import { exportPKCS8, exportSPKI, generateKeyPair, importSPKI, jwtVerify } from 'jose' import { beforeAll, describe, expect, it } from 'vitest' import { buildServiceAccountAssertion, createGoogleCalendarClient, GOOGLE_CALENDAR_SCOPE, GoogleCalendarApiError, GoogleCalendarAuthError, type FetchLike, type GoogleCalendarAuth, type GoogleCalendarServiceAccountCredentials, } from '@/utilities/googleCalendarClient' describe('buildServiceAccountAssertion', () => { let credentials: GoogleCalendarServiceAccountCredentials let publicKeyPem: string beforeAll(async () => { const { publicKey, privateKey } = await generateKeyPair('RS256', { extractable: true }) credentials = { clientEmail: 'teqo-sa@projeto.iam.gserviceaccount.com', privateKey: await exportPKCS8(privateKey), } publicKeyPem = await exportSPKI(publicKey) }) it('signs an RS256 JWT with the service-account claims the token endpoint expects', async () => { const assertion = await buildServiceAccountAssertion(credentials, 1_800_000_000) const { payload } = await jwtVerify(assertion, await importSPKI(publicKeyPem, 'RS256')) expect(payload.iss).toBe(credentials.clientEmail) expect(payload.aud).toBe('https://oauth2.googleapis.com/token') expect(payload.scope).toBe(GOOGLE_CALENDAR_SCOPE) expect(payload.iat).toBe(1_800_000_000) expect((payload.exp as number) - (payload.iat as number)).toBe(3600) }) }) describe('createGoogleCalendarClient', () => { let credentials: GoogleCalendarServiceAccountCredentials let serviceAccountAuth: GoogleCalendarAuth beforeAll(async () => { // The token endpoint is stubbed, but the client still SIGNS the JWT // assertion before posting — a real key keeps the tests honest. const { privateKey } = await generateKeyPair('RS256', { extractable: true }) credentials = { clientEmail: 'teqo-sa@projeto.iam.gserviceaccount.com', privateKey: await exportPKCS8(privateKey), } serviceAccountAuth = { kind: 'service-account', credentials } }) const oauthAuth: GoogleCalendarAuth = { kind: 'oauth', credentials: { clientId: 'client-id.apps.googleusercontent.com', clientSecret: 'client-secret', refreshToken: 'refresh-token', }, } const calendarId = 'c_abc@group.calendar.google.com' /** Minimal fake transport: token endpoint + calendar REST in memory. */ const stubTransport = (events: Array> = []) => { let tokenRequests = 0 const calls: Array<{ url: string; method: string; body?: string; auth?: string }> = [] const fetchImpl: FetchLike = (async (input: RequestInfo | URL, _init?: RequestInit) => { const url = String(input) const method = (_init?.method ?? 'GET').toUpperCase() const body = typeof _init?.body === 'string' ? _init.body : undefined const auth = (_init?.headers as Record | undefined)?.['Authorization'] calls.push({ url, method, body, auth }) if (url === 'https://oauth2.googleapis.com/token') { tokenRequests += 1 return new Response( JSON.stringify({ access_token: `token-${tokenRequests}`, expires_in: 3600 }), { status: 200, headers: { 'Content-Type': 'application/json' }, }, ) } if (url.includes('/events') && method === 'POST') { const event = JSON.parse(body ?? '{}') as Record events.push(event) return new Response(JSON.stringify(event), { status: 200, headers: { 'Content-Type': 'application/json' }, }) } if (url.includes('/events') && method === 'PUT') { const event = JSON.parse(body ?? '{}') as Record const index = events.findIndex((e) => e.id === event.id) if (index >= 0) events[index] = event return new Response(JSON.stringify(event), { status: 200, headers: { 'Content-Type': 'application/json' }, }) } if (url.includes('/events') && method === 'DELETE') { const eventId = url.split('/').pop() const index = events.findIndex((e) => e.id === eventId) if (index >= 0) events.splice(index, 1) return new Response(null, { status: 204 }) } if (url.includes('/events')) { return new Response(JSON.stringify({ items: events }), { status: 200, headers: { 'Content-Type': 'application/json' }, }) } return new Response('not found', { status: 404 }) }) as FetchLike return { fetchImpl, calls, events, getTokenRequests: () => tokenRequests } } it('inserts with the deterministic id, bearer auth and JSON body', async () => { const { fetchImpl, calls, events } = stubTransport() const client = createGoogleCalendarClient(serviceAccountAuth, fetchImpl) await client.insertEvent(calendarId, { id: 'teqo1a', summary: 'S', description: 'D' }) expect(events).toHaveLength(1) expect(events[0].id).toBe('teqo1a') const insertCall = calls.find((call) => call.method === 'POST' && call.url.includes('/events')) expect(insertCall?.url).toContain(`/calendars/${encodeURIComponent(calendarId)}/events`) expect(insertCall?.auth).toBe('Bearer token-1') }) it('lists with the window params and pagination', async () => { const { fetchImpl, calls } = stubTransport() const client = createGoogleCalendarClient(serviceAccountAuth, fetchImpl) await client.listEvents(calendarId, { timeMin: '2026-01-01T00:00:00Z', timeMax: '2026-12-31T00:00:00Z', }) const listCall = calls.find((call) => call.method === 'GET') expect(listCall?.url).toContain('timeMin=2026-01-01T00%3A00%3A00Z') expect(listCall?.url).toContain('timeMax=2026-12-31T00%3A00%3A00Z') expect(listCall?.url).toContain('maxResults=2500') }) it('re-mints the token once on a 401 and retries the call', async () => { let calendarCalls = 0 const fetchImpl: FetchLike = (async (input: RequestInfo | URL, _init?: RequestInit) => { const url = String(input) if (url === 'https://oauth2.googleapis.com/token') { return new Response(JSON.stringify({ access_token: 'fresh-token', expires_in: 3600 }), { status: 200, headers: { 'Content-Type': 'application/json' }, }) } calendarCalls += 1 if (calendarCalls === 1) { return new Response('unauthorized', { status: 401 }) } return new Response(JSON.stringify({ items: [] }), { status: 200, headers: { 'Content-Type': 'application/json' }, }) }) as FetchLike const client = createGoogleCalendarClient(serviceAccountAuth, fetchImpl) const events = await client.listEvents(calendarId, { timeMin: '2026-01-01T00:00:00Z', timeMax: '2026-12-31T00:00:00Z', }) expect(events).toEqual([]) expect(calendarCalls).toBe(2) }) it('throws a typed error on a non-401 failure without leaking the body', async () => { const fetchImpl: FetchLike = (async (input: RequestInfo | URL) => { const url = String(input) if (url === 'https://oauth2.googleapis.com/token') { return new Response(JSON.stringify({ error: 'invalid_grant' }), { status: 403 }) } return new Response('ok', { status: 200 }) }) as FetchLike const client = createGoogleCalendarClient(serviceAccountAuth, fetchImpl) await expect( client.listEvents(calendarId, { timeMin: 'a', timeMax: 'b' }), ).rejects.toBeInstanceOf(GoogleCalendarApiError) }) it('mints an access token with the OAuth refresh grant (C149)', async () => { const { fetchImpl, calls } = stubTransport() const client = createGoogleCalendarClient(oauthAuth, fetchImpl) await client.listEvents(calendarId, { timeMin: 'a', timeMax: 'b' }) const tokenCall = calls.find((call) => call.url === 'https://oauth2.googleapis.com/token') const params = new URLSearchParams(tokenCall?.body) expect(params.get('grant_type')).toBe('refresh_token') expect(params.get('refresh_token')).toBe('refresh-token') expect(params.get('client_id')).toBe('client-id.apps.googleusercontent.com') expect(params.get('client_secret')).toBe('client-secret') const listCall = calls.find((call) => call.method === 'GET') expect(listCall?.auth).toBe('Bearer token-1') }) it('maps invalid_grant to GoogleCalendarAuthError (reconnectable)', async () => { const fetchImpl: FetchLike = (async () => new Response(JSON.stringify({ error: 'invalid_grant' }), { status: 400, headers: { 'Content-Type': 'application/json' }, })) as FetchLike const client = createGoogleCalendarClient(oauthAuth, fetchImpl) await expect( client.listEvents(calendarId, { timeMin: 'a', timeMax: 'b' }), ).rejects.toBeInstanceOf(GoogleCalendarAuthError) }) it('keeps invalid_client as an API error (reconnect would not fix it)', async () => { const fetchImpl: FetchLike = (async () => new Response(JSON.stringify({ error: 'invalid_client' }), { status: 401, headers: { 'Content-Type': 'application/json' }, })) as FetchLike const client = createGoogleCalendarClient(oauthAuth, fetchImpl) await expect( client.listEvents(calendarId, { timeMin: 'a', timeMax: 'b' }), ).rejects.toBeInstanceOf(GoogleCalendarApiError) }) it('re-mints the OAuth token once on a 401 and retries the call', async () => { let calendarCalls = 0 let tokenRequests = 0 const fetchImpl: FetchLike = (async (input: RequestInfo | URL) => { const url = String(input) if (url === 'https://oauth2.googleapis.com/token') { tokenRequests += 1 return new Response( JSON.stringify({ access_token: `oauth-token-${tokenRequests}`, expires_in: 3600 }), { status: 200, headers: { 'Content-Type': 'application/json' } }, ) } calendarCalls += 1 if (calendarCalls === 1) return new Response('unauthorized', { status: 401 }) return new Response(JSON.stringify({ items: [] }), { status: 200, headers: { 'Content-Type': 'application/json' }, }) }) as FetchLike const client = createGoogleCalendarClient(oauthAuth, fetchImpl) const events = await client.listEvents(calendarId, { timeMin: 'a', timeMax: 'b' }) expect(events).toEqual([]) expect(calendarCalls).toBe(2) expect(tokenRequests).toBe(2) }) })