@fulcrum-governance/sdk 0.1.0 → 0.1.3
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 +42 -1
- package/README.md +299 -6
- package/dist/__tests__/clients.test.d.ts +1 -0
- package/dist/__tests__/clients.test.js +847 -0
- package/dist/clients/agent.d.ts +70 -0
- package/dist/clients/agent.js +127 -0
- package/dist/clients/approval.d.ts +67 -0
- package/dist/clients/approval.js +103 -0
- package/dist/clients/budget.d.ts +221 -0
- package/dist/clients/budget.js +181 -0
- package/dist/clients/checkpoint.d.ts +191 -0
- package/dist/clients/checkpoint.js +195 -0
- package/dist/clients/envelope.d.ts +73 -0
- package/dist/clients/envelope.js +95 -0
- package/dist/clients/eventstore.d.ts +87 -0
- package/dist/clients/eventstore.js +113 -0
- package/dist/clients/index.d.ts +15 -0
- package/dist/clients/index.js +35 -0
- package/dist/clients/metrics.d.ts +126 -0
- package/dist/clients/metrics.js +162 -0
- package/dist/clients/policy.d.ts +83 -0
- package/dist/clients/policy.js +102 -0
- package/dist/clients/tenant.d.ts +66 -0
- package/dist/clients/tenant.js +92 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +25 -3
- package/dist/instrumentation/__tests__/autoGovern.test.d.ts +6 -0
- package/dist/instrumentation/__tests__/autoGovern.test.js +416 -0
- package/dist/instrumentation/__tests__/evaluator.test.d.ts +6 -0
- package/dist/instrumentation/__tests__/evaluator.test.js +712 -0
- package/dist/instrumentation/autoGovern.d.ts +57 -0
- package/dist/instrumentation/autoGovern.js +319 -0
- package/dist/instrumentation/evaluator.d.ts +50 -0
- package/dist/instrumentation/evaluator.js +218 -0
- package/dist/instrumentation/index.d.ts +28 -0
- package/dist/instrumentation/index.js +34 -0
- package/dist/instrumentation/types.d.ts +105 -0
- package/dist/instrumentation/types.js +20 -0
- package/package.json +5 -4
- package/proto/fulcrum/agent/v1/agent_service.proto +170 -0
|
@@ -0,0 +1,847 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const clients_1 = require("../clients");
|
|
4
|
+
// Mock fetch globally
|
|
5
|
+
const mockFetch = jest.fn();
|
|
6
|
+
global.fetch = mockFetch;
|
|
7
|
+
describe('PolicyClient', () => {
|
|
8
|
+
const baseUrl = 'http://localhost:3000';
|
|
9
|
+
let client;
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
jest.clearAllMocks();
|
|
12
|
+
client = new clients_1.PolicyClient({ baseUrl });
|
|
13
|
+
});
|
|
14
|
+
describe('initialization', () => {
|
|
15
|
+
it('should create client with default options', () => {
|
|
16
|
+
const c = new clients_1.PolicyClient({ baseUrl: 'http://example.com' });
|
|
17
|
+
expect(c).toBeInstanceOf(clients_1.PolicyClient);
|
|
18
|
+
});
|
|
19
|
+
it('should trim trailing slash from baseUrl', () => {
|
|
20
|
+
const c = new clients_1.PolicyClient({ baseUrl: 'http://example.com/' });
|
|
21
|
+
expect(c.baseUrl).toBe('http://example.com');
|
|
22
|
+
});
|
|
23
|
+
it('should set default timeout to 5000ms', () => {
|
|
24
|
+
expect(client.timeoutMs).toBe(5000);
|
|
25
|
+
});
|
|
26
|
+
it('should accept custom options', () => {
|
|
27
|
+
const c = new clients_1.PolicyClient({
|
|
28
|
+
baseUrl,
|
|
29
|
+
apiKey: 'test-key',
|
|
30
|
+
timeoutMs: 10000,
|
|
31
|
+
});
|
|
32
|
+
expect(c.apiKey).toBe('test-key');
|
|
33
|
+
expect(c.timeoutMs).toBe(10000);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
describe('list', () => {
|
|
37
|
+
it('should fetch policies successfully', async () => {
|
|
38
|
+
const mockPolicies = [
|
|
39
|
+
{ id: '1', name: 'Policy 1', policy_type: 'cost', enabled: true },
|
|
40
|
+
{ id: '2', name: 'Policy 2', policy_type: 'rate', enabled: false },
|
|
41
|
+
];
|
|
42
|
+
mockFetch.mockResolvedValueOnce({
|
|
43
|
+
ok: true,
|
|
44
|
+
json: async () => mockPolicies,
|
|
45
|
+
});
|
|
46
|
+
const policies = await client.list();
|
|
47
|
+
expect(mockFetch).toHaveBeenCalledWith(`${baseUrl}/api/policies`, expect.objectContaining({
|
|
48
|
+
method: 'GET',
|
|
49
|
+
headers: { 'Content-Type': 'application/json' },
|
|
50
|
+
}));
|
|
51
|
+
expect(policies).toEqual(mockPolicies);
|
|
52
|
+
});
|
|
53
|
+
it('should apply filters to query params', async () => {
|
|
54
|
+
mockFetch.mockResolvedValueOnce({
|
|
55
|
+
ok: true,
|
|
56
|
+
json: async () => [],
|
|
57
|
+
});
|
|
58
|
+
await client.list({ status: 'ACTIVE', policy_type: 'cost' });
|
|
59
|
+
expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining('status=ACTIVE'), expect.any(Object));
|
|
60
|
+
expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining('policy_type=cost'), expect.any(Object));
|
|
61
|
+
});
|
|
62
|
+
it('should include Authorization header when apiKey is set', async () => {
|
|
63
|
+
const authedClient = new clients_1.PolicyClient({ baseUrl, apiKey: 'my-key' });
|
|
64
|
+
mockFetch.mockResolvedValueOnce({
|
|
65
|
+
ok: true,
|
|
66
|
+
json: async () => [],
|
|
67
|
+
});
|
|
68
|
+
await authedClient.list();
|
|
69
|
+
expect(mockFetch).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({
|
|
70
|
+
headers: expect.objectContaining({
|
|
71
|
+
Authorization: 'Bearer my-key',
|
|
72
|
+
}),
|
|
73
|
+
}));
|
|
74
|
+
});
|
|
75
|
+
it('should throw PolicyClientError on failure', async () => {
|
|
76
|
+
mockFetch.mockResolvedValueOnce({
|
|
77
|
+
ok: false,
|
|
78
|
+
status: 500,
|
|
79
|
+
json: async () => ({ error: 'Database error' }),
|
|
80
|
+
});
|
|
81
|
+
try {
|
|
82
|
+
await client.list();
|
|
83
|
+
fail('Expected PolicyClientError to be thrown');
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
expect(error).toBeInstanceOf(clients_1.PolicyClientError);
|
|
87
|
+
expect(error.message).toBe('Database error');
|
|
88
|
+
expect(error.statusCode).toBe(500);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
describe('get', () => {
|
|
93
|
+
it('should fetch single policy by ID', async () => {
|
|
94
|
+
const mockPolicy = {
|
|
95
|
+
id: '123',
|
|
96
|
+
name: 'Test Policy',
|
|
97
|
+
policy_type: 'cost',
|
|
98
|
+
};
|
|
99
|
+
mockFetch.mockResolvedValueOnce({
|
|
100
|
+
ok: true,
|
|
101
|
+
json: async () => mockPolicy,
|
|
102
|
+
});
|
|
103
|
+
const policy = await client.get('123');
|
|
104
|
+
expect(mockFetch).toHaveBeenCalledWith(`${baseUrl}/api/policies/123`, expect.objectContaining({ method: 'GET' }));
|
|
105
|
+
expect(policy).toEqual(mockPolicy);
|
|
106
|
+
});
|
|
107
|
+
it('should throw on 404', async () => {
|
|
108
|
+
mockFetch.mockResolvedValueOnce({
|
|
109
|
+
ok: false,
|
|
110
|
+
status: 404,
|
|
111
|
+
json: async () => ({ error: 'Policy not found' }),
|
|
112
|
+
});
|
|
113
|
+
await expect(client.get('nonexistent')).rejects.toThrow(clients_1.PolicyClientError);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
describe('create', () => {
|
|
117
|
+
it('should create policy with required fields', async () => {
|
|
118
|
+
const newPolicy = {
|
|
119
|
+
name: 'New Policy',
|
|
120
|
+
policy_type: 'cost',
|
|
121
|
+
rules: { max_cost: 100 },
|
|
122
|
+
};
|
|
123
|
+
const createdPolicy = { id: '456', ...newPolicy, enabled: true };
|
|
124
|
+
mockFetch.mockResolvedValueOnce({
|
|
125
|
+
ok: true,
|
|
126
|
+
json: async () => createdPolicy,
|
|
127
|
+
});
|
|
128
|
+
const result = await client.create(newPolicy);
|
|
129
|
+
expect(mockFetch).toHaveBeenCalledWith(`${baseUrl}/api/policies`, expect.objectContaining({
|
|
130
|
+
method: 'POST',
|
|
131
|
+
body: JSON.stringify(newPolicy),
|
|
132
|
+
}));
|
|
133
|
+
expect(result).toEqual(createdPolicy);
|
|
134
|
+
});
|
|
135
|
+
it('should create policy with optional fields', async () => {
|
|
136
|
+
const newPolicy = {
|
|
137
|
+
name: 'New Policy',
|
|
138
|
+
policy_type: 'cost',
|
|
139
|
+
rules: { max_cost: 100 },
|
|
140
|
+
enabled: false,
|
|
141
|
+
priority: 10,
|
|
142
|
+
status: 'DRAFT',
|
|
143
|
+
};
|
|
144
|
+
mockFetch.mockResolvedValueOnce({
|
|
145
|
+
ok: true,
|
|
146
|
+
json: async () => ({ id: '789', ...newPolicy }),
|
|
147
|
+
});
|
|
148
|
+
await client.create(newPolicy);
|
|
149
|
+
expect(mockFetch).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({
|
|
150
|
+
body: JSON.stringify(newPolicy),
|
|
151
|
+
}));
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
describe('update', () => {
|
|
155
|
+
it('should update policy with partial data', async () => {
|
|
156
|
+
const updates = { name: 'Updated Name', priority: 5 };
|
|
157
|
+
mockFetch.mockResolvedValueOnce({
|
|
158
|
+
ok: true,
|
|
159
|
+
json: async () => ({ id: '123', ...updates }),
|
|
160
|
+
});
|
|
161
|
+
const result = await client.update('123', updates);
|
|
162
|
+
expect(mockFetch).toHaveBeenCalledWith(`${baseUrl}/api/policies/123`, expect.objectContaining({
|
|
163
|
+
method: 'PATCH',
|
|
164
|
+
body: JSON.stringify(updates),
|
|
165
|
+
}));
|
|
166
|
+
expect(result.name).toBe('Updated Name');
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
describe('delete', () => {
|
|
170
|
+
it('should delete policy', async () => {
|
|
171
|
+
mockFetch.mockResolvedValueOnce({
|
|
172
|
+
ok: true,
|
|
173
|
+
json: async () => ({}),
|
|
174
|
+
});
|
|
175
|
+
await expect(client.delete('123')).resolves.toBeUndefined();
|
|
176
|
+
expect(mockFetch).toHaveBeenCalledWith(`${baseUrl}/api/policies/123`, expect.objectContaining({ method: 'DELETE' }));
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
describe('setEnabled', () => {
|
|
180
|
+
it('should enable policy', async () => {
|
|
181
|
+
mockFetch.mockResolvedValueOnce({
|
|
182
|
+
ok: true,
|
|
183
|
+
json: async () => ({ id: '123', enabled: true }),
|
|
184
|
+
});
|
|
185
|
+
const result = await client.setEnabled('123', true);
|
|
186
|
+
expect(mockFetch).toHaveBeenCalledWith(`${baseUrl}/api/policies/123`, expect.objectContaining({
|
|
187
|
+
method: 'PATCH',
|
|
188
|
+
body: JSON.stringify({ enabled: true }),
|
|
189
|
+
}));
|
|
190
|
+
expect(result.enabled).toBe(true);
|
|
191
|
+
});
|
|
192
|
+
it('should disable policy', async () => {
|
|
193
|
+
mockFetch.mockResolvedValueOnce({
|
|
194
|
+
ok: true,
|
|
195
|
+
json: async () => ({ id: '123', enabled: false }),
|
|
196
|
+
});
|
|
197
|
+
const result = await client.setEnabled('123', false);
|
|
198
|
+
expect(result.enabled).toBe(false);
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
describe('setStatus', () => {
|
|
202
|
+
it('should update policy status', async () => {
|
|
203
|
+
mockFetch.mockResolvedValueOnce({
|
|
204
|
+
ok: true,
|
|
205
|
+
json: async () => ({ id: '123', status: 'INACTIVE' }),
|
|
206
|
+
});
|
|
207
|
+
const result = await client.setStatus('123', 'INACTIVE');
|
|
208
|
+
expect(mockFetch).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({
|
|
209
|
+
body: JSON.stringify({ status: 'INACTIVE' }),
|
|
210
|
+
}));
|
|
211
|
+
expect(result.status).toBe('INACTIVE');
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
describe('ApprovalClient', () => {
|
|
216
|
+
const baseUrl = 'http://localhost:3000';
|
|
217
|
+
let client;
|
|
218
|
+
beforeEach(() => {
|
|
219
|
+
jest.clearAllMocks();
|
|
220
|
+
client = new clients_1.ApprovalClient({ baseUrl });
|
|
221
|
+
});
|
|
222
|
+
describe('initialization', () => {
|
|
223
|
+
it('should create client with default options', () => {
|
|
224
|
+
const c = new clients_1.ApprovalClient({ baseUrl: 'http://example.com' });
|
|
225
|
+
expect(c).toBeInstanceOf(clients_1.ApprovalClient);
|
|
226
|
+
});
|
|
227
|
+
it('should trim trailing slash from baseUrl', () => {
|
|
228
|
+
const c = new clients_1.ApprovalClient({ baseUrl: 'http://example.com/' });
|
|
229
|
+
expect(c.baseUrl).toBe('http://example.com');
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
describe('list', () => {
|
|
233
|
+
it('should fetch approvals successfully', async () => {
|
|
234
|
+
const mockApprovals = [
|
|
235
|
+
{ id: '1', envelope_id: 'env-1', status: 'PENDING' },
|
|
236
|
+
{ id: '2', envelope_id: 'env-2', status: 'APPROVED' },
|
|
237
|
+
];
|
|
238
|
+
mockFetch.mockResolvedValueOnce({
|
|
239
|
+
ok: true,
|
|
240
|
+
json: async () => ({ approvals: mockApprovals }),
|
|
241
|
+
});
|
|
242
|
+
const approvals = await client.list();
|
|
243
|
+
expect(mockFetch).toHaveBeenCalledWith(`${baseUrl}/api/approvals`, expect.objectContaining({ method: 'GET' }));
|
|
244
|
+
expect(approvals).toEqual(mockApprovals);
|
|
245
|
+
});
|
|
246
|
+
it('should apply status filter', async () => {
|
|
247
|
+
mockFetch.mockResolvedValueOnce({
|
|
248
|
+
ok: true,
|
|
249
|
+
json: async () => ({ approvals: [] }),
|
|
250
|
+
});
|
|
251
|
+
await client.list({ status: 'PENDING' });
|
|
252
|
+
expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining('status=PENDING'), expect.any(Object));
|
|
253
|
+
});
|
|
254
|
+
it('should throw ApprovalClientError on failure', async () => {
|
|
255
|
+
mockFetch.mockResolvedValueOnce({
|
|
256
|
+
ok: false,
|
|
257
|
+
status: 500,
|
|
258
|
+
json: async () => ({ error: 'Server error' }),
|
|
259
|
+
});
|
|
260
|
+
await expect(client.list()).rejects.toThrow(clients_1.ApprovalClientError);
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
describe('listPending', () => {
|
|
264
|
+
it('should fetch only pending approvals', async () => {
|
|
265
|
+
mockFetch.mockResolvedValueOnce({
|
|
266
|
+
ok: true,
|
|
267
|
+
json: async () => ({ approvals: [] }),
|
|
268
|
+
});
|
|
269
|
+
await client.listPending();
|
|
270
|
+
expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining('status=PENDING'), expect.any(Object));
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
describe('get', () => {
|
|
274
|
+
it('should fetch single approval by ID', async () => {
|
|
275
|
+
const mockApproval = { id: '123', envelope_id: 'env-1', status: 'PENDING' };
|
|
276
|
+
mockFetch.mockResolvedValueOnce({
|
|
277
|
+
ok: true,
|
|
278
|
+
json: async () => mockApproval,
|
|
279
|
+
});
|
|
280
|
+
const approval = await client.get('123');
|
|
281
|
+
expect(mockFetch).toHaveBeenCalledWith(`${baseUrl}/api/approvals/123`, expect.objectContaining({ method: 'GET' }));
|
|
282
|
+
expect(approval).toEqual(mockApproval);
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
describe('decide', () => {
|
|
286
|
+
it('should submit approval decision', async () => {
|
|
287
|
+
const decision = {
|
|
288
|
+
approval_id: '123',
|
|
289
|
+
decision: 'APPROVED',
|
|
290
|
+
comment: 'Looks good',
|
|
291
|
+
};
|
|
292
|
+
mockFetch.mockResolvedValueOnce({
|
|
293
|
+
ok: true,
|
|
294
|
+
json: async () => ({
|
|
295
|
+
approval: { id: '123', status: 'APPROVED', review_note: 'Looks good' },
|
|
296
|
+
}),
|
|
297
|
+
});
|
|
298
|
+
const result = await client.decide(decision);
|
|
299
|
+
expect(mockFetch).toHaveBeenCalledWith(`${baseUrl}/api/approvals`, expect.objectContaining({
|
|
300
|
+
method: 'POST',
|
|
301
|
+
body: JSON.stringify(decision),
|
|
302
|
+
}));
|
|
303
|
+
expect(result.status).toBe('APPROVED');
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
describe('approve', () => {
|
|
307
|
+
it('should approve with comment', async () => {
|
|
308
|
+
mockFetch.mockResolvedValueOnce({
|
|
309
|
+
ok: true,
|
|
310
|
+
json: async () => ({
|
|
311
|
+
approval: { id: '123', status: 'APPROVED' },
|
|
312
|
+
}),
|
|
313
|
+
});
|
|
314
|
+
const result = await client.approve('123', 'Approved by admin');
|
|
315
|
+
expect(mockFetch).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({
|
|
316
|
+
body: JSON.stringify({
|
|
317
|
+
approval_id: '123',
|
|
318
|
+
decision: 'APPROVED',
|
|
319
|
+
comment: 'Approved by admin',
|
|
320
|
+
}),
|
|
321
|
+
}));
|
|
322
|
+
expect(result.status).toBe('APPROVED');
|
|
323
|
+
});
|
|
324
|
+
it('should approve without comment', async () => {
|
|
325
|
+
mockFetch.mockResolvedValueOnce({
|
|
326
|
+
ok: true,
|
|
327
|
+
json: async () => ({
|
|
328
|
+
approval: { id: '123', status: 'APPROVED' },
|
|
329
|
+
}),
|
|
330
|
+
});
|
|
331
|
+
await client.approve('123');
|
|
332
|
+
expect(mockFetch).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({
|
|
333
|
+
body: JSON.stringify({
|
|
334
|
+
approval_id: '123',
|
|
335
|
+
decision: 'APPROVED',
|
|
336
|
+
comment: undefined,
|
|
337
|
+
}),
|
|
338
|
+
}));
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
describe('deny', () => {
|
|
342
|
+
it('should deny with required comment', async () => {
|
|
343
|
+
mockFetch.mockResolvedValueOnce({
|
|
344
|
+
ok: true,
|
|
345
|
+
json: async () => ({
|
|
346
|
+
approval: { id: '123', status: 'DENIED', review_note: 'Policy violation' },
|
|
347
|
+
}),
|
|
348
|
+
});
|
|
349
|
+
const result = await client.deny('123', 'Policy violation');
|
|
350
|
+
expect(mockFetch).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({
|
|
351
|
+
body: JSON.stringify({
|
|
352
|
+
approval_id: '123',
|
|
353
|
+
decision: 'DENIED',
|
|
354
|
+
comment: 'Policy violation',
|
|
355
|
+
}),
|
|
356
|
+
}));
|
|
357
|
+
expect(result.status).toBe('DENIED');
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
describe('BudgetClient', () => {
|
|
362
|
+
const baseUrl = 'http://localhost:3000';
|
|
363
|
+
let client;
|
|
364
|
+
const mockBudget = {
|
|
365
|
+
id: 'bud_123',
|
|
366
|
+
org_id: 'org_1',
|
|
367
|
+
name: 'Monthly API Budget',
|
|
368
|
+
scope: 'GLOBAL',
|
|
369
|
+
scope_id: null,
|
|
370
|
+
limit_amount: 1000,
|
|
371
|
+
period: 'MONTHLY',
|
|
372
|
+
action: 'WARN',
|
|
373
|
+
alert_thresholds: [50, 75, 90],
|
|
374
|
+
current_spend: 450,
|
|
375
|
+
period_start: '2026-01-01T00:00:00Z',
|
|
376
|
+
created_by: 'user_1',
|
|
377
|
+
created_at: '2026-01-01T00:00:00Z',
|
|
378
|
+
updated_at: '2026-01-01T00:00:00Z',
|
|
379
|
+
last_alert_sent: null,
|
|
380
|
+
status: 'active',
|
|
381
|
+
percentage: 45,
|
|
382
|
+
};
|
|
383
|
+
beforeEach(() => {
|
|
384
|
+
jest.clearAllMocks();
|
|
385
|
+
client = new clients_1.BudgetClient({ baseUrl });
|
|
386
|
+
});
|
|
387
|
+
describe('initialization', () => {
|
|
388
|
+
it('should create client with default options', () => {
|
|
389
|
+
const c = new clients_1.BudgetClient({ baseUrl: 'http://example.com' });
|
|
390
|
+
expect(c).toBeInstanceOf(clients_1.BudgetClient);
|
|
391
|
+
});
|
|
392
|
+
it('should trim trailing slash from baseUrl', () => {
|
|
393
|
+
const c = new clients_1.BudgetClient({ baseUrl: 'http://example.com/' });
|
|
394
|
+
expect(c.baseUrl).toBe('http://example.com');
|
|
395
|
+
});
|
|
396
|
+
it('should set default timeout to 5000ms', () => {
|
|
397
|
+
expect(client.timeoutMs).toBe(5000);
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
describe('list', () => {
|
|
401
|
+
it('should fetch budgets with summary', async () => {
|
|
402
|
+
const mockResponse = {
|
|
403
|
+
budgets: [mockBudget],
|
|
404
|
+
summary: {
|
|
405
|
+
total: 1,
|
|
406
|
+
active: 1,
|
|
407
|
+
exceeded: 0,
|
|
408
|
+
totalLimit: 1000,
|
|
409
|
+
totalSpend: 450,
|
|
410
|
+
},
|
|
411
|
+
};
|
|
412
|
+
mockFetch.mockResolvedValueOnce({
|
|
413
|
+
ok: true,
|
|
414
|
+
json: async () => mockResponse,
|
|
415
|
+
});
|
|
416
|
+
const response = await client.list();
|
|
417
|
+
expect(mockFetch).toHaveBeenCalledWith(`${baseUrl}/api/budgets`, expect.objectContaining({ method: 'GET' }));
|
|
418
|
+
expect(response.budgets).toHaveLength(1);
|
|
419
|
+
expect(response.summary.total).toBe(1);
|
|
420
|
+
});
|
|
421
|
+
it('should apply filters to query params', async () => {
|
|
422
|
+
mockFetch.mockResolvedValueOnce({
|
|
423
|
+
ok: true,
|
|
424
|
+
json: async () => ({ budgets: [], summary: {} }),
|
|
425
|
+
});
|
|
426
|
+
await client.list({ scope: 'GLOBAL', status: 'exceeded' });
|
|
427
|
+
expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining('scope=GLOBAL'), expect.any(Object));
|
|
428
|
+
expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining('status=exceeded'), expect.any(Object));
|
|
429
|
+
});
|
|
430
|
+
it('should throw BudgetClientError on failure', async () => {
|
|
431
|
+
mockFetch.mockResolvedValueOnce({
|
|
432
|
+
ok: false,
|
|
433
|
+
status: 500,
|
|
434
|
+
json: async () => ({ error: 'Database error' }),
|
|
435
|
+
});
|
|
436
|
+
await expect(client.list()).rejects.toThrow(clients_1.BudgetClientError);
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
describe('listBudgets', () => {
|
|
440
|
+
it('should return only budgets array', async () => {
|
|
441
|
+
mockFetch.mockResolvedValueOnce({
|
|
442
|
+
ok: true,
|
|
443
|
+
json: async () => ({
|
|
444
|
+
budgets: [mockBudget],
|
|
445
|
+
summary: { total: 1 },
|
|
446
|
+
}),
|
|
447
|
+
});
|
|
448
|
+
const budgets = await client.listBudgets();
|
|
449
|
+
expect(budgets).toEqual([mockBudget]);
|
|
450
|
+
});
|
|
451
|
+
});
|
|
452
|
+
describe('getSummary', () => {
|
|
453
|
+
it('should return only summary', async () => {
|
|
454
|
+
const summary = {
|
|
455
|
+
total: 5,
|
|
456
|
+
active: 4,
|
|
457
|
+
exceeded: 1,
|
|
458
|
+
totalLimit: 5000,
|
|
459
|
+
totalSpend: 2500,
|
|
460
|
+
};
|
|
461
|
+
mockFetch.mockResolvedValueOnce({
|
|
462
|
+
ok: true,
|
|
463
|
+
json: async () => ({ budgets: [], summary }),
|
|
464
|
+
});
|
|
465
|
+
const result = await client.getSummary();
|
|
466
|
+
expect(result).toEqual(summary);
|
|
467
|
+
});
|
|
468
|
+
});
|
|
469
|
+
describe('get', () => {
|
|
470
|
+
it('should fetch single budget by ID', async () => {
|
|
471
|
+
mockFetch.mockResolvedValueOnce({
|
|
472
|
+
ok: true,
|
|
473
|
+
json: async () => mockBudget,
|
|
474
|
+
});
|
|
475
|
+
const budget = await client.get('bud_123');
|
|
476
|
+
expect(mockFetch).toHaveBeenCalledWith(`${baseUrl}/api/budgets/bud_123`, expect.objectContaining({ method: 'GET' }));
|
|
477
|
+
expect(budget).toEqual(mockBudget);
|
|
478
|
+
});
|
|
479
|
+
});
|
|
480
|
+
describe('create', () => {
|
|
481
|
+
it('should create budget with required fields', async () => {
|
|
482
|
+
const newBudget = {
|
|
483
|
+
name: 'New Budget',
|
|
484
|
+
scope: 'WORKFLOW',
|
|
485
|
+
scope_id: 'wf_123',
|
|
486
|
+
limit_amount: 500,
|
|
487
|
+
period: 'WEEKLY',
|
|
488
|
+
action: 'HARD_LIMIT',
|
|
489
|
+
};
|
|
490
|
+
mockFetch.mockResolvedValueOnce({
|
|
491
|
+
ok: true,
|
|
492
|
+
json: async () => ({
|
|
493
|
+
budget: { id: 'bud_new', ...newBudget, status: 'active', percentage: 0 },
|
|
494
|
+
message: 'Budget created successfully',
|
|
495
|
+
}),
|
|
496
|
+
});
|
|
497
|
+
const result = await client.create(newBudget);
|
|
498
|
+
expect(mockFetch).toHaveBeenCalledWith(`${baseUrl}/api/budgets`, expect.objectContaining({
|
|
499
|
+
method: 'POST',
|
|
500
|
+
body: JSON.stringify(newBudget),
|
|
501
|
+
}));
|
|
502
|
+
expect(result.id).toBe('bud_new');
|
|
503
|
+
});
|
|
504
|
+
});
|
|
505
|
+
describe('update', () => {
|
|
506
|
+
it('should update budget with partial data', async () => {
|
|
507
|
+
const updates = { limit_amount: 2000 };
|
|
508
|
+
mockFetch.mockResolvedValueOnce({
|
|
509
|
+
ok: true,
|
|
510
|
+
json: async () => ({ ...mockBudget, limit_amount: 2000 }),
|
|
511
|
+
});
|
|
512
|
+
const result = await client.update('bud_123', updates);
|
|
513
|
+
expect(mockFetch).toHaveBeenCalledWith(`${baseUrl}/api/budgets/bud_123`, expect.objectContaining({
|
|
514
|
+
method: 'PATCH',
|
|
515
|
+
body: JSON.stringify(updates),
|
|
516
|
+
}));
|
|
517
|
+
expect(result.limit_amount).toBe(2000);
|
|
518
|
+
});
|
|
519
|
+
});
|
|
520
|
+
describe('delete', () => {
|
|
521
|
+
it('should delete budget', async () => {
|
|
522
|
+
mockFetch.mockResolvedValueOnce({
|
|
523
|
+
ok: true,
|
|
524
|
+
json: async () => ({}),
|
|
525
|
+
});
|
|
526
|
+
await expect(client.delete('bud_123')).resolves.toBeUndefined();
|
|
527
|
+
expect(mockFetch).toHaveBeenCalledWith(`${baseUrl}/api/budgets/bud_123`, expect.objectContaining({ method: 'DELETE' }));
|
|
528
|
+
});
|
|
529
|
+
});
|
|
530
|
+
describe('getExceeded', () => {
|
|
531
|
+
it('should fetch exceeded budgets', async () => {
|
|
532
|
+
mockFetch.mockResolvedValueOnce({
|
|
533
|
+
ok: true,
|
|
534
|
+
json: async () => ({
|
|
535
|
+
budgets: [{ ...mockBudget, status: 'exceeded' }],
|
|
536
|
+
summary: { exceeded: 1 },
|
|
537
|
+
}),
|
|
538
|
+
});
|
|
539
|
+
const budgets = await client.getExceeded();
|
|
540
|
+
expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining('status=exceeded'), expect.any(Object));
|
|
541
|
+
expect(budgets[0].status).toBe('exceeded');
|
|
542
|
+
});
|
|
543
|
+
});
|
|
544
|
+
describe('getWarnings', () => {
|
|
545
|
+
it('should fetch warning budgets', async () => {
|
|
546
|
+
mockFetch.mockResolvedValueOnce({
|
|
547
|
+
ok: true,
|
|
548
|
+
json: async () => ({
|
|
549
|
+
budgets: [{ ...mockBudget, status: 'warning' }],
|
|
550
|
+
summary: {},
|
|
551
|
+
}),
|
|
552
|
+
});
|
|
553
|
+
const budgets = await client.getWarnings();
|
|
554
|
+
expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining('status=warning'), expect.any(Object));
|
|
555
|
+
expect(budgets[0].status).toBe('warning');
|
|
556
|
+
});
|
|
557
|
+
});
|
|
558
|
+
describe('calculatePercentage', () => {
|
|
559
|
+
it('should calculate percentage correctly', () => {
|
|
560
|
+
const percentage = client.calculatePercentage(mockBudget);
|
|
561
|
+
expect(percentage).toBe(45);
|
|
562
|
+
});
|
|
563
|
+
it('should return 0 for zero limit', () => {
|
|
564
|
+
const zeroBudget = { ...mockBudget, limit_amount: 0 };
|
|
565
|
+
const percentage = client.calculatePercentage(zeroBudget);
|
|
566
|
+
expect(percentage).toBe(0);
|
|
567
|
+
});
|
|
568
|
+
it('should round percentage', () => {
|
|
569
|
+
const budget = { ...mockBudget, current_spend: 333, limit_amount: 1000 };
|
|
570
|
+
const percentage = client.calculatePercentage(budget);
|
|
571
|
+
expect(percentage).toBe(33);
|
|
572
|
+
});
|
|
573
|
+
});
|
|
574
|
+
describe('isAtThreshold', () => {
|
|
575
|
+
it('should return true when at threshold', () => {
|
|
576
|
+
const budget = { ...mockBudget, current_spend: 750, limit_amount: 1000 };
|
|
577
|
+
expect(client.isAtThreshold(budget, 75)).toBe(true);
|
|
578
|
+
});
|
|
579
|
+
it('should return true when above threshold', () => {
|
|
580
|
+
const budget = { ...mockBudget, current_spend: 900, limit_amount: 1000 };
|
|
581
|
+
expect(client.isAtThreshold(budget, 75)).toBe(true);
|
|
582
|
+
});
|
|
583
|
+
it('should return false when below threshold', () => {
|
|
584
|
+
const budget = { ...mockBudget, current_spend: 500, limit_amount: 1000 };
|
|
585
|
+
expect(client.isAtThreshold(budget, 75)).toBe(false);
|
|
586
|
+
});
|
|
587
|
+
});
|
|
588
|
+
});
|
|
589
|
+
describe('Error classes', () => {
|
|
590
|
+
describe('PolicyClientError', () => {
|
|
591
|
+
it('should have correct name and properties', () => {
|
|
592
|
+
const error = new clients_1.PolicyClientError('Test error', 404);
|
|
593
|
+
expect(error.name).toBe('PolicyClientError');
|
|
594
|
+
expect(error.message).toBe('Test error');
|
|
595
|
+
expect(error.statusCode).toBe(404);
|
|
596
|
+
});
|
|
597
|
+
});
|
|
598
|
+
describe('ApprovalClientError', () => {
|
|
599
|
+
it('should have correct name and properties', () => {
|
|
600
|
+
const error = new clients_1.ApprovalClientError('Test error', 500);
|
|
601
|
+
expect(error.name).toBe('ApprovalClientError');
|
|
602
|
+
expect(error.message).toBe('Test error');
|
|
603
|
+
expect(error.statusCode).toBe(500);
|
|
604
|
+
});
|
|
605
|
+
});
|
|
606
|
+
describe('BudgetClientError', () => {
|
|
607
|
+
it('should have correct name and properties', () => {
|
|
608
|
+
const error = new clients_1.BudgetClientError('Test error', 403);
|
|
609
|
+
expect(error.name).toBe('BudgetClientError');
|
|
610
|
+
expect(error.message).toBe('Test error');
|
|
611
|
+
expect(error.statusCode).toBe(403);
|
|
612
|
+
});
|
|
613
|
+
});
|
|
614
|
+
describe('MetricsClientError', () => {
|
|
615
|
+
it('should have correct name and properties', () => {
|
|
616
|
+
const error = new clients_1.MetricsClientError('Test error', 503);
|
|
617
|
+
expect(error.name).toBe('MetricsClientError');
|
|
618
|
+
expect(error.message).toBe('Test error');
|
|
619
|
+
expect(error.statusCode).toBe(503);
|
|
620
|
+
});
|
|
621
|
+
});
|
|
622
|
+
});
|
|
623
|
+
describe('MetricsClient', () => {
|
|
624
|
+
const baseUrl = 'http://localhost:3000';
|
|
625
|
+
let client;
|
|
626
|
+
const mockPublicMetrics = {
|
|
627
|
+
lastUpdated: '2026-01-09T12:00:00Z',
|
|
628
|
+
policiesEvaluated24h: 47832,
|
|
629
|
+
avgLatencyMs: 8,
|
|
630
|
+
budgetAlerts24h: 127,
|
|
631
|
+
activeTenants: 42,
|
|
632
|
+
blockedRequests24h: 1247,
|
|
633
|
+
cognitiveLayer: {
|
|
634
|
+
semanticJudgeRequests: 284719,
|
|
635
|
+
semanticJudgeViolations: 847,
|
|
636
|
+
oraclePredictions: 12847,
|
|
637
|
+
oracleSavings: 18430,
|
|
638
|
+
immunePoliciesGenerated: 34,
|
|
639
|
+
},
|
|
640
|
+
};
|
|
641
|
+
const mockAuditLog = {
|
|
642
|
+
id: 'audit_123',
|
|
643
|
+
org_id: 'org_1',
|
|
644
|
+
timestamp: '2026-01-09T12:00:00Z',
|
|
645
|
+
actor_id: 'user_1',
|
|
646
|
+
actor_email: 'admin@example.com',
|
|
647
|
+
action: 'policy.created',
|
|
648
|
+
resource_type: 'Policy',
|
|
649
|
+
resource_id: 'pol_123',
|
|
650
|
+
resource_name: 'Test Policy',
|
|
651
|
+
status: 'success',
|
|
652
|
+
};
|
|
653
|
+
beforeEach(() => {
|
|
654
|
+
jest.clearAllMocks();
|
|
655
|
+
client = new clients_1.MetricsClient({ baseUrl });
|
|
656
|
+
});
|
|
657
|
+
describe('initialization', () => {
|
|
658
|
+
it('should create client with default options', () => {
|
|
659
|
+
const c = new clients_1.MetricsClient({ baseUrl: 'http://example.com' });
|
|
660
|
+
expect(c).toBeInstanceOf(clients_1.MetricsClient);
|
|
661
|
+
});
|
|
662
|
+
it('should trim trailing slash from baseUrl', () => {
|
|
663
|
+
const c = new clients_1.MetricsClient({ baseUrl: 'http://example.com/' });
|
|
664
|
+
expect(c.baseUrl).toBe('http://example.com');
|
|
665
|
+
});
|
|
666
|
+
});
|
|
667
|
+
describe('getPublicMetrics', () => {
|
|
668
|
+
it('should fetch public metrics', async () => {
|
|
669
|
+
mockFetch.mockResolvedValueOnce({
|
|
670
|
+
ok: true,
|
|
671
|
+
json: async () => mockPublicMetrics,
|
|
672
|
+
});
|
|
673
|
+
const metrics = await client.getPublicMetrics();
|
|
674
|
+
expect(mockFetch).toHaveBeenCalledWith(`${baseUrl}/api/metrics/public`, expect.objectContaining({ method: 'GET' }));
|
|
675
|
+
expect(metrics.policiesEvaluated24h).toBe(47832);
|
|
676
|
+
expect(metrics.cognitiveLayer.semanticJudgeRequests).toBe(284719);
|
|
677
|
+
});
|
|
678
|
+
it('should throw MetricsClientError on failure', async () => {
|
|
679
|
+
mockFetch.mockResolvedValueOnce({
|
|
680
|
+
ok: false,
|
|
681
|
+
status: 500,
|
|
682
|
+
json: async () => ({ error: 'Server error' }),
|
|
683
|
+
});
|
|
684
|
+
await expect(client.getPublicMetrics()).rejects.toThrow(clients_1.MetricsClientError);
|
|
685
|
+
});
|
|
686
|
+
});
|
|
687
|
+
describe('getCognitiveMetrics', () => {
|
|
688
|
+
it('should return only cognitive layer metrics', async () => {
|
|
689
|
+
mockFetch.mockResolvedValueOnce({
|
|
690
|
+
ok: true,
|
|
691
|
+
json: async () => mockPublicMetrics,
|
|
692
|
+
});
|
|
693
|
+
const cognitive = await client.getCognitiveMetrics();
|
|
694
|
+
expect(cognitive.semanticJudgeRequests).toBe(284719);
|
|
695
|
+
expect(cognitive.oracleSavings).toBe(18430);
|
|
696
|
+
});
|
|
697
|
+
});
|
|
698
|
+
describe('getAverageLatency', () => {
|
|
699
|
+
it('should return average latency', async () => {
|
|
700
|
+
mockFetch.mockResolvedValueOnce({
|
|
701
|
+
ok: true,
|
|
702
|
+
json: async () => mockPublicMetrics,
|
|
703
|
+
});
|
|
704
|
+
const latency = await client.getAverageLatency();
|
|
705
|
+
expect(latency).toBe(8);
|
|
706
|
+
});
|
|
707
|
+
});
|
|
708
|
+
describe('getPolicyEvaluationCount', () => {
|
|
709
|
+
it('should return policy evaluation count', async () => {
|
|
710
|
+
mockFetch.mockResolvedValueOnce({
|
|
711
|
+
ok: true,
|
|
712
|
+
json: async () => mockPublicMetrics,
|
|
713
|
+
});
|
|
714
|
+
const count = await client.getPolicyEvaluationCount();
|
|
715
|
+
expect(count).toBe(47832);
|
|
716
|
+
});
|
|
717
|
+
});
|
|
718
|
+
describe('getAuditLogs', () => {
|
|
719
|
+
it('should fetch audit logs with pagination', async () => {
|
|
720
|
+
const mockResponse = {
|
|
721
|
+
logs: [mockAuditLog],
|
|
722
|
+
pagination: {
|
|
723
|
+
page: 1,
|
|
724
|
+
pageSize: 20,
|
|
725
|
+
totalCount: 1,
|
|
726
|
+
totalPages: 1,
|
|
727
|
+
},
|
|
728
|
+
actors: [{ id: 'user_1', email: 'admin@example.com' }],
|
|
729
|
+
};
|
|
730
|
+
mockFetch.mockResolvedValueOnce({
|
|
731
|
+
ok: true,
|
|
732
|
+
json: async () => mockResponse,
|
|
733
|
+
});
|
|
734
|
+
const response = await client.getAuditLogs();
|
|
735
|
+
expect(mockFetch).toHaveBeenCalledWith(`${baseUrl}/api/audit-logs`, expect.objectContaining({ method: 'GET' }));
|
|
736
|
+
expect(response.logs).toHaveLength(1);
|
|
737
|
+
expect(response.pagination.page).toBe(1);
|
|
738
|
+
});
|
|
739
|
+
it('should apply filters to query params', async () => {
|
|
740
|
+
mockFetch.mockResolvedValueOnce({
|
|
741
|
+
ok: true,
|
|
742
|
+
json: async () => ({ logs: [], pagination: {}, actors: [] }),
|
|
743
|
+
});
|
|
744
|
+
await client.getAuditLogs({
|
|
745
|
+
user_id: 'user_1',
|
|
746
|
+
resource_type: 'Policy',
|
|
747
|
+
search: 'created',
|
|
748
|
+
page: 2,
|
|
749
|
+
});
|
|
750
|
+
expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining('user_id=user_1'), expect.any(Object));
|
|
751
|
+
expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining('resource_type=Policy'), expect.any(Object));
|
|
752
|
+
expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining('search=created'), expect.any(Object));
|
|
753
|
+
expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining('page=2'), expect.any(Object));
|
|
754
|
+
});
|
|
755
|
+
});
|
|
756
|
+
describe('listAuditLogs', () => {
|
|
757
|
+
it('should return only logs array', async () => {
|
|
758
|
+
mockFetch.mockResolvedValueOnce({
|
|
759
|
+
ok: true,
|
|
760
|
+
json: async () => ({
|
|
761
|
+
logs: [mockAuditLog],
|
|
762
|
+
pagination: {},
|
|
763
|
+
actors: [],
|
|
764
|
+
}),
|
|
765
|
+
});
|
|
766
|
+
const logs = await client.listAuditLogs();
|
|
767
|
+
expect(logs).toEqual([mockAuditLog]);
|
|
768
|
+
});
|
|
769
|
+
});
|
|
770
|
+
describe('getLogsByResourceType', () => {
|
|
771
|
+
it('should filter logs by resource type', async () => {
|
|
772
|
+
mockFetch.mockResolvedValueOnce({
|
|
773
|
+
ok: true,
|
|
774
|
+
json: async () => ({
|
|
775
|
+
logs: [mockAuditLog],
|
|
776
|
+
pagination: {},
|
|
777
|
+
actors: [],
|
|
778
|
+
}),
|
|
779
|
+
});
|
|
780
|
+
await client.getLogsByResourceType('Policy');
|
|
781
|
+
expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining('resource_type=Policy'), expect.any(Object));
|
|
782
|
+
});
|
|
783
|
+
});
|
|
784
|
+
describe('getLogsByUser', () => {
|
|
785
|
+
it('should filter logs by user', async () => {
|
|
786
|
+
mockFetch.mockResolvedValueOnce({
|
|
787
|
+
ok: true,
|
|
788
|
+
json: async () => ({
|
|
789
|
+
logs: [mockAuditLog],
|
|
790
|
+
pagination: {},
|
|
791
|
+
actors: [],
|
|
792
|
+
}),
|
|
793
|
+
});
|
|
794
|
+
await client.getLogsByUser('user_123');
|
|
795
|
+
expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining('user_id=user_123'), expect.any(Object));
|
|
796
|
+
});
|
|
797
|
+
});
|
|
798
|
+
describe('getLogsByDateRange', () => {
|
|
799
|
+
it('should filter logs by date range', async () => {
|
|
800
|
+
mockFetch.mockResolvedValueOnce({
|
|
801
|
+
ok: true,
|
|
802
|
+
json: async () => ({
|
|
803
|
+
logs: [mockAuditLog],
|
|
804
|
+
pagination: {},
|
|
805
|
+
actors: [],
|
|
806
|
+
}),
|
|
807
|
+
});
|
|
808
|
+
const startDate = new Date('2026-01-01');
|
|
809
|
+
const endDate = new Date('2026-01-31');
|
|
810
|
+
await client.getLogsByDateRange(startDate, endDate);
|
|
811
|
+
expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining('start_date='), expect.any(Object));
|
|
812
|
+
expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining('end_date='), expect.any(Object));
|
|
813
|
+
});
|
|
814
|
+
});
|
|
815
|
+
describe('searchAuditLogs', () => {
|
|
816
|
+
it('should search logs by term', async () => {
|
|
817
|
+
mockFetch.mockResolvedValueOnce({
|
|
818
|
+
ok: true,
|
|
819
|
+
json: async () => ({
|
|
820
|
+
logs: [mockAuditLog],
|
|
821
|
+
pagination: {},
|
|
822
|
+
actors: [],
|
|
823
|
+
}),
|
|
824
|
+
});
|
|
825
|
+
await client.searchAuditLogs('policy.created');
|
|
826
|
+
expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining('search=policy.created'), expect.any(Object));
|
|
827
|
+
});
|
|
828
|
+
});
|
|
829
|
+
describe('getAuditLogActors', () => {
|
|
830
|
+
it('should return list of actors', async () => {
|
|
831
|
+
const actors = [
|
|
832
|
+
{ id: 'user_1', email: 'admin@example.com' },
|
|
833
|
+
{ id: 'user_2', email: 'user@example.com' },
|
|
834
|
+
];
|
|
835
|
+
mockFetch.mockResolvedValueOnce({
|
|
836
|
+
ok: true,
|
|
837
|
+
json: async () => ({
|
|
838
|
+
logs: [],
|
|
839
|
+
pagination: {},
|
|
840
|
+
actors,
|
|
841
|
+
}),
|
|
842
|
+
});
|
|
843
|
+
const result = await client.getAuditLogActors();
|
|
844
|
+
expect(result).toEqual(actors);
|
|
845
|
+
});
|
|
846
|
+
});
|
|
847
|
+
});
|