Files
gongxue-base/scripts/taro-runtime-config-test.js
2026-07-12 19:26:57 +08:00

173 lines
6.2 KiB
JavaScript

import assert from 'node:assert/strict';
import { pathToFileURL } from 'node:url';
const repoRoot = process.cwd();
globalThis.__TARO_PUBLIC_BUILD_CONFIG__ = {
portal: 'student',
target: 'weapp',
releaseMode: 'production',
weappTenantMode: 'fixed',
apiBaseUrl: 'https://compiled-api.gongxue100.com',
supabaseUrl: 'https://compiled-auth.gongxue100.com',
supabasePublishableKey: 'sb_publishable_compiled_key',
tenantCode: 'compiled-tenant',
};
const envModule = await import(pathToFileURL(`${repoRoot}/apps/taro/src/env.ts`).href);
delete globalThis.__TARO_PUBLIC_BUILD_CONFIG__;
assert.equal(envModule.appEnv.portal, 'student');
assert.equal(envModule.appEnv.apiBaseUrl, 'https://compiled-api.gongxue100.com');
assert.equal(envModule.appEnv.supabaseUrl, 'https://compiled-auth.gongxue100.com');
assert.equal(envModule.appEnv.supabasePublishableKey, 'sb_publishable_compiled_key');
assert.equal(envModule.appEnv.tenantCode, 'compiled-tenant');
assert.equal(envModule.taroRuntimeEnv(), 'weapp');
assert.equal(envModule.isWeappRuntime(), true);
assert.equal(envModule.taroWeappTenantMode(), 'fixed');
envModule.applyRuntimeConfig({
portal: 'tenant-admin',
apiBaseUrl: 'https://api.gongxue100.com///',
supabaseUrl: 'https://auth.gongxue100.com///',
supabasePublishableKey: 'sb_publishable_public_key',
tenantCode: 'tenant-a',
});
assert.equal(envModule.appEnv.portal, 'tenant-admin');
assert.equal(envModule.appEnv.apiBaseUrl, 'https://api.gongxue100.com');
assert.equal(envModule.appEnv.supabaseUrl, 'https://auth.gongxue100.com');
assert.equal(envModule.appEnv.supabasePublishableKey, 'sb_publishable_public_key');
assert.equal(envModule.appEnv.tenantCode, 'tenant-a');
envModule.applyRuntimeConfig({ tenantCode: '' });
assert.equal(envModule.appEnv.tenantCode, '', 'An empty runtime tenantCode must clear a compiled tenant fallback');
envModule.applyRuntimeConfig({
TARO_APP_PORTAL: 'platform-admin',
TARO_APP_API_BASE_URL: 'https://api2.gongxue100.com',
});
assert.equal(envModule.appEnv.portal, 'platform-admin');
assert.equal(envModule.appEnv.apiBaseUrl, 'https://api2.gongxue100.com');
envModule.applyRuntimeConfig({
portal: 'invalid-portal',
apiBaseUrl: '',
});
assert.equal(envModule.appEnv.portal, 'platform-admin');
assert.equal(envModule.appEnv.apiBaseUrl, 'https://api2.gongxue100.com');
assert.throws(
() => envModule.applyRuntimeConfig({
apiBaseUrl: 'https://api.gongxue100.com',
SUPABASE_SERVICE_ROLE_KEY: 'must-not-ship-to-browser',
}),
/Forbidden secret key in runtime config: SUPABASE_SERVICE_ROLE_KEY/,
);
assert.throws(
() => envModule.applyRuntimeConfig({
apiBaseUrl: 'https://api.gongxue100.com',
unexpectedFeatureFlag: 'unsafe-drift',
}),
/Unknown key in runtime config: unexpectedFeatureFlag/,
);
globalThis.__TARO_PUBLIC_BUILD_CONFIG__ = {
portal: 'student',
target: 'h5',
releaseMode: 'production',
weappTenantMode: '',
apiBaseUrl: '',
supabaseUrl: '',
supabasePublishableKey: '',
tenantCode: '',
};
globalThis.window = {
location: { origin: 'https://student.example.test' },
fetch: async () => {
throw new Error('network unavailable');
},
};
const productionH5Url = pathToFileURL(`${repoRoot}/apps/taro/src/env.ts`);
productionH5Url.searchParams.set('runtime-config-test', 'production-h5');
const productionH5Env = await import(productionH5Url.href);
assert.equal(productionH5Env.appEnv.apiBaseUrl, '', 'Production H5 must not compile a local API fallback');
assert.equal(productionH5Env.taroReleaseMode(), 'production');
await assert.rejects(
() => productionH5Env.loadRuntimeConfig(),
/Production H5 runtime-config\.json request failed: network unavailable/,
'Production H5 must fail closed when runtime config cannot be fetched',
);
globalThis.window.fetch = async () => ({ ok: false, status: 404 });
await assert.rejects(
() => productionH5Env.loadRuntimeConfig(),
/Production H5 runtime-config\.json request failed with status 404/,
'Production H5 must fail closed when runtime config is missing',
);
globalThis.window.fetch = async () => ({ ok: true, status: 200, text: async () => '' });
await assert.rejects(
() => productionH5Env.loadRuntimeConfig(),
/Production H5 runtime-config\.json is empty or unreadable/,
'Production H5 must fail closed when runtime config is unreadable',
);
globalThis.window.fetch = async () => ({
ok: true,
status: 200,
text: async () => JSON.stringify({ portal: 'student', apiBaseUrl: '' }),
});
await assert.rejects(
() => productionH5Env.loadRuntimeConfig(),
/Production H5 runtime-config\.json apiBaseUrl must be an absolute HTTPS URL and must not use localhost or loopback/,
'Production H5 must fail closed when runtime config omits its API endpoint',
);
for (const apiBaseUrl of [
'http://api.gongxue100.com',
'https://localhost:8787',
'https://127.0.0.1:8787',
'https://[::1]:8787',
]) {
globalThis.window.fetch = async () => ({
ok: true,
status: 200,
text: async () => JSON.stringify({ portal: 'student', apiBaseUrl }),
});
await assert.rejects(
() => productionH5Env.loadRuntimeConfig(),
/must be an absolute HTTPS URL and must not use localhost or loopback/,
`Production H5 must reject unsafe API endpoint ${apiBaseUrl}`,
);
}
globalThis.window.fetch = async () => ({
ok: true,
status: 200,
text: async () => JSON.stringify({
portal: 'student',
apiBaseUrl: 'https://api.gongxue100.com/',
supabaseUrl: 'https://auth.gongxue100.com/',
supabasePublishableKey: 'sb_publishable_runtime_test',
tenantCode: '',
}),
});
await productionH5Env.loadRuntimeConfig();
assert.equal(productionH5Env.appEnv.apiBaseUrl, 'https://api.gongxue100.com');
assert.equal(productionH5Env.appEnv.tenantCode, '', 'Production H5 must remain domain resolved');
globalThis.window.fetch = async () => ({
ok: true,
status: 200,
text: async () => JSON.stringify({
portal: 'student',
apiBaseUrl: 'https://api.gongxue100.com',
tenantCode: 'campus-north',
}),
});
await assert.rejects(
() => productionH5Env.loadRuntimeConfig(),
/tenantCode must be empty; tenant is resolved from the browser origin/,
'Production H5 must reject a fixed tenant override',
);
delete globalThis.window;
delete globalThis.__TARO_PUBLIC_BUILD_CONFIG__;
console.log('[PASS] Taro runtime config guardrails');