tests/bento-first-org-route.unit.test.ts
Rama main · réplica de lectura del repositorio original.
import { readFile } from 'node:fs/promises'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const {
pgQueryMock,
syncBentoFirstOrgOnRoleBindingWriteMock,
syncBentoFirstOrgOnUserCreateMock,
syncBentoSubscriberTagsMock,
} = vi.hoisted(() => ({
pgQueryMock: vi.fn(async (_sql: string, _params?: unknown[]) => ({ rows: [{ email: 'invitee@example.com' }] })),
syncBentoFirstOrgOnRoleBindingWriteMock: vi.fn(async () => undefined),
syncBentoFirstOrgOnUserCreateMock: vi.fn(async () => undefined),
syncBentoSubscriberTagsMock: vi.fn(async () => true),
}))
vi.mock('../supabase/functions/_backend/utils/bento_first_org.ts', () => ({
syncBentoFirstOrgOnRoleBindingWrite: syncBentoFirstOrgOnRoleBindingWriteMock,
syncBentoFirstOrgOnUserCreate: syncBentoFirstOrgOnUserCreateMock,
}))
vi.mock('../supabase/functions/_backend/utils/bento.ts', async () => {
const actual = await vi.importActual('../supabase/functions/_backend/utils/bento.ts')
return {
...actual,
syncBentoSubscriberTags: syncBentoSubscriberTagsMock,
}
})
vi.mock('../supabase/functions/_backend/utils/pg.ts', async () => {
const actual = await vi.importActual('../supabase/functions/_backend/utils/pg.ts')
return {
...actual,
getPgClient: vi.fn(async () => ({ query: pgQueryMock })),
closeClient: vi.fn(),
}
})
const apiWorker = (await import('../cloudflare_workers/api/index.ts')).default
const API_SECRET = 'test-secret'
const ORG_ID = '22222222-2222-4222-8222-222222222222'
const ROLE_BINDING_ID = '33333333-3333-4333-8333-333333333333'
const ROLE_ID = '44444444-4444-4444-8444-444444444444'
const USER_ID = '11111111-1111-4111-8111-111111111111'
const originalApiSecret = process.env.API_SECRET
const roleBindingRecord = {
app_id: null,
bundle_id: null,
channel_id: null,
expires_at: null,
granted_at: '2026-08-03T09:15:00.000Z',
granted_by: USER_ID,
id: ROLE_BINDING_ID,
is_direct: true,
org_id: ORG_ID,
parent_binding_id: null,
principal_id: USER_ID,
principal_type: 'user',
reason: 'Accepted invitation',
role_id: ROLE_ID,
scope_type: 'org',
}
function requestPayload(payload: unknown, apiSecret: string | null = API_SECRET) {
const headers: Record<string, string> = {
'content-type': 'application/json',
'x-capgo-queue-max-reads': '5',
'x-capgo-queue-name': 'on_user_org_access',
'x-capgo-queue-read-count': '1',
}
if (apiSecret !== null)
headers.apisecret = apiSecret
return apiWorker.fetch(new Request('https://api.capgo.app/triggers/on_user_org_access', {
body: JSON.stringify(payload),
headers,
method: 'POST',
}))
}
function requestRoleBindingWrite(
type: 'INSERT' | 'UPDATE',
apiSecret: string | null = API_SECRET,
record = roleBindingRecord,
) {
return requestPayload({
old_record: type === 'UPDATE' ? record : null,
record,
schema: 'public',
table: 'role_bindings',
type,
}, apiSecret)
}
describe('first-organization lifecycle trigger route', () => {
beforeEach(() => {
process.env.API_SECRET = API_SECRET
vi.clearAllMocks()
syncBentoFirstOrgOnRoleBindingWriteMock.mockResolvedValue(undefined)
syncBentoSubscriberTagsMock.mockResolvedValue(true)
pgQueryMock.mockResolvedValue({ rows: [{ email: 'invitee@example.com' }] })
})
afterEach(() => {
if (originalApiSecret === undefined)
delete process.env.API_SECRET
else
process.env.API_SECRET = originalApiSecret
})
it.each(['INSERT', 'UPDATE'] as const)('dispatches authenticated %s payloads and returns JSON success', async (type) => {
const response = await requestRoleBindingWrite(type)
expect(response.status).toBe(200)
expect(response.headers.get('content-type')).toContain('application/json')
await expect(response.json()).resolves.toEqual({ status: 'ok' })
expect(syncBentoFirstOrgOnRoleBindingWriteMock).toHaveBeenCalledOnce()
expect(syncBentoFirstOrgOnRoleBindingWriteMock).toHaveBeenCalledWith(expect.anything(), ROLE_BINDING_ID)
expect(syncBentoSubscriberTagsMock).toHaveBeenCalledOnce()
expect(syncBentoSubscriberTagsMock).toHaveBeenCalledWith(expect.anything(), {
deleteSegments: [],
email: 'invitee@example.com',
segments: ['org:invite_accepted'],
})
expect(pgQueryMock).toHaveBeenCalledWith('SELECT email FROM public.users WHERE id = $1', [USER_ID])
})
it('skips invite-accepted tagging when reason is not Accepted invitation', async () => {
const response = await requestRoleBindingWrite('INSERT', API_SECRET, {
...roleBindingRecord,
reason: 'SSO org membership provisioning',
})
expect(response.status).toBe(200)
expect(syncBentoFirstOrgOnRoleBindingWriteMock).toHaveBeenCalledOnce()
expect(syncBentoSubscriberTagsMock).not.toHaveBeenCalled()
})
it('returns 5xx so the queue retries when lifecycle delivery fails', async () => {
syncBentoFirstOrgOnRoleBindingWriteMock.mockRejectedValueOnce(new Error('Bento unavailable'))
const response = await requestRoleBindingWrite('INSERT')
expect(response.status).toBeGreaterThanOrEqual(500)
expect(response.status).toBeLessThan(600)
expect(syncBentoFirstOrgOnRoleBindingWriteMock).toHaveBeenCalledOnce()
expect(syncBentoSubscriberTagsMock).not.toHaveBeenCalled()
})
it.each([
['a missing API secret', null],
['an invalid API secret', 'wrong-secret'],
])('rejects %s before invoking the lifecycle helper', async (_label, apiSecret) => {
const response = await requestRoleBindingWrite('INSERT', apiSecret)
expect(response.status).toBe(400)
expect(syncBentoFirstOrgOnRoleBindingWriteMock).not.toHaveBeenCalled()
expect(syncBentoSubscriberTagsMock).not.toHaveBeenCalled()
})
it.each([
['null', null],
['an array', []],
['a string', 'role_bindings'],
['a number', 42],
['a boolean', false],
])('rejects %s JSON payload with a controlled validation error', async (_label, payload) => {
const response = await requestPayload(payload)
expect(response.status).toBe(400)
await expect(response.json()).resolves.toMatchObject({
error: 'invalid_payload',
})
expect(syncBentoFirstOrgOnRoleBindingWriteMock).not.toHaveBeenCalled()
expect(syncBentoSubscriberTagsMock).not.toHaveBeenCalled()
})
it.each([
['a different table', 'table_not_match', { old_record: null, record: roleBindingRecord, schema: 'public', table: 'users', type: 'INSERT' }],
['a delete event', 'type_not_match', { old_record: null, record: roleBindingRecord, schema: 'public', table: 'role_bindings', type: 'DELETE' }],
['a missing record ID', 'invalid_payload', { old_record: null, record: {}, schema: 'public', table: 'role_bindings', type: 'INSERT' }],
['a malformed record ID', 'invalid_payload', { old_record: null, record: { ...roleBindingRecord, id: 'not-a-uuid' }, schema: 'public', table: 'role_bindings', type: 'UPDATE' }],
])('rejects %s with a controlled validation error', async (_label, error, payload) => {
const response = await requestPayload(payload)
expect(response.status).toBe(400)
await expect(response.json()).resolves.toMatchObject({ error })
expect(syncBentoFirstOrgOnRoleBindingWriteMock).not.toHaveBeenCalled()
expect(syncBentoSubscriberTagsMock).not.toHaveBeenCalled()
})
it('registers the same route in the Supabase trigger router', async () => {
// Importing this deployment entry point evaluates Deno.serve and starts a
// server, so verify its static wiring without executing the module.
const source = await readFile(new URL('../supabase/functions/triggers/index.ts', import.meta.url), 'utf8')
expect(source).toContain('appGlobal.route(\'/on_user_org_access\', on_user_org_access)')
})
})