@vafast/api-client 0.1.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.
@@ -0,0 +1,363 @@
1
+ import { describe, expect, it } from 'bun:test'
2
+ import {
3
+ buildQueryString,
4
+ replacePathParams,
5
+ isFile,
6
+ isFileUpload,
7
+ hasFiles,
8
+ createFormData,
9
+ deepMerge,
10
+ delay,
11
+ exponentialBackoff,
12
+ validateStatus,
13
+ parseResponse,
14
+ createError,
15
+ cloneRequest,
16
+ isRetryableError
17
+ } from '../src'
18
+
19
+ describe('Utils', () => {
20
+ describe('buildQueryString', () => {
21
+ it('should build query string from object', () => {
22
+ const params = { page: 1, limit: 10, search: 'john' }
23
+ const result = buildQueryString(params)
24
+ expect(result).toBe('?page=1&limit=10&search=john')
25
+ })
26
+
27
+ it('should handle empty object', () => {
28
+ const result = buildQueryString({})
29
+ expect(result).toBe('')
30
+ })
31
+
32
+ it('should handle undefined and null values', () => {
33
+ const params = { page: 1, limit: undefined, search: null, active: true }
34
+ const result = buildQueryString(params)
35
+ expect(result).toBe('?page=1&active=true')
36
+ })
37
+
38
+ it('should handle array values', () => {
39
+ const params = { tags: ['js', 'ts'], page: 1 }
40
+ const result = buildQueryString(params)
41
+ expect(result).toBe('?tags=js&tags=ts&page=1')
42
+ })
43
+
44
+ it('should handle boolean values', () => {
45
+ const params = { active: true, verified: false }
46
+ const result = buildQueryString(params)
47
+ expect(result).toBe('?active=true&verified=false')
48
+ })
49
+ })
50
+
51
+ describe('replacePathParams', () => {
52
+ it('should replace path parameters', () => {
53
+ const path = '/users/:id/posts/:postId'
54
+ const params = { id: '123', postId: '456' }
55
+ const result = replacePathParams(path, params)
56
+ expect(result).toBe('/users/123/posts/456')
57
+ })
58
+
59
+ it('should handle path with no parameters', () => {
60
+ const path = '/users'
61
+ const params = {}
62
+ const result = replacePathParams(path, params)
63
+ expect(result).toBe('/users')
64
+ })
65
+
66
+ it('should handle numeric parameters', () => {
67
+ const path = '/users/:id'
68
+ const params = { id: 123 }
69
+ const result = replacePathParams(path, params)
70
+ expect(result).toBe('/users/123')
71
+ })
72
+
73
+ it('should handle missing parameters', () => {
74
+ const path = '/users/:id/posts/:postId'
75
+ const params = { id: '123' }
76
+ const result = replacePathParams(path, params)
77
+ expect(result).toBe('/users/123/posts/:postId')
78
+ })
79
+ })
80
+
81
+ describe('isFile', () => {
82
+ it('should identify File objects', () => {
83
+ const file = new File(['content'], 'test.txt')
84
+ expect(isFile(file)).toBe(true)
85
+ })
86
+
87
+ it('should identify Blob objects', () => {
88
+ const blob = new Blob(['content'])
89
+ expect(isFile(blob)).toBe(true)
90
+ })
91
+
92
+ it('should reject non-file objects', () => {
93
+ expect(isFile('string')).toBe(false)
94
+ expect(isFile(123)).toBe(false)
95
+ expect(isFile({})).toBe(false)
96
+ expect(isFile(null)).toBe(false)
97
+ expect(isFile(undefined)).toBe(false)
98
+ })
99
+ })
100
+
101
+ describe('isFileUpload', () => {
102
+ it('should identify FileUpload objects', () => {
103
+ const fileUpload = {
104
+ file: new File(['content'], 'test.txt'),
105
+ filename: 'custom.txt',
106
+ contentType: 'text/plain'
107
+ }
108
+ expect(isFileUpload(fileUpload)).toBe(true)
109
+ })
110
+
111
+ it('should reject non-FileUpload objects', () => {
112
+ expect(isFileUpload({})).toBe(false)
113
+ expect(isFileUpload({ file: 'string' })).toBe(false)
114
+ expect(isFileUpload(null)).toBe(false)
115
+ expect(isFileUpload(undefined)).toBe(false)
116
+ })
117
+ })
118
+
119
+ describe('hasFiles', () => {
120
+ it('should detect files in object', () => {
121
+ const obj = {
122
+ name: 'test',
123
+ file: new File(['content'], 'test.txt'),
124
+ metadata: { size: 100 }
125
+ }
126
+ expect(hasFiles(obj)).toBe(true)
127
+ })
128
+
129
+ it('should detect files in nested objects', () => {
130
+ const obj = {
131
+ user: {
132
+ avatar: new File(['content'], 'avatar.jpg'),
133
+ profile: { name: 'John' }
134
+ }
135
+ }
136
+ expect(hasFiles(obj)).toBe(true)
137
+ })
138
+
139
+ it('should detect files in arrays', () => {
140
+ const obj = {
141
+ files: [new File(['content'], 'file1.txt'), new File(['content'], 'file2.txt')]
142
+ }
143
+ expect(hasFiles(obj)).toBe(true)
144
+ })
145
+
146
+ it('should not detect files in object without files', () => {
147
+ const obj = {
148
+ name: 'test',
149
+ age: 25,
150
+ active: true
151
+ }
152
+ expect(hasFiles(obj)).toBe(false)
153
+ })
154
+ })
155
+
156
+ describe('createFormData', () => {
157
+ it('should create FormData from object', () => {
158
+ const data = {
159
+ name: 'John',
160
+ age: 25,
161
+ file: new File(['content'], 'test.txt')
162
+ }
163
+ const formData = createFormData(data)
164
+ expect(formData).toBeInstanceOf(globalThis.FormData)
165
+ })
166
+
167
+ it('should handle FileUpload objects', () => {
168
+ const data = {
169
+ name: 'John',
170
+ avatar: {
171
+ file: new File(['content'], 'avatar.jpg'),
172
+ filename: 'custom.jpg'
173
+ }
174
+ }
175
+ const formData = createFormData(data)
176
+ expect(formData).toBeInstanceOf(globalThis.FormData)
177
+ })
178
+
179
+ it('should handle array values', () => {
180
+ const data = {
181
+ tags: ['js', 'ts'],
182
+ files: [new File(['content'], 'file1.txt'), new File(['content'], 'file2.txt')]
183
+ }
184
+ const formData = createFormData(data)
185
+ expect(formData).toBeInstanceOf(globalThis.FormData)
186
+ })
187
+
188
+ it('should skip undefined and null values', () => {
189
+ const data = {
190
+ name: 'John',
191
+ age: undefined,
192
+ email: null,
193
+ active: true
194
+ }
195
+ const formData = createFormData(data)
196
+ expect(formData).toBeInstanceOf(globalThis.FormData)
197
+ })
198
+ })
199
+
200
+ describe('deepMerge', () => {
201
+ it('should merge objects deeply', () => {
202
+ const target = { a: 1, b: { c: 2, d: 3 } }
203
+ const source = { b: { d: 4, e: 5 }, f: 6 }
204
+ const result = deepMerge(target, source)
205
+ expect(result).toEqual({ a: 1, b: { c: 2, d: 4, e: 5 }, f: 6 })
206
+ })
207
+
208
+ it('should not modify original objects', () => {
209
+ const target = { a: 1, b: { c: 2 } }
210
+ const source = { b: { d: 3 } }
211
+ const result = deepMerge(target, source)
212
+ expect(target).toEqual({ a: 1, b: { c: 2 } })
213
+ expect(result).toEqual({ a: 1, b: { c: 2, d: 3 } })
214
+ })
215
+
216
+ it('should handle empty objects', () => {
217
+ const target = {}
218
+ const source = {}
219
+ const result = deepMerge(target, source)
220
+ expect(result).toEqual({})
221
+ })
222
+
223
+ it('should handle null and undefined', () => {
224
+ const target = { a: 1, b: { c: 2 } }
225
+ const source = { b: null, c: undefined }
226
+ const result = deepMerge(target, source)
227
+ expect(result).toEqual({ a: 1, b: null, c: undefined })
228
+ })
229
+ })
230
+
231
+ describe('delay', () => {
232
+ it('should delay execution', async () => {
233
+ const start = Date.now()
234
+ await delay(100)
235
+ const end = Date.now()
236
+ expect(end - start).toBeGreaterThanOrEqual(90)
237
+ })
238
+ })
239
+
240
+ describe('exponentialBackoff', () => {
241
+ it('should calculate exponential backoff', () => {
242
+ const result1 = exponentialBackoff(1, 1000, 10000)
243
+ const result2 = exponentialBackoff(2, 1000, 10000)
244
+ const result3 = exponentialBackoff(3, 1000, 10000)
245
+
246
+ expect(result1).toBeGreaterThanOrEqual(1000)
247
+ expect(result2).toBeGreaterThanOrEqual(2000)
248
+ expect(result3).toBeGreaterThanOrEqual(4000)
249
+ })
250
+
251
+ it('should respect max delay', () => {
252
+ const result = exponentialBackoff(10, 1000, 5000)
253
+ // Allow some tolerance for random jitter
254
+ expect(result).toBeLessThanOrEqual(6000)
255
+ })
256
+ })
257
+
258
+ describe('validateStatus', () => {
259
+ it('should validate successful status codes', () => {
260
+ expect(validateStatus(200)).toBe(true)
261
+ expect(validateStatus(201)).toBe(true)
262
+ expect(validateStatus(299)).toBe(true)
263
+ })
264
+
265
+ it('should reject error status codes', () => {
266
+ expect(validateStatus(400)).toBe(false)
267
+ expect(validateStatus(500)).toBe(false)
268
+ expect(validateStatus(404)).toBe(false)
269
+ })
270
+ })
271
+
272
+ describe('parseResponse', () => {
273
+ it('should parse JSON responses', async () => {
274
+ const response = new Response(JSON.stringify({ data: 'test' }), {
275
+ headers: { 'Content-Type': 'application/json' }
276
+ })
277
+ const result = await parseResponse(response)
278
+ expect(result).toEqual({ data: 'test' })
279
+ })
280
+
281
+ it('should parse text responses', async () => {
282
+ const response = new Response('Hello World', {
283
+ headers: { 'Content-Type': 'text/plain' }
284
+ })
285
+ const result = await parseResponse(response)
286
+ expect(result).toBe('Hello World')
287
+ })
288
+
289
+ it('should parse array buffer responses', async () => {
290
+ const buffer = new ArrayBuffer(8)
291
+ const response = new Response(buffer, {
292
+ headers: { 'Content-Type': 'application/octet-stream' }
293
+ })
294
+ const result = await parseResponse(response)
295
+ expect(result).toBeInstanceOf(ArrayBuffer)
296
+ })
297
+
298
+ it('should handle responses without content-type', async () => {
299
+ const response = new Response('Hello World')
300
+ const result = await parseResponse(response)
301
+ expect(result).toBe('Hello World')
302
+ })
303
+ })
304
+
305
+ describe('createError', () => {
306
+ it('should create error with status and data', () => {
307
+ const error = createError(404, 'Not Found', { resource: 'user' })
308
+ expect(error).toBeInstanceOf(Error)
309
+ expect((error as any).status).toBe(404)
310
+ expect((error as any).data).toEqual({ resource: 'user' })
311
+ expect((error as any).name).toBe('ApiError')
312
+ })
313
+
314
+ it('should create error without data', () => {
315
+ const error = createError(500, 'Internal Server Error')
316
+ expect(error).toBeInstanceOf(Error)
317
+ expect((error as any).status).toBe(500)
318
+ expect((error as any).data).toBeUndefined()
319
+ })
320
+ })
321
+
322
+ describe('cloneRequest', () => {
323
+ it('should clone request object', () => {
324
+ const original = new Request('https://example.com', {
325
+ method: 'POST',
326
+ headers: { 'Content-Type': 'application/json' },
327
+ body: JSON.stringify({ test: true })
328
+ })
329
+ const cloned = cloneRequest(original)
330
+ expect(cloned).toBeInstanceOf(Request)
331
+ expect(cloned.url).toBe(original.url)
332
+ expect(cloned.method).toBe(original.method)
333
+ })
334
+ })
335
+
336
+ describe('isRetryableError', () => {
337
+ it('should identify retryable HTTP status codes', () => {
338
+ expect(isRetryableError(new Error(), 408)).toBe(true)
339
+ expect(isRetryableError(new Error(), 429)).toBe(true)
340
+ expect(isRetryableError(new Error(), 500)).toBe(true)
341
+ expect(isRetryableError(new Error(), 502)).toBe(true)
342
+ expect(isRetryableError(new Error(), 503)).toBe(true)
343
+ expect(isRetryableError(new Error(), 504)).toBe(true)
344
+ })
345
+
346
+ it('should reject non-retryable status codes', () => {
347
+ expect(isRetryableError(new Error(), 400)).toBe(false)
348
+ expect(isRetryableError(new Error(), 401)).toBe(false)
349
+ expect(isRetryableError(new Error(), 403)).toBe(false)
350
+ expect(isRetryableError(new Error(), 404)).toBe(false)
351
+ })
352
+
353
+ it('should identify network errors as retryable', () => {
354
+ const networkError = new Error('fetch failed')
355
+ expect(isRetryableError(networkError)).toBe(true)
356
+ })
357
+
358
+ it('should reject other errors as non-retryable', () => {
359
+ const otherError = new Error('Validation failed')
360
+ expect(isRetryableError(otherError)).toBe(false)
361
+ })
362
+ })
363
+ })