@zap-studio/fetch 0.1.0 → 0.1.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.
@@ -1,974 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
- import { z } from "zod";
3
- import { api, safeFetch } from "../src";
4
- import { FetchError } from "../src/errors";
5
-
6
- describe("safeFetch", () => {
7
- let fetchMock: ReturnType<typeof vi.fn>;
8
-
9
- beforeEach(() => {
10
- fetchMock = vi.fn();
11
- global.fetch = fetchMock;
12
- });
13
-
14
- afterEach(() => {
15
- vi.restoreAllMocks();
16
- });
17
-
18
- describe("successful requests", () => {
19
- it("should fetch and validate JSON data", async () => {
20
- const schema = z.object({
21
- id: z.number(),
22
- name: z.string(),
23
- });
24
-
25
- const mockData = { id: 1, name: "Test User" };
26
-
27
- fetchMock.mockResolvedValue({
28
- ok: true,
29
- status: 200,
30
- statusText: "OK",
31
- headers: new Headers({ "content-type": "application/json" }),
32
- json: async () => mockData,
33
- });
34
-
35
- const result = await safeFetch("https://api.example.com/user", schema);
36
-
37
- expect(result).toEqual(mockData);
38
- expect(fetchMock).toHaveBeenCalledWith("https://api.example.com/user", {
39
- body: null,
40
- headers: undefined,
41
- });
42
- });
43
-
44
- it("should handle GET requests", async () => {
45
- const schema = z.object({ success: z.boolean() });
46
- const mockData = { success: true };
47
-
48
- fetchMock.mockResolvedValue({
49
- ok: true,
50
- status: 200,
51
- statusText: "OK",
52
- headers: new Headers({ "content-type": "application/json" }),
53
- json: async () => mockData,
54
- });
55
-
56
- const result = await safeFetch("https://api.example.com/data", schema, {
57
- method: "GET",
58
- });
59
-
60
- expect(result).toEqual(mockData);
61
- expect(fetchMock).toHaveBeenCalledWith(
62
- "https://api.example.com/data",
63
- expect.objectContaining({
64
- method: "GET",
65
- }),
66
- );
67
- });
68
-
69
- it("should handle POST requests with JSON body", async () => {
70
- const schema = z.object({ id: z.number() });
71
- const mockData = { id: 123 };
72
- const requestBody = { name: "New Item" };
73
-
74
- fetchMock.mockResolvedValue({
75
- ok: true,
76
- status: 201,
77
- statusText: "Created",
78
- headers: new Headers({ "content-type": "application/json" }),
79
- json: async () => mockData,
80
- });
81
-
82
- const result = await safeFetch("https://api.example.com/items", schema, {
83
- method: "POST",
84
- body: requestBody,
85
- });
86
-
87
- expect(result).toEqual(mockData);
88
- expect(fetchMock).toHaveBeenCalledWith(
89
- "https://api.example.com/items",
90
- expect.objectContaining({
91
- method: "POST",
92
- body: JSON.stringify(requestBody),
93
- headers: {
94
- "Content-Type": "application/json",
95
- },
96
- }),
97
- );
98
- });
99
-
100
- it("should handle FormData body", async () => {
101
- const schema = z.object({ uploaded: z.boolean() });
102
- const mockData = { uploaded: true };
103
- const formData = new FormData();
104
- formData.append("file", new Blob(["test"]), "test.txt");
105
-
106
- fetchMock.mockResolvedValue({
107
- ok: true,
108
- status: 200,
109
- statusText: "OK",
110
- headers: new Headers({ "content-type": "application/json" }),
111
- json: async () => mockData,
112
- });
113
-
114
- const result = await safeFetch("https://api.example.com/upload", schema, {
115
- method: "POST",
116
- body: formData,
117
- });
118
-
119
- expect(result).toEqual(mockData);
120
- expect(fetchMock).toHaveBeenCalledWith(
121
- "https://api.example.com/upload",
122
- expect.objectContaining({
123
- method: "POST",
124
- body: formData,
125
- headers: undefined,
126
- }),
127
- );
128
- });
129
- it("should handle string body", async () => {
130
- const schema = z.object({ received: z.string() });
131
- const mockData = { received: "text data" };
132
- const textBody = "plain text content";
133
-
134
- fetchMock.mockResolvedValue({
135
- ok: true,
136
- status: 200,
137
- statusText: "OK",
138
- headers: new Headers({ "content-type": "application/json" }),
139
- json: async () => mockData,
140
- });
141
-
142
- const result = await safeFetch("https://api.example.com/text", schema, {
143
- method: "POST",
144
- body: textBody,
145
- });
146
-
147
- expect(result).toEqual(mockData);
148
- expect(fetchMock).toHaveBeenCalledWith(
149
- "https://api.example.com/text",
150
- expect.objectContaining({
151
- method: "POST",
152
- body: textBody,
153
- headers: undefined,
154
- }),
155
- );
156
- });
157
- it("should respect custom Content-Type header", async () => {
158
- const schema = z.object({ success: z.boolean() });
159
- const mockData = { success: true };
160
-
161
- fetchMock.mockResolvedValue({
162
- ok: true,
163
- status: 200,
164
- statusText: "OK",
165
- headers: new Headers({ "content-type": "application/json" }),
166
- json: async () => mockData,
167
- });
168
-
169
- await safeFetch("https://api.example.com/custom", schema, {
170
- method: "POST",
171
- body: { data: "test" },
172
- headers: {
173
- "Content-Type": "application/xml",
174
- },
175
- });
176
-
177
- expect(fetchMock).toHaveBeenCalledWith(
178
- "https://api.example.com/custom",
179
- expect.objectContaining({
180
- headers: {
181
- "Content-Type": "application/xml",
182
- },
183
- }),
184
- );
185
- });
186
- });
187
-
188
- describe("response types", () => {
189
- describe("json", () => {
190
- it("should handle JSON responses with correct content-type", async () => {
191
- const schema = z.object({ id: z.number(), name: z.string() });
192
- const mockData = { id: 1, name: "Test" };
193
-
194
- fetchMock.mockResolvedValue({
195
- ok: true,
196
- status: 200,
197
- statusText: "OK",
198
- headers: new Headers({ "content-type": "application/json" }),
199
- json: async () => mockData,
200
- });
201
-
202
- const result = await safeFetch("https://api.example.com/data", schema, {
203
- responseType: "json",
204
- });
205
-
206
- expect(result).toEqual(mockData);
207
- });
208
-
209
- it("should default to JSON response type when not specified", async () => {
210
- const schema = z.object({ id: z.number() });
211
- const mockData = { id: 1 };
212
-
213
- fetchMock.mockResolvedValue({
214
- ok: true,
215
- status: 200,
216
- statusText: "OK",
217
- headers: new Headers({ "content-type": "application/json" }),
218
- json: async () => mockData,
219
- });
220
-
221
- const result = await safeFetch("https://api.example.com/data", schema);
222
-
223
- expect(result).toEqual(mockData);
224
- });
225
-
226
- it("should throw FetchError when expecting JSON but content-type is missing", async () => {
227
- const schema = z.object({ id: z.number() });
228
-
229
- fetchMock.mockResolvedValue({
230
- ok: true,
231
- status: 200,
232
- statusText: "OK",
233
- headers: new Headers(),
234
- json: async () => ({ id: 1 }),
235
- });
236
-
237
- await expect(
238
- safeFetch("https://api.example.com/data", schema, {
239
- responseType: "json",
240
- }),
241
- ).rejects.toThrow(FetchError);
242
- });
243
-
244
- it("should throw FetchError when expecting JSON but content-type is wrong", async () => {
245
- const schema = z.object({ id: z.number() });
246
-
247
- fetchMock.mockResolvedValue({
248
- ok: true,
249
- status: 200,
250
- statusText: "OK",
251
- headers: new Headers({ "content-type": "text/html" }),
252
- json: async () => ({ id: 1 }),
253
- });
254
-
255
- await expect(
256
- safeFetch("https://api.example.com/data", schema, {
257
- responseType: "json",
258
- }),
259
- ).rejects.toThrow(FetchError);
260
- });
261
- });
262
-
263
- describe("text", () => {
264
- it("should handle text responses with correct content-type", async () => {
265
- const schema = z.string();
266
- const mockText = "Plain text response";
267
-
268
- fetchMock.mockResolvedValue({
269
- ok: true,
270
- status: 200,
271
- statusText: "OK",
272
- headers: new Headers({ "content-type": "text/plain" }),
273
- text: async () => mockText,
274
- });
275
-
276
- const result = await safeFetch("https://api.example.com/text", schema, {
277
- responseType: "text",
278
- });
279
-
280
- expect(result).toBe(mockText);
281
- });
282
-
283
- it("should handle text/html content-type", async () => {
284
- const schema = z.string();
285
- const mockHtml = "<html><body>Hello</body></html>";
286
-
287
- fetchMock.mockResolvedValue({
288
- ok: true,
289
- status: 200,
290
- statusText: "OK",
291
- headers: new Headers({ "content-type": "text/html" }),
292
- text: async () => mockHtml,
293
- });
294
-
295
- const result = await safeFetch("https://api.example.com/page", schema, {
296
- responseType: "text",
297
- });
298
-
299
- expect(result).toBe(mockHtml);
300
- });
301
-
302
- it("should throw FetchError when expecting text but content-type is wrong", async () => {
303
- const schema = z.string();
304
-
305
- fetchMock.mockResolvedValue({
306
- ok: true,
307
- status: 200,
308
- statusText: "OK",
309
- headers: new Headers({ "content-type": "application/json" }),
310
- text: async () => "text",
311
- });
312
-
313
- await expect(
314
- safeFetch("https://api.example.com/text", schema, {
315
- responseType: "text",
316
- }),
317
- ).rejects.toThrow(FetchError);
318
- });
319
- });
320
-
321
- describe("blob", () => {
322
- it("should handle blob responses", async () => {
323
- const schema = z.instanceof(Blob);
324
- const mockBlob = new Blob(["test content"], { type: "text/plain" });
325
-
326
- fetchMock.mockResolvedValue({
327
- ok: true,
328
- status: 200,
329
- statusText: "OK",
330
- headers: new Headers(),
331
- blob: async () => mockBlob,
332
- });
333
-
334
- const result = await safeFetch("https://api.example.com/file", schema, {
335
- responseType: "blob",
336
- });
337
-
338
- expect(result).toBe(mockBlob);
339
- expect(result).toBeInstanceOf(Blob);
340
- });
341
-
342
- it("should handle binary blob responses", async () => {
343
- const schema = z.instanceof(Blob);
344
- const mockBlob = new Blob([new Uint8Array([1, 2, 3, 4])], {
345
- type: "application/octet-stream",
346
- });
347
-
348
- fetchMock.mockResolvedValue({
349
- ok: true,
350
- status: 200,
351
- statusText: "OK",
352
- headers: new Headers(),
353
- blob: async () => mockBlob,
354
- });
355
-
356
- const result = await safeFetch(
357
- "https://api.example.com/binary",
358
- schema,
359
- {
360
- responseType: "blob",
361
- },
362
- );
363
-
364
- expect(result).toBe(mockBlob);
365
- });
366
- });
367
-
368
- describe("arrayBuffer", () => {
369
- it("should handle arrayBuffer responses", async () => {
370
- const schema = z.instanceof(ArrayBuffer);
371
- const mockBuffer = new ArrayBuffer(8);
372
-
373
- fetchMock.mockResolvedValue({
374
- ok: true,
375
- status: 200,
376
- statusText: "OK",
377
- headers: new Headers(),
378
- arrayBuffer: async () => mockBuffer,
379
- });
380
-
381
- const result = await safeFetch(
382
- "https://api.example.com/binary",
383
- schema,
384
- {
385
- responseType: "arrayBuffer",
386
- },
387
- );
388
-
389
- expect(result).toBe(mockBuffer);
390
- expect(result).toBeInstanceOf(ArrayBuffer);
391
- });
392
-
393
- it("should handle arrayBuffer with data", async () => {
394
- const schema = z.instanceof(ArrayBuffer);
395
- const view = new Uint8Array([1, 2, 3, 4, 5]);
396
- const mockBuffer = view.buffer;
397
-
398
- fetchMock.mockResolvedValue({
399
- ok: true,
400
- status: 200,
401
- statusText: "OK",
402
- headers: new Headers(),
403
- arrayBuffer: async () => mockBuffer,
404
- });
405
-
406
- const result = await safeFetch("https://api.example.com/data", schema, {
407
- responseType: "arrayBuffer",
408
- });
409
-
410
- expect(result).toBe(mockBuffer);
411
- expect(new Uint8Array(result as ArrayBuffer)).toEqual(view);
412
- });
413
- });
414
-
415
- describe("bytes", () => {
416
- it("should handle bytes (Uint8Array) responses", async () => {
417
- const schema = z.instanceof(Uint8Array);
418
- const mockBuffer = new Uint8Array([1, 2, 3, 4]).buffer;
419
-
420
- fetchMock.mockResolvedValue({
421
- ok: true,
422
- status: 200,
423
- statusText: "OK",
424
- headers: new Headers(),
425
- arrayBuffer: async () => mockBuffer,
426
- });
427
-
428
- const result = await safeFetch(
429
- "https://api.example.com/bytes",
430
- schema,
431
- {
432
- responseType: "bytes",
433
- },
434
- );
435
-
436
- expect(result).toBeInstanceOf(Uint8Array);
437
- expect(result).toEqual(new Uint8Array([1, 2, 3, 4]));
438
- });
439
-
440
- it("should convert ArrayBuffer to Uint8Array", async () => {
441
- const schema = z.instanceof(Uint8Array);
442
- const data = [10, 20, 30, 40, 50];
443
- const mockBuffer = new Uint8Array(data).buffer;
444
-
445
- fetchMock.mockResolvedValue({
446
- ok: true,
447
- status: 200,
448
- statusText: "OK",
449
- headers: new Headers(),
450
- arrayBuffer: async () => mockBuffer,
451
- });
452
-
453
- const result = await safeFetch(
454
- "https://api.example.com/bytes",
455
- schema,
456
- {
457
- responseType: "bytes",
458
- },
459
- );
460
-
461
- expect(Array.from(result as Uint8Array)).toEqual(data);
462
- });
463
- });
464
-
465
- describe("formData", () => {
466
- it("should handle formData responses with correct content-type", async () => {
467
- const schema = z.instanceof(FormData);
468
- const mockFormData = new FormData();
469
- mockFormData.append("key", "value");
470
- mockFormData.append("name", "test");
471
-
472
- fetchMock.mockResolvedValue({
473
- ok: true,
474
- status: 200,
475
- statusText: "OK",
476
- headers: new Headers({ "content-type": "multipart/form-data" }),
477
- formData: async () => mockFormData,
478
- });
479
-
480
- const result = await safeFetch("https://api.example.com/form", schema, {
481
- responseType: "formData",
482
- });
483
-
484
- expect(result).toBe(mockFormData);
485
- expect(result).toBeInstanceOf(FormData);
486
- });
487
-
488
- it("should throw FetchError when expecting formData but content-type is wrong", async () => {
489
- const schema = z.instanceof(FormData);
490
- const mockFormData = new FormData();
491
-
492
- fetchMock.mockResolvedValue({
493
- ok: true,
494
- status: 200,
495
- statusText: "OK",
496
- headers: new Headers({ "content-type": "application/json" }),
497
- formData: async () => mockFormData,
498
- });
499
-
500
- await expect(
501
- safeFetch("https://api.example.com/form", schema, {
502
- responseType: "formData",
503
- }),
504
- ).rejects.toThrow(FetchError);
505
- });
506
- });
507
-
508
- describe("clone", () => {
509
- it("should handle clone response type", async () => {
510
- const mockResponse = new Response(JSON.stringify({ id: 1 }), {
511
- status: 200,
512
- statusText: "OK",
513
- headers: new Headers({ "content-type": "application/json" }),
514
- });
515
-
516
- const mockClone = mockResponse.clone();
517
-
518
- fetchMock.mockResolvedValue({
519
- ok: true,
520
- status: 200,
521
- statusText: "OK",
522
- headers: new Headers(),
523
- clone: () => mockClone,
524
- });
525
-
526
- const schema = z.instanceof(Response);
527
- const result = await safeFetch("https://api.example.com/data", schema, {
528
- responseType: "clone",
529
- });
530
-
531
- expect(result).toBe(mockClone);
532
- expect(result).toBeInstanceOf(Response);
533
- });
534
-
535
- it("should allow reading cloned response multiple times", async () => {
536
- const mockData = { id: 1, name: "Test" };
537
- const mockResponse = new Response(JSON.stringify(mockData), {
538
- status: 200,
539
- statusText: "OK",
540
- headers: new Headers({ "content-type": "application/json" }),
541
- });
542
-
543
- const mockClone = mockResponse.clone();
544
-
545
- fetchMock.mockResolvedValue({
546
- ok: true,
547
- status: 200,
548
- statusText: "OK",
549
- headers: new Headers(),
550
- clone: () => mockClone,
551
- });
552
-
553
- const schema = z.instanceof(Response);
554
- const result = await safeFetch("https://api.example.com/data", schema, {
555
- responseType: "clone",
556
- });
557
-
558
- // Should be able to read the response
559
- const data = await (result as Response).json();
560
- expect(data).toEqual(mockData);
561
- });
562
- });
563
-
564
- describe("unsupported response type", () => {
565
- it("should throw FetchError for unsupported response type", async () => {
566
- const schema = z.unknown();
567
-
568
- fetchMock.mockResolvedValue({
569
- ok: true,
570
- status: 200,
571
- statusText: "OK",
572
- headers: new Headers(),
573
- });
574
-
575
- await expect(
576
- safeFetch("https://api.example.com/data", schema, {
577
- // @ts-expect-error - Testing invalid response type
578
- responseType: "invalid",
579
- }),
580
- ).rejects.toThrow(FetchError);
581
- });
582
- });
583
- });
584
-
585
- describe("validation", () => {
586
- it("should throw on validation error when throwOnValidationError is true", async () => {
587
- const schema = z.object({
588
- id: z.number(),
589
- email: z.email(),
590
- });
591
-
592
- const invalidData = { id: 1, email: "not-an-email" };
593
-
594
- fetchMock.mockResolvedValue({
595
- ok: true,
596
- status: 200,
597
- statusText: "OK",
598
- headers: new Headers({ "content-type": "application/json" }),
599
- json: async () => invalidData,
600
- });
601
-
602
- await expect(
603
- safeFetch("https://api.example.com/user", schema, {
604
- throwOnValidationError: true,
605
- }),
606
- ).rejects.toThrow();
607
- });
608
-
609
- it("should return safe parse result when throwOnValidationError is false", async () => {
610
- const schema = z.object({
611
- id: z.number(),
612
- email: z.email(),
613
- });
614
-
615
- const invalidData = { id: 1, email: "not-an-email" };
616
-
617
- fetchMock.mockResolvedValue({
618
- ok: true,
619
- status: 200,
620
- statusText: "OK",
621
- headers: new Headers({ "content-type": "application/json" }),
622
- json: async () => invalidData,
623
- });
624
-
625
- const result = await safeFetch("https://api.example.com/user", schema, {
626
- throwOnValidationError: false,
627
- });
628
-
629
- expect(result).toHaveProperty("success");
630
- if ("success" in result && result.success) {
631
- // This block should not be executed for invalid data
632
- }
633
- });
634
-
635
- it("should return successful parse result when data is valid and throwOnValidationError is false", async () => {
636
- const schema = z.object({
637
- id: z.number(),
638
- email: z.email(),
639
- });
640
-
641
- const validData = { id: 1, email: "test@example.com" };
642
-
643
- fetchMock.mockResolvedValue({
644
- ok: true,
645
- status: 200,
646
- statusText: "OK",
647
- headers: new Headers({ "content-type": "application/json" }),
648
- json: async () => validData,
649
- });
650
-
651
- const result = await safeFetch("https://api.example.com/user", schema, {
652
- throwOnValidationError: false,
653
- });
654
-
655
- expect(result).toHaveProperty("success");
656
- if ("success" in result && result.success) {
657
- expect(result.data).toEqual(validData);
658
- }
659
- });
660
- });
661
-
662
- describe("error handling", () => {
663
- it("should throw FetchError on HTTP error status", async () => {
664
- fetchMock.mockResolvedValue({
665
- ok: false,
666
- status: 404,
667
- statusText: "Not Found",
668
- });
669
-
670
- const schema = z.object({ id: z.number() });
671
-
672
- await expect(
673
- safeFetch("https://api.example.com/notfound", schema),
674
- ).rejects.toThrow(FetchError);
675
-
676
- try {
677
- await safeFetch("https://api.example.com/notfound", schema);
678
- } catch (error) {
679
- expect(error).toBeInstanceOf(FetchError);
680
- if (error instanceof FetchError) {
681
- expect(error.status).toBe(404);
682
- expect(error.statusText).toBe("Not Found");
683
- expect(error.message).toContain("404");
684
- }
685
- }
686
- });
687
-
688
- it("should throw FetchError on 500 server error", async () => {
689
- fetchMock.mockResolvedValue({
690
- ok: false,
691
- status: 500,
692
- statusText: "Internal Server Error",
693
- });
694
-
695
- const schema = z.object({ id: z.number() });
696
-
697
- await expect(
698
- safeFetch("https://api.example.com/error", schema),
699
- ).rejects.toThrow(FetchError);
700
- });
701
-
702
- it("should throw FetchError when expecting JSON but receiving different content type", async () => {
703
- fetchMock.mockResolvedValue({
704
- ok: true,
705
- status: 200,
706
- statusText: "OK",
707
- headers: new Headers({ "content-type": "text/html" }),
708
- });
709
-
710
- const schema = z.object({ id: z.number() });
711
-
712
- await expect(
713
- safeFetch("https://api.example.com/html", schema, {
714
- responseType: "json",
715
- }),
716
- ).rejects.toThrow(FetchError);
717
- });
718
- });
719
-
720
- describe("headers", () => {
721
- it("should pass custom headers to fetch", async () => {
722
- const schema = z.object({ success: z.boolean() });
723
- const mockData = { success: true };
724
-
725
- fetchMock.mockResolvedValue({
726
- ok: true,
727
- status: 200,
728
- statusText: "OK",
729
- headers: new Headers({ "content-type": "application/json" }),
730
- json: async () => mockData,
731
- });
732
-
733
- await safeFetch("https://api.example.com/auth", schema, {
734
- headers: {
735
- Authorization: "Bearer token123",
736
- "X-Custom-Header": "custom-value",
737
- },
738
- });
739
-
740
- expect(fetchMock).toHaveBeenCalledWith(
741
- "https://api.example.com/auth",
742
- expect.objectContaining({
743
- headers: {
744
- Authorization: "Bearer token123",
745
- "X-Custom-Header": "custom-value",
746
- },
747
- }),
748
- );
749
- });
750
- });
751
- });
752
-
753
- describe("api convenience methods", () => {
754
- let fetchMock: ReturnType<typeof vi.fn>;
755
-
756
- beforeEach(() => {
757
- fetchMock = vi.fn();
758
- global.fetch = fetchMock;
759
- });
760
-
761
- afterEach(() => {
762
- vi.restoreAllMocks();
763
- });
764
-
765
- it("should make GET request with api.get", async () => {
766
- const schema = z.object({ id: z.number() });
767
- const mockData = { id: 1 };
768
-
769
- fetchMock.mockResolvedValue({
770
- ok: true,
771
- status: 200,
772
- statusText: "OK",
773
- headers: new Headers({ "content-type": "application/json" }),
774
- json: async () => mockData,
775
- });
776
-
777
- const result = await api.get("https://api.example.com/item", schema);
778
-
779
- expect(result).toEqual(mockData);
780
- expect(fetchMock).toHaveBeenCalledWith(
781
- "https://api.example.com/item",
782
- expect.objectContaining({
783
- method: "GET",
784
- }),
785
- );
786
- });
787
-
788
- it("should make POST request with api.post", async () => {
789
- const schema = z.object({ id: z.number() });
790
- const mockData = { id: 123 };
791
- const body = { name: "Test" };
792
-
793
- fetchMock.mockResolvedValue({
794
- ok: true,
795
- status: 201,
796
- statusText: "Created",
797
- headers: new Headers({ "content-type": "application/json" }),
798
- json: async () => mockData,
799
- });
800
-
801
- const result = await api.post(
802
- "https://api.example.com/items",
803
- schema,
804
- body,
805
- );
806
-
807
- expect(result).toEqual(mockData);
808
- expect(fetchMock).toHaveBeenCalledWith(
809
- "https://api.example.com/items",
810
- expect.objectContaining({
811
- method: "POST",
812
- body: JSON.stringify(body),
813
- headers: {
814
- "Content-Type": "application/json",
815
- },
816
- }),
817
- );
818
- });
819
-
820
- it("should make PUT request with api.put", async () => {
821
- const schema = z.object({ id: z.number() });
822
- const mockData = { id: 123 };
823
- const body = { name: "Updated" };
824
-
825
- fetchMock.mockResolvedValue({
826
- ok: true,
827
- status: 200,
828
- statusText: "OK",
829
- headers: new Headers({ "content-type": "application/json" }),
830
- json: async () => mockData,
831
- });
832
-
833
- const result = await api.put(
834
- "https://api.example.com/items/123",
835
- schema,
836
- body,
837
- );
838
-
839
- expect(result).toEqual(mockData);
840
- expect(fetchMock).toHaveBeenCalledWith(
841
- "https://api.example.com/items/123",
842
- expect.objectContaining({
843
- method: "PUT",
844
- body: JSON.stringify(body),
845
- }),
846
- );
847
- });
848
-
849
- it("should make PATCH request with api.patch", async () => {
850
- const schema = z.object({ id: z.number() });
851
- const mockData = { id: 123 };
852
- const body = { name: "Patched" };
853
-
854
- fetchMock.mockResolvedValue({
855
- ok: true,
856
- status: 200,
857
- statusText: "OK",
858
- headers: new Headers({ "content-type": "application/json" }),
859
- json: async () => mockData,
860
- });
861
-
862
- const result = await api.patch(
863
- "https://api.example.com/items/123",
864
- schema,
865
- body,
866
- );
867
-
868
- expect(result).toEqual(mockData);
869
- expect(fetchMock).toHaveBeenCalledWith(
870
- "https://api.example.com/items/123",
871
- expect.objectContaining({
872
- method: "PATCH",
873
- body: JSON.stringify(body),
874
- }),
875
- );
876
- });
877
-
878
- it("should make DELETE request with api.delete", async () => {
879
- const schema = z.object({ success: z.boolean() });
880
- const mockData = { success: true };
881
-
882
- fetchMock.mockResolvedValue({
883
- ok: true,
884
- status: 200,
885
- statusText: "OK",
886
- headers: new Headers({ "content-type": "application/json" }),
887
- json: async () => mockData,
888
- });
889
-
890
- const result = await api.delete(
891
- "https://api.example.com/items/123",
892
- schema,
893
- );
894
-
895
- expect(result).toEqual(mockData);
896
- expect(fetchMock).toHaveBeenCalledWith(
897
- "https://api.example.com/items/123",
898
- expect.objectContaining({
899
- method: "DELETE",
900
- }),
901
- );
902
- });
903
-
904
- it("should pass additional config to api methods", async () => {
905
- const schema = z.object({ id: z.number() });
906
- const mockData = { id: 1 };
907
-
908
- fetchMock.mockResolvedValue({
909
- ok: true,
910
- status: 200,
911
- statusText: "OK",
912
- headers: new Headers({ "content-type": "application/json" }),
913
- json: async () => mockData,
914
- });
915
-
916
- await api.get("https://api.example.com/item", schema, {
917
- headers: {
918
- Authorization: "Bearer token",
919
- },
920
- signal: new AbortController().signal,
921
- });
922
-
923
- expect(fetchMock).toHaveBeenCalledWith(
924
- "https://api.example.com/item",
925
- expect.objectContaining({
926
- method: "GET",
927
- headers: {
928
- Authorization: "Bearer token",
929
- },
930
- signal: expect.any(AbortSignal),
931
- }),
932
- );
933
- });
934
- });
935
-
936
- describe("FetchError", () => {
937
- it("should create FetchError with correct properties", () => {
938
- const mockResponse = new Response(null, {
939
- status: 404,
940
- statusText: "Not Found",
941
- });
942
-
943
- const error = new FetchError(
944
- "HTTP 404: Not Found",
945
- 404,
946
- "Not Found",
947
- mockResponse,
948
- );
949
-
950
- expect(error).toBeInstanceOf(Error);
951
- expect(error).toBeInstanceOf(FetchError);
952
- expect(error.name).toBe("FetchError");
953
- expect(error.message).toBe("HTTP 404: Not Found");
954
- expect(error.status).toBe(404);
955
- expect(error.statusText).toBe("Not Found");
956
- expect(error.response).toBe(mockResponse);
957
- });
958
-
959
- it("should be throwable and catchable", () => {
960
- const mockResponse = new Response(null, {
961
- status: 500,
962
- statusText: "Internal Server Error",
963
- });
964
-
965
- expect(() => {
966
- throw new FetchError(
967
- "Server error",
968
- 500,
969
- "Internal Server Error",
970
- mockResponse,
971
- );
972
- }).toThrow(FetchError);
973
- });
974
- });