@geekmidas/errors 0.0.1 → 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/CHANGELOG.md +7 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -1
- package/src/__tests__/errors.spec.ts +519 -519
- package/src/index.ts +364 -365
- package/tsconfig.json +9 -0
- package/tsdown.config.ts +1 -1
|
@@ -1,591 +1,591 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
3
|
+
BadGatewayError,
|
|
4
|
+
BadRequestError,
|
|
5
|
+
ConflictError,
|
|
6
|
+
createError,
|
|
7
|
+
createHttpError,
|
|
8
|
+
ForbiddenError,
|
|
9
|
+
GatewayTimeoutError,
|
|
10
|
+
HttpError,
|
|
11
|
+
HttpStatusCode,
|
|
12
|
+
InternalServerError,
|
|
13
|
+
isClientError,
|
|
14
|
+
isHttpError,
|
|
15
|
+
isServerError,
|
|
16
|
+
MethodNotAllowedError,
|
|
17
|
+
NotFoundError,
|
|
18
|
+
NotImplementedError,
|
|
19
|
+
ServiceUnavailableError,
|
|
20
|
+
TooManyRequestsError,
|
|
21
|
+
UnauthorizedError,
|
|
22
|
+
UnprocessableEntityError,
|
|
23
|
+
wrapError,
|
|
24
24
|
} from '../index';
|
|
25
25
|
|
|
26
26
|
describe('HttpError', () => {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
27
|
+
it('should create basic HTTP error', () => {
|
|
28
|
+
const error = new HttpError(400, 'Bad request');
|
|
29
|
+
|
|
30
|
+
expect(error).toBeInstanceOf(Error);
|
|
31
|
+
expect(error).toBeInstanceOf(HttpError);
|
|
32
|
+
expect(error.statusCode).toBe(400);
|
|
33
|
+
expect(error.statusMessage).toBe('Bad Request');
|
|
34
|
+
expect(error.message).toBe('Bad request');
|
|
35
|
+
expect(error.name).toBe('HttpError');
|
|
36
|
+
expect(error.isHttpError).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should use default status message when not provided', () => {
|
|
40
|
+
const error = new HttpError(404);
|
|
41
|
+
|
|
42
|
+
expect(error.statusMessage).toBe('Not Found');
|
|
43
|
+
expect(error.message).toBe('HTTP Error'); // Default from constructor
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should include error details', () => {
|
|
47
|
+
const error = new HttpError(400, 'Invalid input', {
|
|
48
|
+
details: { field: 'email', value: 'invalid' },
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
expect(error.details).toEqual({ field: 'email', value: 'invalid' });
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should include error code', () => {
|
|
55
|
+
const error = new HttpError(400, 'Invalid input', {
|
|
56
|
+
code: 'VALIDATION_ERROR',
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
expect(error.code).toBe('VALIDATION_ERROR');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should support error cause chaining', () => {
|
|
63
|
+
const originalError = new Error('Database connection failed');
|
|
64
|
+
const error = new HttpError(500, 'Internal error', {
|
|
65
|
+
cause: originalError,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
expect(error.cause).toBe(originalError);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should serialize to JSON', () => {
|
|
72
|
+
const error = new HttpError(404, 'Not found', {
|
|
73
|
+
details: { id: '123' },
|
|
74
|
+
code: 'NOT_FOUND',
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const json = error.toJSON();
|
|
78
|
+
|
|
79
|
+
expect(json).toMatchObject({
|
|
80
|
+
name: 'HttpError',
|
|
81
|
+
message: 'Not found',
|
|
82
|
+
statusCode: 404,
|
|
83
|
+
statusMessage: 'Not Found',
|
|
84
|
+
code: 'NOT_FOUND',
|
|
85
|
+
details: { id: '123' },
|
|
86
|
+
});
|
|
87
|
+
expect(json.stack).toBeDefined();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should generate error body', () => {
|
|
91
|
+
const error = new HttpError(400, 'Bad request', {
|
|
92
|
+
details: { field: 'email' },
|
|
93
|
+
code: 'INVALID_EMAIL',
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const body = JSON.parse(error.body);
|
|
97
|
+
|
|
98
|
+
expect(body).toEqual({
|
|
99
|
+
message: 'Bad request',
|
|
100
|
+
code: 'INVALID_EMAIL',
|
|
101
|
+
error: { field: 'email' },
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should handle unknown status codes', () => {
|
|
106
|
+
const error = new HttpError(418);
|
|
107
|
+
|
|
108
|
+
expect(error.statusCode).toBe(418);
|
|
109
|
+
expect(error.statusMessage).toBe('Unknown Error');
|
|
110
|
+
});
|
|
111
111
|
});
|
|
112
112
|
|
|
113
113
|
describe('Client Error Classes (4xx)', () => {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
114
|
+
describe('BadRequestError', () => {
|
|
115
|
+
it('should create 400 error', () => {
|
|
116
|
+
const error = new BadRequestError('Invalid input');
|
|
117
|
+
|
|
118
|
+
expect(error.statusCode).toBe(400);
|
|
119
|
+
expect(error.statusMessage).toBe('Bad Request');
|
|
120
|
+
expect(error.message).toBe('Invalid input');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('should include details', () => {
|
|
124
|
+
const error = new BadRequestError('Invalid input', { field: 'email' });
|
|
125
|
+
|
|
126
|
+
expect(error.details).toEqual({ field: 'email' });
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe('UnauthorizedError', () => {
|
|
131
|
+
it('should create 401 error', () => {
|
|
132
|
+
const error = new UnauthorizedError('Invalid token');
|
|
133
|
+
|
|
134
|
+
expect(error.statusCode).toBe(401);
|
|
135
|
+
expect(error.statusMessage).toBe('Unauthorized');
|
|
136
|
+
expect(error.message).toBe('Invalid token');
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe('ForbiddenError', () => {
|
|
141
|
+
it('should create 403 error', () => {
|
|
142
|
+
const error = new ForbiddenError('Access denied');
|
|
143
|
+
|
|
144
|
+
expect(error.statusCode).toBe(403);
|
|
145
|
+
expect(error.statusMessage).toBe('Forbidden');
|
|
146
|
+
expect(error.message).toBe('Access denied');
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe('NotFoundError', () => {
|
|
151
|
+
it('should create 404 error', () => {
|
|
152
|
+
const error = new NotFoundError('User not found');
|
|
153
|
+
|
|
154
|
+
expect(error.statusCode).toBe(404);
|
|
155
|
+
expect(error.statusMessage).toBe('Not Found');
|
|
156
|
+
expect(error.message).toBe('User not found');
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe('MethodNotAllowedError', () => {
|
|
161
|
+
it('should create 405 error', () => {
|
|
162
|
+
const error = new MethodNotAllowedError('DELETE not allowed');
|
|
163
|
+
|
|
164
|
+
expect(error.statusCode).toBe(405);
|
|
165
|
+
expect(error.statusMessage).toBe('Method Not Allowed');
|
|
166
|
+
expect(error.message).toBe('DELETE not allowed');
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('should include allowed methods', () => {
|
|
170
|
+
const error = new MethodNotAllowedError('DELETE not allowed', [
|
|
171
|
+
'GET',
|
|
172
|
+
'POST',
|
|
173
|
+
'PUT',
|
|
174
|
+
]);
|
|
175
|
+
|
|
176
|
+
expect(error.details).toEqual({
|
|
177
|
+
allowedMethods: ['GET', 'POST', 'PUT'],
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
describe('ConflictError', () => {
|
|
183
|
+
it('should create 409 error', () => {
|
|
184
|
+
const error = new ConflictError('Email already exists');
|
|
185
|
+
|
|
186
|
+
expect(error.statusCode).toBe(409);
|
|
187
|
+
expect(error.statusMessage).toBe('Conflict');
|
|
188
|
+
expect(error.message).toBe('Email already exists');
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe('UnprocessableEntityError', () => {
|
|
193
|
+
it('should create 422 error', () => {
|
|
194
|
+
const error = new UnprocessableEntityError('Validation failed');
|
|
195
|
+
|
|
196
|
+
expect(error.statusCode).toBe(422);
|
|
197
|
+
expect(error.statusMessage).toBe('Unprocessable Entity');
|
|
198
|
+
expect(error.message).toBe('Validation failed');
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('should include validation errors', () => {
|
|
202
|
+
const error = new UnprocessableEntityError('Validation failed', {
|
|
203
|
+
email: 'Invalid format',
|
|
204
|
+
age: 'Must be 18+',
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
expect(error.details).toEqual({
|
|
208
|
+
validationErrors: {
|
|
209
|
+
email: 'Invalid format',
|
|
210
|
+
age: 'Must be 18+',
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
describe('TooManyRequestsError', () => {
|
|
217
|
+
it('should create 429 error', () => {
|
|
218
|
+
const error = new TooManyRequestsError('Rate limit exceeded');
|
|
219
|
+
|
|
220
|
+
expect(error.statusCode).toBe(429);
|
|
221
|
+
expect(error.statusMessage).toBe('Too Many Requests');
|
|
222
|
+
expect(error.message).toBe('Rate limit exceeded');
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('should include retry after', () => {
|
|
226
|
+
const error = new TooManyRequestsError('Rate limit exceeded', 60);
|
|
227
|
+
|
|
228
|
+
expect(error.details).toEqual({ retryAfter: 60 });
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
231
|
});
|
|
232
232
|
|
|
233
233
|
describe('Server Error Classes (5xx)', () => {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
234
|
+
describe('InternalServerError', () => {
|
|
235
|
+
it('should create 500 error', () => {
|
|
236
|
+
const error = new InternalServerError('Database error');
|
|
237
|
+
|
|
238
|
+
expect(error.statusCode).toBe(500);
|
|
239
|
+
expect(error.statusMessage).toBe('Internal Server Error');
|
|
240
|
+
expect(error.message).toBe('Database error');
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
describe('NotImplementedError', () => {
|
|
245
|
+
it('should create 501 error', () => {
|
|
246
|
+
const error = new NotImplementedError('Feature not implemented');
|
|
247
|
+
|
|
248
|
+
expect(error.statusCode).toBe(501);
|
|
249
|
+
expect(error.statusMessage).toBe('Not Implemented');
|
|
250
|
+
expect(error.message).toBe('Feature not implemented');
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
describe('BadGatewayError', () => {
|
|
255
|
+
it('should create 502 error', () => {
|
|
256
|
+
const error = new BadGatewayError('Upstream error');
|
|
257
|
+
|
|
258
|
+
expect(error.statusCode).toBe(502);
|
|
259
|
+
expect(error.statusMessage).toBe('Bad Gateway');
|
|
260
|
+
expect(error.message).toBe('Upstream error');
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
describe('ServiceUnavailableError', () => {
|
|
265
|
+
it('should create 503 error', () => {
|
|
266
|
+
const error = new ServiceUnavailableError('Service down');
|
|
267
|
+
|
|
268
|
+
expect(error.statusCode).toBe(503);
|
|
269
|
+
expect(error.statusMessage).toBe('Service Unavailable');
|
|
270
|
+
expect(error.message).toBe('Service down');
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('should include retry after', () => {
|
|
274
|
+
const error = new ServiceUnavailableError('Service down', 300);
|
|
275
|
+
|
|
276
|
+
expect(error.details).toEqual({ retryAfter: 300 });
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
describe('GatewayTimeoutError', () => {
|
|
281
|
+
it('should create 504 error', () => {
|
|
282
|
+
const error = new GatewayTimeoutError('Upstream timeout');
|
|
283
|
+
|
|
284
|
+
expect(error.statusCode).toBe(504);
|
|
285
|
+
expect(error.statusMessage).toBe('Gateway Timeout');
|
|
286
|
+
expect(error.message).toBe('Upstream timeout');
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
289
|
});
|
|
290
290
|
|
|
291
291
|
describe('createError factory', () => {
|
|
292
|
-
|
|
293
|
-
|
|
292
|
+
it('should create badRequest error', () => {
|
|
293
|
+
const error = createError.badRequest('Invalid input');
|
|
294
294
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
295
|
+
expect(error).toBeInstanceOf(BadRequestError);
|
|
296
|
+
expect(error.statusCode).toBe(400);
|
|
297
|
+
});
|
|
298
298
|
|
|
299
|
-
|
|
300
|
-
|
|
299
|
+
it('should create unauthorized error', () => {
|
|
300
|
+
const error = createError.unauthorized('Invalid token');
|
|
301
301
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
302
|
+
expect(error).toBeInstanceOf(UnauthorizedError);
|
|
303
|
+
expect(error.statusCode).toBe(401);
|
|
304
|
+
});
|
|
305
305
|
|
|
306
|
-
|
|
307
|
-
|
|
306
|
+
it('should create forbidden error', () => {
|
|
307
|
+
const error = createError.forbidden('Access denied');
|
|
308
308
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
309
|
+
expect(error).toBeInstanceOf(ForbiddenError);
|
|
310
|
+
expect(error.statusCode).toBe(403);
|
|
311
|
+
});
|
|
312
312
|
|
|
313
|
-
|
|
314
|
-
|
|
313
|
+
it('should create notFound error', () => {
|
|
314
|
+
const error = createError.notFound('Resource not found');
|
|
315
315
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
316
|
+
expect(error).toBeInstanceOf(NotFoundError);
|
|
317
|
+
expect(error.statusCode).toBe(404);
|
|
318
|
+
});
|
|
319
319
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
320
|
+
it('should create methodNotAllowed error', () => {
|
|
321
|
+
const error = createError.methodNotAllowed('Method not allowed', [
|
|
322
|
+
'GET',
|
|
323
|
+
'POST',
|
|
324
|
+
]);
|
|
325
325
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
326
|
+
expect(error).toBeInstanceOf(MethodNotAllowedError);
|
|
327
|
+
expect(error.statusCode).toBe(405);
|
|
328
|
+
});
|
|
329
329
|
|
|
330
|
-
|
|
331
|
-
|
|
330
|
+
it('should create conflict error', () => {
|
|
331
|
+
const error = createError.conflict('Resource exists');
|
|
332
332
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
333
|
+
expect(error).toBeInstanceOf(ConflictError);
|
|
334
|
+
expect(error.statusCode).toBe(409);
|
|
335
|
+
});
|
|
336
336
|
|
|
337
|
-
|
|
338
|
-
|
|
337
|
+
it('should create unprocessableEntity error', () => {
|
|
338
|
+
const error = createError.unprocessableEntity('Validation failed');
|
|
339
339
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
340
|
+
expect(error).toBeInstanceOf(UnprocessableEntityError);
|
|
341
|
+
expect(error.statusCode).toBe(422);
|
|
342
|
+
});
|
|
343
343
|
|
|
344
|
-
|
|
345
|
-
|
|
344
|
+
it('should create tooManyRequests error', () => {
|
|
345
|
+
const error = createError.tooManyRequests('Rate limited', 60);
|
|
346
346
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
347
|
+
expect(error).toBeInstanceOf(TooManyRequestsError);
|
|
348
|
+
expect(error.statusCode).toBe(429);
|
|
349
|
+
});
|
|
350
350
|
|
|
351
|
-
|
|
352
|
-
|
|
351
|
+
it('should create internalServerError error', () => {
|
|
352
|
+
const error = createError.internalServerError('Server error');
|
|
353
353
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
354
|
+
expect(error).toBeInstanceOf(InternalServerError);
|
|
355
|
+
expect(error.statusCode).toBe(500);
|
|
356
|
+
});
|
|
357
357
|
|
|
358
|
-
|
|
359
|
-
|
|
358
|
+
it('should create notImplemented error', () => {
|
|
359
|
+
const error = createError.notImplemented('Not implemented');
|
|
360
360
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
361
|
+
expect(error).toBeInstanceOf(NotImplementedError);
|
|
362
|
+
expect(error.statusCode).toBe(501);
|
|
363
|
+
});
|
|
364
364
|
|
|
365
|
-
|
|
366
|
-
|
|
365
|
+
it('should create badGateway error', () => {
|
|
366
|
+
const error = createError.badGateway('Gateway error');
|
|
367
367
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
368
|
+
expect(error).toBeInstanceOf(BadGatewayError);
|
|
369
|
+
expect(error.statusCode).toBe(502);
|
|
370
|
+
});
|
|
371
371
|
|
|
372
|
-
|
|
373
|
-
|
|
372
|
+
it('should create serviceUnavailable error', () => {
|
|
373
|
+
const error = createError.serviceUnavailable('Service down', 300);
|
|
374
374
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
375
|
+
expect(error).toBeInstanceOf(ServiceUnavailableError);
|
|
376
|
+
expect(error.statusCode).toBe(503);
|
|
377
|
+
});
|
|
378
378
|
|
|
379
|
-
|
|
380
|
-
|
|
379
|
+
it('should create gatewayTimeout error', () => {
|
|
380
|
+
const error = createError.gatewayTimeout('Timeout');
|
|
381
381
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
382
|
+
expect(error).toBeInstanceOf(GatewayTimeoutError);
|
|
383
|
+
expect(error.statusCode).toBe(504);
|
|
384
|
+
});
|
|
385
385
|
});
|
|
386
386
|
|
|
387
387
|
describe('createHttpError function', () => {
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
388
|
+
it('should create error with valid status code', () => {
|
|
389
|
+
const error = createHttpError(404, 'Not found');
|
|
390
|
+
|
|
391
|
+
expect(error).toBeInstanceOf(NotFoundError);
|
|
392
|
+
expect(error.statusCode).toBe(404);
|
|
393
|
+
expect(error.message).toBe('Not found');
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it('should create error with type-safe options', () => {
|
|
397
|
+
const error = createHttpError(429, 'Rate limited', { retryAfter: 60 });
|
|
398
|
+
|
|
399
|
+
expect(error).toBeInstanceOf(TooManyRequestsError);
|
|
400
|
+
expect(error.details).toEqual({ retryAfter: 60 });
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
it('should create error with validation errors', () => {
|
|
404
|
+
const error = createHttpError(422, 'Validation failed', {
|
|
405
|
+
validationErrors: { email: 'Invalid' },
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
expect(error).toBeInstanceOf(UnprocessableEntityError);
|
|
409
|
+
expect(error.details).toEqual({
|
|
410
|
+
validationErrors: { email: 'Invalid' },
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
it('should create error with allowed methods', () => {
|
|
415
|
+
const error = createHttpError(405, 'Method not allowed', {
|
|
416
|
+
allowedMethods: ['GET', 'POST'],
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
expect(error).toBeInstanceOf(MethodNotAllowedError);
|
|
420
|
+
expect(error.details).toEqual({ allowedMethods: ['GET', 'POST'] });
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
it('should create generic error for unknown status code', () => {
|
|
424
|
+
const error = createHttpError(418, 'I am a teapot');
|
|
425
|
+
|
|
426
|
+
expect(error).toBeInstanceOf(HttpError);
|
|
427
|
+
expect(error.statusCode).toBe(418);
|
|
428
|
+
expect(error.message).toBe('I am a teapot');
|
|
429
|
+
});
|
|
430
430
|
});
|
|
431
431
|
|
|
432
432
|
describe('Type guards', () => {
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
433
|
+
describe('isHttpError', () => {
|
|
434
|
+
it('should return true for HttpError instances', () => {
|
|
435
|
+
const error = new HttpError(400);
|
|
436
436
|
|
|
437
|
-
|
|
438
|
-
|
|
437
|
+
expect(isHttpError(error)).toBe(true);
|
|
438
|
+
});
|
|
439
439
|
|
|
440
|
-
|
|
441
|
-
|
|
440
|
+
it('should return true for HttpError subclasses', () => {
|
|
441
|
+
const error = new NotFoundError();
|
|
442
442
|
|
|
443
|
-
|
|
444
|
-
|
|
443
|
+
expect(isHttpError(error)).toBe(true);
|
|
444
|
+
});
|
|
445
445
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
446
|
+
it('should return true for duck-typed errors', () => {
|
|
447
|
+
const error = {
|
|
448
|
+
statusCode: 400,
|
|
449
|
+
statusMessage: 'Bad Request',
|
|
450
|
+
message: 'Error',
|
|
451
|
+
isHttpError: true,
|
|
452
|
+
};
|
|
453
453
|
|
|
454
|
-
|
|
455
|
-
|
|
454
|
+
expect(isHttpError(error)).toBe(true);
|
|
455
|
+
});
|
|
456
456
|
|
|
457
|
-
|
|
458
|
-
|
|
457
|
+
it('should return false for regular errors', () => {
|
|
458
|
+
const error = new Error('Regular error');
|
|
459
459
|
|
|
460
|
-
|
|
461
|
-
|
|
460
|
+
expect(isHttpError(error)).toBe(false);
|
|
461
|
+
});
|
|
462
462
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
463
|
+
it('should return false for non-errors', () => {
|
|
464
|
+
expect(isHttpError(null)).toBe(false);
|
|
465
|
+
expect(isHttpError(undefined)).toBe(false);
|
|
466
|
+
expect(isHttpError('string')).toBe(false);
|
|
467
|
+
expect(isHttpError(123)).toBe(false);
|
|
468
|
+
expect(isHttpError({})).toBe(false);
|
|
469
|
+
});
|
|
470
|
+
});
|
|
471
471
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
472
|
+
describe('isClientError', () => {
|
|
473
|
+
it('should return true for 4xx errors', () => {
|
|
474
|
+
const error = new BadRequestError();
|
|
475
475
|
|
|
476
|
-
|
|
477
|
-
|
|
476
|
+
expect(isClientError(error)).toBe(true);
|
|
477
|
+
});
|
|
478
478
|
|
|
479
|
-
|
|
480
|
-
|
|
479
|
+
it('should return false for 5xx errors', () => {
|
|
480
|
+
const error = new InternalServerError();
|
|
481
481
|
|
|
482
|
-
|
|
483
|
-
|
|
482
|
+
expect(isClientError(error)).toBe(false);
|
|
483
|
+
});
|
|
484
484
|
|
|
485
|
-
|
|
486
|
-
|
|
485
|
+
it('should return false for non-http errors', () => {
|
|
486
|
+
const error = new Error('Regular error');
|
|
487
487
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
488
|
+
expect(isClientError(error)).toBe(false);
|
|
489
|
+
});
|
|
490
|
+
});
|
|
491
491
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
492
|
+
describe('isServerError', () => {
|
|
493
|
+
it('should return true for 5xx errors', () => {
|
|
494
|
+
const error = new InternalServerError();
|
|
495
495
|
|
|
496
|
-
|
|
497
|
-
|
|
496
|
+
expect(isServerError(error)).toBe(true);
|
|
497
|
+
});
|
|
498
498
|
|
|
499
|
-
|
|
500
|
-
|
|
499
|
+
it('should return false for 4xx errors', () => {
|
|
500
|
+
const error = new NotFoundError();
|
|
501
501
|
|
|
502
|
-
|
|
503
|
-
|
|
502
|
+
expect(isServerError(error)).toBe(false);
|
|
503
|
+
});
|
|
504
504
|
|
|
505
|
-
|
|
506
|
-
|
|
505
|
+
it('should return false for non-http errors', () => {
|
|
506
|
+
const error = new Error('Regular error');
|
|
507
507
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
508
|
+
expect(isServerError(error)).toBe(false);
|
|
509
|
+
});
|
|
510
|
+
});
|
|
511
511
|
});
|
|
512
512
|
|
|
513
513
|
describe('wrapError', () => {
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
514
|
+
it('should return HttpError unchanged', () => {
|
|
515
|
+
const original = new NotFoundError('Not found');
|
|
516
|
+
const wrapped = wrapError(original);
|
|
517
|
+
|
|
518
|
+
expect(wrapped).toBe(original);
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
it('should wrap unknown errors as 500', () => {
|
|
522
|
+
const original = new Error('Something went wrong');
|
|
523
|
+
const wrapped = wrapError(original);
|
|
524
|
+
|
|
525
|
+
expect(wrapped).toBeInstanceOf(HttpError);
|
|
526
|
+
expect(wrapped.statusCode).toBe(500);
|
|
527
|
+
expect(wrapped.message).toBe('An unknown error occurred');
|
|
528
|
+
expect(wrapped.details?.originalError).toBe(original);
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
it('should wrap with custom status code', () => {
|
|
532
|
+
const original = new Error('Service unavailable');
|
|
533
|
+
const wrapped = wrapError(original, 503);
|
|
534
|
+
|
|
535
|
+
expect(wrapped.statusCode).toBe(503);
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
it('should wrap with custom message', () => {
|
|
539
|
+
const original = new Error('Database error');
|
|
540
|
+
const wrapped = wrapError(original, 500, 'Failed to query database');
|
|
541
|
+
|
|
542
|
+
expect(wrapped.statusCode).toBe(500);
|
|
543
|
+
expect(wrapped.message).toBe('Failed to query database');
|
|
544
|
+
expect(wrapped.details?.originalError).toBe(original);
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
it('should handle non-error values', () => {
|
|
548
|
+
const wrapped = wrapError('string error');
|
|
549
|
+
|
|
550
|
+
expect(wrapped).toBeInstanceOf(HttpError);
|
|
551
|
+
expect(wrapped.statusCode).toBe(500);
|
|
552
|
+
expect(wrapped.details?.originalError).toBe('string error');
|
|
553
|
+
});
|
|
554
554
|
});
|
|
555
555
|
|
|
556
556
|
describe('HttpStatusCode enum', () => {
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
557
|
+
it('should have 2xx success codes', () => {
|
|
558
|
+
expect(HttpStatusCode.OK).toBe(200);
|
|
559
|
+
expect(HttpStatusCode.CREATED).toBe(201);
|
|
560
|
+
expect(HttpStatusCode.ACCEPTED).toBe(202);
|
|
561
|
+
expect(HttpStatusCode.NO_CONTENT).toBe(204);
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
it('should have 3xx redirect codes', () => {
|
|
565
|
+
expect(HttpStatusCode.MOVED_PERMANENTLY).toBe(301);
|
|
566
|
+
expect(HttpStatusCode.FOUND).toBe(302);
|
|
567
|
+
expect(HttpStatusCode.NOT_MODIFIED).toBe(304);
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
it('should have 4xx client error codes', () => {
|
|
571
|
+
expect(HttpStatusCode.BAD_REQUEST).toBe(400);
|
|
572
|
+
expect(HttpStatusCode.UNAUTHORIZED).toBe(401);
|
|
573
|
+
expect(HttpStatusCode.FORBIDDEN).toBe(403);
|
|
574
|
+
expect(HttpStatusCode.NOT_FOUND).toBe(404);
|
|
575
|
+
expect(HttpStatusCode.METHOD_NOT_ALLOWED).toBe(405);
|
|
576
|
+
expect(HttpStatusCode.NOT_ACCEPTABLE).toBe(406);
|
|
577
|
+
expect(HttpStatusCode.REQUEST_TIMEOUT).toBe(408);
|
|
578
|
+
expect(HttpStatusCode.CONFLICT).toBe(409);
|
|
579
|
+
expect(HttpStatusCode.GONE).toBe(410);
|
|
580
|
+
expect(HttpStatusCode.UNPROCESSABLE_ENTITY).toBe(422);
|
|
581
|
+
expect(HttpStatusCode.TOO_MANY_REQUESTS).toBe(429);
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
it('should have 5xx server error codes', () => {
|
|
585
|
+
expect(HttpStatusCode.INTERNAL_SERVER_ERROR).toBe(500);
|
|
586
|
+
expect(HttpStatusCode.NOT_IMPLEMENTED).toBe(501);
|
|
587
|
+
expect(HttpStatusCode.BAD_GATEWAY).toBe(502);
|
|
588
|
+
expect(HttpStatusCode.SERVICE_UNAVAILABLE).toBe(503);
|
|
589
|
+
expect(HttpStatusCode.GATEWAY_TIMEOUT).toBe(504);
|
|
590
|
+
});
|
|
591
591
|
});
|