@openzeppelin/guardian-client 0.13.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.
Files changed (47) hide show
  1. package/README.md +140 -0
  2. package/dist/auth-request.d.ts +10 -0
  3. package/dist/auth-request.d.ts.map +1 -0
  4. package/dist/auth-request.js +38 -0
  5. package/dist/auth-request.js.map +1 -0
  6. package/dist/auth-request.test.d.ts +2 -0
  7. package/dist/auth-request.test.d.ts.map +1 -0
  8. package/dist/auth-request.test.js +10 -0
  9. package/dist/auth-request.test.js.map +1 -0
  10. package/dist/conversion.d.ts +18 -0
  11. package/dist/conversion.d.ts.map +1 -0
  12. package/dist/conversion.js +187 -0
  13. package/dist/conversion.js.map +1 -0
  14. package/dist/conversion.test.d.ts +2 -0
  15. package/dist/conversion.test.d.ts.map +1 -0
  16. package/dist/conversion.test.js +317 -0
  17. package/dist/conversion.test.js.map +1 -0
  18. package/dist/http.d.ts +33 -0
  19. package/dist/http.d.ts.map +1 -0
  20. package/dist/http.js +191 -0
  21. package/dist/http.js.map +1 -0
  22. package/dist/http.test.d.ts +2 -0
  23. package/dist/http.test.d.ts.map +1 -0
  24. package/dist/http.test.js +493 -0
  25. package/dist/http.test.js.map +1 -0
  26. package/dist/index.d.ts +4 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +3 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/server-types.d.ts +150 -0
  31. package/dist/server-types.d.ts.map +1 -0
  32. package/dist/server-types.js +2 -0
  33. package/dist/server-types.js.map +1 -0
  34. package/dist/types.d.ts +159 -0
  35. package/dist/types.d.ts.map +1 -0
  36. package/dist/types.js +2 -0
  37. package/dist/types.js.map +1 -0
  38. package/package.json +36 -0
  39. package/src/auth-request.test.ts +11 -0
  40. package/src/auth-request.ts +49 -0
  41. package/src/conversion.test.ts +400 -0
  42. package/src/conversion.ts +247 -0
  43. package/src/http.test.ts +597 -0
  44. package/src/http.ts +244 -0
  45. package/src/index.ts +25 -0
  46. package/src/server-types.ts +142 -0
  47. package/src/types.ts +167 -0
@@ -0,0 +1,597 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { GuardianHttpClient, GuardianHttpError } from './http.js';
3
+ import type { Signer, ConfigureResponse, StateObject, DeltaObject, DeltaProposalResponse } from './types.js';
4
+
5
+ // Mock fetch globally
6
+ const mockFetch = vi.fn();
7
+ vi.stubGlobal('fetch', mockFetch);
8
+
9
+ // Mock signer for authenticated requests
10
+ const mockSigner: Signer = {
11
+ commitment: '0x' + '1'.repeat(64),
12
+ publicKey: '0x' + '2'.repeat(64),
13
+ scheme: 'falcon',
14
+ signAccountIdWithTimestamp: vi.fn().mockResolvedValue('0x' + 'a'.repeat(128)),
15
+ signRequest: vi.fn().mockReturnValue('0x' + 'a'.repeat(128)),
16
+ signCommitment: vi.fn().mockReturnValue('0x' + 'b'.repeat(128)),
17
+ };
18
+
19
+ describe('GuardianHttpClient', () => {
20
+ let client: GuardianHttpClient;
21
+
22
+ beforeEach(() => {
23
+ client = new GuardianHttpClient('http://localhost:3000');
24
+ mockFetch.mockReset();
25
+ });
26
+
27
+ afterEach(() => {
28
+ vi.clearAllMocks();
29
+ });
30
+
31
+ describe('constructor', () => {
32
+ it('should create client with baseUrl', () => {
33
+ const c = new GuardianHttpClient('http://example.com:8080');
34
+ expect(c).toBeInstanceOf(GuardianHttpClient);
35
+ });
36
+ });
37
+
38
+ describe('getPubkey', () => {
39
+ it('should return server public key', async () => {
40
+ const expectedPubkey = '0x' + 'abc123'.repeat(10);
41
+ mockFetch.mockResolvedValueOnce({
42
+ ok: true,
43
+ json: async () => ({ commitment: expectedPubkey }),
44
+ });
45
+
46
+ const pubkey = await client.getPubkey();
47
+
48
+ expect(pubkey).toEqual({ commitment: expectedPubkey, pubkey: undefined });
49
+ expect(mockFetch).toHaveBeenCalledWith(
50
+ 'http://localhost:3000/pubkey',
51
+ expect.objectContaining({
52
+ method: 'GET',
53
+ headers: expect.objectContaining({
54
+ 'Content-Type': 'application/json',
55
+ }),
56
+ })
57
+ );
58
+ });
59
+
60
+ it('should throw GuardianHttpError on non-ok response', async () => {
61
+ mockFetch.mockResolvedValueOnce({
62
+ ok: false,
63
+ status: 500,
64
+ statusText: 'Internal Server Error',
65
+ text: async () => 'Server error message',
66
+ });
67
+
68
+ const error = await client.getPubkey().catch((e) => e);
69
+ expect(error).toBeInstanceOf(GuardianHttpError);
70
+ expect(error.status).toBe(500);
71
+ expect(error.statusText).toBe('Internal Server Error');
72
+ });
73
+ });
74
+
75
+ describe('configure', () => {
76
+ it('should configure account with authentication', async () => {
77
+ client.setSigner(mockSigner);
78
+
79
+ // Server returns snake_case
80
+ const serverResponse = {
81
+ success: true,
82
+ message: 'Account configured',
83
+ ack_pubkey: '0x' + 'c'.repeat(64),
84
+ };
85
+
86
+ mockFetch.mockResolvedValueOnce({
87
+ ok: true,
88
+ json: async () => serverResponse,
89
+ });
90
+
91
+ // Client API uses camelCase
92
+ const request = {
93
+ accountId: '0x' + 'd'.repeat(30),
94
+ auth: {
95
+ MidenFalconRpo: {
96
+ cosigner_commitments: ['0x' + 'e'.repeat(64)],
97
+ },
98
+ },
99
+ initialState: { data: 'base64data', accountId: '0x' + 'd'.repeat(30) },
100
+ };
101
+
102
+ const response = await client.configure(request);
103
+
104
+ // Client returns camelCase
105
+ const expectedResponse: ConfigureResponse = {
106
+ success: true,
107
+ message: 'Account configured',
108
+ ackPubkey: '0x' + 'c'.repeat(64),
109
+ };
110
+ expect(response).toEqual(expectedResponse);
111
+
112
+ // Wire format is snake_case
113
+ expect(mockFetch).toHaveBeenCalledWith(
114
+ 'http://localhost:3000/configure',
115
+ expect.objectContaining({
116
+ method: 'POST',
117
+ body: JSON.stringify({
118
+ account_id: '0x' + 'd'.repeat(30),
119
+ auth: { MidenFalconRpo: { cosigner_commitments: ['0x' + 'e'.repeat(64)] } },
120
+ initial_state: { data: 'base64data', account_id: '0x' + 'd'.repeat(30) },
121
+ }),
122
+ headers: expect.objectContaining({
123
+ 'x-pubkey': mockSigner.publicKey,
124
+ 'x-signature': expect.any(String),
125
+ }),
126
+ })
127
+ );
128
+ });
129
+
130
+ it('should throw error when no signer configured', async () => {
131
+ const request = {
132
+ accountId: '0x' + 'd'.repeat(30),
133
+ auth: { MidenFalconRpo: { cosigner_commitments: [] } },
134
+ initialState: { data: 'base64data', accountId: '0x' + 'd'.repeat(30) },
135
+ };
136
+
137
+ await expect(client.configure(request)).rejects.toThrow('No signer configured');
138
+ });
139
+ });
140
+
141
+ describe('getState', () => {
142
+ it('should get account state with authentication', async () => {
143
+ client.setSigner(mockSigner);
144
+
145
+ // Server returns snake_case
146
+ const serverState = {
147
+ account_id: '0x' + 'a'.repeat(30),
148
+ commitment: '0x' + 'b'.repeat(64),
149
+ state_json: { data: 'base64state' },
150
+ created_at: '2024-01-01T00:00:00Z',
151
+ updated_at: '2024-01-02T00:00:00Z',
152
+ };
153
+
154
+ mockFetch.mockResolvedValueOnce({
155
+ ok: true,
156
+ json: async () => serverState,
157
+ });
158
+
159
+ const accountId = '0x' + 'a'.repeat(30);
160
+ const state = await client.getState(accountId);
161
+
162
+ // Client returns camelCase
163
+ const expectedState: StateObject = {
164
+ accountId: '0x' + 'a'.repeat(30),
165
+ commitment: '0x' + 'b'.repeat(64),
166
+ stateJson: { data: 'base64state' },
167
+ createdAt: '2024-01-01T00:00:00Z',
168
+ updatedAt: '2024-01-02T00:00:00Z',
169
+ };
170
+
171
+ expect(state).toEqual(expectedState);
172
+ expect(mockFetch).toHaveBeenCalledWith(
173
+ expect.stringContaining('/state?'),
174
+ expect.objectContaining({
175
+ method: 'GET',
176
+ headers: expect.objectContaining({
177
+ 'x-pubkey': mockSigner.publicKey,
178
+ }),
179
+ })
180
+ );
181
+ });
182
+ });
183
+
184
+ describe('getDeltaProposals', () => {
185
+ it('should get delta proposals for account', async () => {
186
+ client.setSigner(mockSigner);
187
+
188
+ // Server returns snake_case
189
+ const serverProposals = [
190
+ {
191
+ account_id: '0x' + 'a'.repeat(30),
192
+ nonce: 1,
193
+ prev_commitment: '0x' + 'b'.repeat(64),
194
+ delta_payload: {
195
+ tx_summary: { data: 'base64summary' },
196
+ signatures: [],
197
+ },
198
+ status: {
199
+ status: 'pending',
200
+ timestamp: '2024-01-01T00:00:00Z',
201
+ proposer_id: '0x' + 'c'.repeat(64),
202
+ cosigner_sigs: [],
203
+ },
204
+ },
205
+ ];
206
+
207
+ mockFetch.mockResolvedValueOnce({
208
+ ok: true,
209
+ json: async () => ({ proposals: serverProposals }),
210
+ });
211
+
212
+ const accountId = '0x' + 'a'.repeat(30);
213
+ const result = await client.getDeltaProposals(accountId);
214
+
215
+ // Client returns camelCase
216
+ const expectedProposals: DeltaObject[] = [
217
+ {
218
+ accountId: '0x' + 'a'.repeat(30),
219
+ nonce: 1,
220
+ prevCommitment: '0x' + 'b'.repeat(64),
221
+ newCommitment: undefined,
222
+ deltaPayload: {
223
+ txSummary: { data: 'base64summary' },
224
+ signatures: [],
225
+ metadata: undefined,
226
+ },
227
+ ackSig: undefined,
228
+ status: {
229
+ status: 'pending',
230
+ timestamp: '2024-01-01T00:00:00Z',
231
+ proposerId: '0x' + 'c'.repeat(64),
232
+ cosignerSigs: [],
233
+ },
234
+ },
235
+ ];
236
+
237
+ expect(result).toEqual(expectedProposals);
238
+ expect(mockFetch).toHaveBeenCalledWith(
239
+ expect.stringContaining('/delta/proposal?'),
240
+ expect.objectContaining({ method: 'GET' })
241
+ );
242
+ });
243
+ });
244
+
245
+ describe('getDeltaProposal', () => {
246
+ it('should get a single delta proposal by commitment', async () => {
247
+ client.setSigner(mockSigner);
248
+
249
+ const serverProposal = {
250
+ account_id: '0x' + 'a'.repeat(30),
251
+ nonce: 1,
252
+ prev_commitment: '0x' + 'b'.repeat(64),
253
+ delta_payload: {
254
+ tx_summary: { data: 'base64summary' },
255
+ signatures: [],
256
+ metadata: { proposal_type: 'change_threshold' as const, target_threshold: 2, signer_commitments: [] },
257
+ },
258
+ status: {
259
+ status: 'pending' as const,
260
+ timestamp: '2024-01-01T00:00:00Z',
261
+ proposer_id: '0x' + 'c'.repeat(64),
262
+ cosigner_sigs: [],
263
+ },
264
+ };
265
+
266
+ mockFetch.mockResolvedValueOnce({
267
+ ok: true,
268
+ json: async () => serverProposal,
269
+ });
270
+
271
+ const accountId = '0x' + 'a'.repeat(30);
272
+ const commitment = '0x' + 'd'.repeat(64);
273
+ const proposal = await client.getDeltaProposal(accountId, commitment);
274
+
275
+ expect(proposal.accountId).toBe(accountId);
276
+ expect(proposal.nonce).toBe(1);
277
+ expect(mockFetch).toHaveBeenCalledWith(
278
+ expect.stringContaining('/delta/proposal/single?'),
279
+ expect.objectContaining({ method: 'GET' }),
280
+ );
281
+ });
282
+ });
283
+
284
+ describe('pushDeltaProposal', () => {
285
+ it('should push a new delta proposal', async () => {
286
+ client.setSigner(mockSigner);
287
+
288
+ // Server returns snake_case
289
+ const serverResponse = {
290
+ delta: {
291
+ account_id: '0x' + 'a'.repeat(30),
292
+ nonce: 1,
293
+ prev_commitment: '0x' + 'b'.repeat(64),
294
+ delta_payload: {
295
+ tx_summary: { data: 'base64summary' },
296
+ signatures: [],
297
+ },
298
+ status: {
299
+ status: 'pending',
300
+ timestamp: '2024-01-01T00:00:00Z',
301
+ proposer_id: '0x' + 'c'.repeat(64),
302
+ cosigner_sigs: [],
303
+ },
304
+ },
305
+ commitment: '0x' + 'd'.repeat(64),
306
+ };
307
+
308
+ mockFetch.mockResolvedValueOnce({
309
+ ok: true,
310
+ json: async () => serverResponse,
311
+ });
312
+
313
+ // Client API uses camelCase
314
+ const request = {
315
+ accountId: '0x' + 'a'.repeat(30),
316
+ nonce: 1,
317
+ deltaPayload: {
318
+ txSummary: { data: 'base64summary' },
319
+ signatures: [],
320
+ },
321
+ };
322
+
323
+ const result = await client.pushDeltaProposal(request);
324
+
325
+ // Client returns camelCase
326
+ const expectedResponse: DeltaProposalResponse = {
327
+ delta: {
328
+ accountId: '0x' + 'a'.repeat(30),
329
+ nonce: 1,
330
+ prevCommitment: '0x' + 'b'.repeat(64),
331
+ newCommitment: undefined,
332
+ deltaPayload: {
333
+ txSummary: { data: 'base64summary' },
334
+ signatures: [],
335
+ metadata: undefined,
336
+ },
337
+ ackSig: undefined,
338
+ status: {
339
+ status: 'pending',
340
+ timestamp: '2024-01-01T00:00:00Z',
341
+ proposerId: '0x' + 'c'.repeat(64),
342
+ cosignerSigs: [],
343
+ },
344
+ },
345
+ commitment: '0x' + 'd'.repeat(64),
346
+ };
347
+
348
+ expect(result).toEqual(expectedResponse);
349
+
350
+ // Wire format is snake_case
351
+ expect(mockFetch).toHaveBeenCalledWith(
352
+ 'http://localhost:3000/delta/proposal',
353
+ expect.objectContaining({
354
+ method: 'POST',
355
+ body: JSON.stringify({
356
+ account_id: '0x' + 'a'.repeat(30),
357
+ nonce: 1,
358
+ delta_payload: {
359
+ tx_summary: { data: 'base64summary' },
360
+ signatures: [],
361
+ },
362
+ }),
363
+ })
364
+ );
365
+ });
366
+ });
367
+
368
+ describe('signDeltaProposal', () => {
369
+ it('should sign a delta proposal', async () => {
370
+ client.setSigner(mockSigner);
371
+
372
+ // Server returns snake_case
373
+ const serverDelta = {
374
+ account_id: '0x' + 'a'.repeat(30),
375
+ nonce: 1,
376
+ prev_commitment: '0x' + 'b'.repeat(64),
377
+ delta_payload: {
378
+ tx_summary: { data: 'base64summary' },
379
+ signatures: [{ signer_id: '0x' + 'c'.repeat(64), signature: { scheme: 'falcon', signature: '0x' + 'd'.repeat(128) } }],
380
+ },
381
+ status: {
382
+ status: 'pending',
383
+ timestamp: '2024-01-01T00:00:00Z',
384
+ proposer_id: '0x' + 'c'.repeat(64),
385
+ cosigner_sigs: [
386
+ {
387
+ signer_id: '0x' + 'c'.repeat(64),
388
+ signature: { scheme: 'falcon', signature: '0x' + 'd'.repeat(128) },
389
+ timestamp: '2024-01-01T00:00:00Z',
390
+ },
391
+ ],
392
+ },
393
+ };
394
+
395
+ mockFetch.mockResolvedValueOnce({
396
+ ok: true,
397
+ json: async () => serverDelta,
398
+ });
399
+
400
+ // Client API uses camelCase
401
+ const request = {
402
+ accountId: '0x' + 'a'.repeat(30),
403
+ commitment: '0x' + 'e'.repeat(64),
404
+ signature: { scheme: 'falcon' as const, signature: '0x' + 'd'.repeat(128) },
405
+ };
406
+
407
+ const result = await client.signDeltaProposal(request);
408
+
409
+ // Client returns camelCase
410
+ const expectedDelta: DeltaObject = {
411
+ accountId: '0x' + 'a'.repeat(30),
412
+ nonce: 1,
413
+ prevCommitment: '0x' + 'b'.repeat(64),
414
+ newCommitment: undefined,
415
+ deltaPayload: {
416
+ txSummary: { data: 'base64summary' },
417
+ signatures: [{ signerId: '0x' + 'c'.repeat(64), signature: { scheme: 'falcon', signature: '0x' + 'd'.repeat(128) } }],
418
+ metadata: undefined,
419
+ },
420
+ ackSig: undefined,
421
+ status: {
422
+ status: 'pending',
423
+ timestamp: '2024-01-01T00:00:00Z',
424
+ proposerId: '0x' + 'c'.repeat(64),
425
+ cosignerSigs: [
426
+ {
427
+ signerId: '0x' + 'c'.repeat(64),
428
+ signature: { scheme: 'falcon', signature: '0x' + 'd'.repeat(128) },
429
+ timestamp: '2024-01-01T00:00:00Z',
430
+ },
431
+ ],
432
+ },
433
+ };
434
+
435
+ expect(result).toEqual(expectedDelta);
436
+
437
+ // Wire format is snake_case
438
+ expect(mockFetch).toHaveBeenCalledWith(
439
+ 'http://localhost:3000/delta/proposal',
440
+ expect.objectContaining({
441
+ method: 'PUT',
442
+ body: JSON.stringify({
443
+ account_id: '0x' + 'a'.repeat(30),
444
+ commitment: '0x' + 'e'.repeat(64),
445
+ signature: { scheme: 'falcon', signature: '0x' + 'd'.repeat(128) },
446
+ }),
447
+ })
448
+ );
449
+ });
450
+ });
451
+
452
+ describe('pushDelta', () => {
453
+ it('should push a delta for execution and return ack signature', async () => {
454
+ client.setSigner(mockSigner);
455
+
456
+ // Server returns snake_case - execution delta response has raw delta_payload
457
+ const serverResponse = {
458
+ account_id: '0x' + 'a'.repeat(30),
459
+ nonce: 1,
460
+ prev_commitment: '0x' + 'b'.repeat(64),
461
+ new_commitment: '0x' + 'd'.repeat(64),
462
+ delta_payload: { data: 'base64summary' },
463
+ ack_sig: '0x' + 'f'.repeat(128),
464
+ status: {
465
+ status: 'candidate',
466
+ timestamp: '2024-01-01T00:00:00Z',
467
+ },
468
+ };
469
+
470
+ mockFetch.mockResolvedValueOnce({
471
+ ok: true,
472
+ json: async () => serverResponse,
473
+ });
474
+
475
+ // Client API uses camelCase
476
+ const executionDelta = {
477
+ accountId: '0x' + 'a'.repeat(30),
478
+ nonce: 1,
479
+ prevCommitment: '0x' + 'b'.repeat(64),
480
+ deltaPayload: { data: 'base64summary' },
481
+ status: {
482
+ status: 'pending' as const,
483
+ timestamp: '2024-01-01T00:00:00Z',
484
+ proposerId: '0x' + 'c'.repeat(64),
485
+ cosignerSigs: [],
486
+ },
487
+ };
488
+
489
+ const result = await client.pushDelta(executionDelta);
490
+
491
+ // PushDeltaResponse only includes essential fields for execution
492
+ expect(result.accountId).toBe('0x' + 'a'.repeat(30));
493
+ expect(result.nonce).toBe(1);
494
+ expect(result.newCommitment).toBe('0x' + 'd'.repeat(64));
495
+ expect(result.ackSig).toBe('0x' + 'f'.repeat(128));
496
+
497
+ expect(mockFetch).toHaveBeenCalledWith(
498
+ 'http://localhost:3000/delta',
499
+ expect.objectContaining({
500
+ method: 'POST',
501
+ })
502
+ );
503
+ });
504
+ });
505
+
506
+ describe('getDelta', () => {
507
+ it('should get a specific delta by nonce', async () => {
508
+ client.setSigner(mockSigner);
509
+
510
+ // Server returns snake_case
511
+ const serverDelta = {
512
+ account_id: '0x' + 'a'.repeat(30),
513
+ nonce: 5,
514
+ prev_commitment: '0x' + 'b'.repeat(64),
515
+ delta_payload: {
516
+ tx_summary: { data: 'base64summary' },
517
+ signatures: [],
518
+ },
519
+ status: {
520
+ status: 'canonical',
521
+ timestamp: '2024-01-01T00:00:00Z',
522
+ },
523
+ };
524
+
525
+ mockFetch.mockResolvedValueOnce({
526
+ ok: true,
527
+ json: async () => serverDelta,
528
+ });
529
+
530
+ const result = await client.getDelta('0x' + 'a'.repeat(30), 5);
531
+
532
+ // Client returns camelCase
533
+ expect(result.accountId).toBe('0x' + 'a'.repeat(30));
534
+ expect(result.nonce).toBe(5);
535
+ expect(result.prevCommitment).toBe('0x' + 'b'.repeat(64));
536
+ expect(result.status.status).toBe('canonical');
537
+
538
+ expect(mockFetch).toHaveBeenCalledWith(
539
+ expect.stringContaining('/delta?'),
540
+ expect.objectContaining({ method: 'GET' })
541
+ );
542
+ });
543
+ });
544
+
545
+ describe('getDeltaSince', () => {
546
+ it('should get merged delta since a nonce', async () => {
547
+ client.setSigner(mockSigner);
548
+
549
+ // Server returns snake_case
550
+ const serverDelta = {
551
+ account_id: '0x' + 'a'.repeat(30),
552
+ nonce: 10,
553
+ prev_commitment: '0x' + 'b'.repeat(64),
554
+ delta_payload: {
555
+ tx_summary: { data: 'base64mergeddata' },
556
+ signatures: [],
557
+ },
558
+ status: {
559
+ status: 'canonical',
560
+ timestamp: '2024-01-01T00:00:00Z',
561
+ },
562
+ };
563
+
564
+ mockFetch.mockResolvedValueOnce({
565
+ ok: true,
566
+ json: async () => serverDelta,
567
+ });
568
+
569
+ const result = await client.getDeltaSince('0x' + 'a'.repeat(30), 5);
570
+
571
+ // Client returns camelCase
572
+ expect(result.accountId).toBe('0x' + 'a'.repeat(30));
573
+ expect(result.nonce).toBe(10);
574
+ expect(result.deltaPayload.txSummary.data).toBe('base64mergeddata');
575
+
576
+ expect(mockFetch).toHaveBeenCalledWith(
577
+ expect.stringContaining('/delta/since?'),
578
+ expect.objectContaining({ method: 'GET' })
579
+ );
580
+ });
581
+ });
582
+ });
583
+
584
+ describe('GuardianHttpError', () => {
585
+ it('should create error with status, statusText, and body', () => {
586
+ const error = new GuardianHttpError(404, 'Not Found', 'Resource not found');
587
+
588
+ expect(error).toBeInstanceOf(Error);
589
+ expect(error).toBeInstanceOf(GuardianHttpError);
590
+ expect(error.status).toBe(404);
591
+ expect(error.statusText).toBe('Not Found');
592
+ expect(error.body).toBe('Resource not found');
593
+ expect(error.message).toContain('404');
594
+ expect(error.message).toContain('Not Found');
595
+ expect(error.name).toBe('GuardianHttpError');
596
+ });
597
+ });