Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions supabase/functions/_backend/utils/pg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,11 @@ export function requestInfosChannelPostgres(
: and(
eq(channelAlias.app_id, app_id),
eq(channelAlias.name, defaultChannel),
eq(platformQuery, true),
or(
eq(channelAlias.public, true),
eq(channelAlias.allow_device_self_set, true),
),
),
)
.groupBy(channelAlias.id, versionAlias.id)
Expand Down
171 changes: 162 additions & 9 deletions tests/updates.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ interface UpdateRes {
version?: string
message?: string
manifest?: { file_name: string | null, file_hash?: string | null, download_url?: string | null }[]
old?: string
major?: boolean
}

const updateNewScheme = z.object({
Expand Down Expand Up @@ -524,8 +526,8 @@ describe('[POST] /updates parallel tests', () => {
const uuid = randomUUID().toLowerCase()

const baseData = getBaseData(APP_NAME_UPDATE)
baseData.device_id = uuid;
(baseData as any).defaultChannel = 'beta'
baseData.device_id = uuid
baseData.defaultChannel = 'beta'

const response = await postUpdate(baseData)
expect(response.status).toBe(200)
Expand All @@ -534,6 +536,155 @@ describe('[POST] /updates parallel tests', () => {
expect(() => updateNewScheme.parse(json)).not.toThrow()
expect(json.version).toBe('1.361.0')
})

it('hides private defaultChannel before major upgrade checks', async () => {
const supabase = getSupabaseClient()
const versionName = `9.9.${Math.floor(Math.random() * 100000) + 1000}`
const channelName = `private-noself-${randomUUID().slice(0, 8)}`

const version = await createAppVersions(versionName, APP_NAME_UPDATE)
await supabase
.from('app_versions')
.update({ external_url: `https://example.com/${channelName}.zip` })
.eq('id', version.id)
.throwOnError()

await supabase
.from('channels')
.insert({
name: channelName,
app_id: APP_NAME_UPDATE,
version: version.id,
owner_org: ORG_ID,
created_by: USER_ID,
public: false,
disable_auto_update_under_native: false,
disable_auto_update: 'major',
allow_device_self_set: false,
allow_emulator: true,
allow_device: true,
allow_dev: true,
allow_prod: true,
ios: true,
android: true,
electron: true,
})
.throwOnError()

const baseData = getBaseData(APP_NAME_UPDATE)
baseData.platform = 'android'
baseData.defaultChannel = channelName
baseData.version_build = '0.0.1'
baseData.version_name = '0.0.1'

const response = await postUpdate(baseData)
expect(response.status).toBe(200)

const json = await response.json<UpdateRes>()
expect(json.error).toBe('no_channel')
expect(json.version).toBeUndefined()
expect(json.old).toBeUndefined()
expect(json.major).toBeUndefined()
})

it('allows private self-settable platform-compatible defaultChannel', async () => {
const supabase = getSupabaseClient()
const versionName = `9.7.${Math.floor(Math.random() * 100000) + 1000}`
const channelName = `private-selfset-${randomUUID().slice(0, 8)}`

const version = await createAppVersions(versionName, APP_NAME_UPDATE)
await supabase
.from('app_versions')
.update({ external_url: `https://example.com/${channelName}.zip` })
.eq('id', version.id)
.throwOnError()

await supabase
.from('channels')
.insert({
name: channelName,
app_id: APP_NAME_UPDATE,
version: version.id,
owner_org: ORG_ID,
created_by: USER_ID,
public: false,
disable_auto_update_under_native: false,
disable_auto_update: 'none',
allow_device_self_set: true,
allow_emulator: true,
allow_device: true,
allow_dev: true,
allow_prod: true,
ios: false,
android: true,
electron: false,
})
.throwOnError()

const baseData = getBaseData(APP_NAME_UPDATE)
baseData.platform = 'android'
baseData.defaultChannel = channelName
baseData.version_build = '0.0.1'
baseData.version_name = '0.0.1'

const response = await postUpdate(baseData)
expect(response.status).toBe(200)

const json = await response.json<UpdateRes>()
expect(() => updateNewScheme.parse(json)).not.toThrow()
expect(json.version).toBe(versionName)
expect(json.error).toBeUndefined()
})

it('hides platform-incompatible private defaultChannel before platform checks', async () => {
const supabase = getSupabaseClient()
const versionName = `9.8.${Math.floor(Math.random() * 100000) + 1000}`
const channelName = `private-iosonly-${randomUUID().slice(0, 8)}`

const version = await createAppVersions(versionName, APP_NAME_UPDATE)
await supabase
.from('app_versions')
.update({ external_url: `https://example.com/${channelName}.zip` })
.eq('id', version.id)
.throwOnError()

await supabase
.from('channels')
.insert({
name: channelName,
app_id: APP_NAME_UPDATE,
version: version.id,
owner_org: ORG_ID,
created_by: USER_ID,
public: false,
disable_auto_update_under_native: false,
disable_auto_update: 'none',
allow_device_self_set: true,
allow_emulator: true,
allow_device: true,
allow_dev: true,
allow_prod: true,
ios: true,
android: false,
electron: false,
})
.throwOnError()

const baseData = getBaseData(APP_NAME_UPDATE)
baseData.platform = 'android'
baseData.defaultChannel = channelName
baseData.version_build = '0.0.1'
baseData.version_name = '0.0.1'

Comment thread
coderabbitai[bot] marked this conversation as resolved.
const response = await postUpdate(baseData)
expect(response.status).toBe(200)

const json = await response.json<UpdateRes>()
expect(json.error).toBe('no_channel')
expect(json.version).toBeUndefined()
expect(json.old).toBeUndefined()
expect(json.major).toBeUndefined()
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.
})

describe('[POST] /updates invalid data', () => {
Expand Down Expand Up @@ -864,30 +1015,32 @@ describe('update scenarios', () => {
}
})

it('cannot update via private channel', async () => {
it('hides private channel that does not allow self-assignment', async () => {
// First reset the channel to ensure it's working properly
await updateChannel('production', {
public: true,
allow_device_self_set: true,
})

// Now set both conditions for the error
// Now set both conditions that make the channel private.
await updateChannel('production', {
public: false,
allow_device_self_set: false,
})

const baseData = getBaseData(APP_NAME_UPDATE)
baseData.version_name = '1.1.0'
// Need to specify defaultChannel so the non-public channel can be found
;(baseData as any).defaultChannel = 'production'
// A caller-supplied defaultChannel must not reveal that this private channel exists.
baseData.defaultChannel = 'production'

try {
const response = await postUpdate(baseData)
expect(response.status).toBe(200)
const json = await response.json<UpdateRes>()
expect(json.error).toBe('cannot_update_via_private_channel')
expect(json.message).toContain('Cannot update via a private channel')
expect(json.error).toBe('no_channel')
expect(json.version).toBeUndefined()
expect(json.old).toBeUndefined()
expect(json.major).toBeUndefined()
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
finally {
await updateChannel('production', {
Expand Down Expand Up @@ -937,7 +1090,7 @@ describe('update scenarios', () => {
const baseData = getBaseData(APP_NAME_UPDATE)
baseData.device_id = uuid
baseData.version_name = '1.1.0'
;(baseData as any).defaultChannel = 'production'
baseData.defaultChannel = 'production'

const response = await postUpdate(baseData)
expect(response.status).toBe(200)
Expand Down
Loading