tests/channel-post.unit.test.ts
Rama main · réplica de lectura del repositorio original.
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<string, unknown> = {}) => {
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()
})
})