Files
bsky-social-app/scripts/ota/validate-release.test.mjs
T
2026-09-05 18:55:17 +03:00

190 lines
5.7 KiB
JavaScript

import assert from 'node:assert/strict'
import {execFileSync} from 'node:child_process'
import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import test from 'node:test'
const validator = path.resolve(import.meta.dirname, 'validate-release.mjs')
const sha = 'a'.repeat(40)
const runtime = 'b'.repeat(40)
function fixture(channel = 'production') {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'ota-release-'))
fs.mkdirSync(path.join(directory, 'bundle'))
fs.writeFileSync(
path.join(directory, 'report.json'),
JSON.stringify({
schemaVersion: 1,
platform: 'ios',
nativeProfile: 'production',
sourceCommit: sha,
runtimeVersion: runtime,
fingerprintPolicyVersion: 1,
fingerprintToolVersion: '0.20.8',
toolVersions: {
expo: '57.0.8',
expoUpdates: '57.0.10',
node: 'v24.19.0',
packageManager: 'pnpm@11.21.0',
},
fingerprintSources: [
{type: 'contents', id: 'expoConfig', hash: 'c'.repeat(40)},
{
type: 'file',
filePath: 'scripts/ota/fingerprint-policy.json',
hash: 'd'.repeat(40),
},
...['BlueskyClip', 'BlueskyNSE', 'Share-with-Bluesky'].map(name => ({
type: 'dir',
filePath: `modules/${name}`,
hash: 'e'.repeat(40),
})),
],
}),
)
const release = {
schemaVersion: 1,
sourceCommit: sha,
channel,
nativeProfile: 'production',
bundleVersion: '1788537600000',
platforms: {
ios: {
runtimeVersion: runtime,
fingerprintReportRef: 'report.json',
bundleDirectory: 'bundle',
...(channel === 'production' ? {targetNativeBuildNumber: '42'} : {}),
},
},
}
const releaseFile = path.join(directory, 'ota-export.json')
fs.writeFileSync(releaseFile, JSON.stringify(release))
return {directory, release, releaseFile}
}
function validate(releaseFile) {
return execFileSync(
process.execPath,
[validator, '--release-file', releaseFile],
{
encoding: 'utf8',
},
)
}
test('production requires an exact build but permits an unavailable receipt', () => {
const value = fixture()
const result = JSON.parse(validate(value.releaseFile))
assert.deepEqual(result.platforms.ios.receiptVerification, {
status: 'unverified',
reason: 'receipt-unavailable',
})
delete value.release.platforms.ios.targetNativeBuildNumber
fs.writeFileSync(value.releaseFile, JSON.stringify(value.release))
assert.throws(
() => validate(value.releaseFile),
/Production ios target is required/,
)
})
test('testflight rejects build targeting', () => {
const value = fixture('testflight')
value.release.nativeProfile = 'testflight'
value.release.platforms.ios.targetNativeBuildNumber = '42'
const report = JSON.parse(
fs.readFileSync(path.join(value.directory, 'report.json')),
)
report.nativeProfile = 'testflight'
fs.writeFileSync(
path.join(value.directory, 'report.json'),
JSON.stringify(report),
)
fs.writeFileSync(value.releaseFile, JSON.stringify(value.release))
assert.throws(
() => validate(value.releaseFile),
/must not target a native build/,
)
})
test('a supplied receipt with an invalid source SHA fails closed', () => {
const value = fixture()
fs.mkdirSync(path.join(value.directory, 'receipts'))
value.release.platforms.ios.targetNativeBuildReceiptRef =
'receipts/receipt.json'
fs.writeFileSync(
path.join(value.directory, 'receipts/receipt.json'),
JSON.stringify({
schemaVersion: 1,
platform: 'ios',
nativeProfile: 'production',
defaultChannel: 'production',
appVersion: '1.133.0',
nativeBuildNumber: '42',
runtimeVersion: runtime,
sourceCommit: 'short',
fingerprintPolicyVersion: 1,
fingerprintToolVersion: '0.20.8',
artifactDigest: 'c'.repeat(64),
buildRunUrl: 'https://github.example/build/1',
fingerprintReportRef: 'report.json',
}),
)
fs.writeFileSync(path.join(value.directory, 'receipts/report.json'), '{}')
fs.writeFileSync(value.releaseFile, JSON.stringify(value.release))
assert.throws(() => validate(value.releaseFile), /receipt is incompatible/)
})
test('bundle directories cannot escape the release directory', () => {
const value = fixture('testflight')
value.release.nativeProfile = 'testflight'
value.release.platforms.ios.bundleDirectory = '../bundle'
const report = JSON.parse(
fs.readFileSync(path.join(value.directory, 'report.json')),
)
report.nativeProfile = 'testflight'
fs.writeFileSync(
path.join(value.directory, 'report.json'),
JSON.stringify(report),
)
fs.writeFileSync(value.releaseFile, JSON.stringify(value.release))
assert.throws(
() => validate(value.releaseFile),
/escapes the release directory/,
)
})
test('channel and native profile must agree', () => {
const value = fixture('testflight')
assert.throws(
() => validate(value.releaseFile),
/requires the testflight native profile/,
)
})
test('incomplete fingerprint reports are rejected', () => {
const value = fixture()
const report = JSON.parse(
fs.readFileSync(path.join(value.directory, 'report.json')),
)
delete report.fingerprintToolVersion
fs.writeFileSync(
path.join(value.directory, 'report.json'),
JSON.stringify(report),
)
assert.throws(
() => validate(value.releaseFile),
/fingerprint report is incomplete or inconsistent/,
)
})
test('symlinked bundle directories cannot escape the release directory', () => {
const value = fixture()
fs.rmSync(path.join(value.directory, 'bundle'), {recursive: true})
fs.symlinkSync(os.tmpdir(), path.join(value.directory, 'bundle'))
assert.throws(
() => validate(value.releaseFile),
/escapes the release directory/,
)
})