Files
gongxue-base/scripts/api-cors-policy-test.js
2026-07-12 19:26:57 +08:00

139 lines
6.1 KiB
JavaScript

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');