import { HTTPException } from 'hono/http-exception' import { beforeEach, describe, expect, it, vi } from 'vitest' import { ChannelAlreadyExistsError } from '../supabase/functions/_backend/utils/channel_insert.ts' const updateOrCreateChannel = vi.fn() const checkPermission = vi.fn() const checkPermissionPg = vi.fn() const assertCanPromoteChannelInTransaction = vi.fn() const setChannelInTransaction = vi.fn() const adminQuery = vi.fn() const isValidAppId = vi.fn() const getPgClient = vi.fn() const getDrizzleClient = vi.fn() const closeClient = vi.fn() const logPgError = vi.fn() const transactionEvents: string[] = [] const dbClient = { query: vi.fn(), release: vi.fn(), } // Service connection: plain queries on the handler's client, transactions on a checkout. const pgClient = { connect: vi.fn(), query: adminQuery, } const drizzle = {} vi.mock('../supabase/functions/_backend/utils/hono.ts', () => ({ BRES: { status: 'ok' }, simpleError: (error: string, message: string, details: Record = {}) => { const issue = new Error(message) ;(issue as Error & { cause?: unknown }).cause = { error, ...details } throw issue }, })) vi.mock('../supabase/functions/_backend/utils/logging.ts', () => ({ cloudlogErr: vi.fn(), })) vi.mock('../supabase/functions/_backend/utils/rbac.ts', () => ({ checkPermission, checkPermissionPg, })) vi.mock('../supabase/functions/_backend/utils/pg.ts', async () => ({ ...(await import('./helpers/pg-checkout-release-mocks.ts')), closeClient, getDrizzleClient, getPgClient, logPgError, })) vi.mock('../supabase/functions/_backend/utils/supabase.ts', () => ({ updateOrCreateChannel, })) vi.mock('../supabase/functions/_backend/public/bundle/set_channel.ts', () => ({ assertCanPromoteChannelInTransaction, setChannelInTransaction, })) vi.mock('../supabase/functions/_backend/utils/utils.ts', () => ({ isInternalVersionName: (version: string) => version === 'builtin' || version === 'unknown', isValidAppId, })) function jsonRows(rows: unknown[]) { return { rowCount: rows.length, rows: rows.map(row => ({ row })) } } function buildAdminChain(body: { existingChannelId?: number | null existingChannelVersion?: number | null existingRolloutVersion?: number | null existingChannelPublic?: boolean ownerOrg?: string versionId?: number versionError?: { message: string } | null versionQueries?: Array<[string, unknown[]]> fromCalls?: string[] } = {}) { return async (text: string, values: unknown[] = []) => { if (text.includes('FROM public.apps')) { body.fromCalls?.push('apps') return jsonRows([{ owner_org: body.ownerOrg ?? 'org-test' }]) } if (text.includes('FROM public.channels')) { body.fromCalls?.push('channels') return jsonRows(body.existingChannelId == null ? [] : [{ id: body.existingChannelId, version: body.existingChannelVersion ?? null, rollout_version: body.existingRolloutVersion ?? null, public: body.existingChannelPublic ?? false, }]) } if (text.includes('FROM public.app_versions')) { body.fromCalls?.push('app_versions') body.versionQueries?.push([text, values]) if (body.versionError) throw new Error(body.versionError.message) return jsonRows([{ id: body.versionId ?? 123 }]) } throw new Error(`Unexpected admin query: ${text}`) } } function apiKey() { return { user_id: 'user-test', key: 'capg-key' } as any } function context() { return { get: vi.fn((key: string) => { if (key === 'auth') return { authType: 'apikey', userId: 'user-test', apikey: apiKey() } if (key === 'requestId') return 'channel-post-test' return undefined }), json: vi.fn().mockReturnValue({ ok: true }), } as any } function configureTransaction(versionId: number | null = 123, existingChannelId: number | null = null) { dbClient.query.mockImplementation(async (statement: string) => { if (statement === 'BEGIN') { transactionEvents.push('begin') return { rowCount: null, rows: [] } } if (statement === 'COMMIT') { transactionEvents.push('commit') return { rowCount: null, rows: [] } } if (statement === 'ROLLBACK') { transactionEvents.push('rollback') return { rowCount: null, rows: [] } } if (statement.includes('set_config')) { transactionEvents.push('headers') return { rowCount: 1, rows: [] } } if (statement.includes('INSERT INTO public.channels')) { transactionEvents.push('insert') const row = existingChannelId === null ? { id: '77', version: null, rollout_version: null, public: false, paused_at: null, inserted: true } : { id: String(existingChannelId), version: 5, rollout_version: null, public: false, paused_at: null, inserted: false } return { rowCount: 1, rows: [{ row }] } } if (statement.includes('FROM public.app_versions')) { transactionEvents.push('version') return versionId === null ? { rowCount: 0, rows: [] } : { rowCount: 1, rows: [{ id: versionId }] } } throw new Error(`Unexpected transaction statement: ${statement}`) }) } describe('public channel post', () => { beforeEach(() => { vi.clearAllMocks() transactionEvents.splice(0) checkPermission.mockResolvedValue(true) checkPermissionPg.mockImplementation(async (_context: unknown, permission: string) => { transactionEvents.push(`permission:${permission}`) return true }) assertCanPromoteChannelInTransaction.mockImplementation(async () => { transactionEvents.push('promote') }) setChannelInTransaction.mockImplementation(async () => { transactionEvents.push('set') return { channelName: 'preview', versionName: '1.0.0' } }) isValidAppId.mockReturnValue(true) adminQuery.mockImplementation(buildAdminChain()) updateOrCreateChannel.mockResolvedValue({ data: { id: 99 }, error: null }) getPgClient.mockReturnValue(pgClient) pgClient.connect.mockResolvedValue(dbClient) getDrizzleClient.mockReturnValue(drizzle) closeClient.mockResolvedValue(undefined) configureTransaction() }) it('rechecks update permission after a concurrent channel creation', async () => { const { post } = await import('../supabase/functions/_backend/public/channel/post.ts') const conflict = Object.assign(new Error('duplicate channel'), { code: '23505', constraint: 'unique_name_app_id' }) updateOrCreateChannel.mockRejectedValueOnce(conflict) const initialLookup = buildAdminChain() const existingLookup = buildAdminChain({ existingChannelId: 42 }) let channelLookups = 0 adminQuery.mockImplementation((text: string, values: unknown[]) => { if (text.includes('FROM public.channels')) channelLookups++ return channelLookups > 1 ? existingLookup(text, values) : initialLookup(text, values) }) const c = context() await post(c, { app_id: 'com.test.channel-race', channel: 'preview' }, apiKey()) expect(checkPermission).toHaveBeenCalledWith(c, 'channel.update_settings', { appId: 'com.test.channel-race', channelId: 42 }) expect(updateOrCreateChannel).toHaveBeenLastCalledWith(c, expect.anything(), 42, true) }) it('updates the existing channel returned by the insert when the lookup was stale', async () => { const { post } = await import('../supabase/functions/_backend/public/channel/post.ts') // Hyperdrive served a cached "no channel"; the insert found the real row instead of failing. const existing = { id: 42, version: 5, rollout_version: null, public: false, paused_at: null } updateOrCreateChannel.mockRejectedValueOnce(new ChannelAlreadyExistsError(existing)) let channelLookups = 0 const lookup = buildAdminChain() adminQuery.mockImplementation((text: string, values: unknown[]) => { if (text.includes('FROM public.channels')) channelLookups++ return lookup(text, values) }) const c = context() await post(c, { app_id: 'com.test.channel-stale', channel: 'pr-1', disableAutoUpdate: 'none' }, apiKey()) expect(channelLookups).toBe(1) expect(checkPermission).toHaveBeenCalledWith(c, 'channel.update_settings', { appId: 'com.test.channel-stale', channelId: 42 }) expect(updateOrCreateChannel).toHaveBeenCalledTimes(2) expect(updateOrCreateChannel).toHaveBeenLastCalledWith(c, expect.objectContaining({ version: 5, disable_auto_update: 'none' }), 42, true) }) it('does not update the existing channel returned by the insert without update permission', async () => { const { post } = await import('../supabase/functions/_backend/public/channel/post.ts') updateOrCreateChannel.mockRejectedValueOnce(new ChannelAlreadyExistsError({ id: 42, version: null, rollout_version: null, public: false, paused_at: null })) checkPermission.mockImplementation(async (_c: unknown, permission: string) => permission !== 'channel.update_settings') await expect(post(context(), { app_id: 'com.test.channel-stale', channel: 'pr-1' }, apiKey())).rejects.toThrow('You can\'t access this app') expect(updateOrCreateChannel).toHaveBeenCalledOnce() }) it('rolls back create-and-promote and updates the existing channel returned by the insert', async () => { configureTransaction(123, 42) const { post } = await import('../supabase/functions/_backend/public/channel/post.ts') const c = context() await post(c, { app_id: 'com.test.channel-stale', channel: 'pr-1', version: '1.0.0' }, apiKey()) expect(transactionEvents).toContain('rollback') expect(transactionEvents).not.toContain('commit') expect(setChannelInTransaction).not.toHaveBeenCalled() expect(checkPermission).toHaveBeenCalledWith(c, 'channel.update_settings', { appId: 'com.test.channel-stale', channelId: 42 }) expect(updateOrCreateChannel).toHaveBeenCalledWith(c, expect.objectContaining({ version: 123 }), 42, false) }) it('rejects the concurrent update when the key has only creation permission', async () => { const { post } = await import('../supabase/functions/_backend/public/channel/post.ts') updateOrCreateChannel.mockRejectedValueOnce(Object.assign(new Error('duplicate channel'), { code: '23505', constraint: 'unique_name_app_id' })) const initialLookup = buildAdminChain() const existingLookup = buildAdminChain({ existingChannelId: 42 }) let channelLookups = 0 adminQuery.mockImplementation((text: string, values: unknown[]) => { if (text.includes('FROM public.channels')) channelLookups++ return channelLookups > 1 ? existingLookup(text, values) : initialLookup(text, values) }) checkPermission.mockImplementation(async (_c: unknown, permission: string) => permission !== 'channel.update_settings') await expect(post(context(), { app_id: 'com.test.channel-race', channel: 'preview' }, apiKey())).rejects.toThrow('You can\'t access this app') expect(updateOrCreateChannel).toHaveBeenCalledOnce() }) it('does not insert when the channel lookup fails', async () => { const { post } = await import('../supabase/functions/_backend/public/channel/post.ts') const failure = new Error('lookup unavailable') adminQuery.mockRejectedValueOnce(failure) await expect(post(context(), { app_id: 'com.test.channel-lookup', channel: 'preview' }, apiKey())).rejects.toBe(failure) expect(updateOrCreateChannel).not.toHaveBeenCalled() }) it('retries a channel conflict only once', async () => { const { post } = await import('../supabase/functions/_backend/public/channel/post.ts') const conflict = Object.assign(new Error('duplicate channel'), { code: '23505', constraint: 'unique_name_app_id' }) updateOrCreateChannel.mockRejectedValueOnce(conflict).mockRejectedValueOnce(conflict) await expect(post(context(), { app_id: 'com.test.channel-race', channel: 'preview' }, apiKey())).rejects.toBe(conflict) expect(updateOrCreateChannel).toHaveBeenCalledTimes(2) }) it('defaults legacy public mobile channel writes to electron false', async () => { const { post } = await import('../supabase/functions/_backend/public/channel/post.ts') const c = context() await post(c, { app_id: 'com.test.legacy-mobile', channel: 'ios-default', public: true, ios: true, android: false, }, apiKey()) expect(updateOrCreateChannel).toHaveBeenCalledWith( c, expect.objectContaining({ app_id: 'com.test.legacy-mobile', name: 'ios-default', public: true, ios: true, android: false, electron: false, }), null, true, ) expect(c.json).toHaveBeenCalledWith({ status: 'ok' }) }) it('preserves explicit electron platform selection', async () => { const { post } = await import('../supabase/functions/_backend/public/channel/post.ts') const c = context() await post(c, { app_id: 'com.test.explicit-electron', channel: 'all-platforms', public: true, ios: true, android: true, electron: true, }, apiKey()) expect(updateOrCreateChannel).toHaveBeenCalledWith(c, expect.objectContaining({ electron: true }), null, true) }) it('keeps legacy all-platform public writes electron-compatible when both mobile flags are true', async () => { const { post } = await import('../supabase/functions/_backend/public/channel/post.ts') const c = context() await post(c, { app_id: 'com.test.legacy-all-platforms', channel: 'production', public: true, ios: true, android: true, }, apiKey()) expect(updateOrCreateChannel).toHaveBeenCalledWith(c, expect.not.objectContaining({ electron: false }), null, true) }) it('requires app settings permission to create a public channel', async () => { checkPermission .mockResolvedValueOnce(true) .mockResolvedValueOnce(false) const { post } = await import('../supabase/functions/_backend/public/channel/post.ts') const c = context() await expect(post(c, { app_id: 'com.test.preview', channel: 'preview-default', public: true, }, apiKey())).rejects.toMatchObject({ cause: expect.objectContaining({ error: 'cannot_access_app' }), }) expect(checkPermission).toHaveBeenNthCalledWith(1, c, 'app.create_channel', { appId: 'com.test.preview' }) expect(checkPermission).toHaveBeenNthCalledWith(2, c, 'app.update_settings', { appId: 'com.test.preview' }) expect(updateOrCreateChannel).not.toHaveBeenCalled() }) it('requires app settings permission to make an existing private channel public', async () => { adminQuery.mockImplementation(buildAdminChain({ existingChannelId: 42, existingChannelVersion: 123, existingChannelPublic: false, })) checkPermission .mockResolvedValueOnce(true) .mockResolvedValueOnce(false) const { post } = await import('../supabase/functions/_backend/public/channel/post.ts') const c = context() await expect(post(c, { app_id: 'com.test.preview', channel: 'preview-existing', public: true, }, apiKey())).rejects.toMatchObject({ cause: expect.objectContaining({ error: 'cannot_access_app' }), }) expect(checkPermission).toHaveBeenNthCalledWith(1, c, 'channel.update_settings', { appId: 'com.test.preview', channelId: 42 }) expect(checkPermission).toHaveBeenNthCalledWith(2, c, 'app.update_settings', { appId: 'com.test.preview' }) expect(updateOrCreateChannel).not.toHaveBeenCalled() }) it('allows channel settings updates that retain an already-public channel', async () => { adminQuery.mockImplementation(buildAdminChain({ existingChannelId: 42, existingChannelVersion: 123, existingChannelPublic: true, })) const { post } = await import('../supabase/functions/_backend/public/channel/post.ts') const c = context() await post(c, { app_id: 'com.test.already-public', channel: 'production', public: true, allow_emulator: true, }, apiKey()) expect(checkPermission).toHaveBeenCalledTimes(1) expect(checkPermission).toHaveBeenCalledWith(c, 'channel.update_settings', { appId: 'com.test.already-public', channelId: 42 }) expect(updateOrCreateChannel).toHaveBeenCalledWith( c, expect.objectContaining({ version: 123, public: true, allow_emulator: true }), 42, true, ) }) it('maps updatePackage onto the channel row', async () => { adminQuery.mockImplementation(buildAdminChain({ existingChannelId: 42, existingChannelVersion: 123, existingChannelPublic: true, })) const { post } = await import('../supabase/functions/_backend/public/channel/post.ts') const c = context() await post(c, { app_id: 'com.test.update-package', channel: 'production', updatePackage: 'zip_from_builtin', }, apiKey()) expect(updateOrCreateChannel).toHaveBeenCalledWith( c, expect.objectContaining({ update_package: 'zip_from_builtin' }), 42, true, ) }) it('maps the metadata alias onto the version_number strategy', async () => { adminQuery.mockImplementation(buildAdminChain({ existingChannelId: 42, existingChannelVersion: 123, existingChannelPublic: true, })) const { post } = await import('../supabase/functions/_backend/public/channel/post.ts') const c = context() await post(c, { app_id: 'com.test.disable-auto-update', channel: 'production', disableAutoUpdate: 'metadata' as any, }, apiKey()) expect(updateOrCreateChannel).toHaveBeenCalledWith( c, expect.objectContaining({ disable_auto_update: 'version_number' }), 42, true, ) }) it('rejects an unknown disable auto update strategy with a readable error', async () => { adminQuery.mockImplementation(buildAdminChain({ existingChannelId: 42, existingChannelVersion: 123, existingChannelPublic: true, })) const { post } = await import('../supabase/functions/_backend/public/channel/post.ts') const c = context() await expect(post(c, { app_id: 'com.test.disable-auto-update', channel: 'production', disableAutoUpdate: 'meta' as any, }, apiKey())).rejects.toMatchObject({ cause: expect.objectContaining({ error: 'invalid_disable_auto_update' }), }) expect(updateOrCreateChannel).not.toHaveBeenCalled() }) it('maps zip package mismatches from the database', async () => { adminQuery.mockImplementation(buildAdminChain({ existingChannelId: 42, existingChannelVersion: 123, existingChannelPublic: true, })) updateOrCreateChannel.mockRejectedValue(new Error('CHANNEL_ZIP_REQUIRED: Channel "production" requires a zip package, but bundle "1.2.3" has no zip. Upload a full zip (omit --delta-only) or set the channel to delta only / zip and delta.')) const { post } = await import('../supabase/functions/_backend/public/channel/post.ts') const c = context() await expect(post(c, { app_id: 'com.test.update-package', channel: 'production', updatePackage: 'zip', }, apiKey())).rejects.toMatchObject({ cause: expect.objectContaining({ error: 'channel_zip_required' }), }) }) it('preserves the stable version for a settings-only update without channel.read or bundle lookup', async () => { const fromCalls: string[] = [] adminQuery.mockImplementation(buildAdminChain({ existingChannelId: 42, existingChannelVersion: 123, fromCalls, })) const { post } = await import('../supabase/functions/_backend/public/channel/post.ts') const c = context() await post(c, { app_id: 'com.test.settings-only', channel: 'production', public: false, }, apiKey()) expect(checkPermission).toHaveBeenCalledTimes(1) expect(checkPermission).toHaveBeenCalledWith(c, 'channel.update_settings', { appId: 'com.test.settings-only', channelId: 42 }) expect(fromCalls).toEqual(['channels', 'apps']) expect(updateOrCreateChannel).toHaveBeenCalledWith(c, expect.objectContaining({ version: 123, public: false }), 42, true) }) it('requires promote permission before explicit version lookup, even when the name is current', async () => { const fromCalls: string[] = [] adminQuery.mockImplementation(buildAdminChain({ existingChannelId: 42, existingChannelVersion: 123, fromCalls, })) checkPermission .mockResolvedValueOnce(true) .mockResolvedValueOnce(false) const { post } = await import('../supabase/functions/_backend/public/channel/post.ts') await expect(post(context(), { app_id: 'com.test.same-version', channel: 'production', version: '1.0.0', }, apiKey())).rejects.toMatchObject({ cause: expect.objectContaining({ error: 'cannot_access_app' }), }) expect(checkPermission).toHaveBeenNthCalledWith(1, expect.anything(), 'channel.update_settings', { appId: 'com.test.same-version', channelId: 42 }) expect(checkPermission).toHaveBeenNthCalledWith(2, expect.anything(), 'channel.promote_bundle', { appId: 'com.test.same-version', channelId: 42 }) expect(fromCalls).toEqual(['channels', 'apps']) expect(updateOrCreateChannel).not.toHaveBeenCalled() }) it('uses the admin version lookup after a scoped channel key proves settings and promote permissions', async () => { adminQuery.mockImplementation(buildAdminChain({ existingChannelId: 42, existingChannelVersion: 123, versionId: 456, })) const { post } = await import('../supabase/functions/_backend/public/channel/post.ts') const c = context() await post(c, { app_id: 'com.test.channel-admin-version', channel: 'production', version: '2.0.0', }, apiKey()) expect(checkPermission).toHaveBeenNthCalledWith(1, c, 'channel.update_settings', { appId: 'com.test.channel-admin-version', channelId: 42 }) expect(checkPermission).toHaveBeenNthCalledWith(2, c, 'channel.promote_bundle', { appId: 'com.test.channel-admin-version', channelId: 42 }) expect(updateOrCreateChannel).toHaveBeenCalledWith(c, expect.objectContaining({ version: 456 }), 42, false) }) it('writes a rollout promotion as the stable version when body.version is omitted', async () => { adminQuery.mockImplementation(buildAdminChain({ existingChannelId: 42, existingChannelVersion: 123, existingRolloutVersion: 456, })) const { post } = await import('../supabase/functions/_backend/public/channel/post.ts') const c = context() await post(c, { app_id: 'com.test.promote-rollout', channel: 'production', promoteToStable: true, }, apiKey()) expect(checkPermission).toHaveBeenCalledWith(c, 'channel.promote_bundle', { appId: 'com.test.promote-rollout', channelId: 42 }) expect(updateOrCreateChannel).toHaveBeenCalledWith(c, expect.objectContaining({ version: 456, rollout_version: null }), 42, false) }) it('keeps promoteToStable plus rolloutVersion as promote-and-clear', async () => { adminQuery.mockImplementation(buildAdminChain({ existingChannelId: 42, existingChannelVersion: 123, existingRolloutVersion: 456, versionId: 789, })) const { post } = await import('../supabase/functions/_backend/public/channel/post.ts') const c = context() await post(c, { app_id: 'com.test.promote-with-target', channel: 'production', promoteToStable: true, rolloutVersion: '1.5.16', }, apiKey()) expect(updateOrCreateChannel).toHaveBeenCalledWith(c, expect.objectContaining({ version: 789, rollout_version: null, rollout_enabled: false, rollout_percentage_bps: 0, }), 42, false) }) it('promotes the leftover rollout to stable and assigns a new rollout target in one write', async () => { adminQuery.mockImplementation(buildAdminChain({ existingChannelId: 42, existingChannelVersion: 123, existingRolloutVersion: 456, versionId: 789, })) const { post } = await import('../supabase/functions/_backend/public/channel/post.ts') const c = context() await post(c, { app_id: 'com.test.advance-rollout', channel: 'production', advanceRollout: true, rolloutVersion: '1.5.16', rolloutPercentageBps: 1000, }, apiKey()) expect(checkPermission).toHaveBeenCalledWith(c, 'channel.promote_bundle', { appId: 'com.test.advance-rollout', channelId: 42 }) expect(updateOrCreateChannel).toHaveBeenCalledWith(c, expect.objectContaining({ version: 456, rollout_version: 789, rollout_enabled: true, rollout_percentage_bps: 1000, }), 42, false) }) it('unlinks the rollout bundle when progressive rollout is disabled', async () => { adminQuery.mockImplementation(buildAdminChain({ existingChannelId: 42, existingChannelVersion: 123, existingRolloutVersion: 456, })) const { post } = await import('../supabase/functions/_backend/public/channel/post.ts') const c = context() await post(c, { app_id: 'com.test.disable-rollout', channel: 'production', rolloutEnabled: false, }, apiKey()) expect(checkPermission).toHaveBeenCalledWith(c, 'channel.update_settings', { appId: 'com.test.disable-rollout', channelId: 42 }) expect(checkPermission).toHaveBeenCalledWith(c, 'channel.promote_bundle', { appId: 'com.test.disable-rollout', channelId: 42 }) expect(updateOrCreateChannel).toHaveBeenCalledWith(c, expect.objectContaining({ rollout_enabled: false, rollout_version: null, rollout_paused_at: null, rollout_pause_reason: null, }), 42, true) }) it('forces unlink on disable even when rolloutVersion is also sent', async () => { adminQuery.mockImplementation(buildAdminChain({ existingChannelId: 42, existingChannelVersion: 123, existingRolloutVersion: 456, versionId: 789, })) const { post } = await import('../supabase/functions/_backend/public/channel/post.ts') const c = context() await post(c, { app_id: 'com.test.disable-with-version', channel: 'production', rolloutEnabled: false, rolloutVersion: '2.0.0', }, apiKey()) expect(updateOrCreateChannel).toHaveBeenCalledWith(c, expect.objectContaining({ rollout_enabled: false, rollout_version: null, }), 42, true) }) it('does not require promote when disabling with no linked rollout bundle', async () => { adminQuery.mockImplementation(buildAdminChain({ existingChannelId: 42, existingChannelVersion: 123, existingRolloutVersion: null, })) const { post } = await import('../supabase/functions/_backend/public/channel/post.ts') const c = context() await post(c, { app_id: 'com.test.disable-no-rollout', channel: 'production', rolloutEnabled: false, }, apiKey()) expect(checkPermission).toHaveBeenCalledWith(c, 'channel.update_settings', { appId: 'com.test.disable-no-rollout', channelId: 42 }) expect(checkPermission).not.toHaveBeenCalledWith(c, 'channel.promote_bundle', expect.anything()) expect(updateOrCreateChannel).toHaveBeenCalledWith(c, expect.objectContaining({ rollout_enabled: false, rollout_version: null, }), 42, true) }) it('creates and promotes a new channel in one transaction after its scoped grant exists', async () => { const { post } = await import('../supabase/functions/_backend/public/channel/post.ts') const c = context() const key = apiKey() await post(c, { app_id: 'com.test.new-channel-version', channel: 'preview', version: '1.0.0', }, key) expect(updateOrCreateChannel).not.toHaveBeenCalled() expect(assertCanPromoteChannelInTransaction).toHaveBeenCalledWith(c, expect.objectContaining({ app_id: 'com.test.new-channel-version', channel_id: 77, }), key, dbClient) expect(setChannelInTransaction).toHaveBeenCalledWith(c, { app_id: 'com.test.new-channel-version', channel_id: 77, version_id: 123, }, key, dbClient) expect(transactionEvents).toEqual([ 'begin', 'headers', 'permission:app.create_channel', 'insert', 'promote', 'version', 'set', 'commit', ]) expect(c.json).toHaveBeenCalledWith({ status: 'ok', id: 77, public: false }) }) it('rolls back the new channel when its post-insert scoped promotion permission is denied', async () => { const denied = new HTTPException(400, { message: 'You can\'t access this app' }) assertCanPromoteChannelInTransaction.mockImplementation(async () => { transactionEvents.push('promote') throw denied }) const { post } = await import('../supabase/functions/_backend/public/channel/post.ts') await expect(post(context(), { app_id: 'com.test.new-channel-permission-denied', channel: 'preview', version: '1.0.0', }, apiKey())).rejects.toBe(denied) expect(transactionEvents).toEqual([ 'begin', 'headers', 'permission:app.create_channel', 'insert', 'promote', 'rollback', ]) expect(setChannelInTransaction).not.toHaveBeenCalled() }) it('rolls back the new channel when the requested bundle is missing', async () => { configureTransaction(null) const { post } = await import('../supabase/functions/_backend/public/channel/post.ts') await expect(post(context(), { app_id: 'com.test.new-channel-version-missing', channel: 'preview', version: '1.0.0', }, apiKey())).rejects.toThrow() expect(transactionEvents).toEqual([ 'begin', 'headers', 'permission:app.create_channel', 'insert', 'promote', 'version', 'rollback', ]) expect(setChannelInTransaction).not.toHaveBeenCalled() }) it('rolls back the new channel when bundle association fails', async () => { const associationError = new HTTPException(400, { message: 'Cannot associate bundle with the new channel' }) setChannelInTransaction.mockImplementation(async () => { transactionEvents.push('set') throw associationError }) const { post } = await import('../supabase/functions/_backend/public/channel/post.ts') await expect(post(context(), { app_id: 'com.test.new-channel-association-failure', channel: 'preview', version: '1.0.0', }, apiKey())).rejects.toBe(associationError) expect(transactionEvents).toEqual([ 'begin', 'headers', 'permission:app.create_channel', 'insert', 'promote', 'version', 'set', 'rollback', ]) }) it('requires promote bundle permission to explicitly clear a channel version', async () => { adminQuery.mockImplementation(buildAdminChain({ existingChannelId: 42, existingChannelVersion: 123 })) checkPermission .mockResolvedValueOnce(true) .mockResolvedValueOnce(false) const { post } = await import('../supabase/functions/_backend/public/channel/post.ts') await expect(post(context(), { app_id: 'com.test.clear-version', channel: 'production', version: null, }, apiKey())).rejects.toMatchObject({ cause: expect.objectContaining({ error: 'cannot_access_app' }), }) expect(updateOrCreateChannel).not.toHaveBeenCalled() }) it('filters numeric rollout target ids to active bundles', async () => { const versionQueries: Array<[string, unknown[]]> = [] adminQuery.mockImplementation(buildAdminChain({ existingChannelId: 42, existingChannelVersion: 123, versionId: 456, versionQueries, })) const { post } = await import('../supabase/functions/_backend/public/channel/post.ts') await post(context(), { app_id: 'com.test.rollout-id', channel: 'production', rolloutVersion: 456, }, apiKey()) const rolloutIdQuery = versionQueries.find(([, values]) => values.includes(456)) expect(rolloutIdQuery).toBeDefined() expect(rolloutIdQuery![0]).toContain('id = $3') expect(rolloutIdQuery![0]).toContain('deleted = false') expect(checkPermission).toHaveBeenCalledWith(expect.anything(), 'channel.promote_bundle', { appId: 'com.test.rollout-id', channelId: 42 }) }) it('rejects rollout target changes without channel promote permission', async () => { adminQuery.mockImplementation(buildAdminChain({ existingChannelId: 42, existingChannelVersion: 123 })) checkPermission .mockResolvedValueOnce(true) .mockResolvedValueOnce(false) const { post } = await import('../supabase/functions/_backend/public/channel/post.ts') await expect(post(context(), { app_id: 'com.test.rollout-auth', channel: 'production', rolloutVersion: '2.0.0', }, apiKey())).rejects.toMatchObject({ cause: expect.objectContaining({ error: 'cannot_promote_bundle' }), }) expect(updateOrCreateChannel).not.toHaveBeenCalled() }) it('rejects rollout targets when a channel has no stable bundle', async () => { adminQuery.mockImplementation(buildAdminChain({ existingChannelId: 42, existingChannelVersion: null })) const { post } = await import('../supabase/functions/_backend/public/channel/post.ts') await expect(post(context(), { app_id: 'com.test.rollout-no-stable', channel: 'new-rollout-channel', rolloutVersion: '2.0.0', }, apiKey())).rejects.toMatchObject({ cause: expect.objectContaining({ error: 'missing_stable_version' }), }) expect(updateOrCreateChannel).not.toHaveBeenCalled() }) })