Ctrl K
ComunidadGuías…

tests/channel-post.unit.test.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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()
  })
})