Files
Sauci/apps/mobile/scripts/ota-publish.test.mjs
T

197 lines
7.3 KiB
JavaScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import {
assertProductionExportContainsRevenueCatConfig,
buildEoasEnvironment,
buildEoasPublishArgs,
parsePublishArgs,
resolvePublishEnvironment,
} from './ota-publish.mjs';
import { spawnSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
const config = {
build: {
production: {
env: {
EXPO_PUBLIC_API_URL: 'https://api.sauci.app',
EXPO_PUBLIC_SUPABASE_URL: 'https://ckjcrkjpmhqhiucifukx.supabase.co',
EXPO_PUBLIC_SUPABASE_ANON_KEY: 'production-anon-key',
EXPO_PUBLIC_REVENUECAT_IOS_API_KEY: 'appl_test_ios_public_key',
EXPO_PUBLIC_REVENUECAT_ANDROID_API_KEY: 'goog_test_android_public_key',
EXPO_PUBLIC_REVENUECAT_ENTITLEMENT_ID: 'Sauci Pro',
},
},
preview: {
env: {
EXPO_PUBLIC_API_URL: 'https://api.preprod.sauci.app',
RELEASE_CHANNEL: 'staging',
},
},
},
};
test('production export uses the checked-in production profile', () => {
const { branch, env } = resolvePublishEnvironment('production', {}, config);
assert.equal(branch, 'production');
assert.equal(env.RELEASE_CHANNEL, 'production');
assert.equal(env.EXPO_PUBLIC_API_URL, 'https://api.sauci.app');
});
test('production export fails closed without valid RevenueCat public configuration', () => {
for (const [key, value] of [
['EXPO_PUBLIC_REVENUECAT_IOS_API_KEY', ''],
['EXPO_PUBLIC_REVENUECAT_IOS_API_KEY', 'goog_wrong_platform_key'],
['EXPO_PUBLIC_REVENUECAT_ANDROID_API_KEY', ''],
['EXPO_PUBLIC_REVENUECAT_ANDROID_API_KEY', 'appl_wrong_platform_key'],
['EXPO_PUBLIC_REVENUECAT_ENTITLEMENT_ID', ''],
]) {
const productionEnv = { ...config.build.production.env, [key]: value };
assert.throws(
() => resolvePublishEnvironment('production', {}, {
build: { ...config.build, production: { env: productionEnv } },
}),
new RegExp(key),
);
}
});
test('production preflight proves the compiled bundle contains RevenueCat configuration', () => {
const outputDir = mkdtempSync(join(tmpdir(), 'sauci-ota-export-test-'));
const environment = config.build.production.env;
try {
writeFileSync(
join(outputDir, 'index.js'),
[
environment.EXPO_PUBLIC_REVENUECAT_IOS_API_KEY,
environment.EXPO_PUBLIC_REVENUECAT_ANDROID_API_KEY,
environment.EXPO_PUBLIC_REVENUECAT_ENTITLEMENT_ID,
].join(','),
);
assert.doesNotThrow(() => assertProductionExportContainsRevenueCatConfig(outputDir, environment));
writeFileSync(join(outputDir, 'index.js'), environment.EXPO_PUBLIC_REVENUECAT_IOS_API_KEY);
assert.throws(
() => assertProductionExportContainsRevenueCatConfig(outputDir, environment),
/EXPO_PUBLIC_REVENUECAT_ANDROID_API_KEY/,
);
} finally {
rmSync(outputDir, { recursive: true, force: true });
}
});
test('EOAS uses a guarded package runner that receives the resolved production environment', () => {
const eoasEnvironment = buildEoasEnvironment({ ...config.build.production.env, PATH: process.env.PATH });
assert.equal(eoasEnvironment.EOAS_PACKAGE_RUNNER, 'sauci-eoas-npx');
assert.match(eoasEnvironment.PATH, /apps\/mobile\/scripts\/bin/);
assert.equal(eoasEnvironment.SAUCI_EOAS_EXPECTED_EXPO_PUBLIC_REVENUECAT_ENTITLEMENT_ID, 'Sauci Pro');
const runnerPath = fileURLToPath(new URL('./bin/sauci-eoas-npx', import.meta.url));
const successful = spawnSync(process.execPath, [runnerPath, 'expo', 'export'], {
env: { ...eoasEnvironment, SAUCI_EOAS_NPX: '/usr/bin/true' },
encoding: 'utf8',
});
assert.equal(successful.status, 0, successful.stderr);
const missingKey = spawnSync(process.execPath, [runnerPath, 'expo', 'export'], {
env: { ...eoasEnvironment, EXPO_PUBLIC_REVENUECAT_ANDROID_API_KEY: '', SAUCI_EOAS_NPX: '/usr/bin/true' },
encoding: 'utf8',
});
assert.equal(missingKey.status, 1);
assert.match(missingKey.stderr, /EXPO_PUBLIC_REVENUECAT_ANDROID_API_KEY/);
assert.doesNotMatch(missingKey.stderr, /goog_test_android_public_key/);
assert.deepEqual(
buildEoasPublishArgs('production', ['--platform', 'all'], true),
['eoas', 'publish', '--branch', 'production', '--packageRunner', 'sauci-eoas-npx', '--platform', 'all'],
);
assert.deepEqual(
buildEoasPublishArgs('staging', ['--platform', 'ios']),
['eoas', 'publish', '--branch', 'staging', '--platform', 'ios'],
);
});
test('staging export requires the designated non-production Auth configuration', () => {
const environment = {
EXPO_PUBLIC_SUPABASE_URL: 'https://itbzhrvlgvdmzbnhzhyx.supabase.co',
EXPO_PUBLIC_SUPABASE_ANON_KEY: 'staging-anon-key',
};
const { branch, env } = resolvePublishEnvironment('staging', environment, config);
assert.equal(branch, 'staging');
assert.equal(env.RELEASE_CHANNEL, 'staging');
assert.equal(env.EXPO_PUBLIC_API_URL, 'https://api.preprod.sauci.app');
});
test('staging export rejects production Auth configuration', () => {
assert.throws(
() => resolvePublishEnvironment('staging', {
EXPO_PUBLIC_SUPABASE_URL: 'https://ckjcrkjpmhqhiucifukx.supabase.co',
EXPO_PUBLIC_SUPABASE_ANON_KEY: 'production-anon-key',
}, config),
/canonical HTTPS root/,
);
});
test('rejects malformed API and Auth roots', () => {
for (const invalidAuthUrl of [
'http://itbzhrvlgvdmzbnhzhyx.supabase.co',
'https://user@itbzhrvlgvdmzbnhzhyx.supabase.co',
'https://itbzhrvlgvdmzbnhzhyx.supabase.co/auth/v1',
'https://itbzhrvlgvdmzbnhzhyx.supabase.co?source=test',
'https://itbzhrvlgvdmzbnhzhyx.supabase.co#fragment',
'https://itbzhrvlgvdmzbnhzhyx.supabase.co:8443',
]) {
assert.throws(
() => resolvePublishEnvironment('staging', {
EXPO_PUBLIC_SUPABASE_URL: invalidAuthUrl,
EXPO_PUBLIC_SUPABASE_ANON_KEY: 'staging-anon-key',
}, config),
/canonical HTTPS root/,
);
}
assert.throws(
() => resolvePublishEnvironment('staging', {
EXPO_PUBLIC_SUPABASE_URL: 'https://itbzhrvlgvdmzbnhzhyx.supabase.co',
EXPO_PUBLIC_SUPABASE_ANON_KEY: 'staging-anon-key',
}, {
...config,
build: { ...config.build, preview: { env: { ...config.build.preview.env, EXPO_PUBLIC_API_URL: 'https://api.preprod.sauci.app/path' } } },
}),
/canonical HTTPS root/,
);
});
test('permits only documented safe publish arguments', () => {
assert.deepEqual(
parsePublishArgs(['--platform', 'ios', '-m', 'Release notes', '--rollout-percentage', '25', '--nonInteractive']),
['--platform', 'ios', '-m', 'Release notes', '--rollout-percentage', '25', '--nonInteractive'],
);
assert.deepEqual(
parsePublishArgs(['--platform=all', '--message=Release', '--rollout-percentage=99']),
['--platform=all', '--message=Release', '--rollout-percentage=99'],
);
});
test('rejects EOAS target, server, repository, and unknown overrides', () => {
for (const unsafe of [
'--branch',
'--branch=other',
'--channel',
'--channel=other',
'--serverUrl',
'--serverUrl=https://other.example',
'--server-url',
'--disableRepositoryCheck',
'--disable-repository-check',
'--outputDir',
'--unknown',
]) {
assert.throws(() => parsePublishArgs([unsafe]), /unsupported publish argument/);
}
});