Ctrl K
ComunidadGuías…

tests/channel_self.test.ts

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

Archivo original ↗Preguntar a KiBot
import type { SimpleErrorResponse } from '../supabase/functions/_backend/utils/hono.ts'
import type { DeviceLink, HttpMethod } from './test-utils.ts'
import { randomUUID } from 'node:crypto'
import { env } from 'node:process'
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import { fetchTestRequest, getBaseData, getEndpointUrl, getSupabaseClient, PLUGIN_BASE_URL, resetAndSeedAppData, resetAppData, resetAppDataStats, warmEdgeEndpoint } from './test-utils.ts'

interface ChannelInfo {
  id: number
  name: string
  public: boolean
  allow_self_set: boolean
}

type ChannelsListResponse = ChannelInfo[]

const id = randomUUID()
const APPNAME = `com.sa.${id}`
const USE_CLOUDFLARE = env.USE_CLOUDFLARE_WORKERS === 'true'

function getUniqueBaseData(appId: string) {
  const data = getBaseData(appId)
  data.device_id = randomUUID().toLowerCase()
  return data
}

async function fetchEndpoint(method: HttpMethod, bodyIn: object) {
  const url = new URL(`${PLUGIN_BASE_URL}/channel_self`)
  if (method === 'DELETE') {
    for (const [key, value] of Object.entries(bodyIn))
      url.searchParams.append(key, value.toString())
  }

  const body = method !== 'DELETE' ? JSON.stringify(bodyIn) : undefined
  return fetchTestRequest(url.toString(), {
    method,
    body,
  })
}

async function fetchGetChannels(queryParams: Record<string, string>) {
  const url = new URL(`${PLUGIN_BASE_URL}/channel_self`)
  for (const [key, value] of Object.entries(queryParams))
    url.searchParams.append(key, value)

  return fetchTestRequest(url.toString(), {
    method: 'GET',
  })
}

async function getResponseErrorCode(response: Response) {
  const json = await response.json<SimpleErrorResponse>()
  return json.error
}

type Awaitable<T> = T | PromiseLike<T>

async function withSupabaseCall<T extends { error?: { message?: string } | null }>(
  fn: () => Awaitable<T>,
): Promise<T> {
  return await Promise.resolve(fn())
}

beforeAll(async () => {
  await resetAndSeedAppData(APPNAME)
  // Cold first /channel_self request can 502 under Deno shard load; warm before assertions.
  await warmEdgeEndpoint(getEndpointUrl('/channel_self'), {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(getBaseData(APPNAME)),
  })
})
afterAll(async () => {
  await resetAppData(APPNAME)
  await resetAppDataStats(APPNAME)
})

describe('invalids /channel_self tests', () => {
  it('[POST] invalid json', async () => {
    const response = await fetch(`${PLUGIN_BASE_URL}/channel_self`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: 'invalid json ;-)',
    })

    expect(response.ok).toBe(false)
    await response.arrayBuffer()
  })

  it('[POST] empty json', async () => {
    const response = await fetchEndpoint('POST', {})

    const error = await getResponseErrorCode(response)

    expect(error).toBe('invalid_json_body')
  })

  it.each(['POST', 'PUT', 'DELETE'] as const)('[%s] invalid semver', async (method) => {
    const data = getUniqueBaseData(APPNAME)
    data.version_build = 'invalid semver'

    const response = await fetchEndpoint(method, data)
    expect(await getResponseErrorCode(response)).toBe('semver_error')
  })

  it.each([
    ['POST', 'device_id'],
    ['POST', 'app_id'],
    ['PUT', 'device_id'],
    ['PUT', 'app_id'],
    ['DELETE', 'device_id'],
    ['DELETE', 'app_id'],
  ] as const)('[%s] without field (%s)', async (method, field) => {
    const data = getUniqueBaseData(APPNAME)
    delete data[field]

    const response = await fetchEndpoint(method, data)
    expect(response.status).toBe(400)
    expect(await getResponseErrorCode(response)).toBe(`missing_${field}`)
  })

  it('[POST] without channel', async () => {
    const data = getUniqueBaseData(APPNAME)
    delete data.channel

    const response = await fetchEndpoint('POST', data)
    expect(response.status).toBe(200)

    const error = await getResponseErrorCode(response)
    expect(error).toBe('missing_channel')
  })

  it('[POST] with a channel that does not exist', async () => {
    const data = getUniqueBaseData(APPNAME)
    data.channel = 'unexisting_channel'

    const response = await fetchEndpoint('POST', data)
    expect(response.status).toBe(200)

    const error = await getResponseErrorCode(response)
    expect(error).toBe('channel_not_found')
  })

  it('[POST] with a channel that does not allow self assign', async () => {
    const data = getUniqueBaseData(APPNAME)
    if (!data.channel)
      throw new Error('channel is undefined')

    const { error } = await getSupabaseClient().from('channels').update({ allow_device_self_set: false }).eq('name', data.channel).eq('app_id', APPNAME).select('id').single()

    expect(error).toBeNull()

    try {
      const response = await fetchEndpoint('POST', data)
      expect(response.status).toBe(200)

      const responseError = await getResponseErrorCode(response)
      expect(responseError).toBe('public_channel_self_set_not_allowed')
    }
    finally {
      const { error } = await getSupabaseClient().from('channels').update({ allow_device_self_set: true }).eq('name', data.channel).eq('app_id', APPNAME).select('id').single()

      expect(error).toBeNull()
    }
  })

  it('[PUT] with a version that does not exist', async () => {
    // Version check was removed from channel_self - devices with unknown versions can still get channel info
    const data = getUniqueBaseData(APPNAME)
    data.version_name = `1.0.${Math.floor(Math.random() * 10000000)}`

    const response = await fetchEndpoint('PUT', data)
    expect(response.status).toBe(200)

    const responseJSON = await response.json<{ channel: string, status: string }>()
    expect(responseJSON.channel).toBeTruthy()
    expect(responseJSON.status).toBe('default')
  })
})

describe('[GET] /channel_self tests', () => {
  it('[GET] without query params should return error', async () => {
    const response = await fetch(`${PLUGIN_BASE_URL}/channel_self`, {
      method: 'GET',
    })

    expect(response.ok).toBe(false)
    const error = await getResponseErrorCode(response)
    expect(error).toBe('invalid_query_parameters')
  })

  it('[GET] with invalid app_id format', async () => {
    const data = getUniqueBaseData(APPNAME)
    data.app_id = 'invalid-app-id'
    const response = await fetchGetChannels(data as any)

    expect(response.status).toBe(400)
    const error = await getResponseErrorCode(response)
    expect(error).toBe('invalid_query_parameters')
  })

  it('[GET] with missing app_id', async () => {
    const data = getUniqueBaseData(APPNAME)
    delete data.app_id
    const response = await fetchGetChannels(data as any)

    expect(response.ok).toBe(false)
    const error = await getResponseErrorCode(response)
    expect(error).toBe('missing_app_id')
  })

  it('[GET] with invalid platform', async () => {
    const data = getUniqueBaseData(APPNAME)
    data.platform = 'windows'
    const response = await fetchGetChannels(data as any)

    expect(response.status).toBe(400)
    const error = await getResponseErrorCode(response)
    expect(error).toBe('invalid_query_parameters')
  })

  it('[GET] with non-existent app_id', async () => {
    const data = getUniqueBaseData(APPNAME)
    data.app_id = 'com.nonexistent.app'
    const response = await fetchGetChannels(data as any)

    expect(response.status).toBe(429)
    const error = await getResponseErrorCode(response)
    expect(error).toBe('on_premise_app')
  })

  it('[GET] should return compatible channels for iOS', async () => {
    await resetAndSeedAppData(APPNAME)

    const data = getUniqueBaseData(APPNAME)
    data.platform = 'ios'
    data.is_emulator = false
    data.is_prod = true
    const response = await fetchGetChannels(data as any)

    expect(response.ok).toBe(true)
    const json = await response.json<ChannelsListResponse>()

    expect(json).toBeDefined()
    expect(Array.isArray(json)).toBe(true)

    const channelNames = json.map(ch => ch.name)
    expect(channelNames).toContain('beta')
    expect(channelNames).toContain('development')
    expect(channelNames).not.toContain('production')
    expect(channelNames).not.toContain('no_access')
    expect(json).toHaveLength(2)
  })

  it('[GET] should return compatible channels for Android', async () => {
    await resetAndSeedAppData(APPNAME)

    const data = getUniqueBaseData(APPNAME)
    data.platform = 'android'
    data.is_emulator = false
    data.is_prod = true
    const response = await fetchGetChannels(data as any)

    expect(response.ok).toBe(true)
    const json = await response.json() as ChannelsListResponse

    expect(json).toBeDefined()
    expect(Array.isArray(json)).toBe(true)

    const channelNames = json.map(ch => ch.name)
    expect(channelNames).toContain('production')
    expect(channelNames).toContain('beta')
    expect(channelNames).not.toContain('development')
    expect(channelNames).not.toContain('no_access')
    expect(json).toHaveLength(2)
  })

  it('[GET] should return public channels matching platform/device when self-set is disabled', async () => {
    await resetAndSeedAppData(APPNAME)

    // Ensure all channels have self set disabled (should be default)
    const { error: updateError } = await getSupabaseClient()
      .from('channels')
      .update({ allow_device_self_set: false })
      .eq('app_id', APPNAME)

    expect(updateError).toBeNull()

    const data = getUniqueBaseData(APPNAME)
    data.platform = 'ios'
    data.is_emulator = false
    data.is_prod = true
    const response = await fetchGetChannels(data as any)

    expect(response.ok).toBe(true)
    const json = await response.json() as ChannelsListResponse

    expect(json).toBeDefined()
    expect(Array.isArray(json)).toBe(true)
    const channelNames = json.map(ch => ch.name)
    expect(channelNames).toContain('development')
    expect(channelNames).not.toContain('production')
    expect(channelNames).not.toContain('beta')
    expect(channelNames).not.toContain('no_access')
    expect(json).toHaveLength(1)
  })

  it('[GET] should only return channels compatible with platform', async () => {
    await resetAndSeedAppData(APPNAME)

    // Ensure all channels have self set enabled (restore default state)
    const { error: updateError } = await getSupabaseClient()
      .from('channels')
      .update({ allow_device_self_set: true })
      .eq('app_id', APPNAME)

    expect(updateError).toBeNull()

    // Request iOS channels - real device (is_emulator=false)
    const data = getUniqueBaseData(APPNAME)
    data.platform = 'ios'
    data.is_emulator = false
    data.is_prod = true
    const iosResponse = await fetchGetChannels(data as any)

    expect(iosResponse.ok).toBe(true)
    const iosJson = await iosResponse.json() as ChannelsListResponse

    const iosChannelNames = iosJson.map(ch => ch.name)
    expect(iosChannelNames).toContain('beta')
    expect(iosChannelNames).toContain('development')
    expect(iosChannelNames).not.toContain('production')
    expect(iosChannelNames).not.toContain('no_access')
    expect(iosJson).toHaveLength(2)

    // Request Android channels - real device (is_emulator=false)
    data.platform = 'android'
    data.is_emulator = false
    data.is_prod = true
    const androidResponse = await fetchGetChannels(data as any)

    expect(androidResponse.ok).toBe(true)
    const androidJson = await androidResponse.json() as ChannelsListResponse

    const androidChannelNames = androidJson.map(ch => ch.name)
    expect(androidChannelNames).toContain('production')
    expect(androidChannelNames).toContain('beta')
    expect(androidChannelNames).not.toContain('development')
    expect(androidChannelNames).not.toContain('no_access')
    expect(androidJson).toHaveLength(2)

    // Request Electron channels - real device (is_emulator=false)
    data.platform = 'electron'
    data.is_emulator = false
    data.is_prod = true
    const electronResponse = await fetchGetChannels(data as any)

    expect(electronResponse.ok).toBe(true)
    const electronJson = await electronResponse.json() as ChannelsListResponse

    const electronChannelNames = electronJson.map(ch => ch.name)
    // electron_only channel has electron=true, ios=false, android=false
    // beta channel has electron=true, ios=true, android=true
    // production has electron=true, ios=false, android=true
    // development has electron=false
    // no_access has electron=false
    expect(electronChannelNames).toContain('electron_only')
    expect(electronChannelNames).toContain('beta')
    expect(electronChannelNames).toContain('production')
    expect(electronChannelNames).not.toContain('development')
    expect(electronChannelNames).not.toContain('no_access')
    expect(electronJson).toHaveLength(3)
  })

  it('[GET] should return compatible channels for Electron', async () => {
    await resetAndSeedAppData(APPNAME)

    const data = getUniqueBaseData(APPNAME)
    data.platform = 'electron'
    data.is_emulator = false
    data.is_prod = true
    const response = await fetchGetChannels(data as any)

    expect(response.ok).toBe(true)
    const json = await response.json<ChannelsListResponse>()

    expect(json).toBeDefined()
    expect(Array.isArray(json)).toBe(true)

    const channelNames = json.map(ch => ch.name)
    // electron_only is electron-only channel (electron=true, allow_device_self_set=true)
    // beta has electron=true and allow_device_self_set=true
    // production has electron=true but is_public=true
    expect(channelNames).toContain('electron_only')
    expect(channelNames).toContain('beta')
    expect(channelNames).toContain('production')
    expect(channelNames).not.toContain('development') // electron=false
    expect(channelNames).not.toContain('no_access') // electron=false
    expect(json).toHaveLength(3)
  })

  it('[GET] should filter channels based on emulator compatibility', async () => {
    await resetAndSeedAppData(APPNAME)

    // Set beta channel to NOT allow emulators
    const { error: updateError, data: channelData } = await getSupabaseClient()
      .from('channels')
      .update({ allow_emulator: false })
      .eq('name', 'beta')
      .eq('app_id', APPNAME)
      .select('allow_emulator')
      .single()

    expect(updateError).toBeNull()
    expect(channelData?.allow_emulator).toBe(false)

    try {
      const data = getUniqueBaseData(APPNAME)
      data.platform = 'ios'
      data.is_emulator = true
      data.is_prod = true
      // Test emulator device - should NOT get beta channel
      const emulatorResponse = await fetchGetChannels(data as any)

      expect(emulatorResponse.ok).toBe(true)
      const emulatorJson = await emulatorResponse.json() as ChannelsListResponse
      const emulatorChannelNames = emulatorJson.map(ch => ch.name)

      expect(emulatorChannelNames).toContain('development') // should be included for emulators
      expect(emulatorChannelNames).not.toContain('beta') // should be filtered out for emulators

      // Test real device - allow_emulator does not affect physical devices
      data.is_emulator = false
      data.is_prod = true
      const prodResponse = await fetchGetChannels(data as any)

      expect(prodResponse.ok).toBe(true)
      const prodJson = await prodResponse.json() as ChannelsListResponse
      const prodChannelNames = prodJson.map(ch => ch.name)

      expect(prodChannelNames).toContain('beta')
      expect(prodChannelNames).toContain('development')
      expect(prodChannelNames).not.toContain('production')
      expect(prodChannelNames).not.toContain('no_access')
      expect(prodJson).toHaveLength(2)
    }
    finally {
      // Reset beta channel to allow emulators
      const { error: updateError, data: channelData } = await getSupabaseClient()
        .from('channels')
        .update({ allow_emulator: true })
        .eq('name', 'beta')
        .eq('app_id', APPNAME)
        .select('allow_emulator')
        .single()

      expect(updateError).toBeNull()
      expect(channelData?.allow_emulator).toBe(true)
    }
  })

  it('[GET] should filter channels based on dev/prod compatibility', async () => {
    await resetAndSeedAppData(APPNAME)

    // Set development channel to NOT allow dev devices
    const { error: updateError } = await getSupabaseClient()
      .from('channels')
      .update({ allow_dev: false })
      .eq('name', 'development')
      .eq('app_id', APPNAME)

    expect(updateError).toBeNull()

    try {
      // Test dev device - should only get channels that allow dev builds
      const data = getUniqueBaseData(APPNAME)
      data.platform = 'ios'
      data.is_emulator = false
      data.is_prod = false
      const devResponse = await fetchGetChannels(data as any)

      expect(devResponse.ok).toBe(true)
      const devJson = await devResponse.json() as ChannelsListResponse

      const devChannelNames = devJson.map(ch => ch.name)
      expect(devChannelNames).toContain('beta')
      expect(devChannelNames).not.toContain('development')
      expect(devChannelNames).not.toContain('production')
      expect(devChannelNames).not.toContain('no_access')
      expect(devJson).toHaveLength(1)

      // Test production device - should get channels that allow prod builds
      data.is_prod = true
      const prodResponse = await fetchGetChannels(data as any)

      expect(prodResponse.ok).toBe(true)
      const prodJson = await prodResponse.json() as ChannelsListResponse

      const prodChannelNames = prodJson.map(ch => ch.name)
      expect(prodChannelNames).toContain('beta')
      expect(prodChannelNames).toContain('development')
      expect(prodChannelNames).not.toContain('production')
      expect(prodChannelNames).not.toContain('no_access')
      expect(prodJson).toHaveLength(2)
    }
    finally {
      // Reset development channel to allow dev devices
      await getSupabaseClient()
        .from('channels')
        .update({ allow_dev: true })
        .eq('name', 'development')
        .eq('app_id', APPNAME)
    }
  })

  it('[GET] should default prod to true when not specified', async () => {
    await resetAndSeedAppData(APPNAME)

    const data = getUniqueBaseData(APPNAME)
    data.platform = 'ios'
    data.is_emulator = false
    data.is_prod = true
    const response = await fetchGetChannels(data as any)

    expect(response.ok).toBe(true)
    const json = await response.json() as ChannelsListResponse

    const channelNames = json.map(ch => ch.name)
    expect(channelNames).toContain('beta')
    expect(channelNames).toContain('development')
    expect(channelNames).not.toContain('production')
    expect(channelNames).not.toContain('no_access')
    expect(Array.isArray(json)).toBe(true)
    expect(json).toHaveLength(2)
  })

  it('[GET] should return channels for emulator devices', async () => {
    await resetAndSeedAppData(APPNAME)

    const data = getUniqueBaseData(APPNAME)
    data.platform = 'ios'
    data.is_emulator = true
    data.is_prod = true
    const response = await fetchGetChannels(data as any)

    expect(response.ok).toBe(true)
    const json = await response.json() as ChannelsListResponse

    expect(json).toBeDefined()
    expect(Array.isArray(json)).toBe(true)

    // Emulator device should get iOS channels that allow emulators
    const channelNames = json.map(ch => ch.name)
    expect(channelNames).toContain('beta')
    expect(channelNames).toContain('development')

    // Should NOT include channels that have ios=false: production and no_access
    expect(channelNames).not.toContain('production')
    expect(channelNames).not.toContain('no_access')

    expect(json).toHaveLength(2)
  })
})

it('[POST] with a version that does not exist', async () => {
  const data = getUniqueBaseData(APPNAME)
  data.version_name = `1.0.350`

  const response = await fetchEndpoint('POST', data)
  expect(response.status).toBe(200)

  const responseError = await getResponseErrorCode(response)
  expect(responseError).toBeUndefined()
})

it.skipIf(USE_CLOUDFLARE)('[POST] /channel_self creates new channel_device with owner_org', async () => {
  // This test ensures that when a NEW device sets a channel for the first time,
  // the channel_devices record is created with all required fields including owner_org
  // This specifically tests the INSERT path of the upsert operation
  await resetAndSeedAppData(APPNAME)

  // First, enable allow_device_self_set for beta channel (non-default channel)
  const { error: channelUpdateError, data: betaChannel } = await getSupabaseClient()
    .from('channels')
    .update({ allow_device_self_set: true })
    .eq('name', 'beta')
    .eq('app_id', APPNAME)
    .select('id, owner_org')
    .single()

  expect(channelUpdateError).toBeNull()
  expect(betaChannel).toBeTruthy()

  try {
    // Use a brand new device_id that has never been in channel_devices
    const data = getUniqueBaseData(APPNAME)
    data.device_id = randomUUID().toLowerCase()
    data.channel = 'beta' // Use non-default channel to trigger INSERT

    // Verify no existing channel_devices record for this device
    const { data: existingRecord } = await getSupabaseClient()
      .from('channel_devices')
      .select('*')
      .eq('device_id', data.device_id)
      .eq('app_id', APPNAME)

    expect(existingRecord).toHaveLength(0)

    // Call POST endpoint to set channel (this triggers INSERT in upsert)
    const response = await fetchEndpoint('POST', data)
    expect(response.ok).toBeTruthy()
    expect(await response.json()).toEqual({ status: 'ok' })

    // Verify channel_devices record was created with owner_org
    const { data: channelDevice, error: channelDeviceError } = await getSupabaseClient()
      .from('channel_devices')
      .select('device_id, app_id, channel_id, owner_org, is_self_set')
      .eq('device_id', data.device_id)
      .eq('app_id', APPNAME)
      .single()

    expect(channelDeviceError).toBeNull()
    expect(channelDevice).toBeTruthy()
    // Device self-assignment is the only override kind that expires after 90 days.
    expect(channelDevice!.is_self_set).toBe(true)
    expect(channelDevice!.device_id).toBe(data.device_id)
    expect(channelDevice!.app_id).toBe(APPNAME)
    expect(channelDevice!.owner_org).toBeTruthy() // Most important: owner_org must be set
    expect(typeof channelDevice!.owner_org).toBe('string')
    expect(channelDevice!.channel_id).toBe(betaChannel!.id)

    // Verify owner_org matches the channel's owner_org
    expect(channelDevice!.owner_org).toBe(betaChannel!.owner_org)
  }
  finally {
    // Reset beta channel to not allow self set
    const { error: resetError } = await getSupabaseClient()
      .from('channels')
      .update({ allow_device_self_set: false })
      .eq('name', 'beta')
      .eq('app_id', APPNAME)

    expect(resetError).toBeNull()
  }
})

it.skipIf(USE_CLOUDFLARE)('[POST] /channel_self with default channel', async () => {
  await resetAndSeedAppData(APPNAME)

  const data = getUniqueBaseData(APPNAME)
  const deviceId = randomUUID().toLowerCase()
  data.device_id = deviceId

  const { error: channelUpdateError, data: noAccessData } = await getSupabaseClient()
    .from('channels')
    .update({ allow_device_self_set: true })
    .eq('name', 'no_access')
    .eq('app_id', APPNAME)
    .select('id, owner_org, public')
    .single()

  expect(channelUpdateError).toBeNull()
  expect(noAccessData).toBeTruthy()
  expect(noAccessData!.public).toBeFalsy()

  try {
    const { error: overwriteUpsertError } = await getSupabaseClient()
      .from('channel_devices')
      .upsert({
        app_id: APPNAME,
        channel_id: noAccessData!.id,
        device_id: data.device_id,
        owner_org: noAccessData!.owner_org,
      }, { onConflict: 'device_id, app_id' })

    expect(overwriteUpsertError).toBeNull()

    data.channel = 'production'
    const response = await fetchEndpoint('POST', data)
    expect(response.ok).toBeTruthy()
    expect(await response.json()).toEqual({ status: 'ok' })

    const { data: channelDevice, error: channelDeviceError } = await getSupabaseClient()
      .from('channel_devices')
      .select('*')
      .eq('device_id', data.device_id)
      .eq('app_id', APPNAME)

    expect(channelDeviceError).toBeNull()
    expect(channelDevice).toBeTruthy()
    expect(channelDevice).toHaveLength(0)
  }
  finally {
    const { error: channelUpdateError } = await getSupabaseClient().from('channels').update({ allow_device_self_set: false }).eq('name', 'no_access').eq('app_id', APPNAME)

    expect(channelUpdateError).toBeNull()
  }
})

it('[PUT] /channel_self (no overwrite)', async () => {
  await resetAndSeedAppData(APPNAME)

  const data = getUniqueBaseData(APPNAME)
  const deviceId = randomUUID().toLowerCase()
  data.device_id = deviceId

  const response = await fetchEndpoint('PUT', data)
  expect(response.ok).toBe(true)

  const responseJSON = await response.json<{ channel: string, status: string }>()
  const channel = responseJSON.channel
  const status = responseJSON.status

  expect(channel).toBeTruthy()
  expect(status).toBeTruthy()

  expect(status).toBe('default')
  expect(channel).toBe(data.channel)
})

it('[PUT] /channel_self with minimum required fields', async () => {
  await resetAndSeedAppData(APPNAME)

  // Test with only the minimum required fields according to jsonRequestSchema
  const minimalData = {
    app_id: APPNAME,
    device_id: randomUUID().toLowerCase(),
    version_name: '1.0.0',
    version_build: '1.0.0',
    is_emulator: false,
    is_prod: true,
    platform: 'android',
  }

  const response = await fetchEndpoint('PUT', minimalData)
  expect(response.ok).toBe(true)

  const responseJSON = await response.json<{ channel: string, status: string }>()
  expect(responseJSON.channel).toBeTruthy()
  expect(responseJSON.status).toBe('default')
})

it('[PUT] /channel_self with all optional fields included', async () => {
  await resetAndSeedAppData(APPNAME)

  // Test with all fields including optional ones
  const fullData = {
    app_id: APPNAME,
    device_id: randomUUID().toLowerCase(),
    version_name: '1.0.0',
    version_build: '1.0.0',
    is_emulator: true,
    is_prod: false,
    platform: 'ios',
    channel: 'production',
    custom_id: 'test-custom-id',
    version_code: '100',
    version_os: '17.0',
    plugin_version: '6.0.0',
    defaultChannel: 'production',
  }

  const response = await fetchEndpoint('PUT', fullData)
  expect(response.ok).toBe(true)

  const responseJSON = await response.json<{ channel: string, status: string }>()
  expect(responseJSON.channel).toBeTruthy()
  expect(responseJSON.status).toBe('default')
  // When defaultChannel is provided, it should return that channel
  expect(responseJSON.channel).toBe('production')
})

it.skipIf(USE_CLOUDFLARE)('[PUT] /channel_self (with overwrite)', async () => {
  await resetAndSeedAppData(APPNAME)

  const data = getUniqueBaseData(APPNAME)
  const deviceId = randomUUID().toLowerCase()
  data.device_id = deviceId

  const { data: noAccessChannel, error: noAccessChannelError } = await withSupabaseCall(() =>
    getSupabaseClient()
      .from('channels')
      .select('id, owner_org')
      .eq('name', 'no_access')
      .eq('app_id', APPNAME)
      .single(),
  )

  expect(noAccessChannelError).toBeNull()
  expect(noAccessChannel).toBeTruthy()

  if (!noAccessChannel?.owner_org) {
    throw new Error('no_access channel not found or missing owner_org')
  }

  const noAccessId = noAccessChannel.id
  const ownerOrg = noAccessChannel.owner_org

  const { error } = await withSupabaseCall(() =>
    getSupabaseClient()
      .from('channel_devices')
      .upsert({
        app_id: APPNAME,
        channel_id: noAccessId,
        device_id: deviceId,
        owner_org: ownerOrg,
      }, { onConflict: 'device_id, app_id' }),
  )

  expect(error).toBeNull()

  try {
    const response = await fetchEndpoint('PUT', data)
    expect(response.ok).toBe(true)

    const responseJSON = await response.json<{ channel: string, status: string }>()
    const channel = responseJSON.channel
    const status = responseJSON.status

    expect(channel).toBeTruthy()
    expect(status).toBeTruthy()

    expect(status).toBe('override')
    expect(channel).toBe('no_access')
  }
  finally {
    const { error } = await withSupabaseCall(() =>
      getSupabaseClient()
        .from('channel_devices')
        .delete()
        .eq('device_id', deviceId)
        .eq('app_id', APPNAME)
        .eq('owner_org', ownerOrg)
        .eq('channel_id', noAccessId)
        .single(),
    )

    expect(error).toBeNull()
  }
})

it('[PUT] /channel_self with defaultChannel parameter', async () => {
  await resetAndSeedAppData(APPNAME)

  const data = getUniqueBaseData(APPNAME) as DeviceLink
  data.device_id = randomUUID().toLowerCase()
  data.defaultChannel = 'no_access'

  const response = await fetchEndpoint('PUT', data)
  expect(response.ok).toBe(true)

  const responseJSON = await response.json<{ channel: string, status: string }>()
  expect(responseJSON.channel).toBe('no_access')
  expect(responseJSON.status).toBe('default')
})

it('[PUT] /channel_self with non-existent defaultChannel', async () => {
  await resetAndSeedAppData(APPNAME)

  const data = getUniqueBaseData(APPNAME) as DeviceLink
  data.device_id = randomUUID().toLowerCase()
  data.defaultChannel = 'non_existent_channel'

  const response = await fetchEndpoint('PUT', data)
  expect(response.ok).toBe(true)

  const error = await getResponseErrorCode(response)
  expect(error).toBe('channel_not_found')
})

it('[DELETE] /channel_self (no overwrite)', async () => {
  await resetAndSeedAppData(APPNAME)

  const data = getUniqueBaseData(APPNAME)
  data.device_id = randomUUID().toLowerCase()

  const response = await fetchEndpoint('DELETE', data)
  expect(response.status).toBe(200)

  const error = await getResponseErrorCode(response)
  expect(error).toBe('cannot_override')
})

it.skipIf(USE_CLOUDFLARE)('[DELETE] /channel_self (with overwrite)', async () => {
  await resetAndSeedAppData(APPNAME)

  const data = getUniqueBaseData(APPNAME)
  data.device_id = randomUUID().toLowerCase()

  const { data: productionChannel, error: productionChannelError } = await getSupabaseClient().from('channels').select('id, owner_org').eq('name', 'production').eq('app_id', APPNAME).single()

  expect(productionChannelError).toBeNull()
  expect(productionChannel).toBeTruthy()

  const productionId = productionChannel!.id
  const ownerOrg = productionChannel!.owner_org

  const { error } = await getSupabaseClient().from('channel_devices').upsert({
    app_id: APPNAME,
    channel_id: productionId,
    device_id: data.device_id,
    owner_org: ownerOrg,
  }, { onConflict: 'device_id, app_id' })

  expect(error).toBeNull()

  try {
    const response = await fetchEndpoint('DELETE', data)
    expect(response.ok).toBe(true)
    expect(await response.json()).toEqual({ status: 'ok' })

    const { data: channelDevice, error: channelDeviceError } = await getSupabaseClient().from('channel_devices').select('*').eq('device_id', data.device_id).eq('app_id', APPNAME)

    expect(channelDeviceError).toBeNull()
    expect(channelDevice).toBeTruthy()
    expect(channelDevice).toHaveLength(0)
  }
  catch (e) {
    const { error } = await getSupabaseClient().from('channel_devices').delete().eq('device_id', data.device_id).eq('app_id', APPNAME).eq('owner_org', ownerOrg).eq('channel_id', productionId).single()

    expect(error).toBeNull()
    throw e
  }
})

describe.skipIf(!USE_CLOUDFLARE)('[POST/PUT/DELETE] /channel_self Cloudflare KV behavior', () => {
  async function expectNoChannelDeviceRow(deviceId: string) {
    const { count, error } = await getSupabaseClient()
      .from('channel_devices')
      .select('*', { count: 'exact', head: true })
      .eq('device_id', deviceId)
      .eq('app_id', APPNAME)

    expect(error).toBeNull()
    expect(count).toBe(0)
  }

  it('stores old plugin overrides outside channel_devices', async () => {
    await resetAndSeedAppData(APPNAME)

    const { error: channelUpdateError } = await getSupabaseClient()
      .from('channels')
      .update({ allow_device_self_set: true })
      .eq('name', 'beta')
      .eq('app_id', APPNAME)

    expect(channelUpdateError).toBeNull()

    const data = getUniqueBaseData(APPNAME)
    if (!data.device_id)
      throw new Error('device_id is required')
    data.plugin_version = '7.0.0'
    data.channel = 'beta'

    try {
      const postResponse = await fetchEndpoint('POST', data)
      expect(postResponse.ok).toBeTruthy()
      expect(await postResponse.json()).toEqual({ status: 'ok' })
      await expectNoChannelDeviceRow(data.device_id)

      const putResponse = await fetchEndpoint('PUT', data)
      expect(putResponse.ok).toBeTruthy()
      expect(await putResponse.json()).toMatchObject({ channel: 'beta', status: 'override' })
      await expectNoChannelDeviceRow(data.device_id)

      const deleteResponse = await fetchEndpoint('DELETE', data)
      expect(deleteResponse.ok).toBeTruthy()
      expect(await deleteResponse.json()).toEqual({ status: 'ok' })
      await expectNoChannelDeviceRow(data.device_id)

      const afterDeleteResponse = await fetchEndpoint('PUT', data)
      expect(afterDeleteResponse.ok).toBeTruthy()
      expect(await afterDeleteResponse.json()).toMatchObject({ status: 'default' })
    }
    finally {
      const { error: resetError } = await getSupabaseClient()
        .from('channels')
        .update({ allow_device_self_set: false })
        .eq('name', 'beta')
        .eq('app_id', APPNAME)

      expect(resetError).toBeNull()
    }
  })
})

it.skipIf(USE_CLOUDFLARE)('saves default_channel when provided', async () => {
  const uuid = randomUUID().toLowerCase()
  const testDefaultChannel = 'beta'

  // Enable allow_device_self_set for beta channel
  const { error: updateError } = await getSupabaseClient()
    .from('channels')
    .update({ allow_device_self_set: true })
    .eq('name', 'beta')
    .eq('app_id', APPNAME)

  expect(updateError).toBeNull()

  const baseData = getUniqueBaseData(APPNAME)
  baseData.device_id = uuid
  baseData.defaultChannel = testDefaultChannel
  baseData.channel = 'beta' // Required for POST /channel_self

  const response = await fetchEndpoint('POST', baseData)
  expect(response.status).toBe(200)

  // Wait for data to be written
  await new Promise(resolve => setTimeout(resolve, 1000))

  // Verify default_channel was saved
  const { error, data } = await getSupabaseClient()
    .from('devices')
    .select('default_channel')
    .eq('device_id', uuid)
    .eq('app_id', APPNAME)
    .single()

  expect(error).toBeNull()
  expect(data).toBeTruthy()
  expect(data?.default_channel).toBe(testDefaultChannel)

  // Clean up
  await getSupabaseClient().from('devices').delete().eq('device_id', uuid).eq('app_id', APPNAME)
})

describe('[POST] /channel_self - new plugin version (>= 7.34.0) behavior', () => {
  it('should validate and return success without storing in channel_devices for new plugin versions', async () => {
    const data = getUniqueBaseData(APPNAME)
    data.plugin_version = '7.34.0' // New version
    data.channel = 'production'

    const response = await fetchEndpoint('POST', data)
    expect(response.status).toBe(200)

    const result = await response.json<{ status: string, allowSet: boolean }>()
    expect(result.status).toBe('ok')
    expect(result.allowSet).toBe(true)

    // Verify it was NOT stored in channel_devices table
    if (!data.device_id)
      throw new Error('device_id is required')

    const { data: channelDeviceData } = await getSupabaseClient()
      .from('channel_devices')
      .select('*')
      .eq('device_id', data.device_id)
      .eq('app_id', APPNAME)
      .maybeSingle()

    expect(channelDeviceData).toBeNull()
  })

  it('should return error when channel does not allow self-assignment for new plugin versions', async () => {
    const data = getUniqueBaseData(APPNAME)
    data.plugin_version = '7.34.0'
    data.channel = 'production'

    // Disable allow_device_self_set for production channel
    await getSupabaseClient()
      .from('channels')
      .update({ allow_device_self_set: false })
      .eq('name', 'production')
      .eq('app_id', APPNAME)

    try {
      const response = await fetchEndpoint('POST', data)
      expect(response.status).toBe(200)

      const result = await response.json<{ error: string }>()
      expect(result.error).toBe('public_channel_self_set_not_allowed')
    }
    finally {
      // Re-enable allow_device_self_set
      await getSupabaseClient()
        .from('channels')
        .update({ allow_device_self_set: true })
        .eq('name', 'production')
        .eq('app_id', APPNAME)
    }
  })

  it.skipIf(USE_CLOUDFLARE)('should leave old channel_devices entry untouched when migrating from old to new version', async () => {
    const deviceId = randomUUID()
    const data = getUniqueBaseData(APPNAME)
    data.device_id = deviceId

    // Enable allow_device_self_set for beta channel (non-default channel)
    await getSupabaseClient()
      .from('channels')
      .update({ allow_device_self_set: true })
      .eq('name', 'beta')
      .eq('app_id', APPNAME)

    // Also enable it for a second channel so the migration request can avoid
    // the "same set max once per 1 second" limiter (keyed by channel).
    await getSupabaseClient()
      .from('channels')
      .update({ allow_device_self_set: true })
      .eq('name', 'development')
      .eq('app_id', APPNAME)

    try {
      // First, set channel with old version (stores in channel_devices)
      data.plugin_version = '7.33.0'
      data.channel = 'beta' // Use non-default channel

      const oldResponse = await fetchEndpoint('POST', data)
      expect(oldResponse.status).toBe(200)

      // Verify it was stored in channel_devices
      const { data: oldChannelDevice } = await getSupabaseClient()
        .from('channel_devices')
        .select('*')
        .eq('device_id', deviceId)
        .eq('app_id', APPNAME)
        .maybeSingle()

      expect(oldChannelDevice).toBeTruthy()

      // Then, set channel with new version (should not read or write old server-side storage)
      data.plugin_version = '7.34.0'
      data.channel = 'development'

      const newResponse = await fetchEndpoint('POST', data)
      expect(newResponse.status).toBe(200)

      const result = await newResponse.json<{ status: string, allowSet: boolean }>()
      expect(result.status).toBe('ok')
      expect(result.allowSet).toBe(true)

      // Verify old entry was left untouched
      const { data: newChannelDevice } = await getSupabaseClient()
        .from('channel_devices')
        .select('*')
        .eq('device_id', deviceId)
        .eq('app_id', APPNAME)
        .maybeSingle()

      expect(newChannelDevice).toBeTruthy()
      expect(newChannelDevice?.channel_id).toBe(oldChannelDevice?.channel_id)
    }
    finally {
      await getSupabaseClient()
        .from('channel_devices')
        .delete()
        .eq('device_id', deviceId)
        .eq('app_id', APPNAME)

      // Reset beta channel
      await getSupabaseClient()
        .from('channels')
        .update({ allow_device_self_set: false })
        .eq('name', 'beta')
        .eq('app_id', APPNAME)

      await getSupabaseClient()
        .from('channels')
        .update({ allow_device_self_set: false })
        .eq('name', 'development')
        .eq('app_id', APPNAME)
    }
  })
})

describe('[PUT] /channel_self - new plugin version (>= 7.34.0) behavior', () => {
  it('should return channel from request body for new plugin versions', async () => {
    const data = getUniqueBaseData(APPNAME)
    data.plugin_version = '7.34.0'
    data.channel = 'production' // Plugin sends its local channelOverride
    data.defaultChannel = 'production'

    const response = await fetchEndpoint('PUT', data)
    expect(response.status).toBe(200)

    const result = await response.json<{ channel: string, status: string }>()
    expect(result.channel).toBe('production')
    expect(result.status).toBe('override')
  })

  it('should return defaultChannel when no channel override is set', async () => {
    const data = getUniqueBaseData(APPNAME)
    data.plugin_version = '7.34.0'
    data.defaultChannel = 'production'
    // No channel field - no override
    delete data.channel // Remove the channel field to simulate no override

    const response = await fetchEndpoint('PUT', data)
    expect(response.status).toBe(200)

    const result = await response.json<{ channel: string, status: string }>()
    expect(result.channel).toBe('production')
    expect(result.status).toBe('default')
  })
})

describe('[DELETE] /channel_self - new plugin version (>= 7.34.0) behavior', () => {
  it('should return success and leave old channel_devices entries untouched for new plugin versions', async () => {
    const deviceId = randomUUID()
    const data = getUniqueBaseData(APPNAME)
    data.device_id = deviceId
    data.plugin_version = '7.34.0'

    // First create an old channel_devices entry (simulating migration from old version)
    const { data: productionChannel } = await getSupabaseClient()
      .from('channels')
      .select('id, owner_org')
      .eq('name', 'production')
      .eq('app_id', APPNAME)
      .single()

    expect(productionChannel).toBeTruthy()

    await getSupabaseClient()
      .from('channel_devices')
      .insert({
        app_id: APPNAME,
        channel_id: productionChannel!.id,
        device_id: deviceId,
        owner_org: productionChannel!.owner_org,
      })

    try {
      // Verify the old entry exists
      const { data: beforeDelete } = await getSupabaseClient()
        .from('channel_devices')
        .select('*')
        .eq('device_id', deviceId)
        .eq('app_id', APPNAME)
        .maybeSingle()

      expect(beforeDelete).toBeTruthy()

      // Call DELETE with new plugin version
      const response = await fetchEndpoint('DELETE', data)
      expect(response.status).toBe(200)

      const result = await response.json<{ status: string }>()
      expect(result.status).toBe('ok')

      // Verify the old entry was left untouched
      const { data: afterDelete } = await getSupabaseClient()
        .from('channel_devices')
        .select('*')
        .eq('device_id', deviceId)
        .eq('app_id', APPNAME)
        .maybeSingle()

      expect(afterDelete).toBeTruthy()
      expect(afterDelete?.channel_id).toBe(productionChannel!.id)
    }
    finally {
      await getSupabaseClient()
        .from('channel_devices')
        .delete()
        .eq('device_id', deviceId)
        .eq('app_id', APPNAME)
    }
  })

  it('should return success even when no old channel_devices entry exists', async () => {
    const data = getUniqueBaseData(APPNAME)
    data.plugin_version = '7.34.0'
    data.device_id = randomUUID()

    const response = await fetchEndpoint('DELETE', data)
    expect(response.status).toBe(200)

    const result = await response.json<{ status: string }>()
    expect(result.status).toBe('ok')
  })
})