amqp-suite 0.1.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/.github/workflows/npm-publish.yml +33 -0
- package/LICENSE +21 -0
- package/README.md +115 -0
- package/docs/assets/repository_banner.png +0 -0
- package/package.json +36 -0
- package/src/amqp-client.js +183 -0
- package/src/index.d.ts +14 -0
- package/src/index.js +3 -0
- package/tests/amqp.test.js +172 -0
- package/vitest.config.js +8 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
|
|
2
|
+
# For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages
|
|
3
|
+
|
|
4
|
+
name: Node.js Package
|
|
5
|
+
|
|
6
|
+
on:
|
|
7
|
+
release:
|
|
8
|
+
types: [created]
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
build:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
- uses: actions/setup-node@v4
|
|
16
|
+
with:
|
|
17
|
+
node-version: 20
|
|
18
|
+
- run: npm ci
|
|
19
|
+
- run: npm run test:run
|
|
20
|
+
|
|
21
|
+
publish-npm:
|
|
22
|
+
needs: build
|
|
23
|
+
runs-on: ubuntu-latest
|
|
24
|
+
steps:
|
|
25
|
+
- uses: actions/checkout@v4
|
|
26
|
+
- uses: actions/setup-node@v4
|
|
27
|
+
with:
|
|
28
|
+
node-version: 20
|
|
29
|
+
registry-url: https://registry.npmjs.org/
|
|
30
|
+
- run: npm ci
|
|
31
|
+
- run: npm publish
|
|
32
|
+
env:
|
|
33
|
+
NODE_AUTH_TOKEN: ${{secrets.npm_token}}
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Carlos Daniel
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# amqp-suite
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/amqp-suite)
|
|
4
|
+
[](https://www.npmjs.com/package/amqp-suite)
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
[](https://github.com/iamcarlosdaniel/amqp-suite)
|
|
7
|
+
|
|
8
|
+

|
|
9
|
+
|
|
10
|
+
`amqp-suite` is a simple and efficient AMQP (Advanced Message Queuing Protocol) client wrapper for Node.js that handles connection management, message publishing, and consuming messages from queues with a topic exchange. This package abstracts complex connection handling and simplifies AMQP usage in applications by providing easy-to-use methods for connecting, publishing, consuming, and gracefully shutting down the connection.
|
|
11
|
+
|
|
12
|
+
## Features
|
|
13
|
+
|
|
14
|
+
- Automatic Reconnection: Built-in retry logic for connection failures and drops.
|
|
15
|
+
- Simplified Pub/Sub: Designed for 'topic' exchanges to allow flexible routing.
|
|
16
|
+
- Structured Messaging: Automatic JSON serialization and deserialization.
|
|
17
|
+
- Error Handling: Graceful handling of malformed messages and channel crashes.
|
|
18
|
+
- Flow Control: Integrated prefetch support to prevent consumer saturation.
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install amqp-suite
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Quick Start
|
|
27
|
+
|
|
28
|
+
### 1. Initialize and Connect
|
|
29
|
+
|
|
30
|
+
Create an instance of the `AmqpClient` and establish a connection to the RabbitMQ broker.
|
|
31
|
+
|
|
32
|
+
```javascript
|
|
33
|
+
import { AmqpClient } from "amqp-suite";
|
|
34
|
+
|
|
35
|
+
const amqpClient = new AmqpClient("amqp://localhost", "my_exchange");
|
|
36
|
+
|
|
37
|
+
// Connect with optional retry config (retries, delay in ms)
|
|
38
|
+
await amqpClient.connect(5, 2000);
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### 2. Publish Messages
|
|
42
|
+
|
|
43
|
+
The `publish` method ensures that the message is stringified and sent as a persistent buffer.
|
|
44
|
+
|
|
45
|
+
```javascript
|
|
46
|
+
const payload = {
|
|
47
|
+
id: 123,
|
|
48
|
+
event: "user_created",
|
|
49
|
+
timestamp: new Date(),
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
await amqpClient.publish("user.events.create", payload);
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### 3. Consume Messages
|
|
56
|
+
|
|
57
|
+
The `consume` method automatically asserts queues, binds them to the exchange, and handles acknowledgments (`ack`/`nack`).
|
|
58
|
+
|
|
59
|
+
```javascript
|
|
60
|
+
await amqpClient.consume(
|
|
61
|
+
"user_service_queue",
|
|
62
|
+
async (content, msg) => {
|
|
63
|
+
console.log("Received data:", content);
|
|
64
|
+
// Business logic here...
|
|
65
|
+
},
|
|
66
|
+
{ prefetch: 10 }, // Optional: defaults to 10
|
|
67
|
+
"user.events.*" // Binding key (Topic pattern)
|
|
68
|
+
);
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## API Reference
|
|
72
|
+
|
|
73
|
+
### `new AmqpClient(amqpUrl, exchange)`
|
|
74
|
+
|
|
75
|
+
- **amqpUrl**: The connection string (e.g., `amqp://user:pass@localhost:5672`).
|
|
76
|
+
- **exchange**: The name of the topic exchange to use.
|
|
77
|
+
|
|
78
|
+
### `.connect(retries = 5, delay = 5000)`
|
|
79
|
+
|
|
80
|
+
Establishes the connection and creates the channel. If the connection drops, it will automatically attempt to reconnect.
|
|
81
|
+
|
|
82
|
+
- **retries** (optional): Number of reconnection attempts (default: `5`).
|
|
83
|
+
- **delay** (optional): Time interval (in milliseconds) between retries (default: `5000`).
|
|
84
|
+
|
|
85
|
+
### `.publish(routingKey, message, options = {})`
|
|
86
|
+
|
|
87
|
+
Publishes a message to the configured exchange with the given routing key.
|
|
88
|
+
|
|
89
|
+
- **routingKey**: The routing key used to route the message (e.g., `'user.events.create'`).
|
|
90
|
+
- **message**: The message payload, which will be stringified into JSON.
|
|
91
|
+
- **options** (optional): Additional publish options from `amqplib`.
|
|
92
|
+
|
|
93
|
+
### `.consume(queue, onMessage, options = {}, bindingKey = "#")`
|
|
94
|
+
|
|
95
|
+
Consumes messages from the specified queue. The callback `onMessage` is executed when a message is received.
|
|
96
|
+
|
|
97
|
+
- **queue**: The name of the queue to consume messages from.
|
|
98
|
+
- **onMessage**: An async callback function that will be called with the message content and the original message.
|
|
99
|
+
|
|
100
|
+
```javascript
|
|
101
|
+
async (content, rawMessage) => {
|
|
102
|
+
/* logic */
|
|
103
|
+
};
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
- **options** (optional): Additional options like `prefetch` (default is 10).
|
|
107
|
+
- **bindingKey** (optional): The routing pattern to bind the queue (default is `#`).
|
|
108
|
+
|
|
109
|
+
### `.close()`
|
|
110
|
+
|
|
111
|
+
Gracefully closes the channel and the connection.
|
|
112
|
+
|
|
113
|
+
## License
|
|
114
|
+
|
|
115
|
+
This project is licensed under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
Binary file
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "amqp-suite",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A simple wrapper for AMQP 0-9-1 messaging.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": {
|
|
7
|
+
"name": "Carlos Daniel Menchaca Arauz",
|
|
8
|
+
"email": "contact@iamcarlosdaniel.com",
|
|
9
|
+
"url": "http://iamcarlosdaniel.com"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"main": "src/index.js",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": "./src/index.js"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"test": "vitest",
|
|
18
|
+
"test:ui": "vitest --ui",
|
|
19
|
+
"test:run": "vitest run"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"amqp",
|
|
23
|
+
"exchange",
|
|
24
|
+
"pubsub",
|
|
25
|
+
"publisher",
|
|
26
|
+
"consumer",
|
|
27
|
+
"rabbitmq"
|
|
28
|
+
],
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"amqplib": "^0.10.9"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@vitest/ui": "^4.0.16",
|
|
34
|
+
"vitest": "^4.0.16"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import amqp from "amqplib";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview
|
|
5
|
+
* AMQP Service Class to manage AMQP communication.
|
|
6
|
+
* Handles automatic connection, channel management, and structured
|
|
7
|
+
* pub/sub patterns using 'topic' exchanges.
|
|
8
|
+
*/
|
|
9
|
+
class AmqpClient {
|
|
10
|
+
/**
|
|
11
|
+
* Creates an instance of the AMQP helper.
|
|
12
|
+
* @param {string} amqpUrl - The connection string (e.g., 'amqp://localhost').
|
|
13
|
+
* @param {string} exchange - The name of the exchange to be used.
|
|
14
|
+
*/
|
|
15
|
+
constructor(amqpUrl, exchange) {
|
|
16
|
+
/** @type {string} */
|
|
17
|
+
this.amqpUrl = amqpUrl;
|
|
18
|
+
/** @type {string} */
|
|
19
|
+
this.exchange = exchange;
|
|
20
|
+
/** @type {import('amqplib').Connection|null} */
|
|
21
|
+
this.connection = null;
|
|
22
|
+
/** @type {import('amqplib').Channel|null} */
|
|
23
|
+
this.channel = null;
|
|
24
|
+
/** @type {boolean} State flag to prevent multiple simultaneous connection attempts. */
|
|
25
|
+
this.isConnecting = false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Establishes a connection to the AMQP broker and sets up the infrastructure.
|
|
30
|
+
* Implements a recursive retry logic in case of connection failure or drops.
|
|
31
|
+
* @param {number} [retries=5] - Maximum number of reconnection attempts.
|
|
32
|
+
* @param {number} [delay=5000] - Time interval in milliseconds between retries.
|
|
33
|
+
* @returns {Promise<void>}
|
|
34
|
+
* @throws {Error} Throws an error if all retry attempts fail.
|
|
35
|
+
*/
|
|
36
|
+
async connect(retries = 5, delay = 5000) {
|
|
37
|
+
if (this.isConnecting) return;
|
|
38
|
+
this.isConnecting = true;
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
this.connection = await amqp.connect(this.amqpUrl);
|
|
42
|
+
this.channel = await this.connection.createChannel();
|
|
43
|
+
|
|
44
|
+
// Infrastructure initial setup: Using 'topic' for flexible routing
|
|
45
|
+
await this.channel.assertExchange(this.exchange, "topic", {
|
|
46
|
+
durable: true,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
this.connection.on("error", (err) => {
|
|
50
|
+
console.error("AMQP Connection Error:", err);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
this.connection.on("close", () => {
|
|
54
|
+
this.isConnecting = false;
|
|
55
|
+
this.connection = null;
|
|
56
|
+
this.channel = null;
|
|
57
|
+
console.warn(`AMQP connection lost. Retrying in ${delay}ms...`);
|
|
58
|
+
setTimeout(() => this.connect(), delay);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
console.log("AMQP: Connection established successfully");
|
|
62
|
+
this.isConnecting = false;
|
|
63
|
+
} catch (error) {
|
|
64
|
+
this.isConnecting = false;
|
|
65
|
+
if (retries > 0) {
|
|
66
|
+
console.error(
|
|
67
|
+
`AMQP: Connection failed. Retries left: ${retries}. Retrying in ${delay}ms...`
|
|
68
|
+
);
|
|
69
|
+
setTimeout(() => this.connect(retries - 1, delay), delay);
|
|
70
|
+
} else {
|
|
71
|
+
console.error(
|
|
72
|
+
"AMQP: Critical failure. Could not connect after maximum attempts."
|
|
73
|
+
);
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Publishes a message to the configured exchange.
|
|
81
|
+
* @param {string} routingKey - The routing key used to direct the message.
|
|
82
|
+
* @param {Object} message - The message payload (will be stringified to JSON).
|
|
83
|
+
* @param {import('amqplib').Options.Publish} [options={}] - Additional publish options.
|
|
84
|
+
* @returns {Promise<void>}
|
|
85
|
+
*/
|
|
86
|
+
async publish(routingKey, message, options = {}) {
|
|
87
|
+
if (!this.channel) {
|
|
88
|
+
console.log("AMQP: Channel not initialized. Attempting to connect...");
|
|
89
|
+
await this.connect();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const published = this.channel.publish(
|
|
94
|
+
this.exchange,
|
|
95
|
+
routingKey,
|
|
96
|
+
Buffer.from(JSON.stringify(message)),
|
|
97
|
+
{ persistent: true, ...options }
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
if (published) {
|
|
101
|
+
console.log(`AMQP: Message published to [${routingKey}]`);
|
|
102
|
+
} else {
|
|
103
|
+
console.warn(
|
|
104
|
+
"AMQP: Message was buffered locally. Check broker capacity or channel drain."
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
} catch (err) {
|
|
108
|
+
console.error("AMQP: Failed to publish message:", err);
|
|
109
|
+
throw err;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Consumes messages from a specific queue.
|
|
115
|
+
* @param {string} queue - The name of the queue to consume from.
|
|
116
|
+
* @param {function(Object, import('amqplib').ConsumeMessage): Promise<void>} onMessage -
|
|
117
|
+
* Callback executed when a message is received.
|
|
118
|
+
* @param {Object} [options={}] - Additional queue and prefetch options.
|
|
119
|
+
* @param {string} [bindingKey="#"] - The binding pattern to link the queue to the exchange.
|
|
120
|
+
* @returns {Promise<void>}
|
|
121
|
+
*/
|
|
122
|
+
async consume(queue, onMessage, options = {}, bindingKey = "#") {
|
|
123
|
+
if (!this.channel) {
|
|
124
|
+
console.log("AMQP: Channel not initialized. Attempting to connect...");
|
|
125
|
+
await this.connect();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
const { prefetch = 10, ...queueOptions } = options;
|
|
130
|
+
|
|
131
|
+
// Ensure the queue exists and is durable by default
|
|
132
|
+
await this.channel.assertQueue(queue, { durable: true, ...queueOptions });
|
|
133
|
+
await this.channel.bindQueue(queue, this.exchange, bindingKey);
|
|
134
|
+
|
|
135
|
+
// Limit unacknowledged messages to avoid overwhelming the consumer
|
|
136
|
+
await this.channel.prefetch(prefetch);
|
|
137
|
+
|
|
138
|
+
await this.channel.consume(
|
|
139
|
+
queue,
|
|
140
|
+
async (msg) => {
|
|
141
|
+
if (msg !== null) {
|
|
142
|
+
try {
|
|
143
|
+
const content = JSON.parse(msg.content.toString());
|
|
144
|
+
await onMessage(content, msg);
|
|
145
|
+
this.channel.ack(msg);
|
|
146
|
+
} catch (err) {
|
|
147
|
+
console.error(
|
|
148
|
+
`AMQP: Error processing message on queue [${queue}]:`,
|
|
149
|
+
err.message
|
|
150
|
+
);
|
|
151
|
+
// nack(message, requeue: false) to prevent infinite loops on malformed messages
|
|
152
|
+
this.channel.nack(msg, false, false);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
{ noAck: false }
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
console.log(
|
|
160
|
+
`AMQP: Consumer started for queue [${queue}] with binding [${bindingKey}]`
|
|
161
|
+
);
|
|
162
|
+
} catch (err) {
|
|
163
|
+
console.error("AMQP: Failed to initialize consumer:", err);
|
|
164
|
+
throw err;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Gracefully closes the AMQP channel and connection.
|
|
170
|
+
* @returns {Promise<void>}
|
|
171
|
+
*/
|
|
172
|
+
async close() {
|
|
173
|
+
try {
|
|
174
|
+
await this.channel?.close();
|
|
175
|
+
await this.connection?.close();
|
|
176
|
+
console.log("AMQP: Connection closed cleanly.");
|
|
177
|
+
} catch (err) {
|
|
178
|
+
console.error("AMQP: Error during shutdown:", err);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export { AmqpClient };
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
declare module "amqp-wrapper" {
|
|
2
|
+
export class AmqpClient {
|
|
3
|
+
constructor(amqpUrl: string, exchange: string);
|
|
4
|
+
connect(retries?: number, delay?: number): Promise<void>;
|
|
5
|
+
publish(routingKey: string, message: any, options?: any): Promise<void>;
|
|
6
|
+
consume(
|
|
7
|
+
queue: string,
|
|
8
|
+
onMessage: (content: any, msg: any) => Promise<void>,
|
|
9
|
+
options?: any,
|
|
10
|
+
bindingKey?: string
|
|
11
|
+
): Promise<void>;
|
|
12
|
+
close(): Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Unit tests for the AMQP class using Vitest and amqplib mocks.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
6
|
+
import amqp from "amqplib";
|
|
7
|
+
import { AmqpClient } from "amqp-suite";
|
|
8
|
+
|
|
9
|
+
vi.mock("amqplib");
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Test suite for AMQP integration and message handling.
|
|
13
|
+
*/
|
|
14
|
+
describe("AMQP Class", () => {
|
|
15
|
+
/** @type {AmqpClient} */
|
|
16
|
+
let amqpInstance;
|
|
17
|
+
|
|
18
|
+
/** @type {string} */
|
|
19
|
+
const mockUrl = "amqp://localhost";
|
|
20
|
+
|
|
21
|
+
/** @type {string} */
|
|
22
|
+
const mockExchange = "test_exchange";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Mocked AMQP Channel object with Vitest spy functions.
|
|
26
|
+
* @type {Object}
|
|
27
|
+
*/
|
|
28
|
+
const mockChannel = {
|
|
29
|
+
assertExchange: vi.fn().mockResolvedValue(undefined),
|
|
30
|
+
assertQueue: vi.fn().mockResolvedValue({}),
|
|
31
|
+
bindQueue: vi.fn().mockResolvedValue(undefined),
|
|
32
|
+
prefetch: vi.fn().mockResolvedValue(undefined),
|
|
33
|
+
publish: vi.fn().mockReturnValue(true),
|
|
34
|
+
consume: vi.fn().mockResolvedValue({ consumerTag: "abc" }),
|
|
35
|
+
ack: vi.fn(),
|
|
36
|
+
nack: vi.fn(),
|
|
37
|
+
close: vi.fn().mockResolvedValue(undefined),
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Mocked AMQP Connection object.
|
|
42
|
+
* @type {Object}
|
|
43
|
+
*/
|
|
44
|
+
const mockConnection = {
|
|
45
|
+
createChannel: vi.fn().mockResolvedValue(mockChannel),
|
|
46
|
+
on: vi.fn(),
|
|
47
|
+
close: vi.fn().mockResolvedValue(undefined),
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Setup hook to reset mocks and re-initialize the AMQP instance before each test.
|
|
52
|
+
*/
|
|
53
|
+
beforeEach(() => {
|
|
54
|
+
vi.clearAllMocks();
|
|
55
|
+
amqpInstance = new AmqpClient(mockUrl, mockExchange);
|
|
56
|
+
amqp.connect.mockResolvedValue(mockConnection);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Test case: Constructor property assignment.
|
|
61
|
+
*/
|
|
62
|
+
it("should correctly initialize values in the constructor", () => {
|
|
63
|
+
expect(amqpInstance.amqpUrl).toBe(mockUrl);
|
|
64
|
+
expect(amqpInstance.exchange).toBe(mockExchange);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Test case: Connection workflow and exchange assertion.
|
|
69
|
+
*/
|
|
70
|
+
it("should connect and create a channel successfully", async () => {
|
|
71
|
+
await amqpInstance.connect();
|
|
72
|
+
|
|
73
|
+
expect(amqp.connect).toHaveBeenCalledWith(mockUrl);
|
|
74
|
+
expect(mockConnection.createChannel).toHaveBeenCalled();
|
|
75
|
+
expect(mockChannel.assertExchange).toHaveBeenCalledWith(
|
|
76
|
+
mockExchange,
|
|
77
|
+
"topic",
|
|
78
|
+
{ durable: true }
|
|
79
|
+
);
|
|
80
|
+
expect(amqpInstance.connection).not.toBeNull();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Test case: Message publishing logic and buffer conversion.
|
|
85
|
+
*/
|
|
86
|
+
it("should publish a message correctly", async () => {
|
|
87
|
+
await amqpInstance.connect();
|
|
88
|
+
const routingKey = "user.created";
|
|
89
|
+
const message = { id: 1, name: "Test" };
|
|
90
|
+
|
|
91
|
+
await amqpInstance.publish(routingKey, message);
|
|
92
|
+
|
|
93
|
+
expect(mockChannel.publish).toHaveBeenCalledWith(
|
|
94
|
+
mockExchange,
|
|
95
|
+
routingKey,
|
|
96
|
+
expect.any(Buffer),
|
|
97
|
+
expect.objectContaining({ persistent: true })
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Test case: Consumer setup and successful message processing.
|
|
103
|
+
*/
|
|
104
|
+
it("should setup consumer and acknowledge messages on success", async () => {
|
|
105
|
+
await amqpInstance.connect();
|
|
106
|
+
const queueName = "test_queue";
|
|
107
|
+
const bindingKey = "test.key";
|
|
108
|
+
const mockPayload = { data: "hello world" };
|
|
109
|
+
const mockOnMessage = vi.fn().mockResolvedValue(undefined);
|
|
110
|
+
|
|
111
|
+
await amqpInstance.consume(
|
|
112
|
+
queueName,
|
|
113
|
+
mockOnMessage,
|
|
114
|
+
{ prefetch: 5 },
|
|
115
|
+
bindingKey
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
expect(mockChannel.assertQueue).toHaveBeenCalledWith(
|
|
119
|
+
queueName,
|
|
120
|
+
expect.objectContaining({ durable: true })
|
|
121
|
+
);
|
|
122
|
+
expect(mockChannel.bindQueue).toHaveBeenCalledWith(
|
|
123
|
+
queueName,
|
|
124
|
+
mockExchange,
|
|
125
|
+
bindingKey
|
|
126
|
+
);
|
|
127
|
+
expect(mockChannel.prefetch).toHaveBeenCalledWith(5);
|
|
128
|
+
|
|
129
|
+
const consumerCallback = mockChannel.consume.mock.calls[0][1];
|
|
130
|
+
const fakeMsg = {
|
|
131
|
+
content: Buffer.from(JSON.stringify(mockPayload)),
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
await consumerCallback(fakeMsg);
|
|
135
|
+
|
|
136
|
+
expect(mockOnMessage).toHaveBeenCalledWith(mockPayload, fakeMsg);
|
|
137
|
+
expect(mockChannel.ack).toHaveBeenCalledWith(fakeMsg);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Test case: Consumer error handling (nack).
|
|
142
|
+
*/
|
|
143
|
+
it("should nack messages if the processing fails", async () => {
|
|
144
|
+
await amqpInstance.connect();
|
|
145
|
+
const mockOnMessage = vi
|
|
146
|
+
.fn()
|
|
147
|
+
.mockRejectedValue(new Error("Processing failed"));
|
|
148
|
+
|
|
149
|
+
await amqpInstance.consume("error_queue", mockOnMessage);
|
|
150
|
+
|
|
151
|
+
const consumerCallback = mockChannel.consume.mock.calls[0][1];
|
|
152
|
+
const fakeMsg = {
|
|
153
|
+
content: Buffer.from(JSON.stringify({ some: "data" })),
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
await consumerCallback(fakeMsg);
|
|
157
|
+
|
|
158
|
+
expect(mockChannel.nack).toHaveBeenCalledWith(fakeMsg, false, false);
|
|
159
|
+
expect(mockChannel.ack).not.toHaveBeenCalled();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Test case: Graceful shutdown of channel and connection.
|
|
164
|
+
*/
|
|
165
|
+
it("should close the connection cleanly", async () => {
|
|
166
|
+
await amqpInstance.connect();
|
|
167
|
+
await amqpInstance.close();
|
|
168
|
+
|
|
169
|
+
expect(mockChannel.close).toHaveBeenCalled();
|
|
170
|
+
expect(mockConnection.close).toHaveBeenCalled();
|
|
171
|
+
});
|
|
172
|
+
});
|