Ctrl K
ComunidadGuías…

tests/bento-first-org-route.unit.test.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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)')
  })
})