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,433 @@
|
|
|
1
|
+
import { SdkRabbitmq } from '../SdkRabbitmq';
|
|
2
|
+
import { MessageCallback } from '../../interfaces/IMessage';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
|
|
6
|
+
// Mock amqplib for integration tests
|
|
7
|
+
jest.mock('amqplib', () => ({
|
|
8
|
+
connect: jest.fn(),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
describe('SdkRabbitmq Integration Tests', () => {
|
|
12
|
+
let mockConnection: any;
|
|
13
|
+
let mockChannel: any;
|
|
14
|
+
let configPath: string;
|
|
15
|
+
|
|
16
|
+
beforeAll(() => {
|
|
17
|
+
// Setup test configuration file
|
|
18
|
+
configPath = path.join(process.cwd(), 'config.json');
|
|
19
|
+
const testConfig = {
|
|
20
|
+
url: 'amqp://localhost:5672',
|
|
21
|
+
dlq: {
|
|
22
|
+
active: true,
|
|
23
|
+
ttl: 300000,
|
|
24
|
+
maxRetries: 3,
|
|
25
|
+
retryDelay: 5000
|
|
26
|
+
},
|
|
27
|
+
logging: {
|
|
28
|
+
level: 'error', // Reduce log noise in tests
|
|
29
|
+
format: 'json'
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
fs.writeFileSync(configPath, JSON.stringify(testConfig, null, 2));
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
// Reset singleton instance before each test
|
|
38
|
+
SdkRabbitmq.resetInstance();
|
|
39
|
+
|
|
40
|
+
// Setup mock connection and channel
|
|
41
|
+
mockChannel = {
|
|
42
|
+
assertExchange: jest.fn().mockResolvedValue({}),
|
|
43
|
+
assertQueue: jest.fn().mockResolvedValue({ queue: 'test-queue' }),
|
|
44
|
+
bindQueue: jest.fn().mockResolvedValue({}),
|
|
45
|
+
unbindQueue: jest.fn().mockResolvedValue({}),
|
|
46
|
+
publish: jest.fn().mockReturnValue(true),
|
|
47
|
+
consume: jest.fn().mockResolvedValue({ consumerTag: 'test-consumer' }),
|
|
48
|
+
prefetch: jest.fn().mockResolvedValue({}),
|
|
49
|
+
ack: jest.fn(),
|
|
50
|
+
nack: jest.fn(),
|
|
51
|
+
cancel: jest.fn().mockResolvedValue({}),
|
|
52
|
+
close: jest.fn().mockResolvedValue({}),
|
|
53
|
+
on: jest.fn(),
|
|
54
|
+
removeAllListeners: jest.fn(),
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
mockConnection = {
|
|
58
|
+
createChannel: jest.fn().mockResolvedValue(mockChannel),
|
|
59
|
+
close: jest.fn().mockResolvedValue({}),
|
|
60
|
+
on: jest.fn(),
|
|
61
|
+
removeAllListeners: jest.fn(),
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const amqplib = require('amqplib');
|
|
65
|
+
amqplib.connect.mockClear();
|
|
66
|
+
amqplib.connect.mockResolvedValue(mockConnection);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
afterEach(() => {
|
|
70
|
+
// Clean up singleton instance
|
|
71
|
+
SdkRabbitmq.resetInstance();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
afterAll(() => {
|
|
75
|
+
// Clean up test configuration file
|
|
76
|
+
if (fs.existsSync(configPath)) {
|
|
77
|
+
fs.unlinkSync(configPath);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe('Singleton Behavior', () => {
|
|
82
|
+
it('should return the same instance across multiple instantiations', async () => {
|
|
83
|
+
// Test requirement 2.1: Singleton pattern implementation
|
|
84
|
+
const instance1 = await SdkRabbitmq.getInstance();
|
|
85
|
+
const instance2 = await SdkRabbitmq.getInstance();
|
|
86
|
+
const instance3 = await SdkRabbitmq.getInstance();
|
|
87
|
+
|
|
88
|
+
expect(instance1).toBe(instance2);
|
|
89
|
+
expect(instance2).toBe(instance3);
|
|
90
|
+
expect(instance1).toBe(instance3);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should maintain singleton behavior during concurrent instantiation attempts', async () => {
|
|
94
|
+
// Test concurrent access to singleton
|
|
95
|
+
const promises = Array.from({ length: 5 }, () => SdkRabbitmq.getInstance());
|
|
96
|
+
const instances = await Promise.all(promises);
|
|
97
|
+
|
|
98
|
+
// All instances should be the same object
|
|
99
|
+
const firstInstance = instances[0];
|
|
100
|
+
instances.forEach(instance => {
|
|
101
|
+
expect(instance).toBe(firstInstance);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should share state between multiple references to singleton', async () => {
|
|
106
|
+
const instance1 = await SdkRabbitmq.getInstance();
|
|
107
|
+
const instance2 = await SdkRabbitmq.getInstance();
|
|
108
|
+
|
|
109
|
+
// Both instances should report the same ready state
|
|
110
|
+
expect(instance1.isReady()).toBe(instance2.isReady());
|
|
111
|
+
|
|
112
|
+
// Both instances should have the same active consumers
|
|
113
|
+
expect(instance1.getActiveConsumers()).toEqual(instance2.getActiveConsumers());
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('should create new instance after reset', async () => {
|
|
117
|
+
const instance1 = await SdkRabbitmq.getInstance();
|
|
118
|
+
|
|
119
|
+
SdkRabbitmq.resetInstance();
|
|
120
|
+
|
|
121
|
+
const instance2 = await SdkRabbitmq.getInstance();
|
|
122
|
+
expect(instance2).not.toBe(instance1);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe('End-to-End Publish and Subscribe Workflows', () => {
|
|
127
|
+
let sdkInstance: SdkRabbitmq;
|
|
128
|
+
|
|
129
|
+
beforeEach(async () => {
|
|
130
|
+
sdkInstance = await SdkRabbitmq.getInstance();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should successfully publish a message end-to-end', async () => {
|
|
134
|
+
// Test requirement 3.1: Message publishing functionality
|
|
135
|
+
const exchange = 'test-exchange';
|
|
136
|
+
const routingKey = 'test.route';
|
|
137
|
+
const payload = { message: 'Hello World', timestamp: Date.now() };
|
|
138
|
+
|
|
139
|
+
const result = await sdkInstance.publish(exchange, routingKey, payload);
|
|
140
|
+
|
|
141
|
+
expect(result).toBe(true);
|
|
142
|
+
expect(mockChannel.assertExchange).toHaveBeenCalledWith(
|
|
143
|
+
exchange,
|
|
144
|
+
'direct',
|
|
145
|
+
{ durable: true, autoDelete: false }
|
|
146
|
+
);
|
|
147
|
+
expect(mockChannel.publish).toHaveBeenCalledWith(
|
|
148
|
+
exchange,
|
|
149
|
+
routingKey,
|
|
150
|
+
expect.any(Buffer),
|
|
151
|
+
expect.objectContaining({
|
|
152
|
+
persistent: true,
|
|
153
|
+
contentType: 'application/json'
|
|
154
|
+
})
|
|
155
|
+
);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('should successfully set up subscription end-to-end', async () => {
|
|
159
|
+
// Test requirement 4.1: Message subscription functionality
|
|
160
|
+
const exchange = 'test-exchange';
|
|
161
|
+
const queue = 'test-queue';
|
|
162
|
+
const routingKey = 'test.route';
|
|
163
|
+
const callback: MessageCallback = jest.fn();
|
|
164
|
+
|
|
165
|
+
await sdkInstance.subscribe(exchange, queue, routingKey, callback);
|
|
166
|
+
|
|
167
|
+
expect(mockChannel.assertExchange).toHaveBeenCalledWith(
|
|
168
|
+
exchange,
|
|
169
|
+
'direct',
|
|
170
|
+
{ durable: true, autoDelete: false }
|
|
171
|
+
);
|
|
172
|
+
expect(mockChannel.assertQueue).toHaveBeenCalledWith(
|
|
173
|
+
queue,
|
|
174
|
+
{ durable: true, exclusive: false, autoDelete: false }
|
|
175
|
+
);
|
|
176
|
+
expect(mockChannel.bindQueue).toHaveBeenCalledWith(
|
|
177
|
+
queue,
|
|
178
|
+
exchange,
|
|
179
|
+
routingKey
|
|
180
|
+
);
|
|
181
|
+
expect(mockChannel.consume).toHaveBeenCalledWith(
|
|
182
|
+
queue,
|
|
183
|
+
expect.any(Function),
|
|
184
|
+
{ noAck: false }
|
|
185
|
+
);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('should handle complete publish-subscribe workflow', async () => {
|
|
189
|
+
const exchange = 'workflow-exchange';
|
|
190
|
+
const queue = 'workflow-queue';
|
|
191
|
+
const routingKey = 'workflow.test';
|
|
192
|
+
const testPayload = { id: 1, data: 'test-data' };
|
|
193
|
+
|
|
194
|
+
let receivedMessage: any = null;
|
|
195
|
+
const callback: MessageCallback = (message, ack) => {
|
|
196
|
+
receivedMessage = message;
|
|
197
|
+
ack();
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
// Set up subscription first
|
|
201
|
+
await sdkInstance.subscribe(exchange, queue, routingKey, callback);
|
|
202
|
+
|
|
203
|
+
// Publish message
|
|
204
|
+
const publishResult = await sdkInstance.publish(exchange, routingKey, testPayload);
|
|
205
|
+
expect(publishResult).toBe(true);
|
|
206
|
+
|
|
207
|
+
// Simulate message consumption
|
|
208
|
+
const consumeHandler = mockChannel.consume.mock.calls[0][1];
|
|
209
|
+
const mockMessage = {
|
|
210
|
+
content: Buffer.from(JSON.stringify(testPayload)),
|
|
211
|
+
fields: {
|
|
212
|
+
deliveryTag: 1,
|
|
213
|
+
redelivered: false,
|
|
214
|
+
exchange: exchange,
|
|
215
|
+
routingKey: routingKey
|
|
216
|
+
},
|
|
217
|
+
properties: {
|
|
218
|
+
contentType: 'application/json',
|
|
219
|
+
timestamp: Date.now(),
|
|
220
|
+
messageId: 'test-message-id',
|
|
221
|
+
headers: {}
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
consumeHandler(mockMessage);
|
|
226
|
+
|
|
227
|
+
expect(receivedMessage).toEqual(testPayload);
|
|
228
|
+
expect(mockChannel.ack).toHaveBeenCalledWith(mockMessage);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('should handle multiple concurrent operations', async () => {
|
|
232
|
+
const operations = [
|
|
233
|
+
sdkInstance.publish('exchange1', 'route1', { data: 'message1' }),
|
|
234
|
+
sdkInstance.publish('exchange2', 'route2', { data: 'message2' }),
|
|
235
|
+
sdkInstance.subscribe('exchange3', 'queue3', 'route3', jest.fn()),
|
|
236
|
+
sdkInstance.subscribe('exchange4', 'queue4', 'route4', jest.fn()),
|
|
237
|
+
];
|
|
238
|
+
|
|
239
|
+
const results = await Promise.all(operations);
|
|
240
|
+
|
|
241
|
+
// First two operations are publish (should return boolean)
|
|
242
|
+
expect(results[0]).toBe(true);
|
|
243
|
+
expect(results[1]).toBe(true);
|
|
244
|
+
|
|
245
|
+
// Last two operations are subscribe (should return void)
|
|
246
|
+
expect(results[2]).toBeUndefined();
|
|
247
|
+
expect(results[3]).toBeUndefined();
|
|
248
|
+
|
|
249
|
+
// Verify all operations were executed
|
|
250
|
+
expect(mockChannel.publish).toHaveBeenCalledTimes(2);
|
|
251
|
+
expect(mockChannel.consume).toHaveBeenCalledTimes(2);
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
describe('Error Scenarios and Recovery', () => {
|
|
256
|
+
it('should handle connection failures during initialization', async () => {
|
|
257
|
+
// Reset and setup connection failure
|
|
258
|
+
SdkRabbitmq.resetInstance();
|
|
259
|
+
const amqplib = require('amqplib');
|
|
260
|
+
|
|
261
|
+
// Clear previous mock calls and set up failure
|
|
262
|
+
amqplib.connect.mockClear();
|
|
263
|
+
amqplib.connect.mockRejectedValueOnce(new Error('Connection failed'));
|
|
264
|
+
|
|
265
|
+
await expect(SdkRabbitmq.getInstance()).rejects.toThrow('Failed to connect to RabbitMQ: Connection failed');
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('should handle publish failures gracefully', async () => {
|
|
269
|
+
const sdkInstance = await SdkRabbitmq.getInstance();
|
|
270
|
+
mockChannel.publish.mockReturnValueOnce(false);
|
|
271
|
+
|
|
272
|
+
const result = await sdkInstance.publish('test-exchange', 'test.route', { data: 'test' });
|
|
273
|
+
|
|
274
|
+
expect(result).toBe(false);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('should handle publish errors gracefully', async () => {
|
|
278
|
+
const sdkInstance = await SdkRabbitmq.getInstance();
|
|
279
|
+
mockChannel.publish.mockImplementationOnce(() => {
|
|
280
|
+
throw new Error('Publish error');
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
const result = await sdkInstance.publish('test-exchange', 'test.route', { data: 'test' });
|
|
284
|
+
|
|
285
|
+
expect(result).toBe(false);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('should validate required parameters for publish', async () => {
|
|
289
|
+
const sdkInstance = await SdkRabbitmq.getInstance();
|
|
290
|
+
|
|
291
|
+
await expect(
|
|
292
|
+
sdkInstance.publish('', 'route', { data: 'test' })
|
|
293
|
+
).rejects.toThrow('Exchange parameter is required and must be a non-empty string');
|
|
294
|
+
|
|
295
|
+
await expect(
|
|
296
|
+
sdkInstance.publish('exchange', '', { data: 'test' })
|
|
297
|
+
).rejects.toThrow('RoutingKey parameter is required and must be a non-empty string');
|
|
298
|
+
|
|
299
|
+
await expect(
|
|
300
|
+
sdkInstance.publish('exchange', 'route', null)
|
|
301
|
+
).rejects.toThrow('Payload parameter is required and cannot be null or undefined');
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it('should validate required parameters for subscribe', async () => {
|
|
305
|
+
const sdkInstance = await SdkRabbitmq.getInstance();
|
|
306
|
+
const callback: MessageCallback = jest.fn();
|
|
307
|
+
|
|
308
|
+
await expect(
|
|
309
|
+
sdkInstance.subscribe('', 'queue', 'route', callback)
|
|
310
|
+
).rejects.toThrow('Exchange parameter is required and must be a non-empty string');
|
|
311
|
+
|
|
312
|
+
await expect(
|
|
313
|
+
sdkInstance.subscribe('exchange', '', 'route', callback)
|
|
314
|
+
).rejects.toThrow('Queue parameter is required and must be a non-empty string');
|
|
315
|
+
|
|
316
|
+
await expect(
|
|
317
|
+
sdkInstance.subscribe('exchange', 'queue', '', callback)
|
|
318
|
+
).rejects.toThrow('RoutingKey parameter is required and must be a non-empty string');
|
|
319
|
+
|
|
320
|
+
await expect(
|
|
321
|
+
sdkInstance.subscribe('exchange', 'queue', 'route', null as any)
|
|
322
|
+
).rejects.toThrow('Callback parameter is required and must be a function');
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('should handle graceful disconnect', async () => {
|
|
326
|
+
const sdkInstance = await SdkRabbitmq.getInstance();
|
|
327
|
+
|
|
328
|
+
await sdkInstance.disconnect();
|
|
329
|
+
|
|
330
|
+
expect(sdkInstance.isReady()).toBe(false);
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
describe('Queue Binding Management', () => {
|
|
335
|
+
it('should bind queue to exchange successfully', async () => {
|
|
336
|
+
const sdkInstance = await SdkRabbitmq.getInstance();
|
|
337
|
+
|
|
338
|
+
await sdkInstance.bind('test-queue', 'test-exchange', 'test.route');
|
|
339
|
+
|
|
340
|
+
expect(mockChannel.bindQueue).toHaveBeenCalledWith(
|
|
341
|
+
'test-queue',
|
|
342
|
+
'test-exchange',
|
|
343
|
+
'test.route'
|
|
344
|
+
);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it('should unbind queue from exchange successfully', async () => {
|
|
348
|
+
const sdkInstance = await SdkRabbitmq.getInstance();
|
|
349
|
+
|
|
350
|
+
await sdkInstance.unbind('test-queue', 'test-exchange', 'test.route');
|
|
351
|
+
|
|
352
|
+
expect(mockChannel.unbindQueue).toHaveBeenCalledWith(
|
|
353
|
+
'test-queue',
|
|
354
|
+
'test-exchange',
|
|
355
|
+
'test.route'
|
|
356
|
+
);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it('should validate required parameters for bind', async () => {
|
|
360
|
+
const sdkInstance = await SdkRabbitmq.getInstance();
|
|
361
|
+
|
|
362
|
+
await expect(
|
|
363
|
+
sdkInstance.bind('', 'exchange', 'route')
|
|
364
|
+
).rejects.toThrow('Queue parameter is required and must be a non-empty string');
|
|
365
|
+
|
|
366
|
+
await expect(
|
|
367
|
+
sdkInstance.bind('queue', '', 'route')
|
|
368
|
+
).rejects.toThrow('Exchange parameter is required and must be a non-empty string');
|
|
369
|
+
|
|
370
|
+
await expect(
|
|
371
|
+
sdkInstance.bind('queue', 'exchange', '')
|
|
372
|
+
).rejects.toThrow('RoutingKey parameter is required and must be a non-empty string');
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it('should validate required parameters for unbind', async () => {
|
|
376
|
+
const sdkInstance = await SdkRabbitmq.getInstance();
|
|
377
|
+
|
|
378
|
+
await expect(
|
|
379
|
+
sdkInstance.unbind('', 'exchange', 'route')
|
|
380
|
+
).rejects.toThrow('Queue parameter is required and must be a non-empty string');
|
|
381
|
+
|
|
382
|
+
await expect(
|
|
383
|
+
sdkInstance.unbind('queue', '', 'route')
|
|
384
|
+
).rejects.toThrow('Exchange parameter is required and must be a non-empty string');
|
|
385
|
+
|
|
386
|
+
await expect(
|
|
387
|
+
sdkInstance.unbind('queue', 'exchange', '')
|
|
388
|
+
).rejects.toThrow('RoutingKey parameter is required and must be a non-empty string');
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it('should handle bind errors gracefully', async () => {
|
|
392
|
+
const sdkInstance = await SdkRabbitmq.getInstance();
|
|
393
|
+
const error = new Error('Bind failed');
|
|
394
|
+
mockChannel.bindQueue.mockRejectedValue(error);
|
|
395
|
+
|
|
396
|
+
await expect(
|
|
397
|
+
sdkInstance.bind('queue', 'exchange', 'route')
|
|
398
|
+
).rejects.toThrow('Bind failed');
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
it('should handle unbind errors gracefully', async () => {
|
|
402
|
+
const sdkInstance = await SdkRabbitmq.getInstance();
|
|
403
|
+
const error = new Error('Unbind failed');
|
|
404
|
+
mockChannel.unbindQueue.mockRejectedValue(error);
|
|
405
|
+
|
|
406
|
+
await expect(
|
|
407
|
+
sdkInstance.unbind('queue', 'exchange', 'route')
|
|
408
|
+
).rejects.toThrow('Unbind failed');
|
|
409
|
+
});
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
describe('SDK State Management', () => {
|
|
413
|
+
it('should report correct ready state', async () => {
|
|
414
|
+
const sdkInstance = await SdkRabbitmq.getInstance();
|
|
415
|
+
|
|
416
|
+
expect(sdkInstance.isReady()).toBe(true);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it('should track active consumers', async () => {
|
|
420
|
+
const sdkInstance = await SdkRabbitmq.getInstance();
|
|
421
|
+
|
|
422
|
+
expect(sdkInstance.getActiveConsumers()).toEqual([]);
|
|
423
|
+
|
|
424
|
+
await sdkInstance.subscribe('exchange1', 'queue1', 'route1', jest.fn());
|
|
425
|
+
await sdkInstance.subscribe('exchange2', 'queue2', 'route2', jest.fn());
|
|
426
|
+
|
|
427
|
+
const activeConsumers = sdkInstance.getActiveConsumers();
|
|
428
|
+
expect(activeConsumers).toHaveLength(2);
|
|
429
|
+
expect(activeConsumers).toContain('queue1');
|
|
430
|
+
expect(activeConsumers).toContain('queue2');
|
|
431
|
+
});
|
|
432
|
+
});
|
|
433
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { ConfigurationManager } from './ConfigurationManager';
|
|
2
|
+
export { ConnectionManager } from './ConnectionManager';
|
|
3
|
+
export { ResourceCreator } from './ResourceCreator';
|
|
4
|
+
export { MessagePublisher } from './MessagePublisher';
|
|
5
|
+
export { MessageSubscriber } from './MessageSubscriber';
|
|
6
|
+
export { DLQHandler } from './DLQHandler';
|
|
7
|
+
export { Logger } from './Logger';
|
|
8
|
+
export { SdkRabbitmq } from './SdkRabbitmq';
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Export all interfaces
|
|
2
|
+
export * from './interfaces';
|
|
3
|
+
|
|
4
|
+
// Export components
|
|
5
|
+
export * from './components';
|
|
6
|
+
|
|
7
|
+
// Export utilities
|
|
8
|
+
export * from './utils/configSchema';
|
|
9
|
+
|
|
10
|
+
// Main SDK class
|
|
11
|
+
export { SdkRabbitmq } from './components/SdkRabbitmq';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# This file ensures the interfaces directory is tracked by git
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration interface for the SDK RabbitMQ
|
|
3
|
+
*/
|
|
4
|
+
export interface IConfiguration {
|
|
5
|
+
/** RabbitMQ connection URL */
|
|
6
|
+
url: string;
|
|
7
|
+
|
|
8
|
+
/** Dead Letter Queue configuration */
|
|
9
|
+
dlq: {
|
|
10
|
+
/** Whether DLQ is active */
|
|
11
|
+
active: boolean;
|
|
12
|
+
/** Time to live for messages in DLQ (milliseconds) */
|
|
13
|
+
ttl?: number;
|
|
14
|
+
/** Maximum number of retries before sending to DLQ */
|
|
15
|
+
maxRetries?: number;
|
|
16
|
+
/** Delay between retries (milliseconds) */
|
|
17
|
+
retryDelay?: number;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** Logging configuration */
|
|
21
|
+
logging?: {
|
|
22
|
+
/** Log level */
|
|
23
|
+
level: 'error' | 'warn' | 'info' | 'debug';
|
|
24
|
+
/** Log format */
|
|
25
|
+
format: 'json' | 'text';
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Configuration manager interface
|
|
31
|
+
*/
|
|
32
|
+
export interface IConfigurationManager {
|
|
33
|
+
/** Load configuration from config.json */
|
|
34
|
+
loadConfig(): IConfiguration;
|
|
35
|
+
|
|
36
|
+
/** Validate configuration schema */
|
|
37
|
+
validateConfig(config: IConfiguration): void;
|
|
38
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import * as amqp from 'amqplib';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Connection manager interface for managing RabbitMQ connections
|
|
5
|
+
*/
|
|
6
|
+
export interface IConnectionManager {
|
|
7
|
+
/** Establish connection to RabbitMQ */
|
|
8
|
+
connect(): Promise<amqp.Connection>;
|
|
9
|
+
|
|
10
|
+
/** Get current connection instance */
|
|
11
|
+
getConnection(): amqp.Connection | null;
|
|
12
|
+
|
|
13
|
+
/** Check if currently connected */
|
|
14
|
+
isConnected(): boolean;
|
|
15
|
+
|
|
16
|
+
/** Disconnect from RabbitMQ */
|
|
17
|
+
disconnect(): Promise<void>;
|
|
18
|
+
|
|
19
|
+
/** Register callback for connection lost events */
|
|
20
|
+
onConnectionLost(callback: () => void): void;
|
|
21
|
+
|
|
22
|
+
/** Queue operation to be executed after reconnection */
|
|
23
|
+
queueOperation(operation: () => Promise<void>): void;
|
|
24
|
+
|
|
25
|
+
/** Execute operation with automatic queuing during reconnection */
|
|
26
|
+
executeOperation<T>(operation: () => Promise<T>): Promise<T>;
|
|
27
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dead Letter Queue handler interface
|
|
3
|
+
*/
|
|
4
|
+
export interface IDLQHandler {
|
|
5
|
+
/** Set up DLQ for a given original queue */
|
|
6
|
+
setupDLQ(originalQueue: string): Promise<string>;
|
|
7
|
+
|
|
8
|
+
/** Handle a failed message by routing to DLQ */
|
|
9
|
+
handleFailedMessage(message: any, originalQueue: string): Promise<void>;
|
|
10
|
+
|
|
11
|
+
/** Check if DLQ is enabled in configuration */
|
|
12
|
+
isEnabled(): boolean;
|
|
13
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base SDK RabbitMQ error class
|
|
3
|
+
*/
|
|
4
|
+
export class SdkRabbitmqError extends Error {
|
|
5
|
+
constructor(
|
|
6
|
+
message: string,
|
|
7
|
+
public code: string,
|
|
8
|
+
public context?: any
|
|
9
|
+
) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = 'SdkRabbitmqError';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Configuration related errors
|
|
17
|
+
*/
|
|
18
|
+
export class ConfigurationError extends SdkRabbitmqError {
|
|
19
|
+
constructor(message: string, context?: any) {
|
|
20
|
+
super(message, 'CONFIGURATION_ERROR', context);
|
|
21
|
+
this.name = 'ConfigurationError';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Connection related errors
|
|
27
|
+
*/
|
|
28
|
+
export class ConnectionError extends SdkRabbitmqError {
|
|
29
|
+
constructor(message: string, context?: any) {
|
|
30
|
+
super(message, 'CONNECTION_ERROR', context);
|
|
31
|
+
this.name = 'ConnectionError';
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Message publishing errors
|
|
37
|
+
*/
|
|
38
|
+
export class PublishError extends SdkRabbitmqError {
|
|
39
|
+
constructor(message: string, context?: any) {
|
|
40
|
+
super(message, 'PUBLISH_ERROR', context);
|
|
41
|
+
this.name = 'PublishError';
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Message subscription errors
|
|
47
|
+
*/
|
|
48
|
+
export class SubscriptionError extends SdkRabbitmqError {
|
|
49
|
+
constructor(message: string, context?: any) {
|
|
50
|
+
super(message, 'SUBSCRIPTION_ERROR', context);
|
|
51
|
+
this.name = 'SubscriptionError';
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logger interface for structured logging
|
|
3
|
+
*/
|
|
4
|
+
export interface ILogger {
|
|
5
|
+
/** Log error message with optional context */
|
|
6
|
+
error(message: string, context?: any): void;
|
|
7
|
+
|
|
8
|
+
/** Log warning message with optional context */
|
|
9
|
+
warn(message: string, context?: any): void;
|
|
10
|
+
|
|
11
|
+
/** Log info message with optional context */
|
|
12
|
+
info(message: string, context?: any): void;
|
|
13
|
+
|
|
14
|
+
/** Log debug message with optional context */
|
|
15
|
+
debug(message: string, context?: any): void;
|
|
16
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message callback type for handling received messages
|
|
3
|
+
*/
|
|
4
|
+
export type MessageCallback = (
|
|
5
|
+
message: any,
|
|
6
|
+
ack: () => void,
|
|
7
|
+
nack: () => void
|
|
8
|
+
) => void;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Message publisher interface
|
|
12
|
+
*/
|
|
13
|
+
export interface IMessagePublisher {
|
|
14
|
+
/** Publish a message to an exchange */
|
|
15
|
+
publish(exchange: string, routingKey: string, payload: any): Promise<boolean>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Message subscriber interface
|
|
20
|
+
*/
|
|
21
|
+
export interface IMessageSubscriber {
|
|
22
|
+
/** Subscribe to messages from a queue */
|
|
23
|
+
subscribe(
|
|
24
|
+
exchange: string,
|
|
25
|
+
queue: string,
|
|
26
|
+
routingKey: string,
|
|
27
|
+
callback: MessageCallback
|
|
28
|
+
): Promise<void>;
|
|
29
|
+
|
|
30
|
+
/** Unsubscribe from a queue */
|
|
31
|
+
unsubscribe(queue: string): Promise<void>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* RabbitMQ message structure
|
|
36
|
+
*/
|
|
37
|
+
export interface RabbitMQMessage {
|
|
38
|
+
content: Buffer;
|
|
39
|
+
fields: {
|
|
40
|
+
deliveryTag: number;
|
|
41
|
+
redelivered: boolean;
|
|
42
|
+
exchange: string;
|
|
43
|
+
routingKey: string;
|
|
44
|
+
};
|
|
45
|
+
properties: {
|
|
46
|
+
contentType: string;
|
|
47
|
+
timestamp: number;
|
|
48
|
+
messageId: string;
|
|
49
|
+
headers: Record<string, any>;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Processed message structure
|
|
55
|
+
*/
|
|
56
|
+
export interface ProcessedMessage {
|
|
57
|
+
payload: any;
|
|
58
|
+
metadata: {
|
|
59
|
+
exchange: string;
|
|
60
|
+
routingKey: string;
|
|
61
|
+
timestamp: number;
|
|
62
|
+
deliveryTag: number;
|
|
63
|
+
redelivered: boolean;
|
|
64
|
+
};
|
|
65
|
+
}
|