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) { 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() return json.error } type Awaitable = T | PromiseLike async function withSupabaseCall( fn: () => Awaitable, ): Promise { 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() 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() 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') }) })