@onlineapps/conn-infra-error-handler 1.0.0 → 1.0.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.
- package/API.md +0 -0
- package/README.md +225 -0
- package/onlineapps-conn-infra-error-handler-1.0.0.tgz +0 -0
- package/package.json +5 -4
- package/src/index.js +208 -577
- package/{test → tests}/component/error-handling-flow.test.js +56 -34
- package/{test → tests}/unit/ErrorHandlerConnector.test.js +106 -100
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const ErrorHandlerConnector = require('../../src/index');
|
|
4
4
|
|
|
5
|
-
describe('Error Handler Component Tests', () => {
|
|
5
|
+
describe('Error Handler Component Tests @component', () => {
|
|
6
6
|
let errorHandler;
|
|
7
7
|
let mockMQClient;
|
|
8
8
|
let mockLogger;
|
|
@@ -15,8 +15,14 @@ describe('Error Handler Component Tests', () => {
|
|
|
15
15
|
close: jest.fn().mockResolvedValue()
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
-
// Create mock
|
|
18
|
+
// Create mock monitoring (conn-base-monitoring format)
|
|
19
19
|
mockLogger = {
|
|
20
|
+
logger: {
|
|
21
|
+
error: jest.fn(),
|
|
22
|
+
warn: jest.fn(),
|
|
23
|
+
info: jest.fn(),
|
|
24
|
+
debug: jest.fn()
|
|
25
|
+
},
|
|
20
26
|
error: jest.fn(),
|
|
21
27
|
warn: jest.fn(),
|
|
22
28
|
info: jest.fn(),
|
|
@@ -24,12 +30,17 @@ describe('Error Handler Component Tests', () => {
|
|
|
24
30
|
};
|
|
25
31
|
|
|
26
32
|
errorHandler = new ErrorHandlerConnector({
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
+
serviceName: 'test-service',
|
|
34
|
+
serviceVersion: '1.0.0',
|
|
35
|
+
environment: 'test',
|
|
36
|
+
monitoring: mockLogger,
|
|
37
|
+
handling: {
|
|
38
|
+
maxRetries: 3,
|
|
39
|
+
retryDelay: 50,
|
|
40
|
+
retryMultiplier: 2,
|
|
41
|
+
dlqEnabled: true,
|
|
42
|
+
mqClient: mockMQClient
|
|
43
|
+
}
|
|
33
44
|
});
|
|
34
45
|
});
|
|
35
46
|
|
|
@@ -41,7 +52,7 @@ describe('Error Handler Component Tests', () => {
|
|
|
41
52
|
|
|
42
53
|
expect(result).toEqual({ success: true });
|
|
43
54
|
expect(operation).toHaveBeenCalledTimes(1);
|
|
44
|
-
expect(mockLogger.error).not.toHaveBeenCalled();
|
|
55
|
+
expect(mockLogger.logger.error).not.toHaveBeenCalled();
|
|
45
56
|
});
|
|
46
57
|
|
|
47
58
|
it('should retry transient errors and eventually succeed', async () => {
|
|
@@ -69,16 +80,27 @@ describe('Error Handler Component Tests', () => {
|
|
|
69
80
|
// Note: The actual implementation doesn't log max retries exceeded
|
|
70
81
|
});
|
|
71
82
|
|
|
72
|
-
it('should
|
|
83
|
+
it('should classify business errors correctly', async () => {
|
|
73
84
|
const error = Object.assign(new Error('Not Found'), { statusCode: 404 });
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
85
|
+
|
|
86
|
+
// Business errors should be classified correctly
|
|
87
|
+
const errorType = errorHandler.classifyError(error);
|
|
88
|
+
expect(errorType).toBe('BUSINESS');
|
|
89
|
+
|
|
90
|
+
// Business errors should not be retried
|
|
91
|
+
const shouldRetry = errorHandler.shouldRetry(error);
|
|
92
|
+
expect(shouldRetry).toBe(false);
|
|
93
|
+
|
|
94
|
+
// When using handleError, business errors should route to DLQ, not retry
|
|
95
|
+
const result = await errorHandler.handleError({
|
|
96
|
+
moduleName: 'Test',
|
|
97
|
+
operation: 'test',
|
|
98
|
+
error: error
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
expect(result.errorType).toBe('BUSINESS');
|
|
102
|
+
expect(result.action).toBe('dlq');
|
|
103
|
+
expect(result.shouldRetry).toBe(false);
|
|
82
104
|
});
|
|
83
105
|
});
|
|
84
106
|
|
|
@@ -87,7 +109,9 @@ describe('Error Handler Component Tests', () => {
|
|
|
87
109
|
const operation = jest.fn()
|
|
88
110
|
.mockRejectedValue(new Error('Service unavailable'));
|
|
89
111
|
|
|
90
|
-
|
|
112
|
+
// Use executeWithCircuitBreaker instead of getCircuitBreaker
|
|
113
|
+
// For testing circuit breaker behavior, we'll use the core directly
|
|
114
|
+
const breaker = errorHandler.core.circuitBreaker.getCircuitBreaker('test-breaker', operation, {
|
|
91
115
|
errorThresholdPercentage: 50,
|
|
92
116
|
timeout: 100,
|
|
93
117
|
volumeThreshold: 3
|
|
@@ -110,7 +134,7 @@ describe('Error Handler Component Tests', () => {
|
|
|
110
134
|
const operation = jest.fn().mockRejectedValue(new Error('Error'));
|
|
111
135
|
const fallback = jest.fn().mockResolvedValue({ fallback: true });
|
|
112
136
|
|
|
113
|
-
const breaker = errorHandler.getCircuitBreaker('test-breaker-fallback', operation, {
|
|
137
|
+
const breaker = errorHandler.core.circuitBreaker.getCircuitBreaker('test-breaker-fallback', operation, {
|
|
114
138
|
errorThresholdPercentage: 50,
|
|
115
139
|
volumeThreshold: 1
|
|
116
140
|
});
|
|
@@ -147,14 +171,12 @@ describe('Error Handler Component Tests', () => {
|
|
|
147
171
|
|
|
148
172
|
// Execute compensation
|
|
149
173
|
const result = await errorHandler.executeCompensation('test-operation', {
|
|
150
|
-
|
|
151
|
-
context: { service: 'test-service' }
|
|
174
|
+
service: 'test-service'
|
|
152
175
|
});
|
|
153
176
|
|
|
154
177
|
expect(result).toEqual({ compensated: true });
|
|
155
178
|
expect(compensation).toHaveBeenCalledWith({
|
|
156
|
-
|
|
157
|
-
context: { service: 'test-service' }
|
|
179
|
+
service: 'test-service'
|
|
158
180
|
});
|
|
159
181
|
});
|
|
160
182
|
|
|
@@ -167,8 +189,7 @@ describe('Error Handler Component Tests', () => {
|
|
|
167
189
|
// Execute compensation will throw if compensation fails
|
|
168
190
|
await expect(
|
|
169
191
|
errorHandler.executeCompensation('failing-operation', {
|
|
170
|
-
|
|
171
|
-
context: { service: 'test-service' }
|
|
192
|
+
service: 'test-service'
|
|
172
193
|
})
|
|
173
194
|
).rejects.toThrow('Compensation failed');
|
|
174
195
|
|
|
@@ -177,8 +198,7 @@ describe('Error Handler Component Tests', () => {
|
|
|
177
198
|
|
|
178
199
|
it('should not execute compensation if not registered', async () => {
|
|
179
200
|
const result = await errorHandler.executeCompensation('unregistered-operation', {
|
|
180
|
-
|
|
181
|
-
context: { service: 'test-service' }
|
|
201
|
+
service: 'test-service'
|
|
182
202
|
});
|
|
183
203
|
|
|
184
204
|
expect(result).toBeNull();
|
|
@@ -246,21 +266,23 @@ describe('Error Handler Component Tests', () => {
|
|
|
246
266
|
);
|
|
247
267
|
});
|
|
248
268
|
|
|
249
|
-
it('should handle DLQ publish failure', async () => {
|
|
269
|
+
it('should handle DLQ publish failure gracefully', async () => {
|
|
250
270
|
mockMQClient.publish.mockRejectedValue(new Error('DLQ publish failed'));
|
|
271
|
+
const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
|
|
251
272
|
|
|
252
273
|
const error = Object.assign(new Error('Business error'), { statusCode: 404 });
|
|
253
274
|
const message = {
|
|
254
275
|
messageId: 'msg-789',
|
|
255
|
-
service: 'test-service'
|
|
276
|
+
service: 'test-service',
|
|
277
|
+
queue: 'test.queue'
|
|
256
278
|
};
|
|
257
279
|
|
|
258
|
-
// routeToDLQ
|
|
259
|
-
await
|
|
260
|
-
errorHandler.routeToDLQ(mockMQClient, message, error)
|
|
261
|
-
).rejects.toThrow('DLQ publish failed');
|
|
280
|
+
// routeToDLQ should not throw if publish fails (graceful degradation)
|
|
281
|
+
await errorHandler.routeToDLQ(mockMQClient, message, error);
|
|
262
282
|
|
|
263
283
|
expect(mockMQClient.publish).toHaveBeenCalled();
|
|
284
|
+
expect(consoleSpy).toHaveBeenCalled();
|
|
285
|
+
consoleSpy.mockRestore();
|
|
264
286
|
});
|
|
265
287
|
});
|
|
266
288
|
|
|
@@ -2,48 +2,75 @@
|
|
|
2
2
|
|
|
3
3
|
const ErrorHandlerConnector = require('../../src/index');
|
|
4
4
|
|
|
5
|
-
// Mock
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
describe('ErrorHandlerConnector - Unit Tests', () => {
|
|
5
|
+
// Mock monitoring connector
|
|
6
|
+
const mockMonitoring = {
|
|
7
|
+
logger: {
|
|
8
|
+
error: jest.fn(),
|
|
9
|
+
warn: jest.fn(),
|
|
10
|
+
info: jest.fn(),
|
|
11
|
+
log: jest.fn()
|
|
12
|
+
},
|
|
13
|
+
error: jest.fn(),
|
|
14
|
+
warn: jest.fn(),
|
|
15
|
+
info: jest.fn()
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
describe('ErrorHandlerConnector - Unit Tests @unit', () => {
|
|
19
19
|
let errorHandler;
|
|
20
20
|
|
|
21
21
|
beforeEach(() => {
|
|
22
22
|
jest.clearAllMocks();
|
|
23
23
|
errorHandler = new ErrorHandlerConnector({
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
serviceName: 'test-service',
|
|
25
|
+
serviceVersion: '1.0.0',
|
|
26
|
+
environment: 'test',
|
|
27
|
+
monitoring: mockMonitoring,
|
|
28
|
+
handling: {
|
|
29
|
+
maxRetries: 3,
|
|
30
|
+
retryDelay: 100,
|
|
31
|
+
retryMultiplier: 2
|
|
32
|
+
}
|
|
27
33
|
});
|
|
28
34
|
});
|
|
29
35
|
|
|
30
36
|
describe('Constructor', () => {
|
|
31
|
-
it('should create instance with
|
|
32
|
-
const handler = new ErrorHandlerConnector(
|
|
37
|
+
it('should create instance with required config', () => {
|
|
38
|
+
const handler = new ErrorHandlerConnector({
|
|
39
|
+
serviceName: 'test-service',
|
|
40
|
+
monitoring: mockMonitoring
|
|
41
|
+
});
|
|
33
42
|
expect(handler).toBeInstanceOf(ErrorHandlerConnector);
|
|
34
|
-
expect(handler.
|
|
35
|
-
|
|
43
|
+
expect(handler.core).toBeDefined();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should throw if serviceName missing', () => {
|
|
47
|
+
expect(() => {
|
|
48
|
+
new ErrorHandlerConnector({
|
|
49
|
+
monitoring: mockMonitoring
|
|
50
|
+
});
|
|
51
|
+
}).toThrow('serviceName is required');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should throw if monitoring missing', () => {
|
|
55
|
+
expect(() => {
|
|
56
|
+
new ErrorHandlerConnector({
|
|
57
|
+
serviceName: 'test-service'
|
|
58
|
+
});
|
|
59
|
+
}).toThrow('monitoring instance is required');
|
|
36
60
|
});
|
|
37
61
|
|
|
38
62
|
it('should create instance with custom config', () => {
|
|
39
63
|
const handler = new ErrorHandlerConnector({
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
64
|
+
serviceName: 'test-service',
|
|
65
|
+
monitoring: mockMonitoring,
|
|
66
|
+
handling: {
|
|
67
|
+
maxRetries: 5,
|
|
68
|
+
retryDelay: 2000,
|
|
69
|
+
errorThreshold: 60
|
|
70
|
+
}
|
|
43
71
|
});
|
|
44
|
-
expect(handler.maxRetries).toBe(5);
|
|
45
|
-
expect(handler.retryDelay).toBe(2000);
|
|
46
|
-
expect(handler.circuitBreakerOptions.errorThresholdPercentage).toBe(60);
|
|
72
|
+
expect(handler.core.retryHandler.maxRetries).toBe(5);
|
|
73
|
+
expect(handler.core.retryHandler.retryDelay).toBe(2000);
|
|
47
74
|
});
|
|
48
75
|
});
|
|
49
76
|
|
|
@@ -81,14 +108,6 @@ describe('ErrorHandlerConnector - Unit Tests', () => {
|
|
|
81
108
|
const type = errorHandler.classifyError(error);
|
|
82
109
|
expect(type).toBe('UNKNOWN');
|
|
83
110
|
});
|
|
84
|
-
|
|
85
|
-
it('should handle errors with response property', () => {
|
|
86
|
-
const error = new Error('API Error');
|
|
87
|
-
// classifyError checks error.statusCode, not error.response.status
|
|
88
|
-
error.statusCode = 503;
|
|
89
|
-
const type = errorHandler.classifyError(error);
|
|
90
|
-
expect(type).toBe('TRANSIENT');
|
|
91
|
-
});
|
|
92
111
|
});
|
|
93
112
|
|
|
94
113
|
describe('shouldRetry', () => {
|
|
@@ -121,18 +140,10 @@ describe('ErrorHandlerConnector - Unit Tests', () => {
|
|
|
121
140
|
error.statusCode = 400;
|
|
122
141
|
expect(errorHandler.shouldRetry(error)).toBe(false);
|
|
123
142
|
});
|
|
124
|
-
|
|
125
|
-
it('should not retry FATAL errors', () => {
|
|
126
|
-
const error = new Error('Fatal error');
|
|
127
|
-
error.type = 'FATAL';
|
|
128
|
-
expect(errorHandler.shouldRetry(error)).toBe(false);
|
|
129
|
-
});
|
|
130
143
|
});
|
|
131
144
|
|
|
132
145
|
describe('calculateBackoff', () => {
|
|
133
146
|
it('should calculate exponential backoff', () => {
|
|
134
|
-
// calculateBackoff uses: retryDelay * Math.pow(retryMultiplier, attempts - 1)
|
|
135
|
-
// With retryDelay=100, retryMultiplier=2:
|
|
136
147
|
const delay1 = errorHandler.calculateBackoff(1); // 100 * 2^0 = 100
|
|
137
148
|
const delay2 = errorHandler.calculateBackoff(2); // 100 * 2^1 = 200
|
|
138
149
|
const delay3 = errorHandler.calculateBackoff(3); // 100 * 2^2 = 400
|
|
@@ -141,12 +152,6 @@ describe('ErrorHandlerConnector - Unit Tests', () => {
|
|
|
141
152
|
expect(delay2).toBe(200);
|
|
142
153
|
expect(delay3).toBe(400);
|
|
143
154
|
});
|
|
144
|
-
|
|
145
|
-
it('should not exceed maxRetryDelay', () => {
|
|
146
|
-
errorHandler.maxRetryDelay = 5000;
|
|
147
|
-
const delay = errorHandler.calculateBackoff(10);
|
|
148
|
-
expect(delay).toBeLessThanOrEqual(5000);
|
|
149
|
-
});
|
|
150
155
|
});
|
|
151
156
|
|
|
152
157
|
describe('executeWithRetry', () => {
|
|
@@ -169,58 +174,68 @@ describe('ErrorHandlerConnector - Unit Tests', () => {
|
|
|
169
174
|
expect(fn).toHaveBeenCalledTimes(2);
|
|
170
175
|
});
|
|
171
176
|
|
|
172
|
-
it('should
|
|
177
|
+
it('should classify business errors correctly', async () => {
|
|
173
178
|
const error = Object.assign(new Error('Not Found'), { statusCode: 404 });
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
expect(
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
await expect(errorHandler.executeWithRetry(fn)).rejects.toThrow('Network error');
|
|
187
|
-
expect(fn).toHaveBeenCalledTimes(2); // executeWithRetry uses attempts, not initial + retries
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
it('should use custom max attempts from options', async () => {
|
|
191
|
-
const error = Object.assign(new Error('Network error'), { code: 'ECONNRESET' });
|
|
192
|
-
const fn = jest.fn().mockRejectedValue(error);
|
|
193
|
-
|
|
194
|
-
await expect(errorHandler.executeWithRetry(fn, { maxRetries: 1 })).rejects.toThrow('Network error');
|
|
195
|
-
expect(fn).toHaveBeenCalledTimes(1);
|
|
179
|
+
|
|
180
|
+
// Business errors should be classified correctly
|
|
181
|
+
const errorType = errorHandler.classifyError(error);
|
|
182
|
+
expect(errorType).toBe('BUSINESS');
|
|
183
|
+
|
|
184
|
+
// Business errors should not be retried (according to shouldRetry)
|
|
185
|
+
const shouldRetry = errorHandler.shouldRetry(error);
|
|
186
|
+
expect(shouldRetry).toBe(false);
|
|
187
|
+
|
|
188
|
+
// Note: executeWithRetry uses RetryHandler which retries all errors
|
|
189
|
+
// For business errors, use handleError which routes to DLQ instead
|
|
196
190
|
});
|
|
197
191
|
});
|
|
198
192
|
|
|
199
|
-
describe('
|
|
200
|
-
it('should
|
|
193
|
+
describe('executeWithCircuitBreaker', () => {
|
|
194
|
+
it('should execute function with circuit breaker', async () => {
|
|
201
195
|
const fn = jest.fn().mockResolvedValue('success');
|
|
202
|
-
const
|
|
196
|
+
const result = await errorHandler.executeWithCircuitBreaker('test-breaker', fn);
|
|
197
|
+
|
|
198
|
+
expect(result).toBe('success');
|
|
199
|
+
expect(fn).toHaveBeenCalled();
|
|
200
|
+
});
|
|
203
201
|
|
|
204
|
-
|
|
205
|
-
|
|
202
|
+
it('should handle circuit breaker errors', async () => {
|
|
203
|
+
const fn = jest.fn().mockRejectedValue(new Error('Service unavailable'));
|
|
204
|
+
|
|
205
|
+
await expect(
|
|
206
|
+
errorHandler.executeWithCircuitBreaker('test-breaker', fn)
|
|
207
|
+
).rejects.toThrow('Service unavailable');
|
|
206
208
|
});
|
|
209
|
+
});
|
|
207
210
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
211
|
+
describe('logError', () => {
|
|
212
|
+
it('should log error via monitoring', async () => {
|
|
213
|
+
const error = new Error('Test error');
|
|
214
|
+
|
|
215
|
+
await errorHandler.logError({
|
|
216
|
+
moduleName: 'TestModule',
|
|
217
|
+
operation: 'testOperation',
|
|
218
|
+
error: error
|
|
213
219
|
});
|
|
214
|
-
|
|
215
|
-
expect(
|
|
220
|
+
|
|
221
|
+
expect(mockMonitoring.logger.error).toHaveBeenCalled();
|
|
216
222
|
});
|
|
223
|
+
});
|
|
217
224
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
225
|
+
describe('handleError', () => {
|
|
226
|
+
it('should handle and classify error', async () => {
|
|
227
|
+
const error = new Error('Connection refused');
|
|
228
|
+
error.code = 'ECONNREFUSED';
|
|
229
|
+
|
|
230
|
+
const result = await errorHandler.handleError({
|
|
231
|
+
moduleName: 'TestModule',
|
|
232
|
+
operation: 'testOperation',
|
|
233
|
+
error: error
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
expect(result.errorType).toBe('TRANSIENT');
|
|
237
|
+
expect(result.action).toBe('retry');
|
|
238
|
+
expect(mockMonitoring.logger.error).toHaveBeenCalled();
|
|
224
239
|
});
|
|
225
240
|
});
|
|
226
241
|
|
|
@@ -234,20 +249,11 @@ describe('ErrorHandlerConnector - Unit Tests', () => {
|
|
|
234
249
|
expect(response.success).toBe(false);
|
|
235
250
|
expect(response.error.message).toBe('Test error');
|
|
236
251
|
expect(response.error.type).toBe('UNKNOWN');
|
|
237
|
-
expect(response.error.retryable).toBe(false);
|
|
238
252
|
expect(response.error.service).toBe('test-service');
|
|
239
253
|
expect(response.error.operation).toBe('test-op');
|
|
240
254
|
expect(response.error.timestamp).toBeDefined();
|
|
241
255
|
});
|
|
242
256
|
|
|
243
|
-
it('should include error code', () => {
|
|
244
|
-
const error = new Error('Test error');
|
|
245
|
-
error.code = 'TEST_ERROR';
|
|
246
|
-
const response = errorHandler.createErrorResponse(error);
|
|
247
|
-
|
|
248
|
-
expect(response.error.code).toBe('TEST_ERROR');
|
|
249
|
-
});
|
|
250
|
-
|
|
251
257
|
it('should classify error type correctly', () => {
|
|
252
258
|
const error = Object.assign(new Error('Network error'), { code: 'ECONNRESET' });
|
|
253
259
|
const response = errorHandler.createErrorResponse(error);
|
|
@@ -291,4 +297,4 @@ describe('ErrorHandlerConnector - Unit Tests', () => {
|
|
|
291
297
|
expect(ErrorCodes[404]).toBe('BUSINESS');
|
|
292
298
|
});
|
|
293
299
|
});
|
|
294
|
-
});
|
|
300
|
+
});
|