gateway-notes-copy.test.mjs
140 lines 4.6 KB
Raw
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd feat(calendar): enforce agent context tiers in retrieval AP… Human minor ⚠ breaking 1 day ago
1 /**
2 * Gateway POST /api/v1/notes/copy — fetch to canister + bridge mocked.
3 */
4 import { describe, it, before, after } from 'node:test';
5 import assert from 'node:assert/strict';
6 import http from 'node:http';
7 import crypto from 'node:crypto';
8
9 const SECRET = 'x'.repeat(32);
10
11 function bearer(role = 'editor') {
12 const header = Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT' })).toString('base64url');
13 const payload = Buffer.from(JSON.stringify({ sub: 'google:u1', role })).toString('base64url');
14 const data = `${header}.${payload}`;
15 const sig = crypto.createHmac('sha256', SECRET).update(data).digest('base64url');
16 return `${data}.${sig}`;
17 }
18
19 function installFetchMock(fetchCalls) {
20 globalThis.fetch = async (url, opts) => {
21 fetchCalls.push({ url: String(url), opts });
22 const u = String(url);
23 const method = (opts && opts.method) || 'GET';
24 if (u.includes('mock-bridge.test/api/v1/hosted-context')) {
25 return {
26 ok: true,
27 status: 200,
28 async json() {
29 return {
30 allowed_vault_ids: ['default', 'business'],
31 effective_canister_user_id: 'google:u1',
32 role: 'editor',
33 scope: null,
34 };
35 },
36 };
37 }
38 if (u.includes('mock-canister.test/api/v1/notes/') && method === 'GET' && u.includes('inbox') && u.includes('note.md')) {
39 return {
40 ok: true,
41 status: 200,
42 async text() {
43 return JSON.stringify({
44 path: 'inbox/note.md',
45 body: 'Hello',
46 frontmatter: { title: 'T' },
47 });
48 },
49 };
50 }
51 if (u === 'https://mock-canister.test/api/v1/notes' && method === 'POST') {
52 return {
53 ok: true,
54 status: 200,
55 async text() {
56 return JSON.stringify({ path: 'inbox/note.md', written: true });
57 },
58 };
59 }
60 if (u.includes('mock-canister.test/api/v1/notes/') && method === 'DELETE' && u.includes('inbox') && u.includes('note.md')) {
61 return { ok: true, status: 200, async text() { return JSON.stringify({ deleted: true }); } };
62 }
63 if (u.includes('mock-bridge.test/api/v1/index') && method === 'POST') {
64 return {
65 ok: true,
66 status: 200,
67 async text() {
68 return JSON.stringify({ ok: true, embedding_input_tokens: 0 });
69 },
70 };
71 }
72 return { ok: false, status: 500, async text() { return 'unexpected ' + u; } };
73 };
74 }
75
76 describe('gateway POST /api/v1/notes/copy', () => {
77 /** @type {import('http').Server} */
78 let server;
79 /** @type {string} */
80 let base;
81 /** @type {Array<{ url: string, opts: RequestInit | undefined }>} */
82 let fetchCalls;
83 /** @type {typeof fetch} */
84 let origFetch;
85
86 before(async () => {
87 // Keep native fetch for the test client's HTTP calls; gateway uses globalThis.fetch to reach canister/bridge.
88 origFetch = globalThis.fetch.bind(globalThis);
89 process.env.NETLIFY = '1';
90 process.env.CANISTER_URL = 'https://mock-canister.test';
91 process.env.SESSION_SECRET = SECRET;
92 process.env.BRIDGE_URL = 'https://mock-bridge.test';
93 delete process.env.BILLING_ENFORCE;
94 delete process.env.KNOWTATION_AIR_ENDPOINT;
95
96 fetchCalls = [];
97 installFetchMock(fetchCalls);
98
99 const { app } = await import('../hub/gateway/server.mjs');
100 server = http.createServer(app);
101 await new Promise((r) => server.listen(0, '127.0.0.1', r));
102 const addr = server.address();
103 assert.ok(addr && typeof addr === 'object');
104 base = `http://127.0.0.1:${addr.port}`;
105 });
106
107 after(async () => {
108 if (server) await new Promise((r) => server.close(r));
109 globalThis.fetch = origFetch;
110 });
111
112 it('copies between vaults and deletes source when delete_source is true', async () => {
113 fetchCalls.length = 0;
114 const res = await origFetch(base + '/api/v1/notes/copy', {
115 method: 'POST',
116 headers: {
117 Authorization: 'Bearer ' + bearer('editor'),
118 'Content-Type': 'application/json',
119 },
120 body: JSON.stringify({
121 from_vault_id: 'default',
122 to_vault_id: 'business',
123 path: 'inbox/note.md',
124 delete_source: true,
125 }),
126 });
127 assert.equal(res.status, 200);
128 const j = await res.json();
129 assert.equal(j.ok, true);
130 assert.equal(j.moved, true);
131 const methods = fetchCalls.map((c) => (c.opts && c.opts.method) || 'GET');
132 assert.ok(methods.includes('GET'));
133 assert.ok(methods.includes('POST'));
134 assert.ok(methods.includes('DELETE'));
135 const indexPosts = fetchCalls.filter(
136 (c) => String(c.url).includes('/api/v1/index') && c.opts && c.opts.method === 'POST',
137 );
138 assert.equal(indexPosts.length, 2);
139 });
140 });
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