tests/channel-current-bundle.unit.test.ts
Rama main · réplica de lectura del repositorio original.
import { beforeEach, describe, expect, it, vi } from 'vitest'
const mocks = vi.hoisted(() => ({
checkPermission: vi.fn(),
channelRows: vi.fn(),
versionRow: vi.fn(),
principal: vi.fn(),
}))
const rlsClient = { kind: 'rls' }
const adminPg = { kind: 'admin' }
vi.mock('../supabase/functions/_backend/utils/rbac.ts', () => ({
checkPermission: (...args: unknown[]) => mocks.checkPermission(...args),
}))
vi.mock('../supabase/functions/_backend/utils/pg.ts', () => ({
getPgClient: async () => adminPg,
closeClient: vi.fn(),
}))
vi.mock('../supabase/functions/_backend/utils/pg_rls.ts', () => ({
apikeyPrincipal: (_c: unknown, apikey: string) => {
mocks.principal(apikey)
return { kind: 'apikey', apikey }
},
withRlsContext: async (_c: unknown, _principal: unknown, run: (client: unknown) => unknown) => run(rlsClient),
queryJson: async (client: unknown, text: string, values: unknown[]) => {
expect(client).toBe(rlsClient)
return mocks.channelRows(text, values)
},
queryJsonOne: async (client: unknown, text: string, values: unknown[]) => {
expect(client).toBe(adminPg)
return mocks.versionRow(text, values)
},
}))
const { getCurrentBundle } = await import('../supabase/functions/_backend/public/channel/current_bundle.ts')
function createContext() {
return {
json: (body: unknown) => Response.json(body),
} as any
}
describe('channel current bundle endpoint', () => {
beforeEach(() => {
vi.clearAllMocks()
mocks.checkPermission.mockResolvedValue(true)
mocks.channelRows.mockResolvedValue([{ id: 42, disable_auto_update: 'version_number', version: 99 }])
mocks.versionRow.mockResolvedValue({
id: 99,
name: '1.0.0',
min_update_version: '0.9.0',
native_packages: [{ name: '@capacitor/core', version: '6.0.0' }],
})
})
it('returns bundle compatibility fields when channel.read is allowed', async () => {
const response = await getCurrentBundle(
createContext(),
{ app_id: 'com.example.app', channel: 'production' },
{ key: 'test-key' } as any,
)
expect(response.status).toBe(200)
const body = await response.json() as {
bundle_name?: string
bundle_id?: number
min_update_version?: string | null
native_packages?: unknown[]
disable_auto_update?: string
}
expect(body.bundle_name).toBe('1.0.0')
expect(body.bundle_id).toBe(99)
expect(body.min_update_version).toBe('0.9.0')
expect(body.disable_auto_update).toBe('version_number')
expect(body.native_packages).toEqual([{ name: '@capacitor/core', version: '6.0.0' }])
expect(mocks.checkPermission).toHaveBeenCalledWith(expect.anything(), 'channel.read', {
appId: 'com.example.app',
channelId: 42,
})
expect(mocks.principal).toHaveBeenCalledWith('test-key')
expect(mocks.channelRows).toHaveBeenCalledWith(expect.stringContaining('FROM public.channels'), ['com.example.app', 'production'])
})
it('loads the linked version on the service connection so channel.read-only keys see it', async () => {
mocks.versionRow.mockResolvedValue({ id: 99, name: '1.0.0', min_update_version: null, native_packages: null })
const response = await getCurrentBundle(
createContext(),
{ app_id: 'com.example.app', channel: 'production' },
{ key: 'test-key' } as any,
)
expect(response.status).toBe(200)
const body = await response.json() as { bundle_name?: string, min_update_version?: string | null, native_packages?: unknown[] }
expect(body.bundle_name).toBe('1.0.0')
expect(body.min_update_version).toBeNull()
expect(body.native_packages).toEqual([])
expect(mocks.versionRow).toHaveBeenCalledWith(expect.stringContaining('FROM public.app_versions WHERE app_id = $1 AND id = $2'), ['com.example.app', 99])
})
it('returns channel metadata when no bundle is linked', async () => {
mocks.channelRows.mockResolvedValue([{ id: 42, disable_auto_update: 'none', version: null }])
const response = await getCurrentBundle(
createContext(),
{ app_id: 'com.example.app', channel: 'production' },
{ key: 'test-key' } as any,
)
expect(response.status).toBe(200)
const body = await response.json() as { bundle_name?: string | null, disable_auto_update?: string }
expect(body.bundle_name).toBeNull()
expect(body.disable_auto_update).toBe('none')
expect(mocks.versionRow).not.toHaveBeenCalled()
})
it.each([
['the linked bundle checksum', false, 'sha-abc'],
['no checksum for a soft-deleted linked bundle', true, null],
])('returns %s', async (_label, deleted, expected) => {
mocks.versionRow.mockResolvedValue({ id: 99, name: '1.0.0', min_update_version: null, checksum: 'sha-abc', deleted, native_packages: [] })
const response = await getCurrentBundle(
createContext(),
{ app_id: 'com.example.app', channel: 'production' },
{ key: 'test-key' } as any,
)
const body = await response.json() as { checksum?: string | null }
expect(body.checksum).toBe(expected)
})
it('rejects callers without channel.read', async () => {
mocks.checkPermission.mockResolvedValueOnce(false)
await expect(getCurrentBundle(
createContext(),
{ app_id: 'com.example.app', channel: 'production' },
{ key: 'test-key' } as any,
)).rejects.toMatchObject({ status: 400 })
expect(mocks.versionRow).not.toHaveBeenCalled()
})
it('treats an unreadable or ambiguous channel as not found', async () => {
mocks.channelRows.mockResolvedValueOnce([])
await expect(getCurrentBundle(
createContext(),
{ app_id: 'com.example.app', channel: 'production' },
{ key: 'test-key' } as any,
)).rejects.toMatchObject({ status: 400 })
mocks.channelRows.mockResolvedValueOnce([{ id: 1, disable_auto_update: 'none', version: null }, { id: 2, disable_auto_update: 'none', version: null }])
await expect(getCurrentBundle(
createContext(),
{ app_id: 'com.example.app', channel: 'production' },
{ key: 'test-key' } as any,
)).rejects.toMatchObject({ status: 400 })
expect(mocks.checkPermission).not.toHaveBeenCalled()
})
})