@checkstack/backend-api 0.2.0 → 0.3.1
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 +117 -0
- package/package.json +1 -1
- package/src/assertions.test.ts +128 -0
- package/src/assertions.ts +77 -2
- package/src/chart-metadata.ts +1 -24
- package/src/hooks.ts +6 -6
- package/src/notification-strategy.ts +5 -5
- package/src/plugin-admin-contract.ts +13 -15
- package/src/plugin-system.ts +6 -3
- package/src/rpc.test.ts +530 -0
- package/src/rpc.ts +165 -150
- package/src/test-utils.ts +1 -1
- package/src/types.ts +11 -11
package/src/rpc.test.ts
ADDED
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
import { describe, expect, it, mock, beforeEach, type Mock } from "bun:test";
|
|
2
|
+
import { oc } from "@orpc/contract";
|
|
3
|
+
import { call, implement } from "@orpc/server";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { autoAuthMiddleware, RpcContext } from "./rpc";
|
|
6
|
+
import { createMockRpcContext } from "./test-utils";
|
|
7
|
+
import { access, accessPair, ProcedureMetadata } from "@checkstack/common";
|
|
8
|
+
|
|
9
|
+
// =============================================================================
|
|
10
|
+
// TEST CONTRACT DEFINITIONS
|
|
11
|
+
// =============================================================================
|
|
12
|
+
|
|
13
|
+
const _base = oc.$meta<ProcedureMetadata>({});
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Test contracts for different access patterns.
|
|
17
|
+
*/
|
|
18
|
+
const testContracts = {
|
|
19
|
+
// Anonymous endpoint - no auth required
|
|
20
|
+
anonymousEndpoint: _base
|
|
21
|
+
.meta({ userType: "anonymous" })
|
|
22
|
+
.output(z.object({ message: z.string() })),
|
|
23
|
+
|
|
24
|
+
// Public endpoint with global access rules only (no instance access)
|
|
25
|
+
publicGlobalEndpoint: _base
|
|
26
|
+
.meta({
|
|
27
|
+
userType: "public",
|
|
28
|
+
access: [access("resource", "read", "Test access")],
|
|
29
|
+
})
|
|
30
|
+
.output(z.object({ message: z.string() })),
|
|
31
|
+
|
|
32
|
+
// Public endpoint with list filtering
|
|
33
|
+
publicListEndpoint: _base
|
|
34
|
+
.meta({
|
|
35
|
+
userType: "public",
|
|
36
|
+
access: [
|
|
37
|
+
accessPair(
|
|
38
|
+
"system",
|
|
39
|
+
{ read: "View systems", manage: "Manage systems" },
|
|
40
|
+
{ listKey: "systems", readIsPublic: true }
|
|
41
|
+
).read,
|
|
42
|
+
],
|
|
43
|
+
})
|
|
44
|
+
.output(
|
|
45
|
+
z.object({
|
|
46
|
+
systems: z.array(z.object({ id: z.string(), name: z.string() })),
|
|
47
|
+
})
|
|
48
|
+
),
|
|
49
|
+
|
|
50
|
+
// Authenticated endpoint
|
|
51
|
+
authenticatedEndpoint: _base
|
|
52
|
+
.meta({ userType: "authenticated" })
|
|
53
|
+
.output(z.object({ message: z.string() })),
|
|
54
|
+
|
|
55
|
+
// User-only endpoint
|
|
56
|
+
userOnlyEndpoint: _base
|
|
57
|
+
.meta({ userType: "user" })
|
|
58
|
+
.output(z.object({ message: z.string() })),
|
|
59
|
+
|
|
60
|
+
// Service-only endpoint
|
|
61
|
+
serviceOnlyEndpoint: _base
|
|
62
|
+
.meta({ userType: "service" })
|
|
63
|
+
.output(z.object({ message: z.string() })),
|
|
64
|
+
|
|
65
|
+
// Single resource endpoint with idParam
|
|
66
|
+
singleResourceEndpoint: _base
|
|
67
|
+
.meta({
|
|
68
|
+
userType: "public",
|
|
69
|
+
access: [
|
|
70
|
+
accessPair(
|
|
71
|
+
"system",
|
|
72
|
+
{ read: "View systems", manage: "Manage systems" },
|
|
73
|
+
{ idParam: "systemId", readIsPublic: true }
|
|
74
|
+
).read,
|
|
75
|
+
],
|
|
76
|
+
})
|
|
77
|
+
.input(z.object({ systemId: z.string() }))
|
|
78
|
+
.output(z.object({ system: z.object({ id: z.string() }).nullable() })),
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// =============================================================================
|
|
82
|
+
// TEST ROUTER IMPLEMENTATION
|
|
83
|
+
// =============================================================================
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Create the test router using implement + contract pattern.
|
|
87
|
+
*/
|
|
88
|
+
function createTestRouter() {
|
|
89
|
+
return implement(testContracts)
|
|
90
|
+
.$context<RpcContext>()
|
|
91
|
+
.use(autoAuthMiddleware)
|
|
92
|
+
.router({
|
|
93
|
+
anonymousEndpoint: implement(testContracts.anonymousEndpoint)
|
|
94
|
+
.$context<RpcContext>()
|
|
95
|
+
.handler(async () => ({
|
|
96
|
+
message: "success",
|
|
97
|
+
})),
|
|
98
|
+
|
|
99
|
+
publicGlobalEndpoint: implement(testContracts.publicGlobalEndpoint)
|
|
100
|
+
.$context<RpcContext>()
|
|
101
|
+
.handler(async () => ({
|
|
102
|
+
message: "success",
|
|
103
|
+
})),
|
|
104
|
+
|
|
105
|
+
publicListEndpoint: implement(testContracts.publicListEndpoint)
|
|
106
|
+
.$context<RpcContext>()
|
|
107
|
+
.handler(async () => ({
|
|
108
|
+
systems: [
|
|
109
|
+
{ id: "sys-1", name: "System 1" },
|
|
110
|
+
{ id: "sys-2", name: "System 2" },
|
|
111
|
+
{ id: "sys-3", name: "System 3" },
|
|
112
|
+
],
|
|
113
|
+
})),
|
|
114
|
+
|
|
115
|
+
authenticatedEndpoint: implement(testContracts.authenticatedEndpoint)
|
|
116
|
+
.$context<RpcContext>()
|
|
117
|
+
.handler(async () => ({
|
|
118
|
+
message: "success",
|
|
119
|
+
})),
|
|
120
|
+
|
|
121
|
+
userOnlyEndpoint: implement(testContracts.userOnlyEndpoint)
|
|
122
|
+
.$context<RpcContext>()
|
|
123
|
+
.handler(async () => ({
|
|
124
|
+
message: "success",
|
|
125
|
+
})),
|
|
126
|
+
|
|
127
|
+
serviceOnlyEndpoint: implement(testContracts.serviceOnlyEndpoint)
|
|
128
|
+
.$context<RpcContext>()
|
|
129
|
+
.handler(async () => ({
|
|
130
|
+
message: "success",
|
|
131
|
+
})),
|
|
132
|
+
|
|
133
|
+
singleResourceEndpoint: implement(testContracts.singleResourceEndpoint)
|
|
134
|
+
.$context<RpcContext>()
|
|
135
|
+
.handler(async ({ input }) => ({
|
|
136
|
+
system: { id: input.systemId },
|
|
137
|
+
})),
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
describe("autoAuthMiddleware", () => {
|
|
142
|
+
let mockContext: RpcContext;
|
|
143
|
+
let router: ReturnType<typeof createTestRouter>;
|
|
144
|
+
|
|
145
|
+
beforeEach(() => {
|
|
146
|
+
mockContext = createMockRpcContext();
|
|
147
|
+
router = createTestRouter();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// ==========================================================================
|
|
151
|
+
// ANONYMOUS ENDPOINTS (userType: "anonymous")
|
|
152
|
+
// ==========================================================================
|
|
153
|
+
|
|
154
|
+
describe("anonymous endpoints (userType: anonymous)", () => {
|
|
155
|
+
it("should allow access without authentication", async () => {
|
|
156
|
+
const result = await call(router.anonymousEndpoint, undefined, {
|
|
157
|
+
context: mockContext,
|
|
158
|
+
});
|
|
159
|
+
expect(result).toEqual({ message: "success" });
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("should skip all access rule checks", async () => {
|
|
163
|
+
const result = await call(router.anonymousEndpoint, undefined, {
|
|
164
|
+
context: mockContext,
|
|
165
|
+
});
|
|
166
|
+
expect(result).toEqual({ message: "success" });
|
|
167
|
+
// getAnonymousAccessRules should NOT be called for anonymous endpoints
|
|
168
|
+
expect(mockContext.auth.getAnonymousAccessRules).not.toHaveBeenCalled();
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// ==========================================================================
|
|
173
|
+
// PUBLIC ENDPOINTS - Global Access Rules
|
|
174
|
+
// ==========================================================================
|
|
175
|
+
|
|
176
|
+
describe("public endpoints with global access rules", () => {
|
|
177
|
+
it("should allow anonymous users with matching access rule", async () => {
|
|
178
|
+
// Mock anonymous role has the required access rule
|
|
179
|
+
(
|
|
180
|
+
mockContext.auth.getAnonymousAccessRules as Mock<
|
|
181
|
+
() => Promise<string[]>
|
|
182
|
+
>
|
|
183
|
+
).mockResolvedValue(["test-plugin.resource.read"]);
|
|
184
|
+
|
|
185
|
+
const result = await call(router.publicGlobalEndpoint, undefined, {
|
|
186
|
+
context: mockContext,
|
|
187
|
+
});
|
|
188
|
+
expect(result).toEqual({ message: "success" });
|
|
189
|
+
expect(mockContext.auth.getAnonymousAccessRules).toHaveBeenCalled();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("should reject anonymous users without matching access rule", async () => {
|
|
193
|
+
// Mock anonymous role does NOT have the required access rule
|
|
194
|
+
(
|
|
195
|
+
mockContext.auth.getAnonymousAccessRules as Mock<
|
|
196
|
+
() => Promise<string[]>
|
|
197
|
+
>
|
|
198
|
+
).mockResolvedValue([]);
|
|
199
|
+
|
|
200
|
+
await expect(
|
|
201
|
+
call(router.publicGlobalEndpoint, undefined, { context: mockContext })
|
|
202
|
+
).rejects.toThrow();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("should allow authenticated users with matching access rule", async () => {
|
|
206
|
+
mockContext.user = {
|
|
207
|
+
type: "user",
|
|
208
|
+
id: "user-1",
|
|
209
|
+
accessRules: ["test-plugin.resource.read"],
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const result = await call(router.publicGlobalEndpoint, undefined, {
|
|
213
|
+
context: mockContext,
|
|
214
|
+
});
|
|
215
|
+
expect(result).toEqual({ message: "success" });
|
|
216
|
+
expect(mockContext.auth.getAnonymousAccessRules).not.toHaveBeenCalled();
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("should reject authenticated users without matching access rule", async () => {
|
|
220
|
+
mockContext.user = {
|
|
221
|
+
type: "user",
|
|
222
|
+
id: "user-1",
|
|
223
|
+
accessRules: ["other.permission"],
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
await expect(
|
|
227
|
+
call(router.publicGlobalEndpoint, undefined, { context: mockContext })
|
|
228
|
+
).rejects.toThrow();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("should allow users with wildcard access rule", async () => {
|
|
232
|
+
mockContext.user = {
|
|
233
|
+
type: "user",
|
|
234
|
+
id: "user-1",
|
|
235
|
+
accessRules: ["*"],
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const result = await call(router.publicGlobalEndpoint, undefined, {
|
|
239
|
+
context: mockContext,
|
|
240
|
+
});
|
|
241
|
+
expect(result).toEqual({ message: "success" });
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("should allow service users without checking access rules", async () => {
|
|
245
|
+
mockContext.user = {
|
|
246
|
+
type: "service",
|
|
247
|
+
pluginId: "other-plugin",
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const result = await call(router.publicGlobalEndpoint, undefined, {
|
|
251
|
+
context: mockContext,
|
|
252
|
+
});
|
|
253
|
+
expect(result).toEqual({ message: "success" });
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// ==========================================================================
|
|
258
|
+
// PUBLIC ENDPOINTS - List Filtering (Regression test for anonymous access)
|
|
259
|
+
// ==========================================================================
|
|
260
|
+
|
|
261
|
+
describe("public endpoints with list filtering (instanceAccess.listKey)", () => {
|
|
262
|
+
it("should return all items for anonymous users WITH global access", async () => {
|
|
263
|
+
// Anonymous role HAS the required access rule
|
|
264
|
+
(
|
|
265
|
+
mockContext.auth.getAnonymousAccessRules as Mock<
|
|
266
|
+
() => Promise<string[]>
|
|
267
|
+
>
|
|
268
|
+
).mockResolvedValue(["test-plugin.system.read"]);
|
|
269
|
+
|
|
270
|
+
const result = await call(router.publicListEndpoint, undefined, {
|
|
271
|
+
context: mockContext,
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
// Should return all systems since anonymous has global access
|
|
275
|
+
expect(result.systems).toHaveLength(3);
|
|
276
|
+
expect(result.systems.map((s) => s.id)).toEqual([
|
|
277
|
+
"sys-1",
|
|
278
|
+
"sys-2",
|
|
279
|
+
"sys-3",
|
|
280
|
+
]);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it("should return empty list for anonymous users WITHOUT global access", async () => {
|
|
284
|
+
// Anonymous role does NOT have the required access rule
|
|
285
|
+
(
|
|
286
|
+
mockContext.auth.getAnonymousAccessRules as Mock<
|
|
287
|
+
() => Promise<string[]>
|
|
288
|
+
>
|
|
289
|
+
).mockResolvedValue([]);
|
|
290
|
+
|
|
291
|
+
const result = await call(router.publicListEndpoint, undefined, {
|
|
292
|
+
context: mockContext,
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// Should return empty list since anonymous has no access
|
|
296
|
+
expect(result.systems).toHaveLength(0);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it("should return all items for anonymous users with wildcard access", async () => {
|
|
300
|
+
// Anonymous role has wildcard access
|
|
301
|
+
(
|
|
302
|
+
mockContext.auth.getAnonymousAccessRules as Mock<
|
|
303
|
+
() => Promise<string[]>
|
|
304
|
+
>
|
|
305
|
+
).mockResolvedValue(["*"]);
|
|
306
|
+
|
|
307
|
+
const result = await call(router.publicListEndpoint, undefined, {
|
|
308
|
+
context: mockContext,
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
// Should return all systems
|
|
312
|
+
expect(result.systems).toHaveLength(3);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it("should filter items via S2S for authenticated users without global access", async () => {
|
|
316
|
+
mockContext.user = {
|
|
317
|
+
type: "user",
|
|
318
|
+
id: "user-1",
|
|
319
|
+
accessRules: [], // No global access
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
// Mock S2S call returns only accessible IDs
|
|
323
|
+
(
|
|
324
|
+
mockContext.auth.getAccessibleResourceIds as Mock<
|
|
325
|
+
() => Promise<string[]>
|
|
326
|
+
>
|
|
327
|
+
).mockResolvedValue(["sys-1", "sys-3"]);
|
|
328
|
+
|
|
329
|
+
const result = await call(router.publicListEndpoint, undefined, {
|
|
330
|
+
context: mockContext,
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
// Should return only filtered systems
|
|
334
|
+
expect(result.systems).toHaveLength(2);
|
|
335
|
+
expect(result.systems.map((s) => s.id)).toEqual(["sys-1", "sys-3"]);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it("should return all items for authenticated users with global access", async () => {
|
|
339
|
+
mockContext.user = {
|
|
340
|
+
type: "user",
|
|
341
|
+
id: "user-1",
|
|
342
|
+
accessRules: ["test-plugin.system.read"], // Has global access
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
// S2S should return all items when user has global access
|
|
346
|
+
(
|
|
347
|
+
mockContext.auth.getAccessibleResourceIds as Mock<
|
|
348
|
+
() => Promise<string[]>
|
|
349
|
+
>
|
|
350
|
+
).mockResolvedValue(["sys-1", "sys-2", "sys-3"]);
|
|
351
|
+
|
|
352
|
+
const result = await call(router.publicListEndpoint, undefined, {
|
|
353
|
+
context: mockContext,
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
expect(result.systems).toHaveLength(3);
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
// ==========================================================================
|
|
361
|
+
// AUTHENTICATED ENDPOINTS
|
|
362
|
+
// ==========================================================================
|
|
363
|
+
|
|
364
|
+
describe("authenticated endpoints (userType: authenticated)", () => {
|
|
365
|
+
it("should reject unauthenticated requests", async () => {
|
|
366
|
+
// No user in context
|
|
367
|
+
await expect(
|
|
368
|
+
call(router.authenticatedEndpoint, undefined, { context: mockContext })
|
|
369
|
+
).rejects.toThrow("Authentication required");
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it("should allow authenticated users", async () => {
|
|
373
|
+
mockContext.user = {
|
|
374
|
+
type: "user",
|
|
375
|
+
id: "user-1",
|
|
376
|
+
accessRules: [],
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
const result = await call(router.authenticatedEndpoint, undefined, {
|
|
380
|
+
context: mockContext,
|
|
381
|
+
});
|
|
382
|
+
expect(result).toEqual({ message: "success" });
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it("should allow service users", async () => {
|
|
386
|
+
mockContext.user = {
|
|
387
|
+
type: "service",
|
|
388
|
+
pluginId: "other-plugin",
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
const result = await call(router.authenticatedEndpoint, undefined, {
|
|
392
|
+
context: mockContext,
|
|
393
|
+
});
|
|
394
|
+
expect(result).toEqual({ message: "success" });
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
// ==========================================================================
|
|
399
|
+
// USER-ONLY ENDPOINTS
|
|
400
|
+
// ==========================================================================
|
|
401
|
+
|
|
402
|
+
describe("user-only endpoints (userType: user)", () => {
|
|
403
|
+
it("should reject service users", async () => {
|
|
404
|
+
mockContext.user = {
|
|
405
|
+
type: "service",
|
|
406
|
+
pluginId: "other-plugin",
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
await expect(
|
|
410
|
+
call(router.userOnlyEndpoint, undefined, { context: mockContext })
|
|
411
|
+
).rejects.toThrow("This endpoint is for users only");
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
it("should allow real users", async () => {
|
|
415
|
+
mockContext.user = {
|
|
416
|
+
type: "user",
|
|
417
|
+
id: "user-1",
|
|
418
|
+
accessRules: [],
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
const result = await call(router.userOnlyEndpoint, undefined, {
|
|
422
|
+
context: mockContext,
|
|
423
|
+
});
|
|
424
|
+
expect(result).toEqual({ message: "success" });
|
|
425
|
+
});
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
// ==========================================================================
|
|
429
|
+
// SERVICE-ONLY ENDPOINTS
|
|
430
|
+
// ==========================================================================
|
|
431
|
+
|
|
432
|
+
describe("service-only endpoints (userType: service)", () => {
|
|
433
|
+
it("should reject real users", async () => {
|
|
434
|
+
mockContext.user = {
|
|
435
|
+
type: "user",
|
|
436
|
+
id: "user-1",
|
|
437
|
+
accessRules: [],
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
await expect(
|
|
441
|
+
call(router.serviceOnlyEndpoint, undefined, { context: mockContext })
|
|
442
|
+
).rejects.toThrow("This endpoint is for services only");
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
it("should allow service users", async () => {
|
|
446
|
+
mockContext.user = {
|
|
447
|
+
type: "service",
|
|
448
|
+
pluginId: "other-plugin",
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
const result = await call(router.serviceOnlyEndpoint, undefined, {
|
|
452
|
+
context: mockContext,
|
|
453
|
+
});
|
|
454
|
+
expect(result).toEqual({ message: "success" });
|
|
455
|
+
});
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
// ==========================================================================
|
|
459
|
+
// SINGLE RESOURCE ACCESS
|
|
460
|
+
// ==========================================================================
|
|
461
|
+
|
|
462
|
+
describe("single resource access (instanceAccess.idParam)", () => {
|
|
463
|
+
it("should deny anonymous users access to single resources", async () => {
|
|
464
|
+
// Anonymous role has the access rule
|
|
465
|
+
(
|
|
466
|
+
mockContext.auth.getAnonymousAccessRules as Mock<
|
|
467
|
+
() => Promise<string[]>
|
|
468
|
+
>
|
|
469
|
+
).mockResolvedValue(["test-plugin.system.read"]);
|
|
470
|
+
|
|
471
|
+
await expect(
|
|
472
|
+
call(
|
|
473
|
+
router.singleResourceEndpoint,
|
|
474
|
+
{ systemId: "sys-1" },
|
|
475
|
+
{ context: mockContext }
|
|
476
|
+
)
|
|
477
|
+
).rejects.toThrow("Authentication required to access system:sys-1");
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
it("should check team access via S2S for authenticated users", async () => {
|
|
481
|
+
mockContext.user = {
|
|
482
|
+
type: "user",
|
|
483
|
+
id: "user-1",
|
|
484
|
+
accessRules: [],
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
(
|
|
488
|
+
mockContext.auth.checkResourceTeamAccess as Mock<
|
|
489
|
+
() => Promise<{ hasAccess: boolean }>
|
|
490
|
+
>
|
|
491
|
+
).mockResolvedValue({ hasAccess: true });
|
|
492
|
+
|
|
493
|
+
const result = await call(
|
|
494
|
+
router.singleResourceEndpoint,
|
|
495
|
+
{ systemId: "sys-1" },
|
|
496
|
+
{ context: mockContext }
|
|
497
|
+
);
|
|
498
|
+
|
|
499
|
+
expect(result.system?.id).toBe("sys-1");
|
|
500
|
+
expect(mockContext.auth.checkResourceTeamAccess).toHaveBeenCalledWith(
|
|
501
|
+
expect.objectContaining({
|
|
502
|
+
userId: "user-1",
|
|
503
|
+
resourceId: "sys-1",
|
|
504
|
+
})
|
|
505
|
+
);
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
it("should deny access when team access check fails", async () => {
|
|
509
|
+
mockContext.user = {
|
|
510
|
+
type: "user",
|
|
511
|
+
id: "user-1",
|
|
512
|
+
accessRules: [],
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
(
|
|
516
|
+
mockContext.auth.checkResourceTeamAccess as Mock<
|
|
517
|
+
() => Promise<{ hasAccess: boolean }>
|
|
518
|
+
>
|
|
519
|
+
).mockResolvedValue({ hasAccess: false });
|
|
520
|
+
|
|
521
|
+
await expect(
|
|
522
|
+
call(
|
|
523
|
+
router.singleResourceEndpoint,
|
|
524
|
+
{ systemId: "sys-1" },
|
|
525
|
+
{ context: mockContext }
|
|
526
|
+
)
|
|
527
|
+
).rejects.toThrow("Access denied to resource system:sys-1");
|
|
528
|
+
});
|
|
529
|
+
});
|
|
530
|
+
});
|