tests/bundle-get.unit.test.ts
Rama main · réplica de lectura del repositorio original.
import type { Context } from 'hono'
import type { MiddlewareKeyVariables } from '../supabase/functions/_backend/utils/hono.ts'
import { HTTPException } from 'hono/http-exception'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const checkPermissionMock = vi.fn()
const queryJsonMock = vi.fn()
const withRlsContextMock = vi.fn()
vi.mock('../supabase/functions/_backend/utils/rbac.ts', () => ({
checkPermission: (...args: unknown[]) => checkPermissionMock(...args),
}))
vi.mock('../supabase/functions/_backend/utils/pg_rls.ts', () => ({
apikeyPrincipal: (_c: unknown, apikey: string) => ({ kind: 'apikey', apikey }),
withRlsContext: (c: unknown, principal: unknown, run: (client: unknown) => unknown) => {
withRlsContextMock(principal)
return run({})
},
queryJson: (_client: unknown, text: string, values: unknown[]) => queryJsonMock(text, values),
queryJsonOne: async (_client: unknown, text: string, values: unknown[]) => (await queryJsonMock(text, values))[0] ?? null,
}))
vi.mock('../supabase/functions/_backend/utils/utils.ts', async () => {
const actual = await vi.importActual<typeof import('../supabase/functions/_backend/utils/utils.ts')>('../supabase/functions/_backend/utils/utils.ts')
return {
...actual,
isValidAppId: (appId: string) => typeof appId === 'string' && appId.includes('.'),
fetchLimit: 50,
}
})
const { BUNDLE_SUMMARY_COLUMNS, get } = await import('../supabase/functions/_backend/public/bundle/get.ts')
const apikey = {
key: 'test-apikey',
user_id: 'user-1',
} as any
function causeErrorCode(error: HTTPException) {
return (error.cause as { error: string }).error
}
function createContext(auth?: { userId: string }) {
return {
get: vi.fn((key: string) => {
if (key === 'requestId')
return 'req-1'
if (key === 'auth')
return auth
return undefined
}),
json: (body: unknown, status = 200) => Response.json(body, { status }),
} as unknown as Context<MiddlewareKeyVariables>
}
function mockRows(rows: unknown[] | Error) {
if (rows instanceof Error)
queryJsonMock.mockRejectedValue(rows)
else
queryJsonMock.mockResolvedValue(rows)
}
function lastQuery() {
const [text, values] = queryJsonMock.mock.calls.at(-1) as [string, unknown[]]
return { text, values }
}
describe('bundle GET handler', () => {
beforeEach(() => {
vi.clearAllMocks()
checkPermissionMock.mockResolvedValue(true)
})
it('returns 401 when auth context is missing', async () => {
const error = await get(createContext(undefined), { app_id: 'com.example.app' }, apikey).catch(caught => caught) as HTTPException
expect(error).toBeInstanceOf(HTTPException)
expect(error.status).toBe(401)
expect(causeErrorCode(error)).toBe('invalid_apikey')
})
it('returns 404 when permission is denied and the app is not visible', async () => {
checkPermissionMock.mockResolvedValue(false)
mockRows([])
const error = await get(createContext({ userId: 'user-1' }), { app_id: 'com.missing.app' }, apikey).catch(caught => caught) as HTTPException
expect(error).toBeInstanceOf(HTTPException)
expect(error.status).toBe(404)
expect(causeErrorCode(error)).toBe('app_not_found')
})
it('returns 403 when permission is denied but the app exists', async () => {
checkPermissionMock.mockResolvedValue(false)
mockRows([{ app_id: 'com.example.app' }])
const error = await get(createContext({ userId: 'user-1' }), { app_id: 'com.example.app' }, apikey).catch(caught => caught) as HTTPException
expect(error).toBeInstanceOf(HTTPException)
expect(error.status).toBe(403)
expect(causeErrorCode(error)).toBe('cannot_get_bundle')
})
it('returns 500 when bundle query fails', async () => {
mockRows(new Error('db down'))
const error = await get(createContext({ userId: 'user-1' }), { app_id: 'com.example.app' }, apikey).catch(caught => caught) as HTTPException
expect(error).toBeInstanceOf(HTTPException)
expect(error.status).toBe(500)
expect(causeErrorCode(error)).toBe('cannot_get_bundle')
})
it.each([
['bundle rows', [{ id: 1, name: '1.0.0', app_id: 'com.example.app' }]],
['an empty array when the app has no bundles', []],
])('returns %s on success', async (_label, rows) => {
mockRows(rows)
const response = await get(createContext({ userId: 'user-1' }), { app_id: 'com.example.app' }, apikey)
expect(response.status).toBe(200)
expect(await response.json()).toEqual(rows)
expect(withRlsContextMock).toHaveBeenCalledWith({ kind: 'apikey', apikey: 'test-apikey' })
})
it('excludes deleted bundles by default on version lookup', async () => {
mockRows([])
const response = await get(createContext({ userId: 'user-1' }), { app_id: 'com.example.app', version: '1.0.0' }, apikey)
expect(response.status).toBe(200)
expect(await response.json()).toEqual([])
expect(lastQuery().text).toContain('deleted = false')
expect(lastQuery().values).toEqual(['com.example.app', '1.0.0'])
})
it('include_deleted matches soft-deleted bundles for name occupancy checks', async () => {
const row = { id: 12, name: '1.0.0', checksum: 'abc', deleted: true, created_at: '2026-01-01T00:00:00.000Z' }
mockRows([row])
const response = await get(createContext({ userId: 'user-1' }), { app_id: 'com.example.app', version: '1.0.0', include_deleted: '1' }, apikey)
expect(response.status).toBe(200)
expect(await response.json()).toEqual([row])
expect(lastQuery().text).not.toContain('deleted = false')
expect(lastQuery().text).toContain('name = $2')
expect(lastQuery().values).toEqual(['com.example.app', '1.0.0'])
})
it('returns full rows by default', async () => {
mockRows([])
await get(createContext({ userId: 'user-1' }), { app_id: 'com.example.app' }, apikey)
expect(lastQuery().text).toMatch(/^SELECT \* FROM public\.app_versions /)
expect(lastQuery().values).toEqual(['com.example.app', 50, 0])
})
it('fields=summary skips manifest and native_packages', async () => {
mockRows([])
await get(createContext({ userId: 'user-1' }), { app_id: 'com.example.app', fields: 'summary' }, apikey)
expect(lastQuery().text).toContain(`SELECT ${BUNDLE_SUMMARY_COLUMNS} FROM`)
expect(BUNDLE_SUMMARY_COLUMNS).not.toMatch(/manifest,|native_packages/)
expect(BUNDLE_SUMMARY_COLUMNS).toContain('manifest_count')
})
it('limit sets the page size and page counts in that size', async () => {
mockRows([])
await get(createContext({ userId: 'user-1' }), { app_id: 'com.example.app', limit: '1000', page: '2' }, apikey)
expect(lastQuery().values).toEqual(['com.example.app', 1000, 2000])
})
it.each(['0', '1001', 'abc'])('rejects limit=%s', async (limit) => {
const error = await get(createContext({ userId: 'user-1' }), { app_id: 'com.example.app', limit }, apikey).catch(caught => caught) as HTTPException
expect(error).toBeInstanceOf(HTTPException)
expect(causeErrorCode(error)).toBe('invalid_query')
})
})