mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-10-31 10:19:43 +00:00
197 lines
7.3 KiB
JavaScript
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/);
|
|
}
|
|
});
|