@checkstack/integration-backend 0.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.
@@ -0,0 +1,468 @@
1
+ import { describe, it, expect, beforeEach, mock } from "bun:test";
2
+ import { z } from "zod";
3
+ import {
4
+ createConnectionStore,
5
+ type ConnectionStore,
6
+ } from "./connection-store";
7
+ import type { ConfigService, Logger } from "@checkstack/backend-api";
8
+ import { Versioned, configString } from "@checkstack/backend-api";
9
+ import type { IntegrationProviderRegistry } from "./provider-registry";
10
+
11
+ /**
12
+ * Unit tests for ConnectionStore.
13
+ *
14
+ * Tests cover:
15
+ * - Connection CRUD operations
16
+ * - Secret redaction via ConfigService.getRedacted
17
+ * - Connection lookup across providers
18
+ * - Provider cache management
19
+ */
20
+
21
+ // Test connection config schema with secret field
22
+ const testConnectionSchema = z.object({
23
+ baseUrl: configString({}).url(),
24
+ apiKey: configString({ "x-secret": true }),
25
+ });
26
+
27
+ // Mock logger
28
+ const mockLogger = {
29
+ debug: () => {},
30
+ info: () => {},
31
+ warn: () => {},
32
+ error: () => {},
33
+ child: () => mockLogger,
34
+ } as unknown as Logger;
35
+
36
+ // Create a mock config service
37
+ function createMockConfigService() {
38
+ const storage = new Map<string, unknown>();
39
+
40
+ return {
41
+ storage,
42
+ get: mock(async (key: string) => storage.get(key)),
43
+ getRedacted: mock(async (key: string) => {
44
+ const data = storage.get(key);
45
+ if (!data) return undefined;
46
+ // Simulate redaction: if data is an object with apiKey or token, remove it
47
+ if (typeof data === "object" && data !== null) {
48
+ const result = { ...data } as Record<string, unknown>;
49
+ // Remove common secret field names for redaction simulation
50
+ delete result.apiKey;
51
+ delete result.token;
52
+ return result;
53
+ }
54
+ return data;
55
+ }),
56
+ set: mock(
57
+ async (
58
+ key: string,
59
+ _schema: z.ZodType<unknown>,
60
+ _version: number,
61
+ value: unknown
62
+ ) => {
63
+ storage.set(key, value);
64
+ }
65
+ ),
66
+ delete: mock(async (key: string) => {
67
+ storage.delete(key);
68
+ }),
69
+ list: mock(async () => [...storage.keys()]),
70
+ } as unknown as ConfigService & { storage: Map<string, unknown> };
71
+ }
72
+
73
+ // Create a mock provider registry
74
+ function createMockProviderRegistry() {
75
+ const providers = new Map<
76
+ string,
77
+ { qualifiedId: string; connectionSchema?: unknown }
78
+ >();
79
+
80
+ const registry = {
81
+ getProvider: mock((qualifiedId: string) => {
82
+ return providers.get(qualifiedId);
83
+ }),
84
+ getProviders: mock(() => {
85
+ return [...providers.entries()].map(([id, p]) => ({
86
+ qualifiedId: id,
87
+ connectionSchema: p.connectionSchema,
88
+ }));
89
+ }),
90
+ getProviderConnectionSchema: mock((qualifiedId: string) => {
91
+ const provider = providers.get(qualifiedId);
92
+ if (!provider?.connectionSchema) return undefined;
93
+ return { type: "object" }; // Simplified JSON schema
94
+ }),
95
+ } as unknown as IntegrationProviderRegistry;
96
+
97
+ return { registry, providers };
98
+ }
99
+
100
+ describe("ConnectionStore", () => {
101
+ let connectionStore: ConnectionStore;
102
+ let mockConfigService: ReturnType<typeof createMockConfigService>;
103
+ let mockProviders: Map<
104
+ string,
105
+ { qualifiedId: string; connectionSchema?: unknown }
106
+ >;
107
+
108
+ beforeEach(() => {
109
+ mockConfigService = createMockConfigService();
110
+ const { registry, providers } = createMockProviderRegistry();
111
+ mockProviders = providers;
112
+
113
+ // Register a test provider with connection schema
114
+ mockProviders.set("test-plugin.jira", {
115
+ qualifiedId: "test-plugin.jira",
116
+ connectionSchema: new Versioned({
117
+ version: 1,
118
+ schema: testConnectionSchema,
119
+ }),
120
+ });
121
+
122
+ connectionStore = createConnectionStore({
123
+ configService: mockConfigService,
124
+ providerRegistry: registry,
125
+ logger: mockLogger,
126
+ });
127
+ });
128
+
129
+ // ─────────────────────────────────────────────────────────────────────────
130
+ // Create Connection
131
+ // ─────────────────────────────────────────────────────────────────────────
132
+
133
+ describe("createConnection", () => {
134
+ it("creates a new connection with generated ID", async () => {
135
+ const connection = await connectionStore.createConnection({
136
+ providerId: "test-plugin.jira",
137
+ name: "My Jira Connection",
138
+ config: {
139
+ baseUrl: "https://example.atlassian.net",
140
+ apiKey: "secret-key",
141
+ },
142
+ });
143
+
144
+ expect(connection.id).toBeDefined();
145
+ expect(connection.name).toBe("My Jira Connection");
146
+ expect(connection.providerId).toBe("test-plugin.jira");
147
+ expect(connection.config.baseUrl).toBe("https://example.atlassian.net");
148
+ });
149
+
150
+ it("stores connection in config service", async () => {
151
+ await connectionStore.createConnection({
152
+ providerId: "test-plugin.jira",
153
+ name: "Test Connection",
154
+ config: { baseUrl: "https://test.atlassian.net", apiKey: "key123" },
155
+ });
156
+
157
+ expect(mockConfigService.set).toHaveBeenCalled();
158
+ });
159
+
160
+ it("sets createdAt and updatedAt timestamps", async () => {
161
+ const before = new Date();
162
+ const connection = await connectionStore.createConnection({
163
+ providerId: "test-plugin.jira",
164
+ name: "Timestamped Connection",
165
+ config: { baseUrl: "https://time.atlassian.net", apiKey: "timekey" },
166
+ });
167
+ const after = new Date();
168
+
169
+ expect(connection.createdAt.getTime()).toBeGreaterThanOrEqual(
170
+ before.getTime()
171
+ );
172
+ expect(connection.createdAt.getTime()).toBeLessThanOrEqual(
173
+ after.getTime()
174
+ );
175
+ expect(connection.updatedAt.getTime()).toBe(
176
+ connection.createdAt.getTime()
177
+ );
178
+ });
179
+ });
180
+
181
+ // ─────────────────────────────────────────────────────────────────────────
182
+ // List Connections
183
+ // ─────────────────────────────────────────────────────────────────────────
184
+
185
+ describe("listConnections", () => {
186
+ it("returns empty array when no connections exist", async () => {
187
+ const connections = await connectionStore.listConnections(
188
+ "test-plugin.jira"
189
+ );
190
+ expect(connections).toEqual([]);
191
+ });
192
+
193
+ it("returns connections for a specific provider", async () => {
194
+ await connectionStore.createConnection({
195
+ providerId: "test-plugin.jira",
196
+ name: "Connection 1",
197
+ config: { baseUrl: "https://one.atlassian.net", apiKey: "key1" },
198
+ });
199
+ await connectionStore.createConnection({
200
+ providerId: "test-plugin.jira",
201
+ name: "Connection 2",
202
+ config: { baseUrl: "https://two.atlassian.net", apiKey: "key2" },
203
+ });
204
+
205
+ const connections = await connectionStore.listConnections(
206
+ "test-plugin.jira"
207
+ );
208
+ expect(connections.length).toBe(2);
209
+ expect(connections.map((c) => c.name).sort()).toEqual([
210
+ "Connection 1",
211
+ "Connection 2",
212
+ ]);
213
+ });
214
+
215
+ it("returns redacted connections (secrets removed)", async () => {
216
+ await connectionStore.createConnection({
217
+ providerId: "test-plugin.jira",
218
+ name: "Secret Connection",
219
+ config: {
220
+ baseUrl: "https://secret.atlassian.net",
221
+ apiKey: "super-secret",
222
+ },
223
+ });
224
+
225
+ const connections = await connectionStore.listConnections(
226
+ "test-plugin.jira"
227
+ );
228
+ expect(connections.length).toBe(1);
229
+ // The mock simulates redaction by only returning baseUrl
230
+ expect(connections[0].configPreview.baseUrl).toBe(
231
+ "https://secret.atlassian.net"
232
+ );
233
+ expect(connections[0].configPreview.apiKey).toBeUndefined();
234
+ });
235
+ });
236
+
237
+ // ─────────────────────────────────────────────────────────────────────────
238
+ // Get Single Connection
239
+ // ─────────────────────────────────────────────────────────────────────────
240
+
241
+ describe("getConnection", () => {
242
+ it("returns undefined for non-existent connection", async () => {
243
+ const connection = await connectionStore.getConnection("non-existent-id");
244
+ expect(connection).toBeUndefined();
245
+ });
246
+
247
+ it("returns redacted connection by ID", async () => {
248
+ const created = await connectionStore.createConnection({
249
+ providerId: "test-plugin.jira",
250
+ name: "Findable Connection",
251
+ config: { baseUrl: "https://find.atlassian.net", apiKey: "findkey" },
252
+ });
253
+
254
+ const found = await connectionStore.getConnection(created.id);
255
+ expect(found).toBeDefined();
256
+ expect(found?.name).toBe("Findable Connection");
257
+ expect(found?.configPreview.apiKey).toBeUndefined(); // Redacted
258
+ });
259
+ });
260
+
261
+ // ─────────────────────────────────────────────────────────────────────────
262
+ // Get Connection With Credentials
263
+ // ─────────────────────────────────────────────────────────────────────────
264
+
265
+ describe("getConnectionWithCredentials", () => {
266
+ it("returns undefined for non-existent connection", async () => {
267
+ const connection = await connectionStore.getConnectionWithCredentials(
268
+ "non-existent-id"
269
+ );
270
+ expect(connection).toBeUndefined();
271
+ });
272
+
273
+ it("returns full connection with credentials", async () => {
274
+ const created = await connectionStore.createConnection({
275
+ providerId: "test-plugin.jira",
276
+ name: "Full Credentials Connection",
277
+ config: {
278
+ baseUrl: "https://creds.atlassian.net",
279
+ apiKey: "secret-api-key",
280
+ },
281
+ });
282
+
283
+ const found = await connectionStore.getConnectionWithCredentials(
284
+ created.id
285
+ );
286
+ expect(found).toBeDefined();
287
+ expect(found?.name).toBe("Full Credentials Connection");
288
+ expect(found?.config.apiKey).toBe("secret-api-key"); // Not redacted
289
+ });
290
+ });
291
+
292
+ // ─────────────────────────────────────────────────────────────────────────
293
+ // Update Connection
294
+ // ─────────────────────────────────────────────────────────────────────────
295
+
296
+ describe("updateConnection", () => {
297
+ it("throws error for non-existent connection", async () => {
298
+ await expect(
299
+ connectionStore.updateConnection({
300
+ connectionId: "non-existent",
301
+ updates: { name: "New Name" },
302
+ })
303
+ ).rejects.toThrow();
304
+ });
305
+
306
+ it("updates connection name", async () => {
307
+ const created = await connectionStore.createConnection({
308
+ providerId: "test-plugin.jira",
309
+ name: "Original Name",
310
+ config: {
311
+ baseUrl: "https://update.atlassian.net",
312
+ apiKey: "updatekey",
313
+ },
314
+ });
315
+
316
+ const updated = await connectionStore.updateConnection({
317
+ connectionId: created.id,
318
+ updates: { name: "Updated Name" },
319
+ });
320
+
321
+ expect(updated.name).toBe("Updated Name");
322
+ expect(updated.config.baseUrl).toBe("https://update.atlassian.net"); // Unchanged
323
+ });
324
+
325
+ it("updates connection config", async () => {
326
+ const created = await connectionStore.createConnection({
327
+ providerId: "test-plugin.jira",
328
+ name: "Config Update Test",
329
+ config: { baseUrl: "https://old.atlassian.net", apiKey: "oldkey" },
330
+ });
331
+
332
+ const updated = await connectionStore.updateConnection({
333
+ connectionId: created.id,
334
+ updates: {
335
+ config: { baseUrl: "https://new.atlassian.net", apiKey: "newkey" },
336
+ },
337
+ });
338
+
339
+ expect(updated.config.baseUrl).toBe("https://new.atlassian.net");
340
+ expect(updated.config.apiKey).toBe("newkey");
341
+ });
342
+
343
+ it("updates updatedAt timestamp", async () => {
344
+ const created = await connectionStore.createConnection({
345
+ providerId: "test-plugin.jira",
346
+ name: "Timestamp Test",
347
+ config: { baseUrl: "https://time.atlassian.net", apiKey: "timekey" },
348
+ });
349
+
350
+ // Wait a bit to ensure timestamp difference
351
+ await new Promise((resolve) => setTimeout(resolve, 10));
352
+
353
+ const updated = await connectionStore.updateConnection({
354
+ connectionId: created.id,
355
+ updates: { name: "New Name" },
356
+ });
357
+
358
+ expect(updated.updatedAt.getTime()).toBeGreaterThan(
359
+ created.updatedAt.getTime()
360
+ );
361
+ });
362
+ });
363
+
364
+ // ─────────────────────────────────────────────────────────────────────────
365
+ // Delete Connection
366
+ // ─────────────────────────────────────────────────────────────────────────
367
+
368
+ describe("deleteConnection", () => {
369
+ it("returns false for non-existent connection", async () => {
370
+ const result = await connectionStore.deleteConnection("non-existent");
371
+ expect(result).toBe(false);
372
+ });
373
+
374
+ it("deletes existing connection", async () => {
375
+ const created = await connectionStore.createConnection({
376
+ providerId: "test-plugin.jira",
377
+ name: "To Be Deleted",
378
+ config: {
379
+ baseUrl: "https://delete.atlassian.net",
380
+ apiKey: "deletekey",
381
+ },
382
+ });
383
+
384
+ const deleted = await connectionStore.deleteConnection(created.id);
385
+ expect(deleted).toBe(true);
386
+
387
+ const found = await connectionStore.getConnection(created.id);
388
+ expect(found).toBeUndefined();
389
+ });
390
+ });
391
+
392
+ // ─────────────────────────────────────────────────────────────────────────
393
+ // Find Connection Provider
394
+ // ─────────────────────────────────────────────────────────────────────────
395
+
396
+ describe("findConnectionProvider", () => {
397
+ it("returns undefined for non-existent connection", async () => {
398
+ const providerId = await connectionStore.findConnectionProvider(
399
+ "non-existent"
400
+ );
401
+ expect(providerId).toBeUndefined();
402
+ });
403
+
404
+ it("returns provider ID for existing connection", async () => {
405
+ const created = await connectionStore.createConnection({
406
+ providerId: "test-plugin.jira",
407
+ name: "Provider Lookup Test",
408
+ config: {
409
+ baseUrl: "https://lookup.atlassian.net",
410
+ apiKey: "lookupkey",
411
+ },
412
+ });
413
+
414
+ const providerId = await connectionStore.findConnectionProvider(
415
+ created.id
416
+ );
417
+ expect(providerId).toBe("test-plugin.jira");
418
+ });
419
+ });
420
+
421
+ // ─────────────────────────────────────────────────────────────────────────
422
+ // Multiple Providers
423
+ // ─────────────────────────────────────────────────────────────────────────
424
+
425
+ describe("multiple providers", () => {
426
+ beforeEach(() => {
427
+ // Register a second provider
428
+ mockProviders.set("test-plugin.slack", {
429
+ qualifiedId: "test-plugin.slack",
430
+ connectionSchema: new Versioned({
431
+ version: 1,
432
+ schema: z.object({
433
+ webhookUrl: configString({}).url(),
434
+ token: configString({ "x-secret": true }),
435
+ }),
436
+ }),
437
+ });
438
+ });
439
+
440
+ it("isolates connections by provider", async () => {
441
+ await connectionStore.createConnection({
442
+ providerId: "test-plugin.jira",
443
+ name: "Jira Connection",
444
+ config: { baseUrl: "https://jira.atlassian.net", apiKey: "jirakey" },
445
+ });
446
+ await connectionStore.createConnection({
447
+ providerId: "test-plugin.slack",
448
+ name: "Slack Connection",
449
+ config: {
450
+ webhookUrl: "https://hooks.slack.com/abc",
451
+ token: "slack-token",
452
+ },
453
+ });
454
+
455
+ const jiraConnections = await connectionStore.listConnections(
456
+ "test-plugin.jira"
457
+ );
458
+ const slackConnections = await connectionStore.listConnections(
459
+ "test-plugin.slack"
460
+ );
461
+
462
+ expect(jiraConnections.length).toBe(1);
463
+ expect(slackConnections.length).toBe(1);
464
+ expect(jiraConnections[0].name).toBe("Jira Connection");
465
+ expect(slackConnections[0].name).toBe("Slack Connection");
466
+ });
467
+ });
468
+ });