Ctrl K
ComunidadGuías…

tests/build-request-plan-gate.unit.test.ts

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

Archivo original ↗Preguntar a KiBot
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { requestBuild } from '../supabase/functions/_backend/public/build/request.ts'

const {
  mockRlsQuery,
  mockWithRlsContext,
  mockGetPgClient,
  mockCheckPermission,
  mockGetEnv,
  mockSendEventToTracking,
  mockAssertNativeBuildConcurrencyAvailable,
  mockGetPlansUpgradeUrl,
} = vi.hoisted(() => ({
  mockRlsQuery: vi.fn(),
  mockWithRlsContext: vi.fn(),
  mockGetPgClient: vi.fn(),
  mockCheckPermission: vi.fn(),
  mockGetEnv: vi.fn(),
  mockSendEventToTracking: vi.fn(),
  mockAssertNativeBuildConcurrencyAvailable: vi.fn(),
  mockGetPlansUpgradeUrl: vi.fn(),
}))

vi.mock('../supabase/functions/_backend/utils/pg_rls.ts', async (importOriginal) => {
  const actual = await importOriginal<typeof import('../supabase/functions/_backend/utils/pg_rls.ts')>()
  return { ...actual, withRlsContext: mockWithRlsContext }
})

vi.mock('../supabase/functions/_backend/utils/pg.ts', async (importOriginal) => {
  const actual = await importOriginal<typeof import('../supabase/functions/_backend/utils/pg.ts')>()
  return { ...actual, getPgClient: mockGetPgClient, closeClient: vi.fn() }
})

vi.mock('../supabase/functions/_backend/utils/rbac.ts', () => ({
  checkPermission: mockCheckPermission,
}))

vi.mock('../supabase/functions/_backend/utils/utils.ts', () => ({
  getEnv: mockGetEnv,
}))

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

vi.mock('../supabase/functions/_backend/public/build/concurrency.ts', () => ({
  assertNativeBuildConcurrencyAvailable: mockAssertNativeBuildConcurrencyAvailable,
  getPlansUpgradeUrl: mockGetPlansUpgradeUrl,
}))

describe('native build request plan gate', () => {
  const requestId = 'req-build-plan-gate'
  const appId = 'com.test.native.build.plan'
  const orgId = 'org-native-build-plan'

  function createContext() {
    return {
      get: vi.fn().mockImplementation((key: string) => {
        if (key === 'requestId')
          return requestId
        return undefined
      }),
    }
  }

  beforeEach(() => {
    mockRlsQuery.mockReset()
    mockWithRlsContext.mockReset()
    mockGetPgClient.mockReset()
    mockWithRlsContext.mockImplementation(async (_c: unknown, _principal: unknown, run: (client: unknown) => unknown) => run({ query: mockRlsQuery }))
    mockCheckPermission.mockReset()
    mockGetEnv.mockReset()
    mockSendEventToTracking.mockReset()
    mockAssertNativeBuildConcurrencyAvailable.mockReset()
    mockGetPlansUpgradeUrl.mockReset()

    mockCheckPermission.mockResolvedValue(true)
    mockAssertNativeBuildConcurrencyAvailable.mockResolvedValue({
      activeBuilds: 0,
      limit: 2,
      planName: 'Solo',
      upgradeUrl: 'https://console.capgo.app/settings/organization/plans',
    })
    mockGetPlansUpgradeUrl.mockReturnValue('https://console.capgo.app/settings/organization/plans')
  })

  function allowBuildTime(allowed: boolean) {
    mockRlsQuery.mockImplementation(async (text: string) => text.includes('FROM public.apps')
      ? { rows: [{ owner_org: orgId }] }
      : { rows: [{ allowed }] })
  }

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

  it('blocks builder job creation when build time action is over plan', async () => {
    allowBuildTime(false)

    const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response(null, { status: 500 }))

    try {
      await expect(requestBuild(
        createContext() as any,
        { app_id: appId, platform: 'ios' },
        { key: 'api-key-build-plan', user_id: 'user-native-build-plan' } as any,
      )).rejects.toMatchObject({
        status: 429,
        message: 'Cannot request native build, upgrade plan to continue to build: https://console.capgo.app/settings/organization/plans',
        cause: expect.objectContaining({
          error: 'need_plan_upgrade',
          moreInfo: expect.objectContaining({
            app_id: appId,
            org_id: orgId,
            reason: 'build_time',
            upgrade_url: 'https://console.capgo.app/settings/organization/plans',
          }),
        }),
      })

      expect(mockCheckPermission).toHaveBeenCalledWith(expect.anything(), 'app.build_native', { appId })
      expect(mockWithRlsContext).toHaveBeenCalledWith(expect.anything(), { kind: 'apikey', apikey: 'api-key-build-plan' }, expect.any(Function))
      expect(mockRlsQuery).toHaveBeenCalledWith('SELECT owner_org FROM public.apps WHERE app_id = $1', [appId])
      expect(mockRlsQuery).toHaveBeenCalledWith(
        expect.stringContaining('public.is_allowed_action_org_action(orgid => $1, actions => $2::public.action_type[], appid => $3)'),
        [orgId, ['build_time'], appId],
      )
      expect(mockAssertNativeBuildConcurrencyAvailable).not.toHaveBeenCalled()
      expect(fetchMock).not.toHaveBeenCalled()
      expect(mockGetPgClient).not.toHaveBeenCalled()
    }
    finally {
      fetchMock.mockRestore()
    }
  })

  it('blocks builder job creation when native build concurrency is already at the plan limit', async () => {
    const { HTTPException } = await import('hono/http-exception')
    allowBuildTime(true)
    mockAssertNativeBuildConcurrencyAvailable.mockRejectedValue(new HTTPException(429, {
      message: 'Your Solo plan allows 2 concurrent native builds. You already have 2 active. Wait for a build to finish, or upgrade your plan: https://console.capgo.app/settings/organization/plans',
      cause: {
        error: 'native_build_concurrency_limit_exceeded',
        message: 'Your Solo plan allows 2 concurrent native builds. You already have 2 active. Wait for a build to finish, or upgrade your plan: https://console.capgo.app/settings/organization/plans',
        moreInfo: {
          activeBuilds: 2,
          limit: 2,
          planName: 'Solo',
          upgrade_url: 'https://console.capgo.app/settings/organization/plans',
          reason: 'native_build_concurrency',
        },
        suppressDiscordAlert: true,
      },
    }))

    const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response(null, { status: 500 }))

    try {
      await expect(requestBuild(
        createContext() as any,
        { app_id: appId, platform: 'android' },
        { key: 'api-key-build-plan', user_id: 'user-native-build-plan' } as any,
      )).rejects.toMatchObject({
        status: 429,
        cause: expect.objectContaining({
          error: 'native_build_concurrency_limit_exceeded',
        }),
      })

      expect(mockAssertNativeBuildConcurrencyAvailable).toHaveBeenCalledWith(expect.anything(), {
        orgId,
        appId,
        userId: 'user-native-build-plan',
      })
      expect(fetchMock).not.toHaveBeenCalled()
      expect(mockGetPgClient).not.toHaveBeenCalled()
    }
    finally {
      fetchMock.mockRestore()
    }
  })
})