Ctrl K
ComunidadGuías…

tests/build-support-logs.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'

const { checkPermissionMock, pgQueryMock, drizzleExecuteMock } = vi.hoisted(() => ({
  checkPermissionMock: vi.fn(),
  pgQueryMock: vi.fn(),
  drizzleExecuteMock: vi.fn(),
}))

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

vi.mock('../supabase/functions/_backend/utils/pg.ts', () => ({
  getPgClient: vi.fn(() => ({ id: 'pg-client', query: pgQueryMock })),
  getDrizzleClient: vi.fn(() => ({ execute: drizzleExecuteMock })),
  closeClient: vi.fn(),
}))

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

const { hasSupportLogUploadPermission, uploadSupportLogs } = await import('../supabase/functions/_backend/public/build/support_logs.ts')

const apikey = { user_id: 'user-1' } as any

function createContext() {
  return {
    req: { url: 'http://localhost/build/support_logs' },
    env: {
      BUILDER_URL: 'https://builder.test',
      BUILDER_API_KEY: 'builder-key',
    },
    get: vi.fn((key: string) => key === 'requestId' ? 'req-test' : undefined),
    json: vi.fn((data: unknown, status = 200) => new Response(JSON.stringify(data), {
      status,
      headers: { 'content-type': 'application/json' },
    })),
  } as any
}

function mockAdminRows(rows: { user?: unknown, buildRequest?: unknown }) {
  pgQueryMock.mockImplementation(async (text: string) => {
    if (text.includes('FROM public.users'))
      return { rows: rows.user ? [rows.user] : [] }
    if (text.includes('FROM public.build_requests'))
      return { rows: rows.buildRequest ? [rows.buildRequest] : [] }
    throw new Error(`unexpected query ${text}`)
  })
}

function buildRequestLookups() {
  return pgQueryMock.mock.calls.filter(([text]) => String(text).includes('FROM public.build_requests'))
}

beforeEach(() => {
  vi.restoreAllMocks()
  checkPermissionMock.mockReset()
  checkPermissionMock.mockResolvedValue(true)
  pgQueryMock.mockReset()
  drizzleExecuteMock.mockReset()
  vi.stubEnv('BUILDER_URL', 'https://builder.test')
  vi.stubEnv('BUILDER_API_KEY', 'builder-key')
})

afterEach(() => {
  vi.unstubAllEnvs()
  vi.unstubAllGlobals()
})

describe('uploadSupportLogs metadata forwarding', () => {
  it('forwards user email and derives platform from the build request job id', async () => {
    mockAdminRows({
      user: { email: 'dev@capgo.app' },
      buildRequest: { platform: 'ios' },
    })
    const fetchMock = vi.fn(async () => new Response(JSON.stringify({ id: 'log-1', url: 'https://api.test/log-1' }), {
      status: 200,
      headers: { 'content-type': 'application/json' },
    }))
    vi.stubGlobal('fetch', fetchMock)

    const res = await uploadSupportLogs(createContext(), apikey, {
      appId: 'com.test.app',
      jobId: 'job-1',
      gzB64: 'not-empty',
    })

    expect(res.status).toBe(200)
    const lookups = buildRequestLookups()
    expect(lookups).toHaveLength(1)
    expect(lookups[0][0]).toContain('builder_job_id = $1 AND requested_by = $2 AND ($3::text IS NULL OR app_id = $3)')
    expect(lookups[0][1]).toEqual(['job-1', 'user-1', 'com.test.app'])
    expect(fetchMock).toHaveBeenCalledTimes(1)
    const [, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]
    expect(JSON.parse(init.body as string)).toMatchObject({
      appId: 'com.test.app',
      jobId: 'job-1',
      userId: 'user-1',
      email: 'dev@capgo.app',
      platform: 'ios',
    })
  })

  it('forwards the caller supplied platform when there is no job lookup path', async () => {
    mockAdminRows({ user: { email: 'dev@capgo.app' } })
    const fetchMock = vi.fn(async () => new Response(JSON.stringify({ id: 'log-2', url: 'https://api.test/log-2' }), {
      status: 200,
      headers: { 'content-type': 'application/json' },
    }))
    vi.stubGlobal('fetch', fetchMock)

    await uploadSupportLogs(createContext(), apikey, {
      appId: 'com.test.app',
      platform: 'android',
      gzB64: 'not-empty',
    })

    expect(pgQueryMock).toHaveBeenCalledTimes(1)
    expect(buildRequestLookups()).toHaveLength(0)
    const [, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]
    expect(JSON.parse(init.body as string)).toMatchObject({
      email: 'dev@capgo.app',
      platform: 'android',
    })
  })

  it('ignores unsupported caller supplied platform values', async () => {
    mockAdminRows({ user: { email: 'dev@capgo.app' } })
    const fetchMock = vi.fn(async () => new Response(JSON.stringify({ id: 'log-3', url: 'https://api.test/log-3' }), {
      status: 200,
      headers: { 'content-type': 'application/json' },
    }))
    vi.stubGlobal('fetch', fetchMock)

    await uploadSupportLogs(createContext(), apikey, {
      appId: 'com.test.app',
      platform: 'web' as never,
      gzB64: 'not-empty',
    })

    expect(pgQueryMock).toHaveBeenCalledTimes(1)
    expect(buildRequestLookups()).toHaveLength(0)
    const [, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]
    const body = JSON.parse(init.body as string)
    expect(body.email).toBe('dev@capgo.app')
    expect(body.platform).toBeUndefined()
  })
})

describe('support logs RBAC upload gate', () => {
  const rbacApikey = {
    rbac_id: '00000000-0000-0000-0000-000000000123',
  }

  it('allows registered app uploads only with app.build_native', async () => {
    await expect(hasSupportLogUploadPermission(createContext(), rbacApikey as any, 'com.test.app')).resolves.toBe(true)

    expect(checkPermissionMock).toHaveBeenCalledWith(expect.anything(), 'app.build_native', { appId: 'com.test.app' })
    expect(drizzleExecuteMock).not.toHaveBeenCalled()
  })

  it('denies read-only keys for registered apps', async () => {
    checkPermissionMock.mockResolvedValueOnce(false)
    drizzleExecuteMock.mockResolvedValueOnce({ rows: [{ exists: true }] })

    await expect(hasSupportLogUploadPermission(createContext(), rbacApikey as any, 'com.test.app')).resolves.toBe(false)

    expect(drizzleExecuteMock).toHaveBeenCalledTimes(1)
  })

  it('uses current org write capability only when the app is not registered yet', async () => {
    checkPermissionMock.mockResolvedValueOnce(false)
    drizzleExecuteMock
      .mockResolvedValueOnce({ rows: [{ exists: false }] })
      .mockResolvedValueOnce({ rows: [{ allowed: true }] })

    await expect(hasSupportLogUploadPermission(createContext(), rbacApikey as any, 'com.new.app')).resolves.toBe(true)

    expect(drizzleExecuteMock).toHaveBeenCalledTimes(2)
  })

  it('denies app-less uploads without a current write-capable org binding', async () => {
    drizzleExecuteMock.mockResolvedValueOnce({ rows: [{ allowed: false }] })

    await expect(hasSupportLogUploadPermission(createContext(), rbacApikey as any)).resolves.toBe(false)

    expect(checkPermissionMock).not.toHaveBeenCalled()
    expect(drizzleExecuteMock).toHaveBeenCalledTimes(1)
  })
})