gateway-metadata-bulk.test.mjs
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd
feat(calendar): enforce agent context tiers in retrieval AP…
Human
minor
⚠ breaking
1 day ago
| 1 | /** |
| 2 | * Gateway hosted bulk metadata (canister orchestration) — fetch mocked. |
| 3 | */ |
| 4 | import { describe, it, beforeEach, afterEach } from 'node:test'; |
| 5 | import assert from 'node:assert'; |
| 6 | import crypto from 'node:crypto'; |
| 7 | import { createMetadataBulkHandlers } from '../hub/gateway/metadata-bulk-canister.mjs'; |
| 8 | |
| 9 | const SECRET = 'gateway-metadata-bulk-test-secret'; |
| 10 | const CANISTER = 'https://mock-canister.test'; |
| 11 | |
| 12 | /** HS256 JWT for tests (matches `jsonwebtoken` verify in gateway handler). */ |
| 13 | function bearerToken(role = 'editor') { |
| 14 | const header = Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT' })).toString('base64url'); |
| 15 | const payload = Buffer.from(JSON.stringify({ sub: 'google:test-user', role })).toString('base64url'); |
| 16 | const data = `${header}.${payload}`; |
| 17 | const sig = crypto.createHmac('sha256', SECRET).update(data).digest('base64url'); |
| 18 | return `${data}.${sig}`; |
| 19 | } |
| 20 | |
| 21 | describe('gateway metadata-bulk-canister', () => { |
| 22 | /** @type {typeof fetch | undefined} */ |
| 23 | let origFetch; |
| 24 | |
| 25 | beforeEach(() => { |
| 26 | origFetch = globalThis.fetch; |
| 27 | }); |
| 28 | |
| 29 | afterEach(() => { |
| 30 | globalThis.fetch = origFetch; |
| 31 | }); |
| 32 | |
| 33 | it('deleteByProject deletes notes matching effective slug and discards proposals', async () => { |
| 34 | const notesPayload = { |
| 35 | notes: [ |
| 36 | { path: 'inbox/a.md', frontmatter: '{}', body: 'A' }, |
| 37 | { path: 'projects/foo/x.md', frontmatter: '{}', body: 'X' }, |
| 38 | ], |
| 39 | }; |
| 40 | const proposalsPayload = { |
| 41 | proposals: [ |
| 42 | { |
| 43 | proposal_id: 'p1', |
| 44 | path: 'projects/foo/x.md', |
| 45 | status: 'proposed', |
| 46 | vault_id: 'default', |
| 47 | }, |
| 48 | ], |
| 49 | }; |
| 50 | |
| 51 | globalThis.fetch = async (url, opts) => { |
| 52 | const u = String(url); |
| 53 | const method = (opts && opts.method) || 'GET'; |
| 54 | if (u === `${CANISTER}/api/v1/notes` && method === 'GET') { |
| 55 | return { ok: true, status: 200, async text() { return JSON.stringify(notesPayload); } }; |
| 56 | } |
| 57 | if (u === `${CANISTER}/api/v1/notes/projects%2Ffoo%2Fx.md` && method === 'DELETE') { |
| 58 | return { ok: true, status: 200, async text() { return '{"deleted":true}'; } }; |
| 59 | } |
| 60 | if (u === `${CANISTER}/api/v1/proposals` && method === 'GET') { |
| 61 | return { ok: true, status: 200, async text() { return JSON.stringify(proposalsPayload); } }; |
| 62 | } |
| 63 | if (u === `${CANISTER}/api/v1/proposals/p1/discard` && method === 'POST') { |
| 64 | return { ok: true, status: 200, async text() { return '{}'; } }; |
| 65 | } |
| 66 | return { ok: false, status: 404, async text() { return 'unexpected ' + u; } }; |
| 67 | }; |
| 68 | |
| 69 | const handlers = createMetadataBulkHandlers({ |
| 70 | CANISTER_URL: CANISTER, |
| 71 | BRIDGE_URL: '', |
| 72 | SESSION_SECRET: SECRET, |
| 73 | getUserId: () => 'google:test-user', |
| 74 | getHostedAccessContext: async () => null, |
| 75 | }); |
| 76 | |
| 77 | /** @type {any} */ |
| 78 | const res = { |
| 79 | statusCode: 200, |
| 80 | payload: null, |
| 81 | status(c) { |
| 82 | this.statusCode = c; |
| 83 | return this; |
| 84 | }, |
| 85 | json(o) { |
| 86 | this.payload = o; |
| 87 | return this; |
| 88 | }, |
| 89 | }; |
| 90 | |
| 91 | await handlers.deleteByProject( |
| 92 | { |
| 93 | headers: { authorization: 'Bearer ' + bearerToken('editor') }, |
| 94 | body: { project: 'foo' }, |
| 95 | }, |
| 96 | res, |
| 97 | ); |
| 98 | |
| 99 | assert.strictEqual(res.statusCode, 200); |
| 100 | assert.strictEqual(res.payload.deleted, 1); |
| 101 | assert.deepStrictEqual(res.payload.paths, ['projects/foo/x.md']); |
| 102 | assert.strictEqual(res.payload.proposals_discarded, 1); |
| 103 | }); |
| 104 | |
| 105 | it('renameProject posts merged body for each matching note', async () => { |
| 106 | const notesPayload = { |
| 107 | notes: [ |
| 108 | { path: 'inbox/o.md', frontmatter: JSON.stringify({ project: 'oldslug', title: 'T' }), body: 'B' }, |
| 109 | { path: 'inbox/other.md', frontmatter: JSON.stringify({ project: 'x' }), body: 'O' }, |
| 110 | ], |
| 111 | }; |
| 112 | /** @type {unknown[]} */ |
| 113 | const posts = []; |
| 114 | |
| 115 | globalThis.fetch = async (url, opts) => { |
| 116 | const u = String(url); |
| 117 | const method = (opts && opts.method) || 'GET'; |
| 118 | if (u === `${CANISTER}/api/v1/notes` && method === 'GET') { |
| 119 | return { ok: true, status: 200, async text() { return JSON.stringify(notesPayload); } }; |
| 120 | } |
| 121 | if (u === `${CANISTER}/api/v1/notes` && method === 'POST') { |
| 122 | posts.push(JSON.parse(String(opts.body))); |
| 123 | return { ok: true, status: 200, async text() { return '{"written":true}'; } }; |
| 124 | } |
| 125 | return { ok: false, status: 404, async text() { return ''; } }; |
| 126 | }; |
| 127 | |
| 128 | const handlers = createMetadataBulkHandlers({ |
| 129 | CANISTER_URL: CANISTER, |
| 130 | BRIDGE_URL: '', |
| 131 | SESSION_SECRET: SECRET, |
| 132 | getUserId: () => 'google:test-user', |
| 133 | getHostedAccessContext: async () => null, |
| 134 | }); |
| 135 | |
| 136 | /** @type {any} */ |
| 137 | const res = { |
| 138 | statusCode: 200, |
| 139 | payload: null, |
| 140 | status(c) { |
| 141 | this.statusCode = c; |
| 142 | return this; |
| 143 | }, |
| 144 | json(o) { |
| 145 | this.payload = o; |
| 146 | return this; |
| 147 | }, |
| 148 | }; |
| 149 | |
| 150 | await handlers.renameProject( |
| 151 | { |
| 152 | headers: { authorization: 'Bearer ' + bearerToken('editor'), 'x-vault-id': 'default' }, |
| 153 | body: { from: 'oldslug', to: 'newslug' }, |
| 154 | }, |
| 155 | res, |
| 156 | ); |
| 157 | |
| 158 | assert.strictEqual(res.statusCode, 200); |
| 159 | assert.strictEqual(res.payload.updated, 1); |
| 160 | assert.deepStrictEqual(res.payload.paths, ['inbox/o.md']); |
| 161 | assert.strictEqual(posts.length, 1); |
| 162 | const p = /** @type {Record<string, unknown>} */ (posts[0]); |
| 163 | assert.strictEqual(p.path, 'inbox/o.md'); |
| 164 | assert.strictEqual(p.body, 'B'); |
| 165 | assert.strictEqual(/** @type {any} */ (p.frontmatter).project, 'newslug'); |
| 166 | assert.strictEqual(/** @type {any} */ (p.frontmatter).title, 'T'); |
| 167 | }); |
| 168 | |
| 169 | it('deleteByProject returns 403 for viewer role', async () => { |
| 170 | globalThis.fetch = async () => ({ ok: true, status: 200, async text() { return '{"notes":[]}'; } }); |
| 171 | |
| 172 | const handlers = createMetadataBulkHandlers({ |
| 173 | CANISTER_URL: CANISTER, |
| 174 | BRIDGE_URL: '', |
| 175 | SESSION_SECRET: SECRET, |
| 176 | getUserId: () => 'google:test-user', |
| 177 | getHostedAccessContext: async () => null, |
| 178 | }); |
| 179 | |
| 180 | /** @type {any} */ |
| 181 | const res = { |
| 182 | statusCode: 200, |
| 183 | payload: null, |
| 184 | status(c) { |
| 185 | this.statusCode = c; |
| 186 | return this; |
| 187 | }, |
| 188 | json(o) { |
| 189 | this.payload = o; |
| 190 | return this; |
| 191 | }, |
| 192 | }; |
| 193 | |
| 194 | await handlers.deleteByProject( |
| 195 | { |
| 196 | headers: { authorization: 'Bearer ' + bearerToken('viewer') }, |
| 197 | body: { project: 'x' }, |
| 198 | }, |
| 199 | res, |
| 200 | ); |
| 201 | |
| 202 | assert.strictEqual(res.statusCode, 403); |
| 203 | }); |
| 204 | }); |
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