gateway-auth-refresh-wiring.test.mjs
115 lines 6.0 KB
Raw
sha256:fbe982a22c05c6fe2e93876f250deecdb43d883f648b4e1810c7546caa9f17db docs: activate KNOWTATION- board identity and preserve livi… Human minor ⚠ breaking 1 day ago
1 /**
2 * Structural wiring tests — proves the hosted gateway actually mounts the persistent-session
3 * machinery (refresh-token rotation + HttpOnly cookie + real logout) and provisions an
4 * eventual-consistency blob for the auth store. These guard the integration points; the
5 * behavioral guarantees live in the refresh-token-core, gateway-refresh-token-store, and
6 * auth-session suites.
7 */
8
9 import { test, describe } from 'node:test';
10 import assert from 'node:assert/strict';
11 import fs from 'node:fs';
12 import path from 'node:path';
13 import { fileURLToPath } from 'node:url';
14
15 const __dirname = path.dirname(fileURLToPath(import.meta.url));
16 const ROOT = path.resolve(__dirname, '..');
17
18 describe('hosted gateway wires persistent sessions', () => {
19 let src;
20 const load = () => {
21 if (!src) src = fs.readFileSync(path.join(ROOT, 'hub/gateway/server.mjs'), 'utf8');
22 return src;
23 };
24 const routeBlock = (s, route) => {
25 const start = s.indexOf(route);
26 assert.ok(start > 0, `${route} route exists`);
27 const nextRoute = s.indexOf('\napp.', start + route.length);
28 return s.slice(start, nextRoute > 0 ? nextRoute : s.length);
29 };
30
31 test('imports auth-session helpers and the gateway refresh store', () => {
32 const s = load();
33 assert.ok(s.includes("from '../auth-session.mjs'"), 'must import auth-session helpers');
34 assert.ok(s.includes("from './refresh-token-store.mjs'"), 'must import the gateway refresh store');
35 });
36
37 test('mounts POST /api/v1/auth/refresh and /api/v1/auth/logout before the proxies', () => {
38 const s = load();
39 assert.ok(/app\.post\(\s*'\/api\/v1\/auth\/refresh'/.test(s), 'must mount POST /auth/refresh');
40 assert.ok(/app\.post\(\s*'\/api\/v1\/auth\/establish-refresh'/.test(s), 'must mount POST /auth/establish-refresh');
41 assert.ok(/app\.post\(\s*'\/api\/v1\/auth\/logout'/.test(s), 'must mount POST /auth/logout');
42 // Auth routes must be registered before any bridge/canister proxy so they are handled locally.
43 const refreshIdx = s.indexOf("'/api/v1/auth/refresh'");
44 const proxyIdx = s.indexOf('proxyTo(');
45 assert.ok(refreshIdx > 0 && (proxyIdx === -1 || refreshIdx < proxyIdx), 'auth routes precede proxies');
46 });
47
48 test('answers OPTIONS preflight for the credentialed auth routes', () => {
49 const s = load();
50 assert.ok(s.includes("app.options(") && s.includes("'/api/v1/auth/refresh'"), 'must handle OPTIONS preflight');
51 assert.ok(s.includes("'/api/v1/auth/establish-refresh'"), 'OPTIONS must include establish-refresh');
52 });
53
54 test('refresh route uses createRefreshHandler with a sub-only access-token signer', () => {
55 const s = load();
56 const block = s.slice(s.indexOf("'/api/v1/auth/refresh'"), s.indexOf("'/api/v1/auth/refresh'") + 400);
57 assert.ok(block.includes('createRefreshHandler'), 'refresh route must use createRefreshHandler');
58 assert.ok(block.includes('issueAccessTokenForSub'), 'refresh route must re-mint from sub alone');
59 });
60
61 test('both OAuth callbacks issue a refresh cookie before redirect', () => {
62 const s = load();
63 const google = routeBlock(s, "'/auth/callback/google'");
64 const github = routeBlock(s, "'/auth/callback/github'");
65 assert.ok(google.includes('issueRefreshCookieSafe'), 'google callback issues cookie');
66 assert.ok(github.includes('issueRefreshCookieSafe'), 'github callback issues cookie');
67 });
68
69 test('cookie policy adapts SameSite to cross-origin deployments', () => {
70 const s = load();
71 const block = s.slice(s.indexOf('function refreshCookiePolicy'), s.indexOf('function refreshCookiePolicy') + 400);
72 assert.ok(block.includes("'none'") && block.includes("'lax'"), 'policy chooses None (cross-origin) vs Lax (same-origin)');
73 });
74
75 test('logout uses createLogoutHandler (server-side revocation)', () => {
76 const s = load();
77 // The first occurrence is the OPTIONS preflight array; the POST route is the later one.
78 const postIdx = s.lastIndexOf("'/api/v1/auth/logout'");
79 const block = s.slice(postIdx, postIdx + 300);
80 assert.ok(block.includes('createLogoutHandler'), 'logout must use createLogoutHandler');
81 });
82
83 test('refresh-cookie issuance logs success and surfaces failures (no silent swallow)', () => {
84 const s = load();
85 const start = s.indexOf('async function issueRefreshCookieSafe');
86 assert.ok(start > 0, 'issueRefreshCookieSafe must exist');
87 const block = s.slice(start, start + 1200);
88 // Must log a real error in the catch, not swallow it with a bare noop.
89 assert.ok(/catch\s*\(\s*err\s*\)/.test(block), 'catch must bind the error');
90 assert.ok(block.includes('console.error'), 'a refresh-store write failure must be logged');
91 assert.ok(block.includes('authBlobPresent'), 'failure log must record whether the auth blob was provisioned');
92 assert.doesNotMatch(block, /catch\s*\(\s*_\s*\)\s*\{\s*\/\/[^\n]*\n\s*\}/, 'must not silently swallow the error');
93 });
94
95 test('Netlify/Lambda uses eventual refresh store (not strong file)', () => {
96 const s = load();
97 assert.ok(
98 s.includes('AWS_LAMBDA_FUNCTION_NAME'),
99 'must detect Lambda runtime — NETLIFY alone is unset on Functions and caused /var/task/data ENOENT',
100 );
101 });
102
103 test('Netlify function provisions the gateway-auth blob (eventual consistency) and cleans it up', () => {
104 const fn = fs.readFileSync(path.join(ROOT, 'netlify/functions/gateway.mjs'), 'utf8');
105 assert.ok(fn.includes("name: 'gateway-auth'"), 'must provision the gateway-auth store');
106 // Strong consistency is unavailable in Lambda-compat mode (no uncachedEdgeURL → BlobsConsistencyError),
107 // so the auth store must use eventual consistency like billing. See refresh-token-store.mjs.
108 assert.ok(
109 /name: 'gateway-auth',\s*consistency:\s*'eventual'/.test(fn),
110 'auth store must use eventual consistency (strong is unsupported in Lambda-compat mode)',
111 );
112 assert.doesNotMatch(fn, /name: 'gateway-auth',\s*consistency:\s*'strong'/, 'must not request strong consistency');
113 assert.ok(fn.includes('delete globalThis.__knowtation_gateway_auth_blob'), 'must clean up the global');
114 });
115 });
File History 1 commit
sha256:fbe982a22c05c6fe2e93876f250deecdb43d883f648b4e1810c7546caa9f17db docs: activate KNOWTATION- board identity and preserve livi… Human minor 1 day ago