forked from wangziqi/gongxue-base
feat: establish production SaaS foundation
This commit is contained in:
138
scripts/api-cors-policy-test.js
Normal file
138
scripts/api-cors-policy-test.js
Normal file
@@ -0,0 +1,138 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { CorsPolicy, normalizeCorsOrigin } from '../apps/api/src/core/cors.ts';
|
||||
|
||||
assert.deepEqual(normalizeCorsOrigin('HTTPS://Campus-A.Example.com:443'), {
|
||||
origin: 'https://campus-a.example.com',
|
||||
hostname: 'campus-a.example.com',
|
||||
protocol: 'https:',
|
||||
hasNonDefaultPort: false,
|
||||
});
|
||||
assert.equal(normalizeCorsOrigin('https://campus-a.example.com/path'), null);
|
||||
assert.equal(normalizeCorsOrigin('https://campus-a.example.com,https://evil.example'), null);
|
||||
assert.equal(normalizeCorsOrigin('null'), null);
|
||||
|
||||
let now = 1_000;
|
||||
let lookupCalls = 0;
|
||||
const domainStates = new Map([
|
||||
['active.example.test', true],
|
||||
['inactive.example.test', false],
|
||||
]);
|
||||
const policy = new CorsPolicy({
|
||||
staticOrigins: ['https://platform.example.test', 'http://localhost:5173'],
|
||||
tenantDomainsEnabled: true,
|
||||
positiveCacheTtlMs: 1_000,
|
||||
negativeCacheTtlMs: 200,
|
||||
maxCacheEntries: 2,
|
||||
now: () => now,
|
||||
lookupTenantDomain: async host => {
|
||||
lookupCalls += 1;
|
||||
return domainStates.get(host) === true;
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepEqual(await policy.evaluate(''), { allowed: true, allowOrigin: null, reason: 'no-origin' });
|
||||
assert.equal(lookupCalls, 0, 'non-browser requests without Origin must not query tenant domains');
|
||||
assert.deepEqual(await policy.evaluate('HTTPS://PLATFORM.EXAMPLE.TEST:443'), {
|
||||
allowed: true,
|
||||
allowOrigin: 'https://platform.example.test',
|
||||
reason: 'static-origin',
|
||||
});
|
||||
assert.equal(lookupCalls, 0, 'static platform origins must not query tenant domains');
|
||||
assert.equal((await policy.evaluate('http://localhost:5173')).allowed, true);
|
||||
assert.equal((await policy.evaluate('http://active.example.test')).allowed, false, 'dynamic tenant origins must use HTTPS');
|
||||
assert.equal((await policy.evaluate('https://active.example.test:8443')).allowed, false, 'dynamic tenant origins must not use custom ports');
|
||||
assert.equal((await policy.evaluate('https://127.0.0.1')).allowed, false, 'dynamic tenant origins must not allow loopback hosts');
|
||||
|
||||
assert.equal((await policy.evaluate('https://active.example.test')).allowed, true);
|
||||
assert.equal(lookupCalls, 1);
|
||||
assert.equal((await policy.evaluate('https://active.example.test')).allowed, true);
|
||||
assert.equal(lookupCalls, 1, 'active tenant domains must use the positive cache');
|
||||
|
||||
assert.equal((await policy.evaluate('https://inactive.example.test')).allowed, false);
|
||||
assert.equal(lookupCalls, 2);
|
||||
domainStates.set('inactive.example.test', true);
|
||||
now += 199;
|
||||
assert.equal((await policy.evaluate('https://inactive.example.test')).allowed, false);
|
||||
assert.equal(lookupCalls, 2, 'inactive tenant domains must use the negative cache before expiry');
|
||||
now += 1;
|
||||
assert.equal((await policy.evaluate('https://inactive.example.test')).allowed, true);
|
||||
assert.equal(lookupCalls, 3, 'negative cache expiry must refresh the database decision');
|
||||
|
||||
domainStates.set('active.example.test', false);
|
||||
now += 799;
|
||||
assert.equal((await policy.evaluate('https://active.example.test')).allowed, true, 'positive cache must remain stable before expiry');
|
||||
now += 1;
|
||||
assert.equal((await policy.evaluate('https://active.example.test')).allowed, false, 'positive cache expiry must observe a disabled tenant domain');
|
||||
assert.equal(lookupCalls, 4);
|
||||
|
||||
assert.equal((await policy.evaluate('https://unknown.example.test')).allowed, false, 'unknown tenant domains must be rejected');
|
||||
assert.equal(lookupCalls, 5);
|
||||
assert.equal((await policy.evaluate('https://inactive.example.test')).allowed, true);
|
||||
assert.equal(lookupCalls, 6, 'bounded cache must evict the least-recently-used domain after reaching its maximum');
|
||||
|
||||
let concurrentCalls = 0;
|
||||
let releaseLookup;
|
||||
const concurrentPolicy = new CorsPolicy({
|
||||
staticOrigins: [],
|
||||
tenantDomainsEnabled: true,
|
||||
positiveCacheTtlMs: 1_000,
|
||||
negativeCacheTtlMs: 200,
|
||||
maxCacheEntries: 100,
|
||||
lookupTenantDomain: async () => {
|
||||
concurrentCalls += 1;
|
||||
await new Promise(resolve => { releaseLookup = resolve; });
|
||||
return true;
|
||||
},
|
||||
});
|
||||
const pendingA = concurrentPolicy.evaluate('https://concurrent.example.test');
|
||||
const pendingB = concurrentPolicy.evaluate('https://concurrent.example.test');
|
||||
await new Promise(resolve => setImmediate(resolve));
|
||||
assert.equal(concurrentCalls, 1, 'same-host cache misses must share one database lookup');
|
||||
releaseLookup();
|
||||
assert.equal((await pendingA).allowed, true);
|
||||
assert.equal((await pendingB).allowed, true);
|
||||
|
||||
let failureNow = 10_000;
|
||||
let failureCalls = 0;
|
||||
let databaseHealthy = false;
|
||||
const lookupErrors = [];
|
||||
const failurePolicy = new CorsPolicy({
|
||||
staticOrigins: [],
|
||||
tenantDomainsEnabled: true,
|
||||
positiveCacheTtlMs: 1_000,
|
||||
negativeCacheTtlMs: 200,
|
||||
maxCacheEntries: 100,
|
||||
now: () => failureNow,
|
||||
onLookupError: error => lookupErrors.push(error),
|
||||
lookupTenantDomain: async () => {
|
||||
failureCalls += 1;
|
||||
if (!databaseHealthy) throw new Error('database unavailable');
|
||||
return true;
|
||||
},
|
||||
});
|
||||
const failedLookup = await failurePolicy.evaluate('https://recover.example.test');
|
||||
assert.equal(failedLookup.allowed, false, 'tenant domain lookup failures must fail closed');
|
||||
assert.equal(failedLookup.reason, 'tenant-domain-lookup-failed');
|
||||
assert.equal(lookupErrors.length, 1);
|
||||
databaseHealthy = true;
|
||||
failureNow += 199;
|
||||
const cachedFailure = await failurePolicy.evaluate('https://recover.example.test');
|
||||
assert.equal(cachedFailure.allowed, false);
|
||||
assert.equal(cachedFailure.reason, 'tenant-domain-lookup-failed', 'cached lookup failures must retain their failure reason');
|
||||
assert.equal(failureCalls, 1, 'lookup failures must use a short negative cache to avoid database stampedes');
|
||||
failureNow += 1;
|
||||
assert.equal((await failurePolicy.evaluate('https://recover.example.test')).allowed, true);
|
||||
assert.equal(failureCalls, 2, 'a recovered database must be consulted after negative cache expiry');
|
||||
|
||||
const disabledPolicy = new CorsPolicy({
|
||||
staticOrigins: ['https://platform.example.test'],
|
||||
tenantDomainsEnabled: false,
|
||||
positiveCacheTtlMs: 1_000,
|
||||
negativeCacheTtlMs: 200,
|
||||
maxCacheEntries: 100,
|
||||
lookupTenantDomain: async () => true,
|
||||
});
|
||||
assert.equal((await disabledPolicy.evaluate('https://tenant.example.test')).allowed, false);
|
||||
assert.equal((await disabledPolicy.evaluate('https://platform.example.test')).allowed, true);
|
||||
|
||||
console.log('[PASS] dynamic tenant CORS policy, bounded cache and fail-closed contract');
|
||||
Reference in New Issue
Block a user