@onlineapps/error-handler-core 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 +125 -0
- package/jest.config.js +23 -0
- package/onlineapps-error-handler-core-1.0.0.tgz +0 -0
- package/package.json +34 -0
- package/src/CircuitBreaker.js +110 -0
- package/src/CompensationHandler.js +104 -0
- package/src/DLQRouter.js +98 -0
- package/src/ErrorClassifier.js +142 -0
- package/src/ErrorHandlerCore.js +329 -0
- package/src/RetryHandler.js +74 -0
- package/src/UnifiedLogSchema.js +105 -0
- package/src/index.js +30 -0
- package/tests/unit/DLQRouter.test.js +94 -0
- package/tests/unit/ErrorClassifier.test.js +82 -0
- package/tests/unit/ErrorHandlerCore.test.js +183 -0
- package/tests/unit/RetryHandler.test.js +81 -0
- package/tests/unit/UnifiedLogSchema.test.js +75 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ErrorClassifier Unit Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const { ErrorClassifier, ErrorTypes } = require('../../src/ErrorClassifier');
|
|
6
|
+
|
|
7
|
+
describe('ErrorClassifier @unit', () => {
|
|
8
|
+
let classifier;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
classifier = new ErrorClassifier();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
describe('classify', () => {
|
|
15
|
+
test('should classify ECONNREFUSED as TRANSIENT', () => {
|
|
16
|
+
const error = new Error('Connection refused');
|
|
17
|
+
error.code = 'ECONNREFUSED';
|
|
18
|
+
|
|
19
|
+
expect(classifier.classify(error)).toBe(ErrorTypes.TRANSIENT);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('should classify HTTP 404 as BUSINESS', () => {
|
|
23
|
+
const error = new Error('Not Found');
|
|
24
|
+
error.statusCode = 404;
|
|
25
|
+
|
|
26
|
+
expect(classifier.classify(error)).toBe(ErrorTypes.BUSINESS);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('should classify timeout message as TIMEOUT', () => {
|
|
30
|
+
const error = new Error('Operation timeout');
|
|
31
|
+
|
|
32
|
+
expect(classifier.classify(error)).toBe(ErrorTypes.TIMEOUT);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('should classify explicit type', () => {
|
|
36
|
+
const error = new Error('Custom error');
|
|
37
|
+
error.type = 'BUSINESS';
|
|
38
|
+
|
|
39
|
+
expect(classifier.classify(error)).toBe(ErrorTypes.BUSINESS);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('should return UNKNOWN for unclassified error', () => {
|
|
43
|
+
const error = new Error('Some random error');
|
|
44
|
+
|
|
45
|
+
expect(classifier.classify(error)).toBe(ErrorTypes.UNKNOWN);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('should handle null error', () => {
|
|
49
|
+
expect(classifier.classify(null)).toBe(ErrorTypes.UNKNOWN);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('shouldRetry', () => {
|
|
54
|
+
test('should retry TRANSIENT errors', () => {
|
|
55
|
+
const error = new Error('Connection refused');
|
|
56
|
+
error.code = 'ECONNREFUSED';
|
|
57
|
+
|
|
58
|
+
expect(classifier.shouldRetry(error, 0, 3)).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('should not retry after max attempts', () => {
|
|
62
|
+
const error = new Error('Connection refused');
|
|
63
|
+
error.code = 'ECONNREFUSED';
|
|
64
|
+
|
|
65
|
+
expect(classifier.shouldRetry(error, 3, 3)).toBe(false);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('should not retry BUSINESS errors', () => {
|
|
69
|
+
const error = new Error('Not Found');
|
|
70
|
+
error.statusCode = 404;
|
|
71
|
+
|
|
72
|
+
expect(classifier.shouldRetry(error, 0, 3)).toBe(false);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('should retry TIMEOUT errors', () => {
|
|
76
|
+
const error = new Error('Operation timeout');
|
|
77
|
+
|
|
78
|
+
expect(classifier.shouldRetry(error, 0, 3)).toBe(true);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ErrorHandlerCore Unit Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const ErrorHandlerCore = require('../../src/ErrorHandlerCore');
|
|
6
|
+
const { ErrorTypes } = require('../../src/ErrorClassifier');
|
|
7
|
+
|
|
8
|
+
describe('ErrorHandlerCore @unit', () => {
|
|
9
|
+
let errorHandler;
|
|
10
|
+
let mockMonitoring;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
mockMonitoring = {
|
|
14
|
+
logger: {
|
|
15
|
+
error: jest.fn(),
|
|
16
|
+
warn: jest.fn(),
|
|
17
|
+
info: jest.fn()
|
|
18
|
+
},
|
|
19
|
+
// Fallback for conn-base-monitoring wrapper
|
|
20
|
+
error: jest.fn(),
|
|
21
|
+
warn: jest.fn(),
|
|
22
|
+
info: jest.fn()
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
errorHandler = new ErrorHandlerCore({
|
|
26
|
+
serviceName: 'test-service',
|
|
27
|
+
serviceVersion: '1.0.0',
|
|
28
|
+
environment: 'test',
|
|
29
|
+
monitoring: mockMonitoring,
|
|
30
|
+
handling: {
|
|
31
|
+
maxRetries: 3,
|
|
32
|
+
retryDelay: 100
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('constructor', () => {
|
|
38
|
+
test('should throw if serviceName missing', () => {
|
|
39
|
+
expect(() => {
|
|
40
|
+
new ErrorHandlerCore({
|
|
41
|
+
monitoring: mockMonitoring
|
|
42
|
+
});
|
|
43
|
+
}).toThrow('serviceName is required');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('should throw if monitoring missing', () => {
|
|
47
|
+
expect(() => {
|
|
48
|
+
new ErrorHandlerCore({
|
|
49
|
+
serviceName: 'test'
|
|
50
|
+
});
|
|
51
|
+
}).toThrow('monitoring instance is required');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('should initialize with valid config', () => {
|
|
55
|
+
expect(errorHandler.serviceName).toBe('test-service');
|
|
56
|
+
expect(errorHandler.classifier).toBeDefined();
|
|
57
|
+
expect(errorHandler.retryHandler).toBeDefined();
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe('logError', () => {
|
|
62
|
+
test('should log error via monitoring', async () => {
|
|
63
|
+
const error = new Error('Test error');
|
|
64
|
+
|
|
65
|
+
const result = await errorHandler.logError({
|
|
66
|
+
moduleName: 'TestModule',
|
|
67
|
+
operation: 'testOperation',
|
|
68
|
+
error: error
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
expect(mockMonitoring.logger.error).toHaveBeenCalled();
|
|
72
|
+
expect(result).toHaveProperty('timestamp');
|
|
73
|
+
expect(result).toHaveProperty('error');
|
|
74
|
+
expect(result.error.type).toBeDefined();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('should handle monitoring failure gracefully', async () => {
|
|
78
|
+
// Mock logger.error to throw
|
|
79
|
+
mockMonitoring.logger.error.mockImplementation(() => {
|
|
80
|
+
throw new Error('Monitoring failed');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
|
|
84
|
+
|
|
85
|
+
const error = new Error('Test error');
|
|
86
|
+
await errorHandler.logError({
|
|
87
|
+
moduleName: 'TestModule',
|
|
88
|
+
operation: 'testOperation',
|
|
89
|
+
error: error
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
expect(consoleSpy).toHaveBeenCalled();
|
|
93
|
+
consoleSpy.mockRestore();
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe('handleError', () => {
|
|
98
|
+
test('should classify and log error', async () => {
|
|
99
|
+
const error = new Error('Connection refused');
|
|
100
|
+
error.code = 'ECONNREFUSED';
|
|
101
|
+
|
|
102
|
+
const result = await errorHandler.handleError({
|
|
103
|
+
moduleName: 'TestModule',
|
|
104
|
+
operation: 'testOperation',
|
|
105
|
+
error: error
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
expect(result.errorType).toBe(ErrorTypes.TRANSIENT);
|
|
109
|
+
expect(result.action).toBe('retry');
|
|
110
|
+
expect(result.shouldRetry).toBe(true);
|
|
111
|
+
expect(mockMonitoring.logger.error).toHaveBeenCalled();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('should route to DLQ for BUSINESS errors', async () => {
|
|
115
|
+
const error = new Error('Not Found');
|
|
116
|
+
error.statusCode = 404;
|
|
117
|
+
|
|
118
|
+
// Mock MQ client for DLQ
|
|
119
|
+
const mockMQClient = {
|
|
120
|
+
publish: jest.fn().mockResolvedValue()
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
errorHandler.dlqRouter.mqClient = mockMQClient;
|
|
124
|
+
|
|
125
|
+
const result = await errorHandler.handleError({
|
|
126
|
+
moduleName: 'TestModule',
|
|
127
|
+
operation: 'testOperation',
|
|
128
|
+
error: error,
|
|
129
|
+
context: {
|
|
130
|
+
message: { id: 'msg-123' },
|
|
131
|
+
queue: 'test.queue'
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
expect(result.errorType).toBe(ErrorTypes.BUSINESS);
|
|
136
|
+
expect(result.action).toBe('dlq');
|
|
137
|
+
expect(mockMQClient.publish).toHaveBeenCalled();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test('should throw for FATAL errors', async () => {
|
|
141
|
+
const error = new Error('Fatal error');
|
|
142
|
+
error.type = 'FATAL';
|
|
143
|
+
|
|
144
|
+
const result = await errorHandler.handleError({
|
|
145
|
+
moduleName: 'TestModule',
|
|
146
|
+
operation: 'testOperation',
|
|
147
|
+
error: error
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
expect(result.errorType).toBe(ErrorTypes.FATAL);
|
|
151
|
+
expect(result.action).toBe('throw');
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe('executeWithRetry', () => {
|
|
156
|
+
test('should retry on failure', async () => {
|
|
157
|
+
const fn = jest.fn()
|
|
158
|
+
.mockRejectedValueOnce(new Error('Fail 1'))
|
|
159
|
+
.mockResolvedValue('success');
|
|
160
|
+
|
|
161
|
+
const result = await errorHandler.executeWithRetry({
|
|
162
|
+
moduleName: 'TestModule',
|
|
163
|
+
operation: 'testOperation',
|
|
164
|
+
fn: fn
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
expect(result).toBe('success');
|
|
168
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
169
|
+
expect(mockMonitoring.logger.error).toHaveBeenCalled(); // Logs retry attempt
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe('registerCompensation', () => {
|
|
174
|
+
test('should register compensation handler', () => {
|
|
175
|
+
const handler = jest.fn().mockResolvedValue();
|
|
176
|
+
|
|
177
|
+
errorHandler.registerCompensation('test-operation', handler);
|
|
178
|
+
|
|
179
|
+
expect(errorHandler.compensation.hasHandler('test-operation')).toBe(true);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RetryHandler Unit Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const RetryHandler = require('../../src/RetryHandler');
|
|
6
|
+
|
|
7
|
+
describe('RetryHandler @unit', () => {
|
|
8
|
+
let retryHandler;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
retryHandler = new RetryHandler({
|
|
12
|
+
maxRetries: 3,
|
|
13
|
+
retryDelay: 1000,
|
|
14
|
+
retryMultiplier: 2
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe('calculateBackoff', () => {
|
|
19
|
+
test('should calculate exponential backoff', () => {
|
|
20
|
+
expect(retryHandler.calculateBackoff(1)).toBe(1000);
|
|
21
|
+
expect(retryHandler.calculateBackoff(2)).toBe(2000);
|
|
22
|
+
expect(retryHandler.calculateBackoff(3)).toBe(4000);
|
|
23
|
+
expect(retryHandler.calculateBackoff(4)).toBe(8000);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('should cap at maxRetryDelay', () => {
|
|
27
|
+
const handler = new RetryHandler({
|
|
28
|
+
maxRetryDelay: 5000
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
expect(handler.calculateBackoff(10)).toBe(5000);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('executeWithRetry', () => {
|
|
36
|
+
test('should succeed on first attempt', async () => {
|
|
37
|
+
const fn = jest.fn().mockResolvedValue('success');
|
|
38
|
+
|
|
39
|
+
const result = await retryHandler.executeWithRetry(fn);
|
|
40
|
+
|
|
41
|
+
expect(result).toBe('success');
|
|
42
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('should retry on failure', async () => {
|
|
46
|
+
const fn = jest.fn()
|
|
47
|
+
.mockRejectedValueOnce(new Error('Fail 1'))
|
|
48
|
+
.mockRejectedValueOnce(new Error('Fail 2'))
|
|
49
|
+
.mockResolvedValue('success');
|
|
50
|
+
|
|
51
|
+
const result = await retryHandler.executeWithRetry(fn);
|
|
52
|
+
|
|
53
|
+
expect(result).toBe('success');
|
|
54
|
+
expect(fn).toHaveBeenCalledTimes(3);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('should throw after max retries', async () => {
|
|
58
|
+
const error = new Error('Always fails');
|
|
59
|
+
const fn = jest.fn().mockRejectedValue(error);
|
|
60
|
+
|
|
61
|
+
await expect(retryHandler.executeWithRetry(fn)).rejects.toThrow('Always fails');
|
|
62
|
+
expect(fn).toHaveBeenCalledTimes(3);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('should call onRetry callback', async () => {
|
|
66
|
+
const onRetry = jest.fn();
|
|
67
|
+
const fn = jest.fn()
|
|
68
|
+
.mockRejectedValueOnce(new Error('Fail'))
|
|
69
|
+
.mockResolvedValue('success');
|
|
70
|
+
|
|
71
|
+
await retryHandler.executeWithRetry(fn, { onRetry });
|
|
72
|
+
|
|
73
|
+
expect(onRetry).toHaveBeenCalledWith(
|
|
74
|
+
expect.any(Error),
|
|
75
|
+
1,
|
|
76
|
+
1000
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UnifiedLogSchema Unit Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const UnifiedLogSchema = require('../../src/UnifiedLogSchema');
|
|
6
|
+
|
|
7
|
+
describe('UnifiedLogSchema @unit', () => {
|
|
8
|
+
let schema;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
schema = new UnifiedLogSchema();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
describe('create', () => {
|
|
15
|
+
test('should create unified log entry', () => {
|
|
16
|
+
const error = new Error('Test error');
|
|
17
|
+
error.code = 'ETIMEDOUT';
|
|
18
|
+
|
|
19
|
+
const logEntry = schema.create({
|
|
20
|
+
moduleName: 'TestModule',
|
|
21
|
+
operation: 'testOperation',
|
|
22
|
+
error: error,
|
|
23
|
+
context: { userId: 123 },
|
|
24
|
+
serviceName: 'test-service',
|
|
25
|
+
serviceVersion: '1.0.0',
|
|
26
|
+
environment: 'test',
|
|
27
|
+
errorType: 'TRANSIENT'
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
expect(logEntry).toHaveProperty('timestamp');
|
|
31
|
+
expect(logEntry).toHaveProperty('serviceName', 'test-service');
|
|
32
|
+
expect(logEntry).toHaveProperty('moduleName', 'TestModule');
|
|
33
|
+
expect(logEntry).toHaveProperty('operation', 'testOperation');
|
|
34
|
+
expect(logEntry).toHaveProperty('error');
|
|
35
|
+
expect(logEntry.error).toHaveProperty('message', 'Test error');
|
|
36
|
+
expect(logEntry.error).toHaveProperty('code', 'ETIMEDOUT');
|
|
37
|
+
expect(logEntry.error).toHaveProperty('type', 'TRANSIENT');
|
|
38
|
+
expect(logEntry).toHaveProperty('context');
|
|
39
|
+
expect(logEntry.context).toHaveProperty('userId', 123);
|
|
40
|
+
expect(logEntry).toHaveProperty('metadata');
|
|
41
|
+
expect(logEntry.metadata).toHaveProperty('severity');
|
|
42
|
+
expect(logEntry.metadata).toHaveProperty('environment', 'test');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('should determine severity from error type', () => {
|
|
46
|
+
const error = new Error('Fatal error');
|
|
47
|
+
|
|
48
|
+
const logEntry = schema.create({
|
|
49
|
+
moduleName: 'TestModule',
|
|
50
|
+
operation: 'testOperation',
|
|
51
|
+
error: error,
|
|
52
|
+
serviceName: 'test-service',
|
|
53
|
+
errorType: 'FATAL'
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
expect(logEntry.metadata.severity).toBe('fatal');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('should handle missing optional fields', () => {
|
|
60
|
+
const error = new Error('Test error');
|
|
61
|
+
|
|
62
|
+
const logEntry = schema.create({
|
|
63
|
+
moduleName: 'TestModule',
|
|
64
|
+
operation: 'testOperation',
|
|
65
|
+
error: error,
|
|
66
|
+
serviceName: 'test-service'
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
expect(logEntry).toHaveProperty('timestamp');
|
|
70
|
+
expect(logEntry.serviceName).toBe('test-service');
|
|
71
|
+
expect(logEntry.error.type).toBe('UNKNOWN');
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|