teqo-backup/tests/int/googleCalendarOAuth.int.spec.ts

166 lines
6 KiB
TypeScript

// @vitest-environment node
//
// C149 — the OAuth connection stored on the singleton `googleCalendarSync`
// row: recording, error state, disconnect, view/loader derivation. The int
// suite runs files in parallel against ONE shared database, so this file only
// creates CONNECTION-ONLY rows (no `calendarId`): the singleton loader prefers
// a configured row, which means these rows can never shadow the engine specs'
// configured row. Rows this file created are tracked and deleted; a token that
// landed on a shared row (the loader's preference) is cleared, never deleted.
import { getPayload, type Payload } from 'payload'
import { afterEach, beforeAll, describe, expect, it } from 'vitest'
import config from '@/payload.config'
import {
GOOGLE_CALENDAR_OAUTH_CLIENT_ID_ENV,
GOOGLE_CALENDAR_OAUTH_CLIENT_SECRET_ENV,
} from '@/utilities/googleCalendarOAuth'
import {
clearGoogleCalendarOAuthConnection,
loadGoogleCalendarSyncConfig,
readGoogleCalendarSyncView,
recordGoogleCalendarOAuthConnection,
recordGoogleCalendarOAuthError,
} from '@/utilities/googleCalendarSync'
import { serializeGoogleCalendarSyncSpec } from '../helpers/googleCalendarSyncLock'
serializeGoogleCalendarSyncSpec()
let payload: Payload
/** Sets/restores the OAuth client pair around a callback — restored on failure too. */
const withGoogleCalendarOAuthClient = async <T>(run: () => Promise<T>): Promise<T> => {
const previous = {
id: process.env[GOOGLE_CALENDAR_OAUTH_CLIENT_ID_ENV],
secret: process.env[GOOGLE_CALENDAR_OAUTH_CLIENT_SECRET_ENV],
}
process.env[GOOGLE_CALENDAR_OAUTH_CLIENT_ID_ENV] = 'test-client-id'
process.env[GOOGLE_CALENDAR_OAUTH_CLIENT_SECRET_ENV] = 'test-client-secret'
try {
return await run()
} finally {
if (previous.id === undefined) delete process.env[GOOGLE_CALENDAR_OAUTH_CLIENT_ID_ENV]
else process.env[GOOGLE_CALENDAR_OAUTH_CLIENT_ID_ENV] = previous.id
if (previous.secret === undefined) delete process.env[GOOGLE_CALENDAR_OAUTH_CLIENT_SECRET_ENV]
else process.env[GOOGLE_CALENDAR_OAUTH_CLIENT_SECRET_ENV] = previous.secret
}
}
describe('C149 — OAuth connection on the googleCalendarSync row', () => {
beforeAll(async () => {
payload = await getPayload({ config: await config })
})
const ownedIds = new Set<number>()
afterEach(async () => {
// Clear connection fields this file wrote wherever they landed — the
// loader may have picked a row created by a sibling spec, and that row
// must not be deleted from under it.
const touched = await payload.find({
collection: 'googleCalendarSync',
where: { oauthRefreshToken: { like: 'c149-conn%' } },
depth: 0,
pagination: false,
overrideAccess: true,
})
for (const doc of touched.docs) {
if (ownedIds.has(doc.id)) continue
await payload.update({
collection: 'googleCalendarSync',
id: doc.id,
data: {
oauthRefreshToken: null,
oauthScope: null,
oauthConnectedAt: null,
oauthErrorAt: null,
oauthError: null,
},
depth: 0,
overrideAccess: true,
})
}
for (const id of ownedIds) {
await payload.delete({ collection: 'googleCalendarSync', id, overrideAccess: true })
}
ownedIds.clear()
})
const createConnectionRow = async (data: Record<string, unknown> = {}) => {
const doc = await payload.create({
collection: 'googleCalendarSync',
data,
depth: 0,
overrideAccess: true,
})
ownedIds.add(doc.id)
return doc
}
it('records the connection (create when no row exists) and clears previous errors', async () => {
await createConnectionRow({ oauthErrorAt: '2026-08-11T10:00:00.000Z', oauthError: 'antigo' })
await recordGoogleCalendarOAuthConnection(payload, {
refreshToken: 'c149-conn-create',
scope: 'scope-a scope-b',
})
const doc = await loadGoogleCalendarSyncConfig(payload)
expect(doc?.oauthRefreshToken).toBe('c149-conn-create')
expect(doc?.oauthScope).toBe('scope-a scope-b')
expect(doc?.oauthConnectedAt).toBeTruthy()
expect(doc?.oauthErrorAt).toBeNull()
expect(doc?.oauthError).toBeNull()
})
it('records the auth error and clears the whole connection on disconnect', async () => {
await createConnectionRow({ oauthRefreshToken: 'c149-conn-disconnect' })
await recordGoogleCalendarOAuthError(payload, 'A conexão com o Google expirou.')
let doc = await loadGoogleCalendarSyncConfig(payload)
expect(doc?.oauthError).toBe('A conexão com o Google expirou.')
expect(doc?.oauthErrorAt).toBeTruthy()
await clearGoogleCalendarOAuthConnection(payload)
doc = await loadGoogleCalendarSyncConfig(payload)
expect(doc?.oauthRefreshToken).toBeNull()
expect(doc?.oauthScope).toBeNull()
expect(doc?.oauthConnectedAt).toBeNull()
expect(doc?.oauthErrorAt).toBeNull()
expect(doc?.oauthError).toBeNull()
})
it('derives the connection view (connected → error) and oauthAvailable from the env', async () => {
await createConnectionRow({ oauthRefreshToken: 'c149-conn-view' })
await withGoogleCalendarOAuthClient(async () => {
const connected = await readGoogleCalendarSyncView(payload)
expect(connected.connection).toBe('connected')
expect(connected.oauthAvailable).toBe(true)
await recordGoogleCalendarOAuthError(payload, 'Token revogado.')
const broken = await readGoogleCalendarSyncView(payload)
expect(broken.connection).toBe('error')
expect(broken.oauthError).toBe('Token revogado.')
})
})
it('finds a connection-only row even without a calendarId (fresh setup)', async () => {
const row = await createConnectionRow({ oauthRefreshToken: 'c149-conn-loader' })
const loaded = await loadGoogleCalendarSyncConfig(payload)
// A configured sibling (parallel spec) legitimately wins the singleton
// preference; without one, the connection-only row is the row.
if (loaded?.calendarId) {
expect(loaded.id).not.toBe(row.id)
} else {
expect(loaded?.id).toBe(row.id)
}
expect(row.calendarId ?? null).toBeNull()
})
})