gateway-memory-bridge-proxy.test.mjs
177 lines 6.4 KB
Raw
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd feat(calendar): enforce agent context tiers in retrieval AP… Human minor ⚠ breaking 1 day ago
1 /**
2 * Gateway → bridge memory routes: same-origin proxy forwards path, query, Authorization, X-Vault-Id.
3 * Track B3 prep — contract boundary tests before hosted MCP prompts call these URLs.
4 */
5 import { test } from 'node:test';
6 import assert from 'node:assert/strict';
7 import http from 'http';
8 import express from 'express';
9 import crypto from 'crypto';
10 import path from 'path';
11 import { fileURLToPath, pathToFileURL } from 'url';
12
13 const __dirname = path.dirname(fileURLToPath(import.meta.url));
14 const projectRoot = path.resolve(__dirname, '..');
15
16 const SECRET = 'gateway-memory-bridge-proxy-test-secret-32';
17
18 function signTestJwt(payload) {
19 const header = Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT' })).toString('base64url');
20 const body = Buffer.from(JSON.stringify(payload)).toString('base64url');
21 const data = `${header}.${body}`;
22 const sig = crypto.createHmac('sha256', SECRET).update(data).digest('base64url');
23 return `${data}.${sig}`;
24 }
25
26 /**
27 * @param {import('express').Express} mockBridge
28 * @returns {Promise<{ bridgeUrl: string, close: () => Promise<void> }>}
29 */
30 function startMockBridge(mockBridge) {
31 const srv = http.createServer(mockBridge);
32 return new Promise((resolve, reject) => {
33 srv.listen(0, '127.0.0.1', (err) => {
34 if (err) return reject(err);
35 const port = /** @type {import('net').AddressInfo} */ (srv.address()).port;
36 resolve({
37 bridgeUrl: `http://127.0.0.1:${port}`,
38 close: () => new Promise((r) => srv.close(() => r())),
39 });
40 });
41 });
42 }
43
44 test('gateway proxies GET /api/v1/memory to bridge with query + auth headers', async (t) => {
45 /** @type {Array<{ method: string, url: string, auth?: string, vault?: string }>} */
46 const calls = [];
47 const mockBridge = express();
48 mockBridge.get(/.*/, (req, res) => {
49 calls.push({
50 method: req.method,
51 url: req.originalUrl,
52 auth: req.headers.authorization,
53 vault: req.headers['x-vault-id'],
54 });
55 res.json({ events: [], count: 0 });
56 });
57
58 const { bridgeUrl, close } = await startMockBridge(mockBridge);
59 t.after(close);
60
61 process.env.NETLIFY = '1';
62 process.env.CANISTER_URL = 'http://canister.placeholder.test';
63 process.env.SESSION_SECRET = SECRET;
64 process.env.BRIDGE_URL = bridgeUrl;
65
66 const gwEntry = pathToFileURL(path.join(projectRoot, 'hub', 'gateway', 'server.mjs')).href;
67 const { app: gwApp } = await import(`${gwEntry}?gwmem=${Date.now()}`);
68
69 const gwSrv = http.createServer(gwApp);
70 await new Promise((resolve, reject) => {
71 gwSrv.listen(0, '127.0.0.1', (err) => (err ? reject(err) : resolve()));
72 });
73 t.after(() => new Promise((r) => gwSrv.close(() => r())));
74 const gwPort = /** @type {import('net').AddressInfo} */ (gwSrv.address()).port;
75
76 const token = signTestJwt({ sub: 'google:mem-proxy-test', role: 'editor' });
77 const res = await fetch(`http://127.0.0.1:${gwPort}/api/v1/memory?limit=7&type=user`, {
78 headers: {
79 Authorization: `Bearer ${token}`,
80 'X-Vault-Id': 'vault-a',
81 },
82 });
83 const text = await res.text();
84 assert.equal(res.status, 200, text);
85 assert.equal(calls.length, 1);
86 assert.equal(calls[0].method, 'GET');
87 assert.equal(calls[0].url, '/api/v1/memory?limit=7&type=user');
88 assert.equal(calls[0].auth, `Bearer ${token}`);
89 assert.equal(calls[0].vault, 'vault-a');
90 });
91
92 test('gateway proxies GET /api/v1/memory/:key with encoded key', async (t) => {
93 const calls = [];
94 const mockBridge = express();
95 mockBridge.get('/api/v1/memory/:key', (req, res) => {
96 calls.push({ url: req.originalUrl, key: req.params.key });
97 res.json({ key: req.params.key, value: null, updated_at: null });
98 });
99
100 const { bridgeUrl, close } = await startMockBridge(mockBridge);
101 t.after(close);
102
103 process.env.NETLIFY = '1';
104 process.env.CANISTER_URL = 'http://canister.placeholder.test';
105 process.env.SESSION_SECRET = SECRET;
106 process.env.BRIDGE_URL = bridgeUrl;
107
108 const gwEntry = pathToFileURL(path.join(projectRoot, 'hub', 'gateway', 'server.mjs')).href;
109 const { app: gwApp } = await import(`${gwEntry}?gwmem2=${Date.now()}`);
110
111 const gwSrv = http.createServer(gwApp);
112 await new Promise((resolve, reject) => {
113 gwSrv.listen(0, '127.0.0.1', (err) => (err ? reject(err) : resolve()));
114 });
115 t.after(() => new Promise((r) => gwSrv.close(() => r())));
116 const gwPort = /** @type {import('net').AddressInfo} */ (gwSrv.address()).port;
117
118 const token = signTestJwt({ sub: 'google:mem-key-test', role: 'viewer' });
119 const key = 'topic/foo';
120 const res = await fetch(
121 `http://127.0.0.1:${gwPort}/api/v1/memory/${encodeURIComponent(key)}`,
122 { headers: { Authorization: `Bearer ${token}`, 'X-Vault-Id': 'default' } },
123 );
124 assert.equal(res.status, 200);
125 assert.equal(calls.length, 1);
126 assert.equal(calls[0].key, key);
127 });
128
129 test('gateway proxies POST /api/v1/memory/search JSON body to bridge', async (t) => {
130 const calls = [];
131 const mockBridge = express();
132 mockBridge.use(express.json({ limit: '1mb' }));
133 mockBridge.post('/api/v1/memory/search', (req, res) => {
134 calls.push({
135 method: req.method,
136 body: req.body,
137 auth: req.headers.authorization,
138 vault: req.headers['x-vault-id'],
139 });
140 res.json({ results: [], count: 0, note: 'stub' });
141 });
142
143 const { bridgeUrl, close } = await startMockBridge(mockBridge);
144 t.after(close);
145
146 process.env.NETLIFY = '1';
147 process.env.CANISTER_URL = 'http://canister.placeholder.test';
148 process.env.SESSION_SECRET = SECRET;
149 process.env.BRIDGE_URL = bridgeUrl;
150
151 const gwEntry = pathToFileURL(path.join(projectRoot, 'hub', 'gateway', 'server.mjs')).href;
152 const { app: gwApp } = await import(`${gwEntry}?gwmem3=${Date.now()}`);
153
154 const gwSrv = http.createServer(gwApp);
155 await new Promise((resolve, reject) => {
156 gwSrv.listen(0, '127.0.0.1', (err) => (err ? reject(err) : resolve()));
157 });
158 t.after(() => new Promise((r) => gwSrv.close(() => r())));
159 const gwPort = /** @type {import('net').AddressInfo} */ (gwSrv.address()).port;
160
161 const token = signTestJwt({ sub: 'google:mem-search-test', role: 'editor' });
162 const payload = { query: 'hello', limit: 5 };
163 const res = await fetch(`http://127.0.0.1:${gwPort}/api/v1/memory/search`, {
164 method: 'POST',
165 headers: {
166 Authorization: `Bearer ${token}`,
167 'X-Vault-Id': 'default',
168 'Content-Type': 'application/json',
169 },
170 body: JSON.stringify(payload),
171 });
172 const text = await res.text();
173 assert.equal(res.status, 200, text);
174 assert.equal(calls.length, 1);
175 assert.deepEqual(calls[0].body, payload);
176 assert.equal(calls[0].auth, `Bearer ${token}`);
177 });
File History 2 commits
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd feat(calendar): enforce agent context tiers in retrieval AP… Human minor 1 day ago
sha256:9103f98c89257ed2b01c237cea895dabb3e85ea337dccb1161c175e4422355b6 docs: accept Calendar Events v0 spec with Phase 0 security … Human 1 day ago