@rooguys/js 0.1.0 → 1.0.0
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/README.md +342 -141
- package/package.json +1 -1
- package/src/__tests__/fixtures/responses.js +249 -0
- package/src/__tests__/property/batch-event-validation.property.test.js +225 -0
- package/src/__tests__/property/email-validation.property.test.js +272 -0
- package/src/__tests__/property/error-mapping.property.test.js +506 -0
- package/src/__tests__/property/field-selection.property.test.js +297 -0
- package/src/__tests__/property/idempotency-key.property.test.js +350 -0
- package/src/__tests__/property/leaderboard-filter.property.test.js +585 -0
- package/src/__tests__/property/partial-update.property.test.js +251 -0
- package/src/__tests__/property/rate-limit-error.property.test.js +276 -0
- package/src/__tests__/property/rate-limit-extraction.property.test.js +193 -0
- package/src/__tests__/property/request-construction.property.test.js +20 -28
- package/src/__tests__/property/response-format.property.test.js +418 -0
- package/src/__tests__/property/response-parsing.property.test.js +16 -21
- package/src/__tests__/property/timestamp-validation.property.test.js +345 -0
- package/src/__tests__/unit/aha.test.js +57 -26
- package/src/__tests__/unit/config.test.js +7 -1
- package/src/__tests__/unit/errors.test.js +6 -8
- package/src/__tests__/unit/events.test.js +253 -14
- package/src/__tests__/unit/leaderboards.test.js +249 -0
- package/src/__tests__/unit/questionnaires.test.js +6 -6
- package/src/__tests__/unit/users.test.js +275 -12
- package/src/__tests__/utils/generators.js +87 -0
- package/src/__tests__/utils/mockClient.js +71 -5
- package/src/errors.js +156 -0
- package/src/http-client.js +276 -0
- package/src/index.js +856 -66
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Property-Based Test: Error Response Mapping
|
|
3
|
+
* Feature: sdk-documentation-update, Property 3: Error Response Mapping
|
|
4
|
+
* Validates: Requirements 2.2, 8.1, 8.2, 8.4
|
|
5
|
+
*
|
|
6
|
+
* Tests that any API error response with HTTP status code and error body
|
|
7
|
+
* is mapped to the correct typed error (ValidationError for 400, AuthenticationError for 401,
|
|
8
|
+
* NotFoundError for 404, ConflictError for 409, RateLimitError for 429, ServerError for 500)
|
|
9
|
+
* with the error code and request ID preserved.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import fc from 'fast-check';
|
|
13
|
+
import { jest } from '@jest/globals';
|
|
14
|
+
import Rooguys, {
|
|
15
|
+
RooguysError,
|
|
16
|
+
ValidationError,
|
|
17
|
+
AuthenticationError,
|
|
18
|
+
ForbiddenError,
|
|
19
|
+
NotFoundError,
|
|
20
|
+
ConflictError,
|
|
21
|
+
RateLimitError,
|
|
22
|
+
ServerError,
|
|
23
|
+
mapStatusToError,
|
|
24
|
+
} from '../../index.js';
|
|
25
|
+
import { createMockFetch, createMockHeaders, mockErrorResponse } from '../utils/mockClient.js';
|
|
26
|
+
import { arbitraries } from '../utils/generators.js';
|
|
27
|
+
|
|
28
|
+
describe('Property 3: Error Response Mapping', () => {
|
|
29
|
+
let mockFetch;
|
|
30
|
+
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
mockFetch = createMockFetch();
|
|
33
|
+
global.fetch = mockFetch;
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
jest.clearAllMocks();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe('mapStatusToError', () => {
|
|
41
|
+
it('should map HTTP 400 to ValidationError', () => {
|
|
42
|
+
fc.assert(
|
|
43
|
+
fc.property(
|
|
44
|
+
fc.string({ minLength: 5, maxLength: 100 }),
|
|
45
|
+
fc.constantFrom('VALIDATION_ERROR', 'INVALID_INPUT', 'MISSING_FIELD'),
|
|
46
|
+
arbitraries.requestId(),
|
|
47
|
+
fc.option(
|
|
48
|
+
fc.array(
|
|
49
|
+
fc.record({
|
|
50
|
+
field: fc.string({ minLength: 1, maxLength: 30 }),
|
|
51
|
+
message: fc.string({ minLength: 5, maxLength: 100 }),
|
|
52
|
+
}),
|
|
53
|
+
{ minLength: 1, maxLength: 5 }
|
|
54
|
+
),
|
|
55
|
+
{ nil: null }
|
|
56
|
+
),
|
|
57
|
+
(message, code, requestId, fieldErrors) => {
|
|
58
|
+
// Arrange
|
|
59
|
+
const errorBody = {
|
|
60
|
+
error: { code, message, details: fieldErrors },
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// Act
|
|
64
|
+
const error = mapStatusToError(400, errorBody, requestId, {});
|
|
65
|
+
|
|
66
|
+
// Assert
|
|
67
|
+
expect(error).toBeInstanceOf(ValidationError);
|
|
68
|
+
expect(error.statusCode).toBe(400);
|
|
69
|
+
expect(error.message).toBe(message);
|
|
70
|
+
expect(error.code).toBe(code);
|
|
71
|
+
expect(error.requestId).toBe(requestId);
|
|
72
|
+
expect(error.fieldErrors).toEqual(fieldErrors);
|
|
73
|
+
}
|
|
74
|
+
),
|
|
75
|
+
{ numRuns: 100 }
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should map HTTP 401 to AuthenticationError', () => {
|
|
80
|
+
fc.assert(
|
|
81
|
+
fc.property(
|
|
82
|
+
fc.string({ minLength: 5, maxLength: 100 }),
|
|
83
|
+
fc.constantFrom('INVALID_API_KEY', 'MISSING_API_KEY', 'EXPIRED_TOKEN'),
|
|
84
|
+
arbitraries.requestId(),
|
|
85
|
+
(message, code, requestId) => {
|
|
86
|
+
// Arrange
|
|
87
|
+
const errorBody = { error: { code, message } };
|
|
88
|
+
|
|
89
|
+
// Act
|
|
90
|
+
const error = mapStatusToError(401, errorBody, requestId, {});
|
|
91
|
+
|
|
92
|
+
// Assert
|
|
93
|
+
expect(error).toBeInstanceOf(AuthenticationError);
|
|
94
|
+
expect(error.statusCode).toBe(401);
|
|
95
|
+
expect(error.message).toBe(message);
|
|
96
|
+
expect(error.code).toBe(code);
|
|
97
|
+
expect(error.requestId).toBe(requestId);
|
|
98
|
+
}
|
|
99
|
+
),
|
|
100
|
+
{ numRuns: 100 }
|
|
101
|
+
);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should map HTTP 403 to ForbiddenError', () => {
|
|
105
|
+
fc.assert(
|
|
106
|
+
fc.property(
|
|
107
|
+
fc.string({ minLength: 5, maxLength: 100 }),
|
|
108
|
+
fc.constantFrom('FORBIDDEN', 'ACCESS_DENIED', 'INSUFFICIENT_PERMISSIONS'),
|
|
109
|
+
arbitraries.requestId(),
|
|
110
|
+
(message, code, requestId) => {
|
|
111
|
+
// Arrange
|
|
112
|
+
const errorBody = { error: { code, message } };
|
|
113
|
+
|
|
114
|
+
// Act
|
|
115
|
+
const error = mapStatusToError(403, errorBody, requestId, {});
|
|
116
|
+
|
|
117
|
+
// Assert
|
|
118
|
+
expect(error).toBeInstanceOf(ForbiddenError);
|
|
119
|
+
expect(error.statusCode).toBe(403);
|
|
120
|
+
expect(error.message).toBe(message);
|
|
121
|
+
expect(error.code).toBe(code);
|
|
122
|
+
expect(error.requestId).toBe(requestId);
|
|
123
|
+
}
|
|
124
|
+
),
|
|
125
|
+
{ numRuns: 100 }
|
|
126
|
+
);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should map HTTP 404 to NotFoundError', () => {
|
|
130
|
+
fc.assert(
|
|
131
|
+
fc.property(
|
|
132
|
+
fc.string({ minLength: 5, maxLength: 100 }),
|
|
133
|
+
fc.constantFrom('NOT_FOUND', 'USER_NOT_FOUND', 'RESOURCE_NOT_FOUND'),
|
|
134
|
+
arbitraries.requestId(),
|
|
135
|
+
(message, code, requestId) => {
|
|
136
|
+
// Arrange
|
|
137
|
+
const errorBody = { error: { code, message } };
|
|
138
|
+
|
|
139
|
+
// Act
|
|
140
|
+
const error = mapStatusToError(404, errorBody, requestId, {});
|
|
141
|
+
|
|
142
|
+
// Assert
|
|
143
|
+
expect(error).toBeInstanceOf(NotFoundError);
|
|
144
|
+
expect(error.statusCode).toBe(404);
|
|
145
|
+
expect(error.message).toBe(message);
|
|
146
|
+
expect(error.code).toBe(code);
|
|
147
|
+
expect(error.requestId).toBe(requestId);
|
|
148
|
+
}
|
|
149
|
+
),
|
|
150
|
+
{ numRuns: 100 }
|
|
151
|
+
);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('should map HTTP 409 to ConflictError', () => {
|
|
155
|
+
fc.assert(
|
|
156
|
+
fc.property(
|
|
157
|
+
fc.string({ minLength: 5, maxLength: 100 }),
|
|
158
|
+
fc.constantFrom('CONFLICT', 'USER_EXISTS', 'DUPLICATE_ENTRY'),
|
|
159
|
+
arbitraries.requestId(),
|
|
160
|
+
(message, code, requestId) => {
|
|
161
|
+
// Arrange
|
|
162
|
+
const errorBody = { error: { code, message } };
|
|
163
|
+
|
|
164
|
+
// Act
|
|
165
|
+
const error = mapStatusToError(409, errorBody, requestId, {});
|
|
166
|
+
|
|
167
|
+
// Assert
|
|
168
|
+
expect(error).toBeInstanceOf(ConflictError);
|
|
169
|
+
expect(error.statusCode).toBe(409);
|
|
170
|
+
expect(error.message).toBe(message);
|
|
171
|
+
expect(error.code).toBe(code);
|
|
172
|
+
expect(error.requestId).toBe(requestId);
|
|
173
|
+
}
|
|
174
|
+
),
|
|
175
|
+
{ numRuns: 100 }
|
|
176
|
+
);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('should map HTTP 429 to RateLimitError with retryAfter', () => {
|
|
180
|
+
fc.assert(
|
|
181
|
+
fc.property(
|
|
182
|
+
fc.string({ minLength: 5, maxLength: 100 }),
|
|
183
|
+
fc.constantFrom('RATE_LIMIT_EXCEEDED', 'TOO_MANY_REQUESTS'),
|
|
184
|
+
arbitraries.requestId(),
|
|
185
|
+
fc.integer({ min: 1, max: 3600 }),
|
|
186
|
+
(message, code, requestId, retryAfter) => {
|
|
187
|
+
// Arrange
|
|
188
|
+
const errorBody = { error: { code, message } };
|
|
189
|
+
const headers = { 'retry-after': String(retryAfter) };
|
|
190
|
+
|
|
191
|
+
// Act
|
|
192
|
+
const error = mapStatusToError(429, errorBody, requestId, headers);
|
|
193
|
+
|
|
194
|
+
// Assert
|
|
195
|
+
expect(error).toBeInstanceOf(RateLimitError);
|
|
196
|
+
expect(error.statusCode).toBe(429);
|
|
197
|
+
expect(error.message).toBe(message);
|
|
198
|
+
expect(error.code).toBe(code);
|
|
199
|
+
expect(error.requestId).toBe(requestId);
|
|
200
|
+
expect(error.retryAfter).toBe(retryAfter);
|
|
201
|
+
}
|
|
202
|
+
),
|
|
203
|
+
{ numRuns: 100 }
|
|
204
|
+
);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('should map HTTP 500+ to ServerError', () => {
|
|
208
|
+
fc.assert(
|
|
209
|
+
fc.property(
|
|
210
|
+
fc.constantFrom(500, 502, 503, 504),
|
|
211
|
+
fc.string({ minLength: 5, maxLength: 100 }),
|
|
212
|
+
fc.constantFrom('INTERNAL_ERROR', 'SERVER_ERROR', 'SERVICE_UNAVAILABLE'),
|
|
213
|
+
arbitraries.requestId(),
|
|
214
|
+
(status, message, code, requestId) => {
|
|
215
|
+
// Arrange
|
|
216
|
+
const errorBody = { error: { code, message } };
|
|
217
|
+
|
|
218
|
+
// Act
|
|
219
|
+
const error = mapStatusToError(status, errorBody, requestId, {});
|
|
220
|
+
|
|
221
|
+
// Assert
|
|
222
|
+
expect(error).toBeInstanceOf(ServerError);
|
|
223
|
+
expect(error.statusCode).toBe(status);
|
|
224
|
+
expect(error.message).toBe(message);
|
|
225
|
+
expect(error.code).toBe(code);
|
|
226
|
+
expect(error.requestId).toBe(requestId);
|
|
227
|
+
}
|
|
228
|
+
),
|
|
229
|
+
{ numRuns: 100 }
|
|
230
|
+
);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('should preserve requestId in all error types', () => {
|
|
234
|
+
fc.assert(
|
|
235
|
+
fc.property(
|
|
236
|
+
arbitraries.errorCodeStatusPair(),
|
|
237
|
+
fc.string({ minLength: 5, maxLength: 100 }),
|
|
238
|
+
arbitraries.requestId(),
|
|
239
|
+
({ code, status }, message, requestId) => {
|
|
240
|
+
// Arrange
|
|
241
|
+
const errorBody = { error: { code, message } };
|
|
242
|
+
const headers = status === 429 ? { 'retry-after': '60' } : {};
|
|
243
|
+
|
|
244
|
+
// Act
|
|
245
|
+
const error = mapStatusToError(status, errorBody, requestId, headers);
|
|
246
|
+
|
|
247
|
+
// Assert
|
|
248
|
+
expect(error.requestId).toBe(requestId);
|
|
249
|
+
expect(error.code).toBe(code);
|
|
250
|
+
}
|
|
251
|
+
),
|
|
252
|
+
{ numRuns: 100 }
|
|
253
|
+
);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('should handle legacy error format (error as string)', () => {
|
|
257
|
+
fc.assert(
|
|
258
|
+
fc.property(
|
|
259
|
+
fc.constantFrom(400, 401, 404, 500),
|
|
260
|
+
fc.string({ minLength: 5, maxLength: 100 }),
|
|
261
|
+
arbitraries.requestId(),
|
|
262
|
+
(status, message, requestId) => {
|
|
263
|
+
// Arrange - legacy format with error as string
|
|
264
|
+
const errorBody = { error: message };
|
|
265
|
+
|
|
266
|
+
// Act
|
|
267
|
+
const error = mapStatusToError(status, errorBody, requestId, {});
|
|
268
|
+
|
|
269
|
+
// Assert
|
|
270
|
+
expect(error).toBeInstanceOf(RooguysError);
|
|
271
|
+
expect(error.message).toBe(message);
|
|
272
|
+
expect(error.requestId).toBe(requestId);
|
|
273
|
+
}
|
|
274
|
+
),
|
|
275
|
+
{ numRuns: 100 }
|
|
276
|
+
);
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
describe('SDK error handling integration', () => {
|
|
281
|
+
it('should throw correct error type for API errors', async () => {
|
|
282
|
+
await fc.assert(
|
|
283
|
+
fc.asyncProperty(
|
|
284
|
+
arbitraries.apiKey(),
|
|
285
|
+
arbitraries.userId(),
|
|
286
|
+
fc.constantFrom(
|
|
287
|
+
{ status: 400, ErrorClass: ValidationError },
|
|
288
|
+
{ status: 401, ErrorClass: AuthenticationError },
|
|
289
|
+
{ status: 403, ErrorClass: ForbiddenError },
|
|
290
|
+
{ status: 404, ErrorClass: NotFoundError },
|
|
291
|
+
{ status: 409, ErrorClass: ConflictError },
|
|
292
|
+
{ status: 429, ErrorClass: RateLimitError },
|
|
293
|
+
{ status: 500, ErrorClass: ServerError }
|
|
294
|
+
),
|
|
295
|
+
fc.string({ minLength: 5, maxLength: 100 }),
|
|
296
|
+
arbitraries.requestId(),
|
|
297
|
+
async (apiKey, userId, { status, ErrorClass }, message, requestId) => {
|
|
298
|
+
// Arrange
|
|
299
|
+
mockFetch.mockClear();
|
|
300
|
+
const errorResponse = {
|
|
301
|
+
success: false,
|
|
302
|
+
error: {
|
|
303
|
+
code: `ERROR_${status}`,
|
|
304
|
+
message,
|
|
305
|
+
},
|
|
306
|
+
request_id: requestId,
|
|
307
|
+
};
|
|
308
|
+
mockFetch.mockResolvedValue({
|
|
309
|
+
ok: false,
|
|
310
|
+
status,
|
|
311
|
+
statusText: message,
|
|
312
|
+
headers: createMockHeaders({
|
|
313
|
+
'X-Request-Id': requestId,
|
|
314
|
+
'Retry-After': '60',
|
|
315
|
+
}),
|
|
316
|
+
json: async () => errorResponse,
|
|
317
|
+
});
|
|
318
|
+
const sdk = new Rooguys(apiKey);
|
|
319
|
+
|
|
320
|
+
// Act & Assert
|
|
321
|
+
try {
|
|
322
|
+
await sdk.users.get(userId);
|
|
323
|
+
// Should not reach here
|
|
324
|
+
expect(true).toBe(false);
|
|
325
|
+
} catch (error) {
|
|
326
|
+
expect(error).toBeInstanceOf(ErrorClass);
|
|
327
|
+
expect(error.requestId).toBe(requestId);
|
|
328
|
+
expect(error.statusCode).toBe(status);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
),
|
|
332
|
+
{ numRuns: 100 }
|
|
333
|
+
);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it('should include field errors in ValidationError', async () => {
|
|
337
|
+
await fc.assert(
|
|
338
|
+
fc.asyncProperty(
|
|
339
|
+
arbitraries.apiKey(),
|
|
340
|
+
arbitraries.userId(),
|
|
341
|
+
fc.array(
|
|
342
|
+
fc.record({
|
|
343
|
+
field: fc.string({ minLength: 1, maxLength: 30 }),
|
|
344
|
+
message: fc.string({ minLength: 5, maxLength: 100 }),
|
|
345
|
+
}),
|
|
346
|
+
{ minLength: 1, maxLength: 5 }
|
|
347
|
+
),
|
|
348
|
+
arbitraries.requestId(),
|
|
349
|
+
async (apiKey, userId, fieldErrors, requestId) => {
|
|
350
|
+
// Arrange
|
|
351
|
+
mockFetch.mockClear();
|
|
352
|
+
const errorResponse = {
|
|
353
|
+
success: false,
|
|
354
|
+
error: {
|
|
355
|
+
code: 'VALIDATION_ERROR',
|
|
356
|
+
message: 'Validation failed',
|
|
357
|
+
details: fieldErrors,
|
|
358
|
+
},
|
|
359
|
+
request_id: requestId,
|
|
360
|
+
};
|
|
361
|
+
mockFetch.mockResolvedValue({
|
|
362
|
+
ok: false,
|
|
363
|
+
status: 400,
|
|
364
|
+
statusText: 'Bad Request',
|
|
365
|
+
headers: createMockHeaders({ 'X-Request-Id': requestId }),
|
|
366
|
+
json: async () => errorResponse,
|
|
367
|
+
});
|
|
368
|
+
const sdk = new Rooguys(apiKey);
|
|
369
|
+
|
|
370
|
+
// Act & Assert
|
|
371
|
+
try {
|
|
372
|
+
await sdk.users.get(userId);
|
|
373
|
+
expect(true).toBe(false);
|
|
374
|
+
} catch (error) {
|
|
375
|
+
expect(error).toBeInstanceOf(ValidationError);
|
|
376
|
+
expect(error.fieldErrors).toEqual(fieldErrors);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
),
|
|
380
|
+
{ numRuns: 100 }
|
|
381
|
+
);
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it('should include retryAfter in RateLimitError', async () => {
|
|
385
|
+
await fc.assert(
|
|
386
|
+
fc.asyncProperty(
|
|
387
|
+
arbitraries.apiKey(),
|
|
388
|
+
arbitraries.userId(),
|
|
389
|
+
fc.integer({ min: 1, max: 3600 }),
|
|
390
|
+
arbitraries.requestId(),
|
|
391
|
+
async (apiKey, userId, retryAfter, requestId) => {
|
|
392
|
+
// Arrange
|
|
393
|
+
mockFetch.mockClear();
|
|
394
|
+
const errorResponse = {
|
|
395
|
+
success: false,
|
|
396
|
+
error: {
|
|
397
|
+
code: 'RATE_LIMIT_EXCEEDED',
|
|
398
|
+
message: 'Rate limit exceeded',
|
|
399
|
+
},
|
|
400
|
+
request_id: requestId,
|
|
401
|
+
};
|
|
402
|
+
mockFetch.mockResolvedValue({
|
|
403
|
+
ok: false,
|
|
404
|
+
status: 429,
|
|
405
|
+
statusText: 'Too Many Requests',
|
|
406
|
+
headers: createMockHeaders({
|
|
407
|
+
'X-Request-Id': requestId,
|
|
408
|
+
'Retry-After': String(retryAfter),
|
|
409
|
+
}),
|
|
410
|
+
json: async () => errorResponse,
|
|
411
|
+
});
|
|
412
|
+
const sdk = new Rooguys(apiKey);
|
|
413
|
+
|
|
414
|
+
// Act & Assert
|
|
415
|
+
try {
|
|
416
|
+
await sdk.users.get(userId);
|
|
417
|
+
expect(true).toBe(false);
|
|
418
|
+
} catch (error) {
|
|
419
|
+
expect(error).toBeInstanceOf(RateLimitError);
|
|
420
|
+
expect(error.retryAfter).toBe(retryAfter);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
),
|
|
424
|
+
{ numRuns: 100 }
|
|
425
|
+
);
|
|
426
|
+
});
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
describe('Error serialization', () => {
|
|
430
|
+
it('should serialize errors to JSON with all properties', () => {
|
|
431
|
+
fc.assert(
|
|
432
|
+
fc.property(
|
|
433
|
+
fc.string({ minLength: 5, maxLength: 100 }),
|
|
434
|
+
fc.constantFrom('VALIDATION_ERROR', 'NOT_FOUND', 'SERVER_ERROR'),
|
|
435
|
+
arbitraries.requestId(),
|
|
436
|
+
fc.constantFrom(400, 404, 500),
|
|
437
|
+
(message, code, requestId, statusCode) => {
|
|
438
|
+
// Arrange
|
|
439
|
+
const error = new RooguysError(message, { code, requestId, statusCode });
|
|
440
|
+
|
|
441
|
+
// Act
|
|
442
|
+
const json = error.toJSON();
|
|
443
|
+
|
|
444
|
+
// Assert
|
|
445
|
+
expect(json.name).toBe('RooguysError');
|
|
446
|
+
expect(json.message).toBe(message);
|
|
447
|
+
expect(json.code).toBe(code);
|
|
448
|
+
expect(json.requestId).toBe(requestId);
|
|
449
|
+
expect(json.statusCode).toBe(statusCode);
|
|
450
|
+
}
|
|
451
|
+
),
|
|
452
|
+
{ numRuns: 100 }
|
|
453
|
+
);
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
it('should serialize ValidationError with fieldErrors', () => {
|
|
457
|
+
fc.assert(
|
|
458
|
+
fc.property(
|
|
459
|
+
fc.string({ minLength: 5, maxLength: 100 }),
|
|
460
|
+
arbitraries.requestId(),
|
|
461
|
+
fc.array(
|
|
462
|
+
fc.record({
|
|
463
|
+
field: fc.string({ minLength: 1, maxLength: 30 }),
|
|
464
|
+
message: fc.string({ minLength: 5, maxLength: 100 }),
|
|
465
|
+
}),
|
|
466
|
+
{ minLength: 1, maxLength: 5 }
|
|
467
|
+
),
|
|
468
|
+
(message, requestId, fieldErrors) => {
|
|
469
|
+
// Arrange
|
|
470
|
+
const error = new ValidationError(message, { requestId, fieldErrors });
|
|
471
|
+
|
|
472
|
+
// Act
|
|
473
|
+
const json = error.toJSON();
|
|
474
|
+
|
|
475
|
+
// Assert
|
|
476
|
+
expect(json.name).toBe('ValidationError');
|
|
477
|
+
expect(json.fieldErrors).toEqual(fieldErrors);
|
|
478
|
+
}
|
|
479
|
+
),
|
|
480
|
+
{ numRuns: 100 }
|
|
481
|
+
);
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
it('should serialize RateLimitError with retryAfter', () => {
|
|
485
|
+
fc.assert(
|
|
486
|
+
fc.property(
|
|
487
|
+
fc.string({ minLength: 5, maxLength: 100 }),
|
|
488
|
+
arbitraries.requestId(),
|
|
489
|
+
fc.integer({ min: 1, max: 3600 }),
|
|
490
|
+
(message, requestId, retryAfter) => {
|
|
491
|
+
// Arrange
|
|
492
|
+
const error = new RateLimitError(message, { requestId, retryAfter });
|
|
493
|
+
|
|
494
|
+
// Act
|
|
495
|
+
const json = error.toJSON();
|
|
496
|
+
|
|
497
|
+
// Assert
|
|
498
|
+
expect(json.name).toBe('RateLimitError');
|
|
499
|
+
expect(json.retryAfter).toBe(retryAfter);
|
|
500
|
+
}
|
|
501
|
+
),
|
|
502
|
+
{ numRuns: 100 }
|
|
503
|
+
);
|
|
504
|
+
});
|
|
505
|
+
});
|
|
506
|
+
});
|