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