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
35 changes: 28 additions & 7 deletions supabase/functions/_backend/public/build/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,29 @@ interface BuilderJobResponse {
status: string
}

/**
* Construct the JSON body forwarded to the builder's POST /jobs endpoint.
* Extracted for testability — the handler calls this, and unit tests assert the shape.
*/
export function buildBuilderPayload(input: {
orgId: string
uploadPath: string
platform: string
buildOptions: Record<string, unknown>
buildCredentials: Record<string, string>
}) {
return {
userId: input.orgId,
artifactKey: input.uploadPath,
fastlane: { lane: input.platform },
buildOptions: input.buildOptions,
buildCredentials: input.buildCredentials,
}
}

/** Exported for unit tests — follows bundleUsageTestUtils pattern. */
export const builderPayloadTestUtils = { buildBuilderPayload }

export async function requestBuild(
c: Context,
body: RequestBuildBody,
Expand Down Expand Up @@ -152,15 +175,13 @@ export async function requestBuild(
'x-api-key': builderApiKey,
'Content-Type': 'application/json',
},
body: JSON.stringify({
userId: org_id, // Use org_id as anonymized identifier
artifactKey: upload_path, // Pass the artifact key to builder
fastlane: {
lane: platform,
},
body: JSON.stringify(buildBuilderPayload({
orgId: org_id,
uploadPath: upload_path,
platform,
buildOptions: build_options,
buildCredentials: build_credentials,
}),
})),
})

if (builderResponse.ok) {
Expand Down
106 changes: 106 additions & 0 deletions tests/builder-payload.unit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { describe, expect, it } from 'vitest'
import { builderPayloadTestUtils } from '../supabase/functions/_backend/public/build/request.ts'

const { buildBuilderPayload } = builderPayloadTestUtils

describe('builder payload shape', () => {
it('maps build_options (snake_case input) to buildOptions (camelCase output)', () => {
const payload = buildBuilderPayload({
orgId: 'org-123',
uploadPath: 'orgs/org-123/apps/com.test/native-builds/session.zip',
platform: 'ios',
buildOptions: { platform: 'ios', buildMode: 'release', cliVersion: '7.83.0' },
buildCredentials: {},
})

expect(payload).toHaveProperty('buildOptions')
expect(payload.buildOptions).toEqual({ platform: 'ios', buildMode: 'release', cliVersion: '7.83.0' })
// Must NOT contain the snake_case input key
expect(payload).not.toHaveProperty('build_options')
})

it('maps build_credentials (snake_case input) to buildCredentials (camelCase output)', () => {
const payload = buildBuilderPayload({
orgId: 'org-123',
uploadPath: 'orgs/org-123/apps/com.test/native-builds/session.zip',
platform: 'android',
buildOptions: {},
buildCredentials: { KEYSTORE_KEY_ALIAS: 'alias', KEYSTORE_KEY_PASSWORD: 'val' },
})

expect(payload).toHaveProperty('buildCredentials')
expect(payload.buildCredentials).toEqual({ KEYSTORE_KEY_ALIAS: 'alias', KEYSTORE_KEY_PASSWORD: 'val' })
// Must NOT contain the snake_case input key
expect(payload).not.toHaveProperty('build_credentials')
})

it('does not include a legacy flat credentials field', () => {
const payload = buildBuilderPayload({
orgId: 'org-123',
uploadPath: 'path.zip',
platform: 'ios',
buildOptions: {},
buildCredentials: { SOME_SECRET: 'val' },
})

expect(payload).not.toHaveProperty('credentials')
})

it('includes userId, artifactKey, and fastlane with correct values', () => {
const payload = buildBuilderPayload({
orgId: 'org-456',
uploadPath: 'orgs/org-456/apps/com.example/native-builds/uuid.zip',
platform: 'android',
buildOptions: {},
buildCredentials: {},
})

expect(payload.userId).toBe('org-456')
expect(payload.artifactKey).toBe('orgs/org-456/apps/com.example/native-builds/uuid.zip')
expect(payload.fastlane).toEqual({ lane: 'android' })
})

it('contains exactly the expected top-level keys', () => {
const payload = buildBuilderPayload({
orgId: 'org-789',
uploadPath: 'path/to/artifact.zip',
platform: 'ios',
buildOptions: { foo: 'bar' },
buildCredentials: { baz: 'qux' },
})

const keys = Object.keys(payload).sort()
expect(keys).toEqual([
'artifactKey',
'buildCredentials',
'buildOptions',
'fastlane',
'userId',
])
})

it('passes through buildOptions and buildCredentials contents unchanged', () => {
const complexOptions = {
platform: 'ios',
buildMode: 'debug',
cliVersion: '7.84.0',
nested: { deep: { value: 42 } },
array: [1, 2, 3],
}
const complexCredentials = {
BUILD_CERTIFICATE_BASE64: 'base64data',
P12_PASSWORD: 'test-val',
}

const payload = buildBuilderPayload({
orgId: 'org-test',
uploadPath: 'test/path.zip',
platform: 'ios',
buildOptions: complexOptions,
buildCredentials: complexCredentials,
})

expect(payload.buildOptions).toEqual(complexOptions)
expect(payload.buildCredentials).toEqual(complexCredentials)
})
})