@sirrlock/mcp 0.1.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +100 -27
- package/dist/helpers.d.ts +8 -0
- package/dist/helpers.js +34 -0
- package/dist/helpers.js.map +1 -1
- package/dist/helpers.test.js +68 -0
- package/dist/helpers.test.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +621 -87
- package/dist/index.js.map +1 -1
- package/dist/index.test.d.ts +8 -0
- package/dist/index.test.js +619 -0
- package/dist/index.test.js.map +1 -0
- package/dist/integration.test.js +123 -0
- package/dist/integration.test.js.map +1 -1
- package/package.json +5 -4
|
@@ -0,0 +1,619 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Unit tests for MCP tool handlers.
|
|
4
|
+
*
|
|
5
|
+
* Uses a mock HTTP server so tests run without a real sirrd instance.
|
|
6
|
+
* Assertions cover: HTTP method, path, request body, response formatting,
|
|
7
|
+
* 404 handling (returns text, not error), non-2xx handling (returns Error:).
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
const globals_1 = require("@jest/globals");
|
|
11
|
+
const http_1 = require("http");
|
|
12
|
+
const child_process_1 = require("child_process");
|
|
13
|
+
const readline_1 = require("readline");
|
|
14
|
+
class MockServer {
|
|
15
|
+
constructor(port) {
|
|
16
|
+
this.responseQueue = [];
|
|
17
|
+
this.lastRequest = null;
|
|
18
|
+
this.port = port;
|
|
19
|
+
this.url = `http://127.0.0.1:${port}`;
|
|
20
|
+
this.server = (0, http_1.createServer)(async (req, res) => {
|
|
21
|
+
const chunks = [];
|
|
22
|
+
for await (const chunk of req)
|
|
23
|
+
chunks.push(chunk);
|
|
24
|
+
const raw = Buffer.concat(chunks).toString();
|
|
25
|
+
let body = null;
|
|
26
|
+
try {
|
|
27
|
+
body = JSON.parse(raw);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
body = raw || null;
|
|
31
|
+
}
|
|
32
|
+
this.lastRequest = { method: req.method, path: req.url, body };
|
|
33
|
+
const next = this.responseQueue.shift() ?? { status: 200, body: {} };
|
|
34
|
+
const ct = next.contentType ?? "application/json";
|
|
35
|
+
res.writeHead(next.status, { "Content-Type": ct });
|
|
36
|
+
res.end(ct === "application/json" ? JSON.stringify(next.body) : String(next.body));
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
start() {
|
|
40
|
+
return new Promise((resolve) => this.server.listen(this.port, "127.0.0.1", resolve));
|
|
41
|
+
}
|
|
42
|
+
stop() {
|
|
43
|
+
return new Promise((resolve, reject) => this.server.close((err) => (err ? reject(err) : resolve())));
|
|
44
|
+
}
|
|
45
|
+
/** Enqueue a JSON response to be served for the next request. */
|
|
46
|
+
next(status, body) {
|
|
47
|
+
this.responseQueue.push({ status, body });
|
|
48
|
+
return this;
|
|
49
|
+
}
|
|
50
|
+
/** Enqueue a plain-text response (simulates rate-limiter / proxy errors). */
|
|
51
|
+
nextText(status, text) {
|
|
52
|
+
this.responseQueue.push({ status, body: text, contentType: "text/plain" });
|
|
53
|
+
return this;
|
|
54
|
+
}
|
|
55
|
+
clearQueue() {
|
|
56
|
+
this.responseQueue = [];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
class McpClient {
|
|
60
|
+
constructor(proc) {
|
|
61
|
+
this.proc = proc;
|
|
62
|
+
this.pending = new Map();
|
|
63
|
+
this.nextId = 1;
|
|
64
|
+
this.rl = (0, readline_1.createInterface)({ input: proc.stdout });
|
|
65
|
+
this.rl.on("line", (line) => {
|
|
66
|
+
if (!line.trim())
|
|
67
|
+
return;
|
|
68
|
+
const msg = JSON.parse(line);
|
|
69
|
+
if (msg.id != null) {
|
|
70
|
+
const resolve = this.pending.get(msg.id);
|
|
71
|
+
if (resolve) {
|
|
72
|
+
this.pending.delete(msg.id);
|
|
73
|
+
resolve(msg);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
close() { this.rl.close(); this.proc.kill(); }
|
|
79
|
+
rpc(method, params) {
|
|
80
|
+
const id = this.nextId++;
|
|
81
|
+
return new Promise((resolve) => {
|
|
82
|
+
this.pending.set(id, resolve);
|
|
83
|
+
this.proc.stdin.write(JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n");
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
async initialize() {
|
|
87
|
+
await this.rpc("initialize", {
|
|
88
|
+
protocolVersion: "2024-11-05",
|
|
89
|
+
capabilities: {},
|
|
90
|
+
clientInfo: { name: "unit-test", version: "1" },
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
async call(tool, args = {}) {
|
|
94
|
+
const resp = await this.rpc("tools/call", { name: tool, arguments: args });
|
|
95
|
+
if (resp.error)
|
|
96
|
+
throw new Error(`MCP error: ${resp.error.message}`);
|
|
97
|
+
return resp.result?.content?.[0]?.text ?? "";
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// ── Fixtures ──────────────────────────────────────────────────────────────────
|
|
101
|
+
const MOCK_PORT = 19082;
|
|
102
|
+
const TOKEN = "unit-test-token";
|
|
103
|
+
const MCP_BIN = `${__dirname}/../dist/index.js`;
|
|
104
|
+
let mock;
|
|
105
|
+
let client;
|
|
106
|
+
let mcpProc;
|
|
107
|
+
(0, globals_1.beforeAll)(async () => {
|
|
108
|
+
mock = new MockServer(MOCK_PORT);
|
|
109
|
+
await mock.start();
|
|
110
|
+
mcpProc = (0, child_process_1.spawn)("node", [MCP_BIN], {
|
|
111
|
+
env: { ...process.env, SIRR_SERVER: mock.url, SIRRLOCK_URL: mock.url, SIRR_TOKEN: TOKEN },
|
|
112
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
113
|
+
});
|
|
114
|
+
mcpProc.stderr.on("data", (d) => {
|
|
115
|
+
const msg = d.toString();
|
|
116
|
+
if (!msg.includes("[sirr-mcp] Warning"))
|
|
117
|
+
process.stderr.write(msg);
|
|
118
|
+
});
|
|
119
|
+
client = new McpClient(mcpProc);
|
|
120
|
+
await client.initialize();
|
|
121
|
+
}, 15000);
|
|
122
|
+
(0, globals_1.afterAll)(async () => {
|
|
123
|
+
client?.close();
|
|
124
|
+
await mock?.stop();
|
|
125
|
+
});
|
|
126
|
+
(0, globals_1.afterEach)(() => {
|
|
127
|
+
mock.clearQueue();
|
|
128
|
+
});
|
|
129
|
+
// ── check_secret ─────────────────────────────────────────────────────────────
|
|
130
|
+
(0, globals_1.describe)("check_secret", () => {
|
|
131
|
+
(0, globals_1.it)("HEAD /secrets/{key} — active secret returns metadata without consuming a read", async () => {
|
|
132
|
+
const future = Math.floor(Date.now() / 1000) + 3600;
|
|
133
|
+
mock.next(200, {});
|
|
134
|
+
// Mock server doesn't send response headers naturally, but we test the path/method
|
|
135
|
+
const text = await client.call("check_secret", { key: "MY_KEY" });
|
|
136
|
+
(0, globals_1.expect)(mock.lastRequest?.method).toBe("HEAD");
|
|
137
|
+
(0, globals_1.expect)(mock.lastRequest?.path).toBe("/secrets/MY_KEY");
|
|
138
|
+
// 200 with no X-Sirr-Status header defaults to "active"
|
|
139
|
+
(0, globals_1.expect)(text).toContain("active");
|
|
140
|
+
});
|
|
141
|
+
(0, globals_1.it)("404 → not found message", async () => {
|
|
142
|
+
mock.next(404, {});
|
|
143
|
+
const text = await client.call("check_secret", { key: "GONE" });
|
|
144
|
+
(0, globals_1.expect)(text).toContain("not found");
|
|
145
|
+
(0, globals_1.expect)(text).not.toContain("Error:");
|
|
146
|
+
});
|
|
147
|
+
(0, globals_1.it)("410 → sealed message", async () => {
|
|
148
|
+
mock.next(410, {});
|
|
149
|
+
const text = await client.call("check_secret", { key: "SEALED" });
|
|
150
|
+
(0, globals_1.expect)(text).toContain("sealed");
|
|
151
|
+
(0, globals_1.expect)(text).not.toContain("Error:");
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
// ── get_secret ────────────────────────────────────────────────────────────────
|
|
155
|
+
(0, globals_1.describe)("get_secret", () => {
|
|
156
|
+
(0, globals_1.it)("GET /secrets/{id} (public dead drop) and returns value", async () => {
|
|
157
|
+
mock.next(200, { id: "abc123", value: "supersecret" });
|
|
158
|
+
const text = await client.call("get_secret", { id: "abc123" });
|
|
159
|
+
(0, globals_1.expect)(mock.lastRequest?.method).toBe("GET");
|
|
160
|
+
(0, globals_1.expect)(mock.lastRequest?.path).toBe("/secrets/abc123");
|
|
161
|
+
(0, globals_1.expect)(text).toBe("supersecret");
|
|
162
|
+
});
|
|
163
|
+
(0, globals_1.it)("GET /orgs/{org}/secrets/{key} (org-scoped) and returns value", async () => {
|
|
164
|
+
mock.next(200, { id: "hex64id", value: "orgsecret" });
|
|
165
|
+
const text = await client.call("get_secret", { key: "MY_KEY", org: "my-org" });
|
|
166
|
+
(0, globals_1.expect)(mock.lastRequest?.method).toBe("GET");
|
|
167
|
+
(0, globals_1.expect)(mock.lastRequest?.path).toBe("/orgs/my-org/secrets/MY_KEY");
|
|
168
|
+
(0, globals_1.expect)(text).toBe("orgsecret");
|
|
169
|
+
});
|
|
170
|
+
(0, globals_1.it)("error if neither id nor key provided", async () => {
|
|
171
|
+
const text = await client.call("get_secret", {});
|
|
172
|
+
(0, globals_1.expect)(text).toContain("Error:");
|
|
173
|
+
(0, globals_1.expect)(text).toContain("id");
|
|
174
|
+
});
|
|
175
|
+
(0, globals_1.it)("error if key provided without org (and no SIRR_ORG env)", async () => {
|
|
176
|
+
const text = await client.call("get_secret", { key: "MY_KEY" });
|
|
177
|
+
(0, globals_1.expect)(text).toContain("Error:");
|
|
178
|
+
(0, globals_1.expect)(text).toContain("org");
|
|
179
|
+
});
|
|
180
|
+
(0, globals_1.it)("404 → not-found message, not an error (public id)", async () => {
|
|
181
|
+
mock.next(404, { error: "not found" });
|
|
182
|
+
const text = await client.call("get_secret", { id: "MISSING" });
|
|
183
|
+
(0, globals_1.expect)(text).toContain("not found");
|
|
184
|
+
(0, globals_1.expect)(text).not.toContain("Error:");
|
|
185
|
+
});
|
|
186
|
+
(0, globals_1.it)("410 → not-found message (burned public secret)", async () => {
|
|
187
|
+
mock.next(410, { error: "gone" });
|
|
188
|
+
const text = await client.call("get_secret", { id: "BURNED" });
|
|
189
|
+
(0, globals_1.expect)(text).toContain("not found");
|
|
190
|
+
(0, globals_1.expect)(text).not.toContain("Error:");
|
|
191
|
+
});
|
|
192
|
+
(0, globals_1.it)("403 → Error: in output (public id)", async () => {
|
|
193
|
+
mock.next(403, { error: "forbidden" });
|
|
194
|
+
const text = await client.call("get_secret", { id: "DENIED" });
|
|
195
|
+
(0, globals_1.expect)(text).toContain("Error:");
|
|
196
|
+
(0, globals_1.expect)(text).toContain("403");
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
// ── push_secret ───────────────────────────────────────────────────────────────
|
|
200
|
+
(0, globals_1.describe)("push_secret", () => {
|
|
201
|
+
(0, globals_1.it)("POST /secrets with value only (no key)", async () => {
|
|
202
|
+
mock.next(201, { id: "deadbeef01020304" });
|
|
203
|
+
const text = await client.call("push_secret", { value: "bar" });
|
|
204
|
+
(0, globals_1.expect)(mock.lastRequest?.method).toBe("POST");
|
|
205
|
+
(0, globals_1.expect)(mock.lastRequest?.path).toBe("/secrets");
|
|
206
|
+
(0, globals_1.expect)((mock.lastRequest?.body)["key"]).toBeUndefined();
|
|
207
|
+
(0, globals_1.expect)((mock.lastRequest?.body)["value"]).toBe("bar");
|
|
208
|
+
(0, globals_1.expect)(text).toContain("deadbeef01020304");
|
|
209
|
+
});
|
|
210
|
+
(0, globals_1.it)("returns URL in output", async () => {
|
|
211
|
+
mock.next(201, { id: "abc123" });
|
|
212
|
+
const text = await client.call("push_secret", { value: "v" });
|
|
213
|
+
(0, globals_1.expect)(text).toContain("/s/abc123");
|
|
214
|
+
});
|
|
215
|
+
(0, globals_1.it)("reports TTL in output when ttl_seconds provided", async () => {
|
|
216
|
+
mock.next(201, { id: "ttlid" });
|
|
217
|
+
const text = await client.call("push_secret", { value: "v", ttl_seconds: 3600 });
|
|
218
|
+
(0, globals_1.expect)(text).toContain("1h");
|
|
219
|
+
});
|
|
220
|
+
(0, globals_1.it)("reports burn limit in output when max_reads provided", async () => {
|
|
221
|
+
mock.next(201, { id: "burnid" });
|
|
222
|
+
const text = await client.call("push_secret", { value: "v", max_reads: 1 });
|
|
223
|
+
(0, globals_1.expect)(text).toContain("1 read(s)");
|
|
224
|
+
});
|
|
225
|
+
(0, globals_1.it)("does not send null for omitted optional fields", async () => {
|
|
226
|
+
mock.next(201, { id: "bareid" });
|
|
227
|
+
await client.call("push_secret", { value: "v" });
|
|
228
|
+
const body = mock.lastRequest?.body;
|
|
229
|
+
(0, globals_1.expect)(body["ttl_seconds"]).toBeUndefined();
|
|
230
|
+
(0, globals_1.expect)(body["max_reads"]).toBeUndefined();
|
|
231
|
+
(0, globals_1.expect)(body["key"]).toBeUndefined();
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
// ── set_secret ────────────────────────────────────────────────────────────────
|
|
235
|
+
(0, globals_1.describe)("set_secret", () => {
|
|
236
|
+
(0, globals_1.it)("POST /orgs/{org}/secrets with key and value", async () => {
|
|
237
|
+
mock.next(201, { key: "FOO", id: "hexid001" });
|
|
238
|
+
const text = await client.call("set_secret", { org: "my-org", key: "FOO", value: "bar" });
|
|
239
|
+
(0, globals_1.expect)(mock.lastRequest?.method).toBe("POST");
|
|
240
|
+
(0, globals_1.expect)(mock.lastRequest?.path).toBe("/orgs/my-org/secrets");
|
|
241
|
+
(0, globals_1.expect)((mock.lastRequest?.body)["key"]).toBe("FOO");
|
|
242
|
+
(0, globals_1.expect)((mock.lastRequest?.body)["value"]).toBe("bar");
|
|
243
|
+
(0, globals_1.expect)(text).toContain("FOO");
|
|
244
|
+
(0, globals_1.expect)(text).toContain("hexid001");
|
|
245
|
+
});
|
|
246
|
+
(0, globals_1.it)("409 → conflict message with isError", async () => {
|
|
247
|
+
mock.next(409, { error: "conflict" });
|
|
248
|
+
const text = await client.call("set_secret", { org: "my-org", key: "FOO", value: "bar" });
|
|
249
|
+
(0, globals_1.expect)(text).toContain("Conflict");
|
|
250
|
+
(0, globals_1.expect)(text).toContain("FOO");
|
|
251
|
+
(0, globals_1.expect)(text).toContain("patch_secret");
|
|
252
|
+
});
|
|
253
|
+
(0, globals_1.it)("401 → Error: in output", async () => {
|
|
254
|
+
mock.next(401, { error: "unauthorized" });
|
|
255
|
+
const text = await client.call("set_secret", { org: "my-org", key: "FOO", value: "bar" });
|
|
256
|
+
(0, globals_1.expect)(text).toContain("Error:");
|
|
257
|
+
(0, globals_1.expect)(text).toContain("401");
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
// ── patch_secret ──────────────────────────────────────────────────────────────
|
|
261
|
+
(0, globals_1.describe)("patch_secret", () => {
|
|
262
|
+
(0, globals_1.it)("PATCH /secrets/{key} with body", async () => {
|
|
263
|
+
mock.next(200, { key: "FOO", read_count: 0, max_reads: 5, expires_at: null });
|
|
264
|
+
await client.call("patch_secret", { key: "FOO", max_reads: 5 });
|
|
265
|
+
(0, globals_1.expect)(mock.lastRequest?.method).toBe("PATCH");
|
|
266
|
+
(0, globals_1.expect)(mock.lastRequest?.path).toBe("/secrets/FOO");
|
|
267
|
+
(0, globals_1.expect)((mock.lastRequest?.body)["max_reads"]).toBe(5);
|
|
268
|
+
});
|
|
269
|
+
(0, globals_1.it)("only sends provided fields", async () => {
|
|
270
|
+
mock.next(200, { key: "FOO", read_count: 0, max_reads: null, expires_at: null });
|
|
271
|
+
await client.call("patch_secret", { key: "FOO", ttl_seconds: 7200 });
|
|
272
|
+
const body = mock.lastRequest?.body;
|
|
273
|
+
(0, globals_1.expect)(body["ttl_seconds"]).toBe(7200);
|
|
274
|
+
(0, globals_1.expect)(body["max_reads"]).toBeUndefined();
|
|
275
|
+
(0, globals_1.expect)(body["value"]).toBeUndefined();
|
|
276
|
+
});
|
|
277
|
+
(0, globals_1.it)("formats response with expiry and max_reads", async () => {
|
|
278
|
+
const future = Math.floor(Date.now() / 1000) + 7200;
|
|
279
|
+
mock.next(200, { key: "FOO", read_count: 1, max_reads: 5, expires_at: future });
|
|
280
|
+
const text = await client.call("patch_secret", { key: "FOO" });
|
|
281
|
+
(0, globals_1.expect)(text).toContain("FOO");
|
|
282
|
+
(0, globals_1.expect)(text).toContain("2h");
|
|
283
|
+
(0, globals_1.expect)(text).toContain("Max reads: 5 (1 used)");
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
// ── list_secrets ──────────────────────────────────────────────────────────────
|
|
287
|
+
(0, globals_1.describe)("list_secrets", () => {
|
|
288
|
+
(0, globals_1.it)("GET /secrets and formats list", async () => {
|
|
289
|
+
mock.next(200, {
|
|
290
|
+
secrets: [
|
|
291
|
+
{ key: "KEY_A", read_count: 2, max_reads: null, expires_at: null },
|
|
292
|
+
{ key: "KEY_B", read_count: 0, max_reads: 1, expires_at: null },
|
|
293
|
+
],
|
|
294
|
+
});
|
|
295
|
+
const text = await client.call("list_secrets");
|
|
296
|
+
(0, globals_1.expect)(mock.lastRequest?.method).toBe("GET");
|
|
297
|
+
(0, globals_1.expect)(mock.lastRequest?.path).toBe("/secrets");
|
|
298
|
+
(0, globals_1.expect)(text).toContain("KEY_A");
|
|
299
|
+
(0, globals_1.expect)(text).toContain("KEY_B");
|
|
300
|
+
(0, globals_1.expect)(text).toContain("0/1 reads");
|
|
301
|
+
});
|
|
302
|
+
(0, globals_1.it)("empty vault → 'No active secrets'", async () => {
|
|
303
|
+
mock.next(200, { secrets: [] });
|
|
304
|
+
const text = await client.call("list_secrets");
|
|
305
|
+
(0, globals_1.expect)(text).toBe("No active secrets.");
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
// ── delete_secret ─────────────────────────────────────────────────────────────
|
|
309
|
+
(0, globals_1.describe)("delete_secret", () => {
|
|
310
|
+
(0, globals_1.it)("DELETE /secrets/{key}", async () => {
|
|
311
|
+
mock.next(200, { deleted: true });
|
|
312
|
+
await client.call("delete_secret", { key: "TO_DEL" });
|
|
313
|
+
(0, globals_1.expect)(mock.lastRequest?.method).toBe("DELETE");
|
|
314
|
+
(0, globals_1.expect)(mock.lastRequest?.path).toBe("/secrets/TO_DEL");
|
|
315
|
+
});
|
|
316
|
+
(0, globals_1.it)("404 → not-found message, not an error", async () => {
|
|
317
|
+
mock.next(404, { error: "not found" });
|
|
318
|
+
const text = await client.call("delete_secret", { key: "MISSING" });
|
|
319
|
+
(0, globals_1.expect)(text).toContain("not found");
|
|
320
|
+
(0, globals_1.expect)(text).not.toContain("Error:");
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
// ── prune_secrets ─────────────────────────────────────────────────────────────
|
|
324
|
+
(0, globals_1.describe)("prune_secrets", () => {
|
|
325
|
+
(0, globals_1.it)("POST /prune and reports count", async () => {
|
|
326
|
+
mock.next(200, { pruned: 3 });
|
|
327
|
+
const text = await client.call("prune_secrets");
|
|
328
|
+
(0, globals_1.expect)(mock.lastRequest?.method).toBe("POST");
|
|
329
|
+
(0, globals_1.expect)(mock.lastRequest?.path).toBe("/prune");
|
|
330
|
+
(0, globals_1.expect)(text).toContain("3");
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
// ── sirr_audit ────────────────────────────────────────────────────────────────
|
|
334
|
+
(0, globals_1.describe)("sirr_audit", () => {
|
|
335
|
+
(0, globals_1.it)("GET /audit with query params", async () => {
|
|
336
|
+
mock.next(200, { events: [
|
|
337
|
+
{ id: 1, timestamp: 1000, action: "secret.read", key: "K", source_ip: "1.2.3.4", success: true },
|
|
338
|
+
] });
|
|
339
|
+
const text = await client.call("sirr_audit", { since: 500, action: "secret.read", limit: 10 });
|
|
340
|
+
(0, globals_1.expect)(mock.lastRequest?.method).toBe("GET");
|
|
341
|
+
(0, globals_1.expect)(mock.lastRequest?.path).toContain("/audit");
|
|
342
|
+
(0, globals_1.expect)(mock.lastRequest?.path).toContain("since=500");
|
|
343
|
+
(0, globals_1.expect)(mock.lastRequest?.path).toContain("action=secret.read");
|
|
344
|
+
(0, globals_1.expect)(mock.lastRequest?.path).toContain("limit=10");
|
|
345
|
+
(0, globals_1.expect)(text).toContain("secret.read");
|
|
346
|
+
});
|
|
347
|
+
(0, globals_1.it)("passes until param in query string", async () => {
|
|
348
|
+
mock.next(200, { events: [] });
|
|
349
|
+
await client.call("sirr_audit", { since: 100, until: 200 });
|
|
350
|
+
(0, globals_1.expect)(mock.lastRequest?.path).toContain("since=100");
|
|
351
|
+
(0, globals_1.expect)(mock.lastRequest?.path).toContain("until=200");
|
|
352
|
+
});
|
|
353
|
+
(0, globals_1.it)("empty events → 'No audit events found'", async () => {
|
|
354
|
+
mock.next(200, { events: [] });
|
|
355
|
+
const text = await client.call("sirr_audit");
|
|
356
|
+
(0, globals_1.expect)(text).toBe("No audit events found.");
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
// ── sirr_webhook_create / list / delete ───────────────────────────────────────
|
|
360
|
+
(0, globals_1.describe)("sirr_webhook_create", () => {
|
|
361
|
+
(0, globals_1.it)("POST /webhooks with url and events", async () => {
|
|
362
|
+
mock.next(201, { id: "wh_1", secret: "whsec_abc" });
|
|
363
|
+
const text = await client.call("sirr_webhook_create", { url: "https://example.com/hook", events: ["secret.created"] });
|
|
364
|
+
(0, globals_1.expect)(mock.lastRequest?.method).toBe("POST");
|
|
365
|
+
(0, globals_1.expect)(mock.lastRequest?.path).toBe("/webhooks");
|
|
366
|
+
const body = mock.lastRequest?.body;
|
|
367
|
+
(0, globals_1.expect)(body["url"]).toBe("https://example.com/hook");
|
|
368
|
+
(0, globals_1.expect)(body["events"]).toEqual(["secret.created"]);
|
|
369
|
+
(0, globals_1.expect)(text).toContain("wh_1");
|
|
370
|
+
(0, globals_1.expect)(text).toContain("whsec_abc");
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
(0, globals_1.describe)("sirr_webhook_list", () => {
|
|
374
|
+
(0, globals_1.it)("GET /webhooks and formats list", async () => {
|
|
375
|
+
mock.next(200, { webhooks: [{ id: "wh_1", url: "https://example.com", events: ["*"], created_at: 0 }] });
|
|
376
|
+
const text = await client.call("sirr_webhook_list");
|
|
377
|
+
(0, globals_1.expect)(mock.lastRequest?.path).toBe("/webhooks");
|
|
378
|
+
(0, globals_1.expect)(text).toContain("wh_1");
|
|
379
|
+
});
|
|
380
|
+
(0, globals_1.it)("empty → 'No webhooks registered'", async () => {
|
|
381
|
+
mock.next(200, { webhooks: [] });
|
|
382
|
+
(0, globals_1.expect)(await client.call("sirr_webhook_list")).toBe("No webhooks registered.");
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
(0, globals_1.describe)("sirr_webhook_delete", () => {
|
|
386
|
+
(0, globals_1.it)("DELETE /webhooks/{id}", async () => {
|
|
387
|
+
mock.next(200, { deleted: true });
|
|
388
|
+
await client.call("sirr_webhook_delete", { id: "wh_1" });
|
|
389
|
+
(0, globals_1.expect)(mock.lastRequest?.method).toBe("DELETE");
|
|
390
|
+
(0, globals_1.expect)(mock.lastRequest?.path).toBe("/webhooks/wh_1");
|
|
391
|
+
});
|
|
392
|
+
(0, globals_1.it)("404 → not-found message, not an error", async () => {
|
|
393
|
+
mock.next(404, { error: "not found" });
|
|
394
|
+
const text = await client.call("sirr_webhook_delete", { id: "wh_missing" });
|
|
395
|
+
(0, globals_1.expect)(text).toContain("not found");
|
|
396
|
+
(0, globals_1.expect)(text).not.toContain("Error:");
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
// ── sirr_key_list ─────────────────────────────────────────────────────────────
|
|
400
|
+
(0, globals_1.describe)("sirr_key_list", () => {
|
|
401
|
+
(0, globals_1.it)("GET /me and extracts keys array", async () => {
|
|
402
|
+
const future = Math.floor(Date.now() / 1000) + 86400;
|
|
403
|
+
mock.next(200, {
|
|
404
|
+
id: "p1", name: "alice", role: "admin", org_id: "org1", metadata: {}, created_at: 0,
|
|
405
|
+
keys: [{ id: "key_1", name: "my-key", valid_after: 0, valid_before: future, created_at: 0 }],
|
|
406
|
+
});
|
|
407
|
+
const text = await client.call("sirr_key_list");
|
|
408
|
+
(0, globals_1.expect)(mock.lastRequest?.path).toBe("/me");
|
|
409
|
+
(0, globals_1.expect)(text).toContain("key_1");
|
|
410
|
+
(0, globals_1.expect)(text).toContain("my-key");
|
|
411
|
+
});
|
|
412
|
+
(0, globals_1.it)("empty keys → 'No API keys'", async () => {
|
|
413
|
+
mock.next(200, { id: "p1", name: "alice", role: "admin", org_id: "org1", metadata: {}, created_at: 0, keys: [] });
|
|
414
|
+
(0, globals_1.expect)(await client.call("sirr_key_list")).toBe("No API keys.");
|
|
415
|
+
});
|
|
416
|
+
});
|
|
417
|
+
// ── sirr_me ───────────────────────────────────────────────────────────────────
|
|
418
|
+
(0, globals_1.describe)("sirr_me", () => {
|
|
419
|
+
(0, globals_1.it)("GET /me and returns JSON", async () => {
|
|
420
|
+
mock.next(200, { id: "p1", name: "alice", role: "admin", org_id: "org1" });
|
|
421
|
+
const text = await client.call("sirr_me");
|
|
422
|
+
(0, globals_1.expect)(mock.lastRequest?.method).toBe("GET");
|
|
423
|
+
(0, globals_1.expect)(mock.lastRequest?.path).toBe("/me");
|
|
424
|
+
(0, globals_1.expect)(text).toContain("alice");
|
|
425
|
+
});
|
|
426
|
+
});
|
|
427
|
+
// ── sirr_update_me ────────────────────────────────────────────────────────────
|
|
428
|
+
(0, globals_1.describe)("sirr_update_me", () => {
|
|
429
|
+
(0, globals_1.it)("PATCH /me with metadata body", async () => {
|
|
430
|
+
mock.next(200, { id: "p1", name: "alice", role: "admin", org_id: "org1", metadata: { env: "prod" } });
|
|
431
|
+
await client.call("sirr_update_me", { metadata: { env: "prod" } });
|
|
432
|
+
(0, globals_1.expect)(mock.lastRequest?.method).toBe("PATCH");
|
|
433
|
+
(0, globals_1.expect)(mock.lastRequest?.path).toBe("/me");
|
|
434
|
+
const body = mock.lastRequest?.body;
|
|
435
|
+
(0, globals_1.expect)(body["metadata"]).toEqual({ env: "prod" });
|
|
436
|
+
(0, globals_1.expect)(body["name"]).toBeUndefined();
|
|
437
|
+
(0, globals_1.expect)(body["email"]).toBeUndefined();
|
|
438
|
+
});
|
|
439
|
+
});
|
|
440
|
+
// ── sirr_create_key ───────────────────────────────────────────────────────────
|
|
441
|
+
(0, globals_1.describe)("sirr_create_key", () => {
|
|
442
|
+
(0, globals_1.it)("POST /me/keys with name and optional validity", async () => {
|
|
443
|
+
const future = Math.floor(Date.now() / 1000) + 3600;
|
|
444
|
+
mock.next(201, { id: "key_2", name: "ci-key", key: "sk_live_abc123", valid_after: 0, valid_before: future });
|
|
445
|
+
const text = await client.call("sirr_create_key", { name: "ci-key", valid_for_seconds: 3600 });
|
|
446
|
+
(0, globals_1.expect)(mock.lastRequest?.method).toBe("POST");
|
|
447
|
+
(0, globals_1.expect)(mock.lastRequest?.path).toBe("/me/keys");
|
|
448
|
+
const body = mock.lastRequest?.body;
|
|
449
|
+
(0, globals_1.expect)(body["name"]).toBe("ci-key");
|
|
450
|
+
(0, globals_1.expect)(body["valid_for_seconds"]).toBe(3600);
|
|
451
|
+
(0, globals_1.expect)(body["permissions"]).toBeUndefined();
|
|
452
|
+
(0, globals_1.expect)(body["prefix"]).toBeUndefined();
|
|
453
|
+
(0, globals_1.expect)(text).toContain("sk_live_abc123");
|
|
454
|
+
(0, globals_1.expect)(text).toContain("ci-key");
|
|
455
|
+
});
|
|
456
|
+
});
|
|
457
|
+
// ── sirr_delete_key ───────────────────────────────────────────────────────────
|
|
458
|
+
(0, globals_1.describe)("sirr_delete_key", () => {
|
|
459
|
+
(0, globals_1.it)("DELETE /me/keys/{keyId}", async () => {
|
|
460
|
+
mock.next(200, { deleted: true });
|
|
461
|
+
await client.call("sirr_delete_key", { keyId: "key_2" });
|
|
462
|
+
(0, globals_1.expect)(mock.lastRequest?.method).toBe("DELETE");
|
|
463
|
+
(0, globals_1.expect)(mock.lastRequest?.path).toBe("/me/keys/key_2");
|
|
464
|
+
});
|
|
465
|
+
(0, globals_1.it)("404 → not-found message, not an error", async () => {
|
|
466
|
+
mock.next(404, { error: "not found" });
|
|
467
|
+
const text = await client.call("sirr_delete_key", { keyId: "gone" });
|
|
468
|
+
(0, globals_1.expect)(text).toContain("not found");
|
|
469
|
+
(0, globals_1.expect)(text).not.toContain("Error:");
|
|
470
|
+
});
|
|
471
|
+
});
|
|
472
|
+
// ── org management ────────────────────────────────────────────────────────────
|
|
473
|
+
(0, globals_1.describe)("sirr_org_create", () => {
|
|
474
|
+
(0, globals_1.it)("POST /orgs with name", async () => {
|
|
475
|
+
mock.next(201, { id: "org_1", name: "acme" });
|
|
476
|
+
const text = await client.call("sirr_org_create", { name: "acme" });
|
|
477
|
+
(0, globals_1.expect)(mock.lastRequest?.method).toBe("POST");
|
|
478
|
+
(0, globals_1.expect)(mock.lastRequest?.path).toBe("/orgs");
|
|
479
|
+
(0, globals_1.expect)((mock.lastRequest?.body)["name"]).toBe("acme");
|
|
480
|
+
(0, globals_1.expect)(text).toContain("org_1");
|
|
481
|
+
(0, globals_1.expect)(text).toContain("acme");
|
|
482
|
+
});
|
|
483
|
+
});
|
|
484
|
+
(0, globals_1.describe)("sirr_org_list", () => {
|
|
485
|
+
(0, globals_1.it)("GET /orgs and formats list", async () => {
|
|
486
|
+
mock.next(200, { orgs: [{ id: "org_1", name: "acme", metadata: {}, created_at: 0 }] });
|
|
487
|
+
const text = await client.call("sirr_org_list");
|
|
488
|
+
(0, globals_1.expect)(mock.lastRequest?.method).toBe("GET");
|
|
489
|
+
(0, globals_1.expect)(mock.lastRequest?.path).toBe("/orgs");
|
|
490
|
+
(0, globals_1.expect)(text).toContain("org_1");
|
|
491
|
+
});
|
|
492
|
+
(0, globals_1.it)("empty → 'No organizations'", async () => {
|
|
493
|
+
mock.next(200, { orgs: [] });
|
|
494
|
+
(0, globals_1.expect)(await client.call("sirr_org_list")).toBe("No organizations.");
|
|
495
|
+
});
|
|
496
|
+
});
|
|
497
|
+
(0, globals_1.describe)("sirr_org_delete", () => {
|
|
498
|
+
(0, globals_1.it)("DELETE /orgs/{org_id}", async () => {
|
|
499
|
+
mock.next(200, { deleted: true });
|
|
500
|
+
await client.call("sirr_org_delete", { org_id: "org_1" });
|
|
501
|
+
(0, globals_1.expect)(mock.lastRequest?.method).toBe("DELETE");
|
|
502
|
+
(0, globals_1.expect)(mock.lastRequest?.path).toBe("/orgs/org_1");
|
|
503
|
+
});
|
|
504
|
+
(0, globals_1.it)("404 → not-found message, not an error", async () => {
|
|
505
|
+
mock.next(404, { error: "not found" });
|
|
506
|
+
const text = await client.call("sirr_org_delete", { org_id: "missing" });
|
|
507
|
+
(0, globals_1.expect)(text).toContain("not found");
|
|
508
|
+
(0, globals_1.expect)(text).not.toContain("Error:");
|
|
509
|
+
});
|
|
510
|
+
});
|
|
511
|
+
// ── principal management ──────────────────────────────────────────────────────
|
|
512
|
+
(0, globals_1.describe)("sirr_principal_create", () => {
|
|
513
|
+
(0, globals_1.it)("POST /orgs/{org_id}/principals with name and role", async () => {
|
|
514
|
+
mock.next(201, { id: "p_1", name: "bob", role: "reader", org_id: "org_1" });
|
|
515
|
+
const text = await client.call("sirr_principal_create", { org_id: "org_1", name: "bob", role: "reader" });
|
|
516
|
+
(0, globals_1.expect)(mock.lastRequest?.method).toBe("POST");
|
|
517
|
+
(0, globals_1.expect)(mock.lastRequest?.path).toBe("/orgs/org_1/principals");
|
|
518
|
+
const body = mock.lastRequest?.body;
|
|
519
|
+
(0, globals_1.expect)(body["name"]).toBe("bob");
|
|
520
|
+
(0, globals_1.expect)(body["role"]).toBe("reader");
|
|
521
|
+
(0, globals_1.expect)(text).toContain("p_1");
|
|
522
|
+
});
|
|
523
|
+
});
|
|
524
|
+
(0, globals_1.describe)("sirr_principal_list", () => {
|
|
525
|
+
(0, globals_1.it)("GET /orgs/{org_id}/principals", async () => {
|
|
526
|
+
mock.next(200, { principals: [{ id: "p_1", name: "bob", role: "reader", org_id: "org_1", created_at: 0 }] });
|
|
527
|
+
const text = await client.call("sirr_principal_list", { org_id: "org_1" });
|
|
528
|
+
(0, globals_1.expect)(mock.lastRequest?.path).toBe("/orgs/org_1/principals");
|
|
529
|
+
(0, globals_1.expect)(text).toContain("bob");
|
|
530
|
+
(0, globals_1.expect)(text).toContain("reader");
|
|
531
|
+
});
|
|
532
|
+
});
|
|
533
|
+
(0, globals_1.describe)("sirr_principal_delete", () => {
|
|
534
|
+
(0, globals_1.it)("DELETE /orgs/{org_id}/principals/{principal_id}", async () => {
|
|
535
|
+
mock.next(200, { deleted: true });
|
|
536
|
+
await client.call("sirr_principal_delete", { org_id: "org_1", principal_id: "p_1" });
|
|
537
|
+
(0, globals_1.expect)(mock.lastRequest?.method).toBe("DELETE");
|
|
538
|
+
(0, globals_1.expect)(mock.lastRequest?.path).toBe("/orgs/org_1/principals/p_1");
|
|
539
|
+
});
|
|
540
|
+
(0, globals_1.it)("404 → not-found message, not an error", async () => {
|
|
541
|
+
mock.next(404, { error: "not found" });
|
|
542
|
+
const text = await client.call("sirr_principal_delete", { org_id: "org_1", principal_id: "gone" });
|
|
543
|
+
(0, globals_1.expect)(text).toContain("not found");
|
|
544
|
+
(0, globals_1.expect)(text).not.toContain("Error:");
|
|
545
|
+
});
|
|
546
|
+
});
|
|
547
|
+
// ── role management ───────────────────────────────────────────────────────────
|
|
548
|
+
(0, globals_1.describe)("sirr_role_create", () => {
|
|
549
|
+
(0, globals_1.it)("POST /orgs/{org_id}/roles with name and permissions", async () => {
|
|
550
|
+
mock.next(201, { name: "reader", permissions: "RL", org_id: "org_1" });
|
|
551
|
+
const text = await client.call("sirr_role_create", { org_id: "org_1", name: "reader", permissions: "RL" });
|
|
552
|
+
(0, globals_1.expect)(mock.lastRequest?.method).toBe("POST");
|
|
553
|
+
(0, globals_1.expect)(mock.lastRequest?.path).toBe("/orgs/org_1/roles");
|
|
554
|
+
(0, globals_1.expect)((mock.lastRequest?.body)["permissions"]).toBe("RL");
|
|
555
|
+
(0, globals_1.expect)(text).toContain("reader");
|
|
556
|
+
(0, globals_1.expect)(text).toContain("RL");
|
|
557
|
+
});
|
|
558
|
+
});
|
|
559
|
+
(0, globals_1.describe)("sirr_role_list", () => {
|
|
560
|
+
(0, globals_1.it)("GET /orgs/{org_id}/roles", async () => {
|
|
561
|
+
mock.next(200, { roles: [
|
|
562
|
+
{ name: "admin", permissions: "CRPDLMA", built_in: true, created_at: 0 },
|
|
563
|
+
{ name: "reader", permissions: "RL", built_in: false, created_at: 0 },
|
|
564
|
+
] });
|
|
565
|
+
const text = await client.call("sirr_role_list", { org_id: "org_1" });
|
|
566
|
+
(0, globals_1.expect)(mock.lastRequest?.path).toBe("/orgs/org_1/roles");
|
|
567
|
+
(0, globals_1.expect)(text).toContain("admin");
|
|
568
|
+
(0, globals_1.expect)(text).toContain("(built-in)");
|
|
569
|
+
(0, globals_1.expect)(text).toContain("reader");
|
|
570
|
+
});
|
|
571
|
+
});
|
|
572
|
+
(0, globals_1.describe)("sirr_role_delete", () => {
|
|
573
|
+
(0, globals_1.it)("DELETE /orgs/{org_id}/roles/{role_name}", async () => {
|
|
574
|
+
mock.next(200, { deleted: true });
|
|
575
|
+
await client.call("sirr_role_delete", { org_id: "org_1", role_name: "reader" });
|
|
576
|
+
(0, globals_1.expect)(mock.lastRequest?.method).toBe("DELETE");
|
|
577
|
+
(0, globals_1.expect)(mock.lastRequest?.path).toBe("/orgs/org_1/roles/reader");
|
|
578
|
+
});
|
|
579
|
+
(0, globals_1.it)("404 → not-found message, not an error", async () => {
|
|
580
|
+
mock.next(404, { error: "not found" });
|
|
581
|
+
const text = await client.call("sirr_role_delete", { org_id: "org_1", role_name: "gone" });
|
|
582
|
+
(0, globals_1.expect)(text).toContain("not found");
|
|
583
|
+
(0, globals_1.expect)(text).not.toContain("Error:");
|
|
584
|
+
});
|
|
585
|
+
});
|
|
586
|
+
// ── share_secret ──────────────────────────────────────────────────────────────
|
|
587
|
+
(0, globals_1.describe)("share_secret", () => {
|
|
588
|
+
(0, globals_1.it)("returns a sirrlock share URL on success", async () => {
|
|
589
|
+
mock.next(200, { key: "a3f9c2d1e4b5" });
|
|
590
|
+
const text = await client.call("share_secret", { value: "hunter2" });
|
|
591
|
+
(0, globals_1.expect)(mock.lastRequest?.method).toBe("POST");
|
|
592
|
+
(0, globals_1.expect)(mock.lastRequest?.path).toBe("/api/public/secret");
|
|
593
|
+
(0, globals_1.expect)((mock.lastRequest?.body)["value"]).toBe("hunter2");
|
|
594
|
+
(0, globals_1.expect)(text).toContain("/s/a3f9c2d1e4b5");
|
|
595
|
+
(0, globals_1.expect)(text).toContain("burns after one read");
|
|
596
|
+
});
|
|
597
|
+
(0, globals_1.it)("returns an error when the upstream call fails", async () => {
|
|
598
|
+
mock.next(503, { error: "unavailable" });
|
|
599
|
+
const text = await client.call("share_secret", { value: "hunter2" });
|
|
600
|
+
(0, globals_1.expect)(text).toContain("Error:");
|
|
601
|
+
});
|
|
602
|
+
});
|
|
603
|
+
// ── error handling ────────────────────────────────────────────────────────────
|
|
604
|
+
(0, globals_1.describe)("error handling", () => {
|
|
605
|
+
(0, globals_1.it)("non-JSON error body → Error: with text content", async () => {
|
|
606
|
+
mock.nextText(429, "Too Many Requests");
|
|
607
|
+
const text = await client.call("list_secrets");
|
|
608
|
+
(0, globals_1.expect)(text).toContain("Error:");
|
|
609
|
+
(0, globals_1.expect)(text).toContain("429");
|
|
610
|
+
});
|
|
611
|
+
(0, globals_1.it)("non-2xx JSON error → Error: with status and message", async () => {
|
|
612
|
+
mock.next(401, { error: "invalid token" });
|
|
613
|
+
const text = await client.call("list_secrets");
|
|
614
|
+
(0, globals_1.expect)(text).toContain("Error:");
|
|
615
|
+
(0, globals_1.expect)(text).toContain("401");
|
|
616
|
+
(0, globals_1.expect)(text).toContain("invalid token");
|
|
617
|
+
});
|
|
618
|
+
});
|
|
619
|
+
//# sourceMappingURL=index.test.js.map
|