@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.
- package/CHANGELOG.md +85 -0
- package/drizzle/0000_glossy_red_hulk.sql +28 -0
- package/drizzle/0001_rich_fixer.sql +2 -0
- package/drizzle/meta/0000_snapshot.json +191 -0
- package/drizzle/meta/0001_snapshot.json +190 -0
- package/drizzle/meta/_journal.json +20 -0
- package/drizzle.config.ts +8 -0
- package/package.json +36 -0
- package/src/connection-store.test.ts +468 -0
- package/src/connection-store.ts +463 -0
- package/src/delivery-coordinator.ts +390 -0
- package/src/event-registry.test.ts +396 -0
- package/src/event-registry.ts +99 -0
- package/src/hook-subscriber.ts +104 -0
- package/src/index.ts +306 -0
- package/src/provider-registry.test.ts +314 -0
- package/src/provider-registry.ts +107 -0
- package/src/provider-types.ts +257 -0
- package/src/router.ts +858 -0
- package/src/schema.ts +78 -0
- package/tsconfig.json +6 -0
|
@@ -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
|
+
});
|