mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 06:43:06 +00:00
- runSyncPass bidirecional: regra do relógio decide direção (Google mais novo aplica título/horário; Teqo re-asserta), cancelamento via showDeleted + snapshot lastSeenEventIds, registro no updates feed - webhook público /campanha/agenda/google-webhook/[secret] fail-closed (secret + X-Goog headers constant-time), canal lazy sem cron (ensure em toda passada, renovação com id novo + stop do antigo) - migration hand-written (campos de canal/snapshot em googleCalendarSync); mutationKind dedicado libera título via Google sem quebrar slug canônico - UI: seção 'Edições pelo Google' no dialog + autor 'Google Calendar' no feed - testes: unit (reverse mapping, validação webhook), int (motor bidirecional com stub, 10 cenários), e2e (webhook 404 fail-closed)
153 lines
4.7 KiB
TypeScript
153 lines
4.7 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
import type { Activity, GoogleCalendarSync as GoogleCalendarSyncDoc } from '@/payload-types'
|
|
import type { PayloadRequest } from 'payload'
|
|
|
|
/**
|
|
* Hook → engine wiring (the guards themselves are unit-tested in
|
|
* `googleCalendarSync.unit.spec.ts`): the hooks must call the engine exactly
|
|
* when the guard says so, and NEVER throw into the write path. The hooks live
|
|
* in their own module (`googleCalendarSyncHooks.ts`), so mocking the engine
|
|
* at the import boundary reliably intercepts the wiring.
|
|
*/
|
|
vi.mock('@/utilities/googleCalendarSync', async (importOriginal) => {
|
|
const original = await importOriginal<typeof import('@/utilities/googleCalendarSync')>()
|
|
return { ...original, runCampaignCalendarSync: vi.fn() }
|
|
})
|
|
|
|
import { runCampaignCalendarSync } from '@/utilities/googleCalendarSync'
|
|
import {
|
|
activityGoogleCalendarSyncHook,
|
|
googleCalendarSyncConfigHook,
|
|
} from '@/utilities/googleCalendarSyncHooks'
|
|
|
|
const mockedRun = vi.mocked(runCampaignCalendarSync)
|
|
|
|
const fakeReq = { payload: {} } as unknown as PayloadRequest
|
|
const activity = (overrides: Record<string, unknown> = {}) =>
|
|
({ id: 1, title: 'Caminhada', status: 'confirmado', ...overrides }) as unknown as Activity
|
|
const config = (overrides: Record<string, unknown> = {}) =>
|
|
({
|
|
id: 1,
|
|
calendarId: 'c_a@group.calendar.google.com',
|
|
...overrides,
|
|
}) as unknown as GoogleCalendarSyncDoc
|
|
|
|
const settledOutcome = {
|
|
status: 'synced' as const,
|
|
created: 0,
|
|
updated: 0,
|
|
deleted: 0,
|
|
reverseEdits: 0,
|
|
at: '',
|
|
}
|
|
|
|
describe('activityGoogleCalendarSyncHook wiring', () => {
|
|
beforeEach(() => {
|
|
mockedRun.mockReset()
|
|
mockedRun.mockResolvedValue(settledOutcome)
|
|
})
|
|
|
|
it('calls the engine on create, delete and relevant updates; skips task-only updates', async () => {
|
|
await activityGoogleCalendarSyncHook({
|
|
doc: activity(),
|
|
operation: 'create',
|
|
req: fakeReq,
|
|
})
|
|
expect(mockedRun).toHaveBeenCalledTimes(1)
|
|
|
|
await activityGoogleCalendarSyncHook({
|
|
doc: activity({ tasks: [{ title: 'Novo' }] }),
|
|
previousDoc: activity({ tasks: [{ title: 'Antigo' }] }),
|
|
operation: 'update',
|
|
req: fakeReq,
|
|
})
|
|
expect(mockedRun).toHaveBeenCalledTimes(1)
|
|
|
|
await activityGoogleCalendarSyncHook({
|
|
doc: activity({ title: 'Caminhada (remarcada)' }),
|
|
previousDoc: activity(),
|
|
operation: 'update',
|
|
req: fakeReq,
|
|
})
|
|
expect(mockedRun).toHaveBeenCalledTimes(2)
|
|
|
|
// Delete: afterDelete hooks carry no `operation` — absence means delete.
|
|
await activityGoogleCalendarSyncHook({
|
|
doc: activity(),
|
|
req: fakeReq,
|
|
})
|
|
expect(mockedRun).toHaveBeenCalledTimes(3)
|
|
})
|
|
|
|
it('never throws into the activity write path', async () => {
|
|
mockedRun.mockRejectedValue(new Error('boom'))
|
|
await expect(
|
|
activityGoogleCalendarSyncHook({
|
|
doc: activity(),
|
|
operation: 'create',
|
|
req: fakeReq,
|
|
}),
|
|
).resolves.toBeDefined()
|
|
})
|
|
})
|
|
|
|
describe('googleCalendarSyncConfigHook wiring', () => {
|
|
beforeEach(() => {
|
|
mockedRun.mockReset()
|
|
mockedRun.mockResolvedValue(settledOutcome)
|
|
})
|
|
|
|
it('calls the engine on create, calendarId change and re-enable; skips state writes and disable', async () => {
|
|
await googleCalendarSyncConfigHook({
|
|
doc: config(),
|
|
operation: 'create',
|
|
req: fakeReq,
|
|
})
|
|
expect(mockedRun).toHaveBeenCalledTimes(1)
|
|
|
|
await googleCalendarSyncConfigHook({
|
|
doc: config({ lastSyncedAt: '2026-08-11T10:00:00.000Z' }),
|
|
previousDoc: config(),
|
|
operation: 'update',
|
|
req: fakeReq,
|
|
})
|
|
expect(mockedRun).toHaveBeenCalledTimes(1)
|
|
|
|
await googleCalendarSyncConfigHook({
|
|
doc: config({ disabledAt: '2026-08-11T10:00:00.000Z' }),
|
|
previousDoc: config(),
|
|
operation: 'update',
|
|
req: fakeReq,
|
|
})
|
|
expect(mockedRun).toHaveBeenCalledTimes(1)
|
|
|
|
await googleCalendarSyncConfigHook({
|
|
doc: config({ calendarId: 'c_b@group.calendar.google.com' }),
|
|
previousDoc: config(),
|
|
operation: 'update',
|
|
req: fakeReq,
|
|
})
|
|
expect(mockedRun).toHaveBeenCalledTimes(2)
|
|
|
|
await googleCalendarSyncConfigHook({
|
|
doc: config(),
|
|
previousDoc: config({ disabledAt: '2026-08-11T10:00:00.000Z' }),
|
|
operation: 'update',
|
|
req: fakeReq,
|
|
})
|
|
expect(mockedRun).toHaveBeenCalledTimes(3)
|
|
})
|
|
|
|
it('never throws into admin writes', async () => {
|
|
mockedRun.mockRejectedValue(new Error('boom'))
|
|
await expect(
|
|
googleCalendarSyncConfigHook({
|
|
doc: config({ calendarId: 'c_b@group.calendar.google.com' }),
|
|
previousDoc: config(),
|
|
operation: 'update',
|
|
req: fakeReq,
|
|
}),
|
|
).resolves.toBeDefined()
|
|
})
|
|
})
|