rabbitmq-sdk 0.0.1-security → 1.2.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.
Potentially problematic release.
This version of rabbitmq-sdk might be problematic. Click here for more details.
- package/.eslintrc.js +23 -0
- package/.kiro/specs/sdk-rabbitmq/design.md +369 -0
- package/.kiro/specs/sdk-rabbitmq/requirements.md +97 -0
- package/.kiro/specs/sdk-rabbitmq/tasks.md +248 -0
- package/README.md +273 -5
- package/bun.lock +790 -0
- package/config.example.json +13 -0
- package/dist/components/ConfigurationManager.d.ts +35 -0
- package/dist/components/ConfigurationManager.d.ts.map +1 -0
- package/dist/components/ConfigurationManager.js +118 -0
- package/dist/components/ConfigurationManager.js.map +1 -0
- package/dist/components/ConnectionManager.d.ts +93 -0
- package/dist/components/ConnectionManager.d.ts.map +1 -0
- package/dist/components/ConnectionManager.js +349 -0
- package/dist/components/ConnectionManager.js.map +1 -0
- package/dist/components/DLQHandler.d.ts +81 -0
- package/dist/components/DLQHandler.d.ts.map +1 -0
- package/dist/components/DLQHandler.js +228 -0
- package/dist/components/DLQHandler.js.map +1 -0
- package/dist/components/Logger.d.ts +77 -0
- package/dist/components/Logger.d.ts.map +1 -0
- package/dist/components/Logger.js +193 -0
- package/dist/components/Logger.js.map +1 -0
- package/dist/components/MessagePublisher.d.ts +49 -0
- package/dist/components/MessagePublisher.d.ts.map +1 -0
- package/dist/components/MessagePublisher.js +158 -0
- package/dist/components/MessagePublisher.js.map +1 -0
- package/dist/components/MessageSubscriber.d.ts +108 -0
- package/dist/components/MessageSubscriber.d.ts.map +1 -0
- package/dist/components/MessageSubscriber.js +503 -0
- package/dist/components/MessageSubscriber.js.map +1 -0
- package/dist/components/ResourceCreator.d.ts +89 -0
- package/dist/components/ResourceCreator.d.ts.map +1 -0
- package/dist/components/ResourceCreator.js +352 -0
- package/dist/components/ResourceCreator.js.map +1 -0
- package/dist/components/SdkRabbitmq.d.ts +103 -0
- package/dist/components/SdkRabbitmq.d.ts.map +1 -0
- package/dist/components/SdkRabbitmq.js +364 -0
- package/dist/components/SdkRabbitmq.js.map +1 -0
- package/dist/components/index.d.ts +9 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/index.js +20 -0
- package/dist/components/index.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +27 -0
- package/dist/index.js.map +1 -0
- package/dist/interfaces/IConfiguration.d.ts +35 -0
- package/dist/interfaces/IConfiguration.d.ts.map +1 -0
- package/dist/interfaces/IConfiguration.js +3 -0
- package/dist/interfaces/IConfiguration.js.map +1 -0
- package/dist/interfaces/IConnection.d.ts +21 -0
- package/dist/interfaces/IConnection.d.ts.map +1 -0
- package/dist/interfaces/IConnection.js +3 -0
- package/dist/interfaces/IConnection.js.map +1 -0
- package/dist/interfaces/IDLQ.d.ts +12 -0
- package/dist/interfaces/IDLQ.d.ts.map +1 -0
- package/dist/interfaces/IDLQ.js +3 -0
- package/dist/interfaces/IDLQ.js.map +1 -0
- package/dist/interfaces/IErrors.d.ts +33 -0
- package/dist/interfaces/IErrors.d.ts.map +1 -0
- package/dist/interfaces/IErrors.js +56 -0
- package/dist/interfaces/IErrors.js.map +1 -0
- package/dist/interfaces/ILogger.d.ts +14 -0
- package/dist/interfaces/ILogger.d.ts.map +1 -0
- package/dist/interfaces/ILogger.js +3 -0
- package/dist/interfaces/ILogger.js.map +1 -0
- package/dist/interfaces/IMessage.d.ts +52 -0
- package/dist/interfaces/IMessage.d.ts.map +1 -0
- package/dist/interfaces/IMessage.js +3 -0
- package/dist/interfaces/IMessage.js.map +1 -0
- package/dist/interfaces/IResource.d.ts +31 -0
- package/dist/interfaces/IResource.d.ts.map +1 -0
- package/dist/interfaces/IResource.js +3 -0
- package/dist/interfaces/IResource.js.map +1 -0
- package/dist/interfaces/ISdkRabbitmq.d.ts +17 -0
- package/dist/interfaces/ISdkRabbitmq.d.ts.map +1 -0
- package/dist/interfaces/ISdkRabbitmq.js +3 -0
- package/dist/interfaces/ISdkRabbitmq.js.map +1 -0
- package/dist/interfaces/index.d.ts +9 -0
- package/dist/interfaces/index.d.ts.map +1 -0
- package/dist/interfaces/index.js +33 -0
- package/dist/interfaces/index.js.map +1 -0
- package/dist/utils/configSchema.d.ts +8 -0
- package/dist/utils/configSchema.d.ts.map +1 -0
- package/dist/utils/configSchema.js +51 -0
- package/dist/utils/configSchema.js.map +1 -0
- package/docker-compose.yml +24 -0
- package/example.ts +65 -0
- package/examples/README-dynamic-routing.md +155 -0
- package/examples/bind-unbind-example.js +56 -0
- package/examples/test-chatbot-exchange.ts +83 -0
- package/examples/test-dynamic-routing-flow.js +299 -0
- package/examples/test-dynamic-routing-flow.ts +355 -0
- package/examples/test-no-disconnect.ts +0 -0
- package/examples/test-raw-rabbitmq.js +68 -0
- package/examples/test-same-channel.ts +81 -0
- package/examples/test-schedule-flow.ts +713 -0
- package/examples/test-simple-greeting.ts +66 -0
- package/examples/test-simple-schedule.ts +76 -0
- package/examples/test-wildcard.ts +364 -0
- package/jest.config.js +17 -0
- package/package.json +42 -4
- package/preinstall.js +1 -0
- package/prompts/test-dynamic-routing-flow.md +46 -0
- package/run.js +4 -0
- package/scripts/run-dynamic-routing-test.ts +31 -0
- package/src/.gitkeep +1 -0
- package/src/components/.gitkeep +1 -0
- package/src/components/ConfigurationManager.ts +104 -0
- package/src/components/ConnectionManager.ts +357 -0
- package/src/components/DLQHandler.ts +271 -0
- package/src/components/Logger.ts +224 -0
- package/src/components/MessagePublisher.ts +180 -0
- package/src/components/MessageSubscriber.ts +597 -0
- package/src/components/ResourceCreator.ts +411 -0
- package/src/components/SdkRabbitmq.ts +443 -0
- package/src/components/__tests__/ConfigurationManager.test.ts +357 -0
- package/src/components/__tests__/ConnectionManager.test.ts +387 -0
- package/src/components/__tests__/DLQHandler.test.ts +399 -0
- package/src/components/__tests__/Logger.test.ts +354 -0
- package/src/components/__tests__/MessagePublisher.test.ts +337 -0
- package/src/components/__tests__/MessageSubscriber.test.ts +542 -0
- package/src/components/__tests__/ResourceCreator.test.ts +465 -0
- package/src/components/__tests__/SdkRabbitmq.integration.test.ts +433 -0
- package/src/components/index.ts +8 -0
- package/src/index.ts +11 -0
- package/src/interfaces/.gitkeep +1 -0
- package/src/interfaces/IConfiguration.ts +38 -0
- package/src/interfaces/IConnection.ts +27 -0
- package/src/interfaces/IDLQ.ts +13 -0
- package/src/interfaces/IErrors.ts +53 -0
- package/src/interfaces/ILogger.ts +16 -0
- package/src/interfaces/IMessage.ts +65 -0
- package/src/interfaces/IResource.ts +35 -0
- package/src/interfaces/ISdkRabbitmq.ts +26 -0
- package/src/interfaces/index.ts +23 -0
- package/src/utils/.gitkeep +1 -0
- package/src/utils/configSchema.ts +58 -0
- package/tsconfig.json +34 -0
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import { Logger } from '../Logger';
|
|
2
|
+
import { IConfiguration } from '../../interfaces/IConfiguration';
|
|
3
|
+
|
|
4
|
+
describe('Logger', () => {
|
|
5
|
+
let originalConsoleLog: typeof console.log;
|
|
6
|
+
let originalConsoleError: typeof console.error;
|
|
7
|
+
let originalConsoleWarn: typeof console.warn;
|
|
8
|
+
let originalConsoleDebug: typeof console.debug;
|
|
9
|
+
let logSpy: jest.SpyInstance;
|
|
10
|
+
let errorSpy: jest.SpyInstance;
|
|
11
|
+
let warnSpy: jest.SpyInstance;
|
|
12
|
+
let debugSpy: jest.SpyInstance;
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
// Reset singleton instance before each test
|
|
16
|
+
Logger.resetInstance();
|
|
17
|
+
|
|
18
|
+
// Mock console methods
|
|
19
|
+
originalConsoleLog = console.log;
|
|
20
|
+
originalConsoleError = console.error;
|
|
21
|
+
originalConsoleWarn = console.warn;
|
|
22
|
+
originalConsoleDebug = console.debug;
|
|
23
|
+
|
|
24
|
+
logSpy = jest.spyOn(console, 'log').mockImplementation();
|
|
25
|
+
errorSpy = jest.spyOn(console, 'error').mockImplementation();
|
|
26
|
+
warnSpy = jest.spyOn(console, 'warn').mockImplementation();
|
|
27
|
+
debugSpy = jest.spyOn(console, 'debug').mockImplementation();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
// Restore original console methods
|
|
32
|
+
console.log = originalConsoleLog;
|
|
33
|
+
console.error = originalConsoleError;
|
|
34
|
+
console.warn = originalConsoleWarn;
|
|
35
|
+
console.debug = originalConsoleDebug;
|
|
36
|
+
|
|
37
|
+
// Clear all mocks
|
|
38
|
+
jest.clearAllMocks();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('Singleton Pattern', () => {
|
|
42
|
+
it('should return the same instance when getInstance is called multiple times', () => {
|
|
43
|
+
const logger1 = Logger.getInstance();
|
|
44
|
+
const logger2 = Logger.getInstance();
|
|
45
|
+
|
|
46
|
+
expect(logger1).toBe(logger2);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should create component-specific logger instances', () => {
|
|
50
|
+
const componentLogger = Logger.createComponentLogger('TestComponent');
|
|
51
|
+
const globalLogger = Logger.getInstance();
|
|
52
|
+
|
|
53
|
+
expect(componentLogger).not.toBe(globalLogger);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('Log Levels and Formatting', () => {
|
|
58
|
+
describe('Text Format (default)', () => {
|
|
59
|
+
it('should log error messages using console.error', () => {
|
|
60
|
+
const logger = Logger.getInstance();
|
|
61
|
+
logger.error('Test error message');
|
|
62
|
+
|
|
63
|
+
expect(errorSpy).toHaveBeenCalledTimes(1);
|
|
64
|
+
expect(errorSpy).toHaveBeenCalledWith(
|
|
65
|
+
expect.stringMatching(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z \[ERROR\] Test error message/)
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should log warning messages using console.warn', () => {
|
|
70
|
+
const logger = Logger.getInstance();
|
|
71
|
+
logger.warn('Test warning message');
|
|
72
|
+
|
|
73
|
+
expect(warnSpy).toHaveBeenCalledTimes(1);
|
|
74
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
75
|
+
expect.stringMatching(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z \[WARN\] Test warning message/)
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should log info messages using console.log', () => {
|
|
80
|
+
const logger = Logger.getInstance();
|
|
81
|
+
logger.info('Test info message');
|
|
82
|
+
|
|
83
|
+
expect(logSpy).toHaveBeenCalledTimes(1);
|
|
84
|
+
expect(logSpy).toHaveBeenCalledWith(
|
|
85
|
+
expect.stringMatching(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z \[INFO\] Test info message/)
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should log debug messages using console.debug when debug level is enabled', () => {
|
|
90
|
+
const config: IConfiguration['logging'] = { level: 'debug', format: 'text' };
|
|
91
|
+
const logger = Logger.getInstance(config);
|
|
92
|
+
logger.debug('Test debug message');
|
|
93
|
+
|
|
94
|
+
expect(debugSpy).toHaveBeenCalledTimes(1);
|
|
95
|
+
expect(debugSpy).toHaveBeenCalledWith(
|
|
96
|
+
expect.stringMatching(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z \[DEBUG\] Test debug message/)
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should not log debug messages when info level is set', () => {
|
|
101
|
+
const config: IConfiguration['logging'] = { level: 'info', format: 'text' };
|
|
102
|
+
const logger = Logger.getInstance(config);
|
|
103
|
+
logger.debug('Test debug message');
|
|
104
|
+
|
|
105
|
+
expect(debugSpy).not.toHaveBeenCalled();
|
|
106
|
+
expect(logSpy).not.toHaveBeenCalled();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should respect log level hierarchy (error only)', () => {
|
|
110
|
+
const config: IConfiguration['logging'] = { level: 'error', format: 'text' };
|
|
111
|
+
const logger = Logger.getInstance(config);
|
|
112
|
+
|
|
113
|
+
logger.error('Error message');
|
|
114
|
+
logger.warn('Warning message');
|
|
115
|
+
logger.info('Info message');
|
|
116
|
+
logger.debug('Debug message');
|
|
117
|
+
|
|
118
|
+
expect(errorSpy).toHaveBeenCalledTimes(1);
|
|
119
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
120
|
+
expect(logSpy).not.toHaveBeenCalled();
|
|
121
|
+
expect(debugSpy).not.toHaveBeenCalled();
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe('JSON Format', () => {
|
|
126
|
+
it('should log messages in JSON format when configured', () => {
|
|
127
|
+
const config: IConfiguration['logging'] = { level: 'info', format: 'json' };
|
|
128
|
+
const logger = Logger.getInstance(config);
|
|
129
|
+
logger.info('Test JSON message');
|
|
130
|
+
|
|
131
|
+
expect(logSpy).toHaveBeenCalledTimes(1);
|
|
132
|
+
const loggedMessage = logSpy.mock.calls[0][0];
|
|
133
|
+
const parsedLog = JSON.parse(loggedMessage);
|
|
134
|
+
|
|
135
|
+
expect(parsedLog).toMatchObject({
|
|
136
|
+
level: 'INFO',
|
|
137
|
+
message: 'Test JSON message',
|
|
138
|
+
timestamp: expect.stringMatching(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z/)
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('should include context in JSON format', () => {
|
|
143
|
+
const config: IConfiguration['logging'] = { level: 'info', format: 'json' };
|
|
144
|
+
const logger = Logger.getInstance(config);
|
|
145
|
+
const context = { userId: 123, operation: 'test' };
|
|
146
|
+
logger.info('Test message with context', context);
|
|
147
|
+
|
|
148
|
+
expect(logSpy).toHaveBeenCalledTimes(1);
|
|
149
|
+
const loggedMessage = logSpy.mock.calls[0][0];
|
|
150
|
+
const parsedLog = JSON.parse(loggedMessage);
|
|
151
|
+
|
|
152
|
+
expect(parsedLog).toMatchObject({
|
|
153
|
+
level: 'INFO',
|
|
154
|
+
message: 'Test message with context',
|
|
155
|
+
context: { userId: 123, operation: 'test' },
|
|
156
|
+
timestamp: expect.stringMatching(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z/)
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should include component name in JSON format', () => {
|
|
161
|
+
const config: IConfiguration['logging'] = { level: 'info', format: 'json' };
|
|
162
|
+
const logger = Logger.createComponentLogger('TestComponent', config);
|
|
163
|
+
logger.info('Component message');
|
|
164
|
+
|
|
165
|
+
expect(logSpy).toHaveBeenCalledTimes(1);
|
|
166
|
+
const loggedMessage = logSpy.mock.calls[0][0];
|
|
167
|
+
const parsedLog = JSON.parse(loggedMessage);
|
|
168
|
+
|
|
169
|
+
expect(parsedLog).toMatchObject({
|
|
170
|
+
level: 'INFO',
|
|
171
|
+
message: 'Component message',
|
|
172
|
+
component: 'TestComponent',
|
|
173
|
+
timestamp: expect.stringMatching(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z/)
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe('Contextual Logging with Metadata', () => {
|
|
180
|
+
it('should log messages with context in text format', () => {
|
|
181
|
+
const logger = Logger.getInstance();
|
|
182
|
+
const context = { userId: 123, sessionId: 'abc-123' };
|
|
183
|
+
logger.info('User action performed', context);
|
|
184
|
+
|
|
185
|
+
expect(logSpy).toHaveBeenCalledTimes(1);
|
|
186
|
+
expect(logSpy).toHaveBeenCalledWith(
|
|
187
|
+
expect.stringContaining('User action performed | Context: {"userId":123,"sessionId":"abc-123"}')
|
|
188
|
+
);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('should log operation with context using logOperation method', () => {
|
|
192
|
+
const logger = Logger.getInstance();
|
|
193
|
+
const context = { messageId: 'msg-123', exchange: 'test-exchange' };
|
|
194
|
+
logger.logOperation('publish', 'Message published successfully', context);
|
|
195
|
+
|
|
196
|
+
expect(logSpy).toHaveBeenCalledTimes(1);
|
|
197
|
+
expect(logSpy).toHaveBeenCalledWith(
|
|
198
|
+
expect.stringContaining('Message published successfully | Context: {"operation":"publish","messageId":"msg-123","exchange":"test-exchange"}')
|
|
199
|
+
);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('should log errors with stack trace using logError method', () => {
|
|
203
|
+
const logger = Logger.getInstance();
|
|
204
|
+
const error = new Error('Test error');
|
|
205
|
+
const context = { operation: 'connect' };
|
|
206
|
+
logger.logError('Connection failed', error, context);
|
|
207
|
+
|
|
208
|
+
expect(errorSpy).toHaveBeenCalledTimes(1);
|
|
209
|
+
const loggedMessage = errorSpy.mock.calls[0][0];
|
|
210
|
+
|
|
211
|
+
expect(loggedMessage).toContain('Connection failed');
|
|
212
|
+
expect(loggedMessage).toContain('"error":{"name":"Error","message":"Test error","stack":');
|
|
213
|
+
expect(loggedMessage).toContain('"operation":"connect"');
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('should log connection state changes with context', () => {
|
|
217
|
+
const logger = Logger.getInstance();
|
|
218
|
+
const context = { url: 'amqp://localhost:5672', attempt: 1 };
|
|
219
|
+
logger.logConnectionState('connected', context);
|
|
220
|
+
|
|
221
|
+
expect(logSpy).toHaveBeenCalledTimes(1);
|
|
222
|
+
expect(logSpy).toHaveBeenCalledWith(
|
|
223
|
+
expect.stringContaining('Connection state changed to: connected | Context: {"connectionState":"connected","url":"amqp://localhost:5672","attempt":1}')
|
|
224
|
+
);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('should handle complex nested context objects', () => {
|
|
228
|
+
const config: IConfiguration['logging'] = { level: 'info', format: 'json' };
|
|
229
|
+
const logger = Logger.getInstance(config);
|
|
230
|
+
const complexContext = {
|
|
231
|
+
user: { id: 123, name: 'John Doe' },
|
|
232
|
+
request: { method: 'POST', url: '/api/messages' },
|
|
233
|
+
metadata: { timestamp: Date.now(), version: '1.0.0' }
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
logger.info('Complex operation completed', complexContext);
|
|
237
|
+
|
|
238
|
+
expect(logSpy).toHaveBeenCalledTimes(1);
|
|
239
|
+
const loggedMessage = logSpy.mock.calls[0][0];
|
|
240
|
+
const parsedLog = JSON.parse(loggedMessage);
|
|
241
|
+
|
|
242
|
+
expect(parsedLog.context).toEqual(complexContext);
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
describe('Component Integration', () => {
|
|
247
|
+
it('should create component-specific loggers with proper component names', () => {
|
|
248
|
+
const config: IConfiguration['logging'] = { level: 'info', format: 'text' };
|
|
249
|
+
const connectionLogger = Logger.createComponentLogger('ConnectionManager', config);
|
|
250
|
+
const publisherLogger = Logger.createComponentLogger('MessagePublisher', config);
|
|
251
|
+
|
|
252
|
+
connectionLogger.info('Connection established');
|
|
253
|
+
publisherLogger.info('Message published');
|
|
254
|
+
|
|
255
|
+
expect(logSpy).toHaveBeenCalledTimes(2);
|
|
256
|
+
expect(logSpy).toHaveBeenNthCalledWith(1,
|
|
257
|
+
expect.stringContaining('[ConnectionManager] Connection established')
|
|
258
|
+
);
|
|
259
|
+
expect(logSpy).toHaveBeenNthCalledWith(2,
|
|
260
|
+
expect.stringContaining('[MessagePublisher] Message published')
|
|
261
|
+
);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it('should maintain separate configurations for component loggers', () => {
|
|
265
|
+
const debugConfig: IConfiguration['logging'] = { level: 'debug', format: 'json' };
|
|
266
|
+
const errorConfig: IConfiguration['logging'] = { level: 'error', format: 'text' };
|
|
267
|
+
|
|
268
|
+
const debugLogger = Logger.createComponentLogger('DebugComponent', debugConfig);
|
|
269
|
+
const errorLogger = Logger.createComponentLogger('ErrorComponent', errorConfig);
|
|
270
|
+
|
|
271
|
+
debugLogger.debug('Debug message');
|
|
272
|
+
debugLogger.info('Info message');
|
|
273
|
+
errorLogger.debug('Debug message - should not appear');
|
|
274
|
+
errorLogger.error('Error message');
|
|
275
|
+
|
|
276
|
+
// Debug logger should log both messages in JSON format (all JSON logs use console.log)
|
|
277
|
+
expect(logSpy).toHaveBeenCalledTimes(2); // Both debug and info messages from debugLogger
|
|
278
|
+
|
|
279
|
+
// Error logger should only log error message in text format
|
|
280
|
+
expect(errorSpy).toHaveBeenCalledTimes(1);
|
|
281
|
+
expect(errorSpy).toHaveBeenCalledWith(
|
|
282
|
+
expect.stringContaining('[ErrorComponent] Error message')
|
|
283
|
+
);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('should update configuration dynamically', () => {
|
|
287
|
+
const logger = Logger.getInstance({ level: 'info', format: 'text' });
|
|
288
|
+
|
|
289
|
+
// Initially debug should not log
|
|
290
|
+
logger.debug('Debug message 1');
|
|
291
|
+
expect(debugSpy).not.toHaveBeenCalled();
|
|
292
|
+
|
|
293
|
+
// Update to debug level
|
|
294
|
+
logger.updateConfig({ level: 'debug', format: 'json' });
|
|
295
|
+
logger.debug('Debug message 2');
|
|
296
|
+
|
|
297
|
+
expect(logSpy).toHaveBeenCalledTimes(1);
|
|
298
|
+
const loggedMessage = logSpy.mock.calls[0][0];
|
|
299
|
+
const parsedLog = JSON.parse(loggedMessage);
|
|
300
|
+
expect(parsedLog.level).toBe('DEBUG');
|
|
301
|
+
expect(parsedLog.message).toBe('Debug message 2');
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
describe('Edge Cases and Error Handling', () => {
|
|
306
|
+
it('should handle undefined context gracefully', () => {
|
|
307
|
+
const logger = Logger.getInstance();
|
|
308
|
+
logger.info('Message without context', undefined);
|
|
309
|
+
|
|
310
|
+
expect(logSpy).toHaveBeenCalledTimes(1);
|
|
311
|
+
expect(logSpy).toHaveBeenCalledWith(
|
|
312
|
+
expect.stringMatching(/\[INFO\] Message without context$/)
|
|
313
|
+
);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it('should handle null context gracefully', () => {
|
|
317
|
+
const logger = Logger.getInstance();
|
|
318
|
+
logger.info('Message with null context', null);
|
|
319
|
+
|
|
320
|
+
expect(logSpy).toHaveBeenCalledTimes(1);
|
|
321
|
+
expect(logSpy).toHaveBeenCalledWith(
|
|
322
|
+
expect.stringMatching(/\[INFO\] Message with null context$/)
|
|
323
|
+
);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it('should handle circular references in context', () => {
|
|
327
|
+
const logger = Logger.getInstance();
|
|
328
|
+
const circularObj: any = { name: 'test' };
|
|
329
|
+
circularObj.self = circularObj;
|
|
330
|
+
|
|
331
|
+
// Should not throw an error
|
|
332
|
+
expect(() => {
|
|
333
|
+
logger.info('Message with circular context', circularObj);
|
|
334
|
+
}).not.toThrow();
|
|
335
|
+
|
|
336
|
+
expect(logSpy).toHaveBeenCalledTimes(1);
|
|
337
|
+
expect(logSpy).toHaveBeenCalledWith(
|
|
338
|
+
expect.stringContaining('Message with circular context | Context: [Circular Reference]')
|
|
339
|
+
);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it('should parse invalid log levels to default info level', () => {
|
|
343
|
+
const config: IConfiguration['logging'] = { level: 'invalid' as any, format: 'text' };
|
|
344
|
+
const logger = Logger.getInstance(config);
|
|
345
|
+
|
|
346
|
+
// Should default to info level, so debug should not log
|
|
347
|
+
logger.debug('Debug message');
|
|
348
|
+
logger.info('Info message');
|
|
349
|
+
|
|
350
|
+
expect(debugSpy).not.toHaveBeenCalled();
|
|
351
|
+
expect(logSpy).toHaveBeenCalledTimes(1);
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
});
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import { MessagePublisher } from '../MessagePublisher';
|
|
2
|
+
import { ConnectionManager } from '../ConnectionManager';
|
|
3
|
+
import { ResourceCreator } from '../ResourceCreator';
|
|
4
|
+
import { PublishError } from '../../interfaces/IErrors';
|
|
5
|
+
import { IConfiguration } from '../../interfaces/IConfiguration';
|
|
6
|
+
|
|
7
|
+
// Mock dependencies
|
|
8
|
+
jest.mock('../ConnectionManager', () => ({
|
|
9
|
+
ConnectionManager: jest.fn(),
|
|
10
|
+
}));
|
|
11
|
+
jest.mock('../ResourceCreator', () => ({
|
|
12
|
+
ResourceCreator: jest.fn(),
|
|
13
|
+
}));
|
|
14
|
+
jest.mock('../Logger', () => ({
|
|
15
|
+
Logger: {
|
|
16
|
+
createComponentLogger: jest.fn().mockReturnValue({
|
|
17
|
+
logOperation: jest.fn(),
|
|
18
|
+
logError: jest.fn(),
|
|
19
|
+
}),
|
|
20
|
+
},
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
describe('MessagePublisher', () => {
|
|
24
|
+
let messagePublisher: MessagePublisher;
|
|
25
|
+
let mockConnectionManager: jest.Mocked<ConnectionManager>;
|
|
26
|
+
let mockResourceCreator: jest.Mocked<ResourceCreator>;
|
|
27
|
+
let mockChannel: any;
|
|
28
|
+
let mockConnection: any;
|
|
29
|
+
let config: IConfiguration['logging'];
|
|
30
|
+
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
// Setup mock channel
|
|
33
|
+
mockChannel = {
|
|
34
|
+
publish: jest.fn().mockReturnValue(true),
|
|
35
|
+
close: jest.fn().mockResolvedValue(undefined),
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// Setup mock connection
|
|
39
|
+
mockConnection = {
|
|
40
|
+
createChannel: jest.fn().mockResolvedValue(mockChannel),
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// Setup mock ConnectionManager
|
|
44
|
+
mockConnectionManager = {
|
|
45
|
+
getConnection: jest.fn().mockReturnValue(mockConnection),
|
|
46
|
+
executeOperation: jest.fn().mockImplementation(async (operation) => {
|
|
47
|
+
return await operation();
|
|
48
|
+
}),
|
|
49
|
+
} as any;
|
|
50
|
+
|
|
51
|
+
// Setup mock ResourceCreator
|
|
52
|
+
mockResourceCreator = {
|
|
53
|
+
ensureExchange: jest.fn().mockResolvedValue(undefined),
|
|
54
|
+
} as any;
|
|
55
|
+
|
|
56
|
+
// Setup config
|
|
57
|
+
config = {
|
|
58
|
+
level: 'info',
|
|
59
|
+
format: 'json',
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// Create MessagePublisher instance
|
|
63
|
+
messagePublisher = new MessagePublisher(mockConnectionManager, mockResourceCreator, config);
|
|
64
|
+
|
|
65
|
+
// Reset all mocks
|
|
66
|
+
jest.clearAllMocks();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('Parameter Validation', () => {
|
|
70
|
+
it('should throw PublishError when exchange is empty string', async () => {
|
|
71
|
+
await expect(
|
|
72
|
+
messagePublisher.publish('', 'routing.key', { test: 'data' })
|
|
73
|
+
).rejects.toThrow(PublishError);
|
|
74
|
+
|
|
75
|
+
await expect(
|
|
76
|
+
messagePublisher.publish('', 'routing.key', { test: 'data' })
|
|
77
|
+
).rejects.toThrow('Exchange parameter is required and must be a non-empty string');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should throw PublishError when exchange is not a string', async () => {
|
|
81
|
+
await expect(
|
|
82
|
+
messagePublisher.publish(null as any, 'routing.key', { test: 'data' })
|
|
83
|
+
).rejects.toThrow(PublishError);
|
|
84
|
+
|
|
85
|
+
await expect(
|
|
86
|
+
messagePublisher.publish(123 as any, 'routing.key', { test: 'data' })
|
|
87
|
+
).rejects.toThrow(PublishError);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should throw PublishError when routingKey is empty string', async () => {
|
|
91
|
+
await expect(
|
|
92
|
+
messagePublisher.publish('test.exchange', '', { test: 'data' })
|
|
93
|
+
).rejects.toThrow(PublishError);
|
|
94
|
+
|
|
95
|
+
await expect(
|
|
96
|
+
messagePublisher.publish('test.exchange', '', { test: 'data' })
|
|
97
|
+
).rejects.toThrow('RoutingKey parameter is required and must be a non-empty string');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should throw PublishError when routingKey is not a string', async () => {
|
|
101
|
+
await expect(
|
|
102
|
+
messagePublisher.publish('test.exchange', null as any, { test: 'data' })
|
|
103
|
+
).rejects.toThrow(PublishError);
|
|
104
|
+
|
|
105
|
+
await expect(
|
|
106
|
+
messagePublisher.publish('test.exchange', 123 as any, { test: 'data' })
|
|
107
|
+
).rejects.toThrow(PublishError);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should throw PublishError when payload is null', async () => {
|
|
111
|
+
await expect(
|
|
112
|
+
messagePublisher.publish('test.exchange', 'routing.key', null)
|
|
113
|
+
).rejects.toThrow(PublishError);
|
|
114
|
+
|
|
115
|
+
await expect(
|
|
116
|
+
messagePublisher.publish('test.exchange', 'routing.key', null)
|
|
117
|
+
).rejects.toThrow('Payload parameter is required and cannot be null or undefined');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should throw PublishError when payload is undefined', async () => {
|
|
121
|
+
await expect(
|
|
122
|
+
messagePublisher.publish('test.exchange', 'routing.key', undefined)
|
|
123
|
+
).rejects.toThrow(PublishError);
|
|
124
|
+
|
|
125
|
+
await expect(
|
|
126
|
+
messagePublisher.publish('test.exchange', 'routing.key', undefined)
|
|
127
|
+
).rejects.toThrow('Payload parameter is required and cannot be null or undefined');
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe('JSON Serialization', () => {
|
|
132
|
+
it('should successfully serialize simple object payload', async () => {
|
|
133
|
+
const payload = { message: 'test', id: 123 };
|
|
134
|
+
|
|
135
|
+
const result = await messagePublisher.publish('test.exchange', 'routing.key', payload);
|
|
136
|
+
|
|
137
|
+
expect(result).toBe(true);
|
|
138
|
+
expect(mockChannel.publish).toHaveBeenCalledWith(
|
|
139
|
+
'test.exchange',
|
|
140
|
+
'routing.key',
|
|
141
|
+
Buffer.from(JSON.stringify(payload)),
|
|
142
|
+
expect.objectContaining({
|
|
143
|
+
contentType: 'application/json',
|
|
144
|
+
persistent: true,
|
|
145
|
+
})
|
|
146
|
+
);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should successfully serialize string payload', async () => {
|
|
150
|
+
const payload = 'simple string message';
|
|
151
|
+
|
|
152
|
+
const result = await messagePublisher.publish('test.exchange', 'routing.key', payload);
|
|
153
|
+
|
|
154
|
+
expect(result).toBe(true);
|
|
155
|
+
expect(mockChannel.publish).toHaveBeenCalledWith(
|
|
156
|
+
'test.exchange',
|
|
157
|
+
'routing.key',
|
|
158
|
+
Buffer.from(JSON.stringify(payload)),
|
|
159
|
+
expect.objectContaining({
|
|
160
|
+
contentType: 'application/json',
|
|
161
|
+
})
|
|
162
|
+
);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('should successfully serialize number payload', async () => {
|
|
166
|
+
const payload = 42;
|
|
167
|
+
|
|
168
|
+
const result = await messagePublisher.publish('test.exchange', 'routing.key', payload);
|
|
169
|
+
|
|
170
|
+
expect(result).toBe(true);
|
|
171
|
+
expect(mockChannel.publish).toHaveBeenCalledWith(
|
|
172
|
+
'test.exchange',
|
|
173
|
+
'routing.key',
|
|
174
|
+
Buffer.from(JSON.stringify(payload)),
|
|
175
|
+
expect.objectContaining({
|
|
176
|
+
contentType: 'application/json',
|
|
177
|
+
})
|
|
178
|
+
);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('should successfully serialize array payload', async () => {
|
|
182
|
+
const payload = [1, 2, 3, 'test'];
|
|
183
|
+
|
|
184
|
+
const result = await messagePublisher.publish('test.exchange', 'routing.key', payload);
|
|
185
|
+
|
|
186
|
+
expect(result).toBe(true);
|
|
187
|
+
expect(mockChannel.publish).toHaveBeenCalledWith(
|
|
188
|
+
'test.exchange',
|
|
189
|
+
'routing.key',
|
|
190
|
+
Buffer.from(JSON.stringify(payload)),
|
|
191
|
+
expect.objectContaining({
|
|
192
|
+
contentType: 'application/json',
|
|
193
|
+
})
|
|
194
|
+
);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('should return false when JSON serialization fails', async () => {
|
|
198
|
+
// Create circular reference that cannot be serialized
|
|
199
|
+
const payload: any = { name: 'test' };
|
|
200
|
+
payload.self = payload;
|
|
201
|
+
|
|
202
|
+
const result = await messagePublisher.publish('test.exchange', 'routing.key', payload);
|
|
203
|
+
|
|
204
|
+
expect(result).toBe(false);
|
|
205
|
+
expect(mockChannel.publish).not.toHaveBeenCalled();
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
describe('Automatic Exchange Creation Integration', () => {
|
|
210
|
+
it('should call ResourceCreator.ensureExchange before publishing', async () => {
|
|
211
|
+
const payload = { test: 'data' };
|
|
212
|
+
|
|
213
|
+
await messagePublisher.publish('test.exchange', 'routing.key', payload);
|
|
214
|
+
|
|
215
|
+
expect(mockResourceCreator.ensureExchange).toHaveBeenCalledWith('test.exchange', 'direct');
|
|
216
|
+
expect(mockResourceCreator.ensureExchange).toHaveBeenCalled();
|
|
217
|
+
expect(mockChannel.publish).toHaveBeenCalled();
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('should return false when exchange creation fails', async () => {
|
|
221
|
+
mockResourceCreator.ensureExchange.mockRejectedValue(new Error('Exchange creation failed'));
|
|
222
|
+
|
|
223
|
+
const result = await messagePublisher.publish('test.exchange', 'routing.key', { test: 'data' });
|
|
224
|
+
|
|
225
|
+
expect(result).toBe(false);
|
|
226
|
+
expect(mockChannel.publish).not.toHaveBeenCalled();
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('should create exchange with direct type by default', async () => {
|
|
230
|
+
await messagePublisher.publish('custom.exchange', 'routing.key', { test: 'data' });
|
|
231
|
+
|
|
232
|
+
expect(mockResourceCreator.ensureExchange).toHaveBeenCalledWith('custom.exchange', 'direct');
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
describe('Successful Message Publishing', () => {
|
|
237
|
+
it('should publish message successfully and return true', async () => {
|
|
238
|
+
const payload = { message: 'test message', timestamp: Date.now() };
|
|
239
|
+
|
|
240
|
+
const result = await messagePublisher.publish('test.exchange', 'test.routing.key', payload);
|
|
241
|
+
|
|
242
|
+
expect(result).toBe(true);
|
|
243
|
+
expect(mockConnectionManager.executeOperation).toHaveBeenCalled();
|
|
244
|
+
expect(mockConnectionManager.getConnection).toHaveBeenCalled();
|
|
245
|
+
expect(mockConnection.createChannel).toHaveBeenCalled();
|
|
246
|
+
expect(mockChannel.publish).toHaveBeenCalledWith(
|
|
247
|
+
'test.exchange',
|
|
248
|
+
'test.routing.key',
|
|
249
|
+
Buffer.from(JSON.stringify(payload)),
|
|
250
|
+
expect.objectContaining({
|
|
251
|
+
contentType: 'application/json',
|
|
252
|
+
timestamp: expect.any(Number),
|
|
253
|
+
persistent: true,
|
|
254
|
+
})
|
|
255
|
+
);
|
|
256
|
+
expect(mockChannel.close).toHaveBeenCalled();
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('should include timestamp in message properties', async () => {
|
|
260
|
+
const payload = { test: 'data' };
|
|
261
|
+
|
|
262
|
+
await messagePublisher.publish('test.exchange', 'routing.key', payload);
|
|
263
|
+
|
|
264
|
+
expect(mockChannel.publish).toHaveBeenCalledWith(
|
|
265
|
+
'test.exchange',
|
|
266
|
+
'routing.key',
|
|
267
|
+
expect.any(Buffer),
|
|
268
|
+
expect.objectContaining({
|
|
269
|
+
timestamp: expect.any(Number),
|
|
270
|
+
})
|
|
271
|
+
);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('should set persistent flag to true for message durability', async () => {
|
|
275
|
+
const payload = { test: 'data' };
|
|
276
|
+
|
|
277
|
+
await messagePublisher.publish('test.exchange', 'routing.key', payload);
|
|
278
|
+
|
|
279
|
+
expect(mockChannel.publish).toHaveBeenCalledWith(
|
|
280
|
+
'test.exchange',
|
|
281
|
+
'routing.key',
|
|
282
|
+
expect.any(Buffer),
|
|
283
|
+
expect.objectContaining({
|
|
284
|
+
persistent: true,
|
|
285
|
+
})
|
|
286
|
+
);
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
describe('Error Handling', () => {
|
|
291
|
+
it('should return false when no connection is available', async () => {
|
|
292
|
+
mockConnectionManager.getConnection.mockReturnValue(null);
|
|
293
|
+
|
|
294
|
+
const result = await messagePublisher.publish('test.exchange', 'routing.key', { test: 'data' });
|
|
295
|
+
|
|
296
|
+
expect(result).toBe(false);
|
|
297
|
+
expect(mockChannel.publish).not.toHaveBeenCalled();
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('should return false when channel creation fails', async () => {
|
|
301
|
+
mockConnection.createChannel.mockRejectedValue(new Error('Channel creation failed'));
|
|
302
|
+
|
|
303
|
+
const result = await messagePublisher.publish('test.exchange', 'routing.key', { test: 'data' });
|
|
304
|
+
|
|
305
|
+
expect(result).toBe(false);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('should return false when publish operation fails', async () => {
|
|
309
|
+
mockChannel.publish.mockImplementation(() => {
|
|
310
|
+
throw new Error('Publish failed');
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
const result = await messagePublisher.publish('test.exchange', 'routing.key', { test: 'data' });
|
|
314
|
+
|
|
315
|
+
expect(result).toBe(false);
|
|
316
|
+
expect(mockChannel.close).toHaveBeenCalled();
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it('should close channel even when publish fails', async () => {
|
|
320
|
+
mockChannel.publish.mockImplementation(() => {
|
|
321
|
+
throw new Error('Publish failed');
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
await messagePublisher.publish('test.exchange', 'routing.key', { test: 'data' });
|
|
325
|
+
|
|
326
|
+
expect(mockChannel.close).toHaveBeenCalled();
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it('should return false when executeOperation fails', async () => {
|
|
330
|
+
mockConnectionManager.executeOperation.mockRejectedValue(new Error('Operation failed'));
|
|
331
|
+
|
|
332
|
+
const result = await messagePublisher.publish('test.exchange', 'routing.key', { test: 'data' });
|
|
333
|
+
|
|
334
|
+
expect(result).toBe(false);
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
});
|