Ctrl K
ComunidadGuías…

tests/canceled-org-retention-alerts.unit.test.ts

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

Archivo original ↗Preguntar a KiBot
import { readFileSync } from 'node:fs'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import apiWorker from '../cloudflare_workers/api/index.ts'

const {
  cloudlogMock,
  sendEventToTrackingMock,
} = vi.hoisted(() => ({
  cloudlogMock: vi.fn(),
  sendEventToTrackingMock: vi.fn(async () => undefined),
}))

vi.mock('../supabase/functions/_backend/utils/logging.ts', () => ({
  accessLog: vi.fn(),
  cloudlog: cloudlogMock,
}))

vi.mock('../supabase/functions/_backend/utils/tracking.ts', () => ({
  sendEventToTracking: sendEventToTrackingMock,
}))

vi.mock('../supabase/functions/_backend/utils/hono.ts', async () => {
  const actual = await vi.importActual('../supabase/functions/_backend/utils/hono.ts')
  return {
    ...actual,
    middlewareAPISecret: async (_c: unknown, next: () => Promise<void>) => await next(),
  }
})

const ORG_BUNDLES = 'a0c1e2f3-1111-4aaa-8bbb-000000000101'
const ORG_APPS = 'a0c1e2f3-1111-4aaa-8bbb-000000000102'
const ORG_BAD = 'a0c1e2f3-1111-4aaa-8bbb-000000000103'
const ORG_PROTO = 'a0c1e2f3-1111-4aaa-8bbb-000000000104'
const ORG_FALLBACK = 'a0c1e2f3-1111-4aaa-8bbb-000000000105'

async function postRetentionAlert(body: unknown) {
  const { app } = await import('../supabase/functions/_backend/triggers/canceled_org_retention_alerts.ts')
  return app.request('http://localhost/', {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify(body),
  })
}

describe('canceled_org_retention_alerts', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    sendEventToTrackingMock.mockResolvedValue(undefined)
  })

  afterEach(() => {
    vi.resetModules()
  })

  it('sends a once Bento/tracking event for bundle deletion warnings', async () => {
    const response = await postRetentionAlert({
      org_id: ORG_BUNDLES,
      org_name: 'Retention Org',
      management_email: 'billing@example.com',
      alert_type: 'bundles_deletion_warning',
      access_end: '2026-05-01T00:00:00.000Z',
      days_until_deletion: 5,
      app_ids: ['com.example.app'],
    })

    expect(response.status).toBe(200)
    expect(await response.json()).toEqual({ status: 'ok' })
    expect(sendEventToTrackingMock).toHaveBeenCalledWith(
      expect.anything(),
      expect.objectContaining({
        channel: 'usage',
        event: 'Bundles will be deleted',
        sentToBento: true,
        user_id: ORG_BUNDLES,
        groups: { organization: ORG_BUNDLES },
        bento: expect.objectContaining({
          once: true,
          event: 'org:bundles_will_be_deleted',
          preferenceKey: 'usage_limit',
          audience: 'billing',
          recordDelivery: true,
          uniqId: 'retention:bundles_deletion_warning:2026-05-01T00:00:00Z',
          data: expect.objectContaining({
            org_id: ORG_BUNDLES,
            app_ids: ['com.example.app'],
            days_until_deletion: 5,
          }),
        }),
      }),
      { background: false, strict: true },
    )
  })

  it('sends a once Bento/tracking event for app deletion warnings', async () => {
    const response = await postRetentionAlert({
      org_id: ORG_APPS,
      org_name: 'Retention Org',
      alert_type: 'app_deletion_warning',
      access_end: '2026-04-20T12:30:00.000Z',
      app_ids: ['com.example.one', 'com.example.two'],
    })

    expect(response.status).toBe(200)
    expect(sendEventToTrackingMock).toHaveBeenCalledWith(
      expect.anything(),
      expect.objectContaining({
        event: 'Apps will be deleted',
        bento: expect.objectContaining({
          once: true,
          event: 'org:apps_will_be_deleted',
          uniqId: 'retention:app_deletion_warning:2026-04-20T12:30:00Z',
          data: expect.objectContaining({
            app_count: 2,
          }),
        }),
      }),
      { background: false, strict: true },
    )
  })

  it.each([
    ['an unsupported alert type', { org_id: ORG_BAD, alert_type: 'not_a_real_alert' }],
    ['prototype key constructor as alert type', { org_id: ORG_PROTO, alert_type: 'constructor' }],
    ['prototype key toString as alert type', { org_id: ORG_PROTO, alert_type: 'toString' }],
    ['prototype key valueOf as alert type', { org_id: ORG_PROTO, alert_type: 'valueOf' }],
    ['a missing org_id', { alert_type: 'bundles_deletion_warning' }],
    ['a non-uuid org_id', { org_id: 'not-a-uuid', alert_type: 'bundles_deletion_warning' }],
    ['a null JSON body', null],
  ])('rejects %s with a 4xx and no tracking', async (_label, body) => {
    const response = await postRetentionAlert(body)

    expect(response.status).toBeGreaterThanOrEqual(400)
    expect(response.status).toBeLessThan(500)
    expect(sendEventToTrackingMock).not.toHaveBeenCalled()
  })

  it('falls back access_end uniqId and NaN days_until_deletion', async () => {
    const response = await postRetentionAlert({
      org_id: ORG_FALLBACK,
      alert_type: 'app_deletion_warning',
      access_end: 'not-a-date',
      days_until_deletion: 'nope',
    })

    expect(response.status).toBe(200)
    expect(sendEventToTrackingMock).toHaveBeenCalledWith(
      expect.anything(),
      expect.objectContaining({
        bento: expect.objectContaining({
          uniqId: 'retention:app_deletion_warning:invalid',
          data: expect.objectContaining({
            days_until_deletion: 5,
          }),
        }),
      }),
      { background: false, strict: true },
    )
  })

  it('registers the trigger on the Supabase triggers router', () => {
    // The Supabase entrypoint calls Deno.serve, so it cannot be imported here; the Cloudflare route is exercised below.
    const supabaseRouter = readFileSync(new URL('../supabase/functions/triggers/index.ts', import.meta.url), 'utf8')
    expect(supabaseRouter).toContain('route(\'/canceled_org_retention_alerts\', canceled_org_retention_alerts)')
  })

  it('serves POST /triggers/canceled_org_retention_alerts on the Cloudflare API worker', async () => {
    const response = await apiWorker.fetch(new Request('https://api.capgo.app/triggers/canceled_org_retention_alerts', {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({}),
    }))

    expect(response.status).not.toBe(404)
  })

  it('falls back to unknown uniqId for non-string access_end', async () => {
    const response = await postRetentionAlert({
      org_id: ORG_FALLBACK,
      alert_type: 'bundles_deletion_warning',
      access_end: { nested: true },
    })

    expect(response.status).toBe(200)
    expect(sendEventToTrackingMock).toHaveBeenCalledWith(
      expect.anything(),
      expect.objectContaining({
        bento: expect.objectContaining({
          uniqId: 'retention:bundles_deletion_warning:unknown',
        }),
      }),
      { background: false, strict: true },
    )
  })
})