From a5357d425c0d50f91556a326e3544d948a400b46 Mon Sep 17 00:00:00 2001 From: WcaleNieWolny Date: Sat, 28 Feb 2026 15:47:09 +0100 Subject: [PATCH] Add unit tests for builder payload shape contract MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extract buildBuilderPayload() from the inline fetch body so the snake_case → camelCase mapping and exact key set can be tested. 6 vitest cases verify: camelCase output, no legacy credentials field, correct metadata keys, and pass-through of contents. --- .../_backend/public/build/request.ts | 35 ++++-- tests/builder-payload.unit.test.ts | 106 ++++++++++++++++++ 2 files changed, 134 insertions(+), 7 deletions(-) create mode 100644 tests/builder-payload.unit.test.ts diff --git a/supabase/functions/_backend/public/build/request.ts b/supabase/functions/_backend/public/build/request.ts index 1e73203913..fe762f7ae7 100644 --- a/supabase/functions/_backend/public/build/request.ts +++ b/supabase/functions/_backend/public/build/request.ts @@ -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 + buildCredentials: Record +}) { + 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, @@ -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) { diff --git a/tests/builder-payload.unit.test.ts b/tests/builder-payload.unit.test.ts new file mode 100644 index 0000000000..e2b02ea9d2 --- /dev/null +++ b/tests/builder-payload.unit.test.ts @@ -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) + }) +})