tests/channel.test.ts
Rama main · réplica de lectura del repositorio original.
import { randomUUID } from 'node:crypto'
import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest'
import { getCanonicalAppVersionR2Path } from '../supabase/functions/_backend/utils/app_version_r2_path.ts'
import { BASE_URL, createAppVersions, getEndpointUrl, getSupabaseClient, headers, ORG_ID, resetAndSeedAppData, resetAppData, resetAppDataStats, warmEdgeEndpoint, insertManifestRows, } from './test-utils.ts'
const id = randomUUID()
const APPNAME = `com.app.c.${id}`
let productionVersionId: number | null = null
beforeAll(async () => {
await resetAndSeedAppData(APPNAME)
const { data } = await getSupabaseClient()
.from('app_versions')
.select('id')
.eq('app_id', APPNAME)
.eq('name', '1.0.0')
.single()
.throwOnError()
productionVersionId = data.id
await warmEdgeEndpoint('/bundle', {
method: 'PUT',
headers,
body: JSON.stringify({ app_id: APPNAME }),
})
})
afterEach(async () => {
const client = getSupabaseClient()
await client
.from('channels')
.update({ update_package: 'all', ...(productionVersionId ? { version: productionVersionId } : {}) })
.eq('app_id', APPNAME)
.eq('name', 'production')
.throwOnError()
})
afterAll(async () => {
await resetAppData(APPNAME)
await resetAppDataStats(APPNAME)
})
describe('[GET] /channel operations', () => {
it('get channels', async () => {
const params = new URLSearchParams({ app_id: APPNAME })
const response = await fetch(`${BASE_URL}/channel?${params.toString()}`, {
method: 'GET',
headers,
})
const data = await response.json<{ name: string, ios: boolean, android: boolean }[]>()
expect(response.status).toBe(200)
expect(Array.isArray(data)).toBe(true)
const { data: expected } = await getSupabaseClient()
.from('channels')
.select('ios, android')
.eq('app_id', APPNAME)
.eq('name', 'production')
.single()
.throwOnError()
expect(data.find(channel => channel.name === 'production')).toMatchObject(expected)
})
it('get specific channel', async () => {
const params = new URLSearchParams({ app_id: APPNAME, channel: 'production' })
const response = await fetch(`${BASE_URL}/channel?${params.toString()}`, {
method: 'GET',
headers,
})
const data = await response.json<{ name: string }>()
expect(response.status).toBe(200)
expect(data.name).toBe('production')
const { data: expected } = await getSupabaseClient()
.from('channels')
.select('ios, android')
.eq('app_id', APPNAME)
.eq('name', 'production')
.single()
.throwOnError()
expect(data).toMatchObject(expected)
})
it('invalid app_id', async () => {
const params = new URLSearchParams({ app_id: 'invalid_app' })
const response = await fetch(`${BASE_URL}/channel?${params.toString()}`, {
method: 'GET',
headers,
})
await response.arrayBuffer()
expect(response.status).toBe(400)
})
})
describe('[POST] /channel operations', () => {
it('create channel', async () => {
const response = await fetch(getEndpointUrl('/channel'), {
method: 'POST',
headers,
body: JSON.stringify({
app_id: APPNAME,
channel: `test_channel_${id.slice(0, 8)}`,
public: false,
}),
})
const data = await response.json<{ status: string, error?: string, message?: string }>()
expect(response.status, JSON.stringify(data)).toBe(200)
expect(data.status).toBe('ok')
})
it('publicizes a private channel through POST /channel when caller has app.update_settings', async () => {
const channelName = `publicize_${id.slice(0, 8)}`
const channelEndpoint = getEndpointUrl('/channel')
const client = getSupabaseClient()
const createResponse = await fetch(channelEndpoint, {
method: 'POST',
headers,
body: JSON.stringify({
app_id: APPNAME,
channel: channelName,
public: false,
}),
})
const createData = await createResponse.json<{ status: string, error?: string, message?: string }>()
expect(createResponse.status, JSON.stringify(createData)).toBe(200)
const { data: productionBefore } = await client
.from('channels')
.select('public')
.eq('app_id', APPNAME)
.eq('name', 'production')
.single()
.throwOnError()
try {
if (productionBefore.public) {
await client
.from('channels')
.update({ public: false })
.eq('app_id', APPNAME)
.eq('name', 'production')
.throwOnError()
}
const publicizeResponse = await fetch(channelEndpoint, {
method: 'POST',
headers,
body: JSON.stringify({
app_id: APPNAME,
channel: channelName,
public: true,
}),
})
const publicizeData = await publicizeResponse.json<{ status: string, error?: string, message?: string }>()
expect(publicizeResponse.status, JSON.stringify(publicizeData)).toBe(200)
expect(publicizeData.status).toBe('ok')
const { data: channel } = await client
.from('channels')
.select('public')
.eq('app_id', APPNAME)
.eq('name', channelName)
.single()
.throwOnError()
expect(channel.public).toBe(true)
}
finally {
await client
.from('channels')
.update({ public: false })
.eq('app_id', APPNAME)
.eq('name', channelName)
.throwOnError()
if (productionBefore.public) {
await client
.from('channels')
.update({ public: true })
.eq('app_id', APPNAME)
.eq('name', 'production')
.throwOnError()
}
}
})
it('update channel', async () => {
const response = await fetch(`${BASE_URL}/channel`, {
method: 'POST',
headers,
body: JSON.stringify({
app_id: APPNAME,
channel: 'production',
disableAutoUpdateUnderNative: false,
}),
})
const data = await response.json<{ status: string }>()
expect(response.status).toBe(200)
expect(data.status).toBe('ok')
})
it('update channel package mode', async () => {
const response = await fetch(`${BASE_URL}/channel`, {
method: 'POST',
headers,
body: JSON.stringify({
app_id: APPNAME,
channel: 'production',
updatePackage: 'zip',
}),
})
const data = await response.json<{ status: string }>()
expect(response.status).toBe(200)
expect(data.status).toBe('ok')
const getResponse = await fetch(`${BASE_URL}/channel?${new URLSearchParams({ app_id: APPNAME, channel: 'production' }).toString()}`, {
method: 'GET',
headers,
})
const channel = await getResponse.json<{ updatePackage: string }>()
expect(getResponse.status).toBe(200)
expect(channel.updatePackage).toBe('zip')
})
it('invalid app_id', async () => {
const response = await fetch(`${BASE_URL}/channel`, {
method: 'POST',
headers,
body: JSON.stringify({
app_id: 'invalid_app',
channel: 'test_channel',
}),
})
await response.arrayBuffer()
expect(response.status).toBe(400)
})
})
describe('channel update package vs bundle compatibility', () => {
async function insertDelta(versionId: number) {
await insertManifestRows([{
app_version_id: versionId,
file_name: `pkg-${versionId}.js`,
s3_path: `/pkg-${versionId}.js`,
file_hash: `hash-${versionId}`,
file_size: 16,
}])
}
async function productionChannel() {
const { data } = await getSupabaseClient()
.from('channels')
.select('id, version')
.eq('app_id', APPNAME)
.eq('name', 'production')
.single()
.throwOnError()
return data
}
it('refuses delta-only when the current bundle has no delta files', async () => {
const response = await fetch(`${BASE_URL}/channel`, {
method: 'POST',
headers,
body: JSON.stringify({
app_id: APPNAME,
channel: 'production',
updatePackage: 'delta',
}),
})
const data = await response.json<{ error?: string, message?: string }>()
expect(response.status).toBe(400)
expect(data.error).toBe('channel_delta_required')
expect(data.message).toContain('no delta files')
expect(data.message).toContain('1.0.0')
})
it('refuses zip-only when assigning a delta-only bundle', async () => {
const deltaOnly = await createAppVersions(`1.0.delta-${randomUUID().slice(0, 8)}`, APPNAME, {
checksum: 'delta-only',
storage_provider: 'r2-direct',
r2_path: null,
})
await insertDelta(deltaOnly.id)
await fetch(`${BASE_URL}/channel`, {
method: 'POST',
headers,
body: JSON.stringify({
app_id: APPNAME,
channel: 'production',
updatePackage: 'zip',
}),
})
const response = await fetch(`${BASE_URL}/channel`, {
method: 'POST',
headers,
body: JSON.stringify({
app_id: APPNAME,
channel: 'production',
version: deltaOnly.name,
}),
})
const data = await response.json<{ error?: string, message?: string }>()
expect(response.status).toBe(400)
expect(data.error).toBe('channel_zip_required')
expect(data.message).toContain('no zip')
expect(data.message).toContain(deltaOnly.name)
})
it('refuses assigning a zip-only bundle to a delta-only channel', async () => {
const withDeltaName = `1.0.both-${randomUUID().slice(0, 8)}`
const withDelta = await createAppVersions(withDeltaName, APPNAME, {
checksum: 'zip-and-delta',
storage_provider: 'r2',
r2_path: getCanonicalAppVersionR2Path(ORG_ID, APPNAME, withDeltaName),
})
await insertDelta(withDelta.id)
await fetch(`${BASE_URL}/channel`, {
method: 'POST',
headers,
body: JSON.stringify({
app_id: APPNAME,
channel: 'production',
version: withDelta.name,
updatePackage: 'delta',
}),
})
const zipOnlyName = `1.0.zip-${randomUUID().slice(0, 8)}`
const zipOnly = await createAppVersions(zipOnlyName, APPNAME, {
checksum: 'zip-only',
storage_provider: 'r2',
r2_path: getCanonicalAppVersionR2Path(ORG_ID, APPNAME, zipOnlyName),
})
const response = await fetch(`${BASE_URL}/channel`, {
method: 'POST',
headers,
body: JSON.stringify({
app_id: APPNAME,
channel: 'production',
version: zipOnly.name,
}),
})
const data = await response.json<{ error?: string, message?: string }>()
expect(response.status).toBe(400)
expect(data.error).toBe('channel_delta_required')
expect(data.message).toContain('no delta files')
expect(data.message).toContain(zipOnly.name)
})
it('refuses zip-only assignment through PUT /bundle', async () => {
const channel = await productionChannel()
const deltaOnly = await createAppVersions(`1.0.put-delta-${randomUUID().slice(0, 8)}`, APPNAME, {
checksum: 'put-delta-only',
storage_provider: 'r2-direct',
r2_path: null,
})
await insertDelta(deltaOnly.id)
await fetch(`${BASE_URL}/channel`, {
method: 'POST',
headers,
body: JSON.stringify({
app_id: APPNAME,
channel: 'production',
updatePackage: 'zip',
}),
})
const response = await fetch(`${BASE_URL}/bundle`, {
method: 'PUT',
headers,
body: JSON.stringify({
app_id: APPNAME,
version_id: deltaOnly.id,
channel_id: channel.id,
}),
})
const data = await response.json<{ error?: string, message?: string }>()
expect(response.status).toBe(400)
expect(data.error).toBe('channel_zip_required')
expect(data.message).toContain('no zip')
})
it('refuses an incompatible setting change through a console-style channel update', async () => {
const zipOnlyName = `1.0.ui-zip-${randomUUID().slice(0, 8)}`
const zipOnly = await createAppVersions(zipOnlyName, APPNAME, {
checksum: 'ui-zip-only',
storage_provider: 'r2',
r2_path: getCanonicalAppVersionR2Path(ORG_ID, APPNAME, zipOnlyName),
})
await getSupabaseClient()
.from('channels')
.update({ version: zipOnly.id, update_package: 'all' })
.eq('app_id', APPNAME)
.eq('name', 'production')
.throwOnError()
const { error } = await getSupabaseClient()
.from('channels')
.update({ update_package: 'delta' })
.eq('app_id', APPNAME)
.eq('name', 'production')
expect(error).toBeTruthy()
expect(error?.message).toContain('CHANNEL_DELTA_REQUIRED')
expect(error?.message).toContain('no delta files')
})
})
describe('[DELETE] /channel operations', () => {
it('invalid channel', async () => {
const response = await fetch(`${BASE_URL}/channel`, {
method: 'DELETE',
headers,
body: JSON.stringify({
channel: 'invalid_channel',
app_id: APPNAME,
}),
})
await response.arrayBuffer()
expect(response.status).toBe(400)
})
it('delete channel', async () => {
const response = await fetch(`${BASE_URL}/channel`, {
method: 'DELETE',
headers,
body: JSON.stringify({
channel: 'production',
app_id: APPNAME,
}),
})
const data = await response.json<{ status: string }>()
expect(response.status).toBe(200)
expect(data.status).toBe('ok')
})
})