forked from wangziqi/gongxue-base
139 lines
6.1 KiB
JavaScript
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');
|