@mojaloop/central-services-shared 18.26.1-snapshot.1 → 18.26.2

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/CHANGELOG.md CHANGED
@@ -2,6 +2,20 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ ### [18.26.2](https://github.com/mojaloop/central-services-shared/compare/v18.26.1...v18.26.2) (2025-05-20)
6
+
7
+
8
+ ### Chore
9
+
10
+ * update peer dependencies versions ([#453](https://github.com/mojaloop/central-services-shared/issues/453)) ([87932e5](https://github.com/mojaloop/central-services-shared/commit/87932e5382bc89a0a39638859334cba064b5b433))
11
+
12
+ ### [18.26.1](https://github.com/mojaloop/central-services-shared/compare/v18.26.0...v18.26.1) (2025-05-20)
13
+
14
+
15
+ ### Chore
16
+
17
+ * update dependencies ([#452](https://github.com/mojaloop/central-services-shared/issues/452)) ([b5c2e4f](https://github.com/mojaloop/central-services-shared/commit/b5c2e4f5c9cefac35ece322c50c9e20375f12317))
18
+
5
19
  ## [18.26.0](https://github.com/mojaloop/central-services-shared/compare/v18.25.0...v18.26.0) (2025-05-13)
6
20
 
7
21
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mojaloop/central-services-shared",
3
- "version": "18.26.1-snapshot.1",
3
+ "version": "18.26.2",
4
4
  "description": "Shared code for mojaloop central services",
5
5
  "license": "Apache-2.0",
6
6
  "author": "ModusBox",
@@ -80,27 +80,26 @@
80
80
  "openapi-backend": "5.12.0",
81
81
  "raw-body": "3.0.0",
82
82
  "rc": "1.2.8",
83
- "redis": "^5.0.1",
84
83
  "shins": "2.6.0",
85
84
  "ulidx": "2.4.1",
86
85
  "uuid4": "2.0.3",
87
86
  "widdershins": "4.0.1",
88
- "yaml": "2.7.1"
87
+ "yaml": "2.8.0"
89
88
  },
90
89
  "devDependencies": {
91
- "@mojaloop/central-services-error-handling": "13.0.7",
92
- "@mojaloop/central-services-logger": "11.8.1",
93
- "@mojaloop/central-services-metrics": "12.5.0",
94
- "@mojaloop/event-sdk": "14.4.0",
95
- "@mojaloop/sdk-standard-components": "19.14.0",
96
- "@opentelemetry/auto-instrumentations-node": "^0.58.1",
90
+ "@mojaloop/central-services-error-handling": "13.1.0",
91
+ "@mojaloop/central-services-logger": "11.9.0",
92
+ "@mojaloop/central-services-metrics": "12.6.0",
93
+ "@mojaloop/event-sdk": "14.5.1",
94
+ "@mojaloop/sdk-standard-components": "19.15.0",
95
+ "@opentelemetry/auto-instrumentations-node": "^0.59.0",
97
96
  "@types/hapi__joi": "17.1.15",
98
97
  "ajv": "^8.17.1",
99
98
  "ajv-formats": "^3.0.1",
100
99
  "ajv-keywords": "^5.1.0",
101
100
  "audit-ci": "7.1.0",
102
101
  "base64url": "3.0.1",
103
- "chance": "1.1.12",
102
+ "chance": "1.1.13",
104
103
  "npm-check-updates": "18.0.1",
105
104
  "nyc": "17.1.0",
106
105
  "portfinder": "1.0.37",
@@ -138,10 +137,10 @@
138
137
  "yargs-parser": "21.1.1"
139
138
  },
140
139
  "peerDependencies": {
141
- "@mojaloop/central-services-error-handling": "13.0.7",
142
- "@mojaloop/central-services-logger": "11.8.1",
143
- "@mojaloop/central-services-metrics": "12.5.0",
144
- "@mojaloop/event-sdk": "14.4.0",
140
+ "@mojaloop/central-services-error-handling": "13.1.0",
141
+ "@mojaloop/central-services-logger": "11.9.0",
142
+ "@mojaloop/central-services-metrics": "12.6.0",
143
+ "@mojaloop/event-sdk": "14.5.1",
145
144
  "ajv": "8.x.x",
146
145
  "ajv-formats": "3.x.x",
147
146
  "ajv-keywords": "5.x.x"
@@ -28,13 +28,20 @@
28
28
  ******/
29
29
 
30
30
  'use strict'
31
- const { createClient, createCluster } = require('redis')
31
+ const Redis = require('ioredis')
32
32
  const { createLogger } = require('../createLogger')
33
33
  const isClusterConfig = (config) => { return 'cluster' in config }
34
34
  const { rethrowRedisError } = require('../rethrow')
35
+ const { REDIS_SUCCESS, REDIS_IS_CONNECTED_STATUSES } = require('../../constants')
35
36
 
36
37
  class PubSub {
37
38
  constructor (config, publisherClient, subscriberClient) {
39
+ // prepare redis connection instances
40
+ // once the client enters the subscribed state it is not supposed to issue
41
+ // any other commands, except for additional SUBSCRIBE, PSUBSCRIBE,
42
+ // UNSUBSCRIBE, PUNSUBSCRIBE, PING and QUIT commands.
43
+ // So we create two clients, one for subscribing another for
44
+ // and publishing
38
45
  this.config = config
39
46
  this.isCluster = isClusterConfig(config)
40
47
  this.log = createLogger(this.constructor.name)
@@ -45,17 +52,10 @@ class PubSub {
45
52
  }
46
53
 
47
54
  createRedisClient () {
48
- if (this.isCluster) {
49
- // node-redis cluster config expects rootNodes: [{url: 'redis://host:port'}]
50
- const rootNodes = this.config.cluster.map(
51
- node => ({ url: `redis://${node.host}:${node.port}` })
52
- )
53
- return createCluster({ rootNodes })
54
- } else {
55
- // node-redis expects url: 'redis://host:port'
56
- const url = `redis://${this.config.host}:${this.config.port}`
57
- return createClient({ url, ...this.config })
58
- }
55
+ this.config.lazyConnect ??= true
56
+ return this.isCluster
57
+ ? new Redis.Cluster(this.config.cluster, { ...this.config, shardedSubscribers: true })
58
+ : new Redis(this.config)
59
59
  }
60
60
 
61
61
  async connect () {
@@ -71,12 +71,12 @@ class PubSub {
71
71
 
72
72
  async disconnect () {
73
73
  try {
74
- await this.publisherClient.quit()
75
- await this.subscriberClient.quit()
76
- this.subscriberClient.removeAllListeners && this.subscriberClient.removeAllListeners()
74
+ const publisherResponse = await this.publisherClient.quit()
75
+ const subscriberResponse = await this.subscriberClient.quit()
76
+ const isDisconnected = publisherResponse === REDIS_SUCCESS && subscriberResponse === REDIS_SUCCESS
77
+ this.subscriberClient.removeAllListeners()
77
78
  this.log.info('Redis clients disconnected successfully')
78
- // node-redis returns 'OK' on quit
79
- return true
79
+ return isDisconnected
80
80
  } catch (err) {
81
81
  this.log.error('Error disconnecting Redis clients:', err)
82
82
  rethrowRedisError(err)
@@ -97,9 +97,8 @@ class PubSub {
97
97
  }
98
98
 
99
99
  get isConnected () {
100
- // node-redis: status is 'ready' when connected
101
- const publisherConnected = this.publisherClient.isOpen
102
- const subscriberConnected = this.subscriberClient.isOpen
100
+ const publisherConnected = REDIS_IS_CONNECTED_STATUSES.includes(this.publisherClient.status)
101
+ const subscriberConnected = REDIS_IS_CONNECTED_STATUSES.includes(this.subscriberClient.status)
103
102
  this.log.debug('Redis connection status', {
104
103
  publisherConnected,
105
104
  subscriberConnected
@@ -108,9 +107,7 @@ class PubSub {
108
107
  }
109
108
 
110
109
  addEventListeners (client) {
111
- client.on('connect', () => this.log.info('Redis client connecting'))
112
- client.on('ready', () => this.log.info('Redis client ready'))
113
- client.on('end', () => this.log.info('Redis client connection closed'))
110
+ client.on('connect', () => this.log.info('Redis client connected'))
114
111
  client.on('error', (err) => this.log.error('Redis client error:', err))
115
112
  }
116
113
 
@@ -118,11 +115,10 @@ class PubSub {
118
115
  try {
119
116
  if (this.isCluster) {
120
117
  await this.publisherClient.spublish(channel, JSON.stringify(message))
121
- this.log.info(`Message SPUBLISHED to channel: ${channel}`)
122
118
  } else {
123
119
  await this.publisherClient.publish(channel, JSON.stringify(message))
124
- this.log.info(`Message published to channel: ${channel}`)
125
120
  }
121
+ this.log.info(`Message published to channel: ${channel}`)
126
122
  } catch (err) {
127
123
  this.log.error('Error publishing message:', err)
128
124
  rethrowRedisError(err)
@@ -132,20 +128,21 @@ class PubSub {
132
128
  async subscribe (channel, callback) {
133
129
  try {
134
130
  if (this.isCluster) {
135
- await this.subscriberClient.ssubscribe(channel, (message, subscribedChannel) => {
131
+ await this.subscriberClient.ssubscribe(channel)
132
+ this.subscriberClient.on('smessage', (subscribedChannel, message) => {
136
133
  if (subscribedChannel === channel) {
137
134
  callback(JSON.parse(message))
138
135
  }
139
136
  })
140
- this.log.info(`SSUBSCRIBED to channel: ${channel}`)
141
137
  } else {
142
- await this.subscriberClient.subscribe(channel, (message, subscribedChannel) => {
138
+ await this.subscriberClient.subscribe(channel)
139
+ this.subscriberClient.on('message', (subscribedChannel, message) => {
143
140
  if (subscribedChannel === channel) {
144
141
  callback(JSON.parse(message))
145
142
  }
146
143
  })
147
- this.log.info(`Subscribed to channel: ${channel}`)
148
144
  }
145
+ this.log.info(`Subscribed to channel: ${channel}`)
149
146
  return channel
150
147
  } catch (err) {
151
148
  this.log.error('Error subscribing to channel:', err)
@@ -157,11 +154,10 @@ class PubSub {
157
154
  try {
158
155
  if (this.isCluster) {
159
156
  await this.subscriberClient.sunsubscribe(channel)
160
- this.log.info(`SUNSUBSCRIBED from channel: ${channel}`)
161
157
  } else {
162
158
  await this.subscriberClient.unsubscribe(channel)
163
- this.log.info(`Unsubscribed from channel: ${channel}`)
164
159
  }
160
+ this.log.info(`Unsubscribed from channel: ${channel}`)
165
161
  } catch (err) {
166
162
  this.log.error('Error unsubscribing from channel:', err)
167
163
  rethrowRedisError(err)
@@ -22,486 +22,502 @@
22
22
  * Mojaloop Foundation
23
23
  - Name Surname <name.surname@mojaloop.io>
24
24
 
25
- * Kevin Leyow <kevin.leyow@infitx.com>
25
+ * Kevin Leyow <kevin.leyow@modusbox.com>
26
26
 
27
27
  --------------
28
28
  ******/
29
29
  const Test = require('tapes')(require('tape'))
30
30
  const sinon = require('sinon')
31
- let PubSub = require('../../../../src/util/redis/pubSub')
32
- const proxyquire = require('proxyquire')
31
+ const Redis = require('ioredis')
32
+ const PubSub = require('../../../../src/util/redis/pubSub')
33
+ const { constructSystemExtensionError } = require('../../../../src/util/rethrow')
33
34
 
34
- Test('PubSub', pubSubTest => {
35
- let sandbox, publisherStub, subscriberStub, pubSub, logStub
35
+ Test('PubSub', (t) => {
36
+ let sandbox
36
37
 
37
- pubSubTest.beforeEach(t => {
38
+ t.beforeEach((t) => {
38
39
  sandbox = sinon.createSandbox()
39
- logStub = {
40
- info: sandbox.stub(),
41
- error: sandbox.stub(),
42
- debug: sandbox.stub()
43
- }
44
- publisherStub = {
45
- connect: sandbox.stub().resolves(),
46
- quit: sandbox.stub().resolves(),
47
- ping: sandbox.stub().resolves('PONG'),
48
- publish: sandbox.stub().resolves(),
49
- spublish: undefined,
50
- isOpen: true,
51
- on: sandbox.stub().returnsThis(),
52
- removeAllListeners: sandbox.stub()
53
- }
54
- subscriberStub = {
55
- connect: sandbox.stub().resolves(),
56
- quit: sandbox.stub().resolves(),
57
- ping: sandbox.stub().resolves('PONG'),
58
- subscribe: sandbox.stub().resolves(),
59
- unsubscribe: sandbox.stub().resolves(),
60
- ssubscribe: undefined,
61
- sunsubscribe: undefined,
62
- isOpen: true,
63
- on: sandbox.stub().returnsThis(),
64
- removeAllListeners: sandbox.stub()
65
- }
66
- sandbox.stub(require('../../../../src/util/createLogger'), 'createLogger').returns(logStub)
40
+ sandbox.stub(Redis.prototype, 'publish')
41
+ sandbox.stub(Redis.prototype, 'subscribe')
42
+ sandbox.stub(Redis.prototype, 'unsubscribe')
43
+ sandbox.stub(Redis.prototype, 'on')
44
+ sandbox.stub(Redis.Cluster.prototype, 'on')
67
45
  t.end()
68
46
  })
69
47
 
70
- pubSubTest.afterEach(t => {
48
+ t.afterEach((t) => {
71
49
  sandbox.restore()
72
50
  t.end()
73
51
  })
74
52
 
75
- pubSubTest.test('should connect publisher and subscriber clients', async t => {
76
- pubSub = new PubSub({ host: 'localhost', port: 6379 }, publisherStub, subscriberStub)
53
+ t.test('should create a Redis client and subscriber', (t) => {
54
+ const config = { lazyConnect: true }
55
+ const pubSub = new PubSub(config)
56
+ t.ok(pubSub.publisherClient instanceof Redis, 'publisherClient is an instance of Redis')
57
+ t.ok(pubSub.subscriberClient instanceof Redis, 'subscriberClient is an instance of Redis')
58
+ t.end()
59
+ })
77
60
 
78
- await pubSub.connect()
79
- t.ok(publisherStub.connect.called, 'publisher connect called')
80
- t.ok(subscriberStub.connect.called, 'subscriber connect called')
61
+ t.test('should publish a message to a channel', async (t) => {
62
+ const config = {}
63
+ const pubSub = new PubSub(config)
64
+ const channel = 'test-channel'
65
+ const message = { key: 'value' }
66
+ await pubSub.publish(channel, message)
67
+
68
+ t.ok(pubSub.publisherClient.publish.calledWith(channel, JSON.stringify(message)), 'publish called with correct arguments')
81
69
  t.end()
82
70
  })
83
71
 
84
- pubSubTest.test('should disconnect publisher and subscriber clients', async t => {
85
- pubSub = new PubSub({ host: 'localhost', port: 6379 }, publisherStub, subscriberStub)
72
+ t.test('should handle error when publishing a message', async (t) => {
73
+ const config = {}
74
+ const pubSub = new PubSub(config)
75
+ const channel = 'test-channel'
76
+ const message = { key: 'value' }
77
+ const error = new Error('Publish error')
86
78
 
87
- await pubSub.disconnect()
88
- t.ok(publisherStub.quit.called, 'publisher quit called')
89
- t.ok(subscriberStub.quit.called, 'subscriber quit called')
90
- t.ok(subscriberStub.removeAllListeners.called, 'subscriber removeAllListeners called')
79
+ pubSub.publisherClient.publish.rejects(error)
80
+
81
+ try {
82
+ await pubSub.publish(channel, message)
83
+ t.fail('Should have thrown an error')
84
+ } catch (err) {
85
+ t.deepEqual(err, constructSystemExtensionError(error, '["redis"]'), 'Error thrown and rethrown correctly')
86
+ }
91
87
  t.end()
92
88
  })
93
89
 
94
- pubSubTest.test('should return true on healthy healthCheck', async t => {
95
- pubSub = new PubSub({ host: 'localhost', port: 6379 }, publisherStub, subscriberStub)
90
+ t.test('should subscribe to a channel and handle messages', async (t) => {
91
+ const config = {}
92
+ const pubSub = new PubSub(config)
93
+ const channel = 'test-channel'
94
+ const callback = sinon.stub()
95
+ const message = JSON.stringify({ key: 'value' })
96
+
97
+ await pubSub.subscribe(channel, callback)
98
+ pubSub.subscriberClient.on.callArgWith(1, channel, message)
96
99
 
97
- const result = await pubSub.healthCheck()
98
- t.equal(result, true, 'healthCheck returns true')
99
- t.ok(publisherStub.ping.called, 'publisher ping called')
100
- t.ok(subscriberStub.ping.called, 'subscriber ping called')
100
+ t.ok(pubSub.subscriberClient.subscribe.calledWith(channel), 'subscribe called with correct channel')
101
+ t.ok(callback.calledWith(JSON.parse(message)), 'callback called with parsed message')
101
102
  t.end()
102
103
  })
103
104
 
104
- pubSubTest.test('should return false on unhealthy healthCheck', async t => {
105
- pubSub = new PubSub({ host: 'localhost', port: 6379 }, publisherStub, subscriberStub)
105
+ t.test('should handle error when subscribing to a channel', async (t) => {
106
+ const config = {}
107
+ const pubSub = new PubSub(config)
108
+ const channel = 'test-channel'
109
+ const callback = sinon.stub()
110
+ const error = new Error('Subscribe error')
106
111
 
107
- publisherStub.ping.rejects(new Error('fail'))
108
- const result = await pubSub.healthCheck()
109
- t.equal(result, false, 'healthCheck returns false')
112
+ pubSub.subscriberClient.subscribe.rejects(error)
113
+
114
+ try {
115
+ await pubSub.subscribe(channel, callback)
116
+ t.fail('Should have thrown an error')
117
+ } catch (err) {
118
+ t.deepEqual(err, constructSystemExtensionError(error, '["redis"]'), 'Error thrown and rethrown correctly')
119
+ }
110
120
  t.end()
111
121
  })
112
122
 
113
- pubSubTest.test('should return isConnected status', t => {
114
- pubSub = new PubSub({ host: 'localhost', port: 6379 }, publisherStub, subscriberStub)
123
+ t.test('should unsubscribe from a channel', async (t) => {
124
+ const config = {}
125
+ const pubSub = new PubSub(config)
126
+ const channel = 'test-channel'
115
127
 
116
- const status = pubSub.isConnected
117
- t.deepEqual(status, { publisherConnected: true, subscriberConnected: true }, 'isConnected returns correct status')
128
+ await pubSub.unsubscribe(channel)
129
+
130
+ t.ok(pubSub.subscriberClient.unsubscribe.calledWith(channel), 'unsubscribe called with correct channel')
118
131
  t.end()
119
132
  })
120
133
 
121
- pubSubTest.test('should publish message to channel', async t => {
122
- pubSub = new PubSub({ host: 'localhost', port: 6379 }, publisherStub, subscriberStub)
134
+ t.test('should handle error when unsubscribing from a channel', async (t) => {
135
+ const config = {}
136
+ const pubSub = new PubSub(config)
137
+ const channel = 'test-channel'
138
+ const error = new Error('Unsubscribe error')
139
+
140
+ pubSub.subscriberClient.unsubscribe.rejects(error)
123
141
 
124
- await pubSub.publish('test-channel', { foo: 'bar' })
125
- t.ok(publisherStub.publish.calledWith('test-channel', JSON.stringify({ foo: 'bar' })), 'publish called with correct args')
142
+ try {
143
+ await pubSub.unsubscribe(channel)
144
+ t.fail('Should have thrown an error')
145
+ } catch (err) {
146
+ t.deepEqual(err, constructSystemExtensionError(error, '["redis"]'), 'Error thrown and rethrown correctly')
147
+ }
126
148
  t.end()
127
149
  })
128
150
 
129
- pubSubTest.test('should publish message using spublish in cluster mode', async t => {
130
- pubSub = new PubSub({ host: 'localhost', port: 6379 }, publisherStub, subscriberStub)
151
+ t.test('should broadcast a message to multiple channels', async (t) => {
152
+ const config = {}
153
+ const pubSub = new PubSub(config)
154
+ const channels = ['channel1', 'channel2']
155
+ const message = { key: 'value' }
156
+
157
+ await pubSub.broadcast(channels, message)
131
158
 
132
- pubSub.isCluster = true
133
- publisherStub.spublish = sandbox.stub().resolves()
134
- await pubSub.publish('test-channel', { foo: 'bar' })
135
- t.ok(publisherStub.spublish.calledWith('test-channel', JSON.stringify({ foo: 'bar' })), 'spublish called with correct args')
159
+ t.ok(pubSub.publisherClient.publish.calledTwice, 'publish called twice')
160
+ t.ok(pubSub.publisherClient.publish.firstCall.calledWith(channels[0], JSON.stringify(message)), 'publish called with first channel and message')
161
+ t.ok(pubSub.publisherClient.publish.secondCall.calledWith(channels[1], JSON.stringify(message)), 'publish called with second channel and message')
136
162
  t.end()
137
163
  })
138
164
 
139
- pubSubTest.test('should subscribe to channel', async t => {
140
- pubSub = new PubSub({ host: 'localhost', port: 6379 }, publisherStub, subscriberStub)
165
+ t.test('should handle error when broadcasting a message', async (t) => {
166
+ const config = {}
167
+ const pubSub = new PubSub(config)
168
+ const channels = ['channel1', 'channel2']
169
+ const message = { key: 'value' }
170
+ const error = new Error('Broadcast error')
171
+
172
+ pubSub.publisherClient.publish.onFirstCall().rejects(error)
141
173
 
142
- const callback = sandbox.stub()
143
- await pubSub.subscribe('test-channel', callback)
144
- t.ok(subscriberStub.subscribe.called, 'subscribe called')
174
+ try {
175
+ await pubSub.broadcast(channels, message)
176
+ t.fail('Should have thrown an error')
177
+ } catch (err) {
178
+ t.deepEqual(err, constructSystemExtensionError(error, '["redis"]'), 'Error thrown and rethrown correctly')
179
+ }
145
180
  t.end()
146
181
  })
147
182
 
148
- pubSubTest.test('should subscribe using ssubscribe in cluster mode', async t => {
149
- pubSub = new PubSub({ host: 'localhost', port: 6379 }, publisherStub, subscriberStub)
183
+ t.test('should connect Redis clients successfully', async (t) => {
184
+ const config = {}
185
+ const pubSub = new PubSub(config)
186
+
187
+ sandbox.stub(pubSub.publisherClient, 'connect').resolves()
188
+ sandbox.stub(pubSub.subscriberClient, 'connect').resolves()
189
+
190
+ await pubSub.connect()
150
191
 
151
- pubSub.isCluster = true
152
- subscriberStub.ssubscribe = sandbox.stub().resolves()
153
- const callback = sandbox.stub()
154
- await pubSub.subscribe('test-channel', callback)
155
- t.ok(subscriberStub.ssubscribe.called, 'ssubscribe called')
192
+ t.ok(pubSub.publisherClient.connect.calledOnce, 'publisherClient connect called once')
193
+ t.ok(pubSub.subscriberClient.connect.calledOnce, 'subscriberClient connect called once')
156
194
  t.end()
157
195
  })
158
196
 
159
- pubSubTest.test('should unsubscribe from channel', async t => {
160
- pubSub = new PubSub({ host: 'localhost', port: 6379 }, publisherStub, subscriberStub)
197
+ t.test('should handle error when connecting Redis clients', async (t) => {
198
+ const config = {}
199
+ const pubSub = new PubSub(config)
200
+ const error = new Error('Connect error')
201
+
202
+ sandbox.stub(pubSub.publisherClient, 'connect').rejects(error)
203
+ sandbox.stub(pubSub.subscriberClient, 'connect').resolves()
161
204
 
162
- await pubSub.unsubscribe('test-channel')
163
- t.ok(subscriberStub.unsubscribe.calledWith('test-channel'), 'unsubscribe called')
205
+ try {
206
+ await pubSub.connect()
207
+ t.fail('Should have thrown an error')
208
+ } catch (err) {
209
+ t.deepEqual(err, constructSystemExtensionError(error, '["redis"]'), 'Error thrown and rethrown correctly')
210
+ }
164
211
  t.end()
165
212
  })
166
213
 
167
- pubSubTest.test('should unsubscribe using sunsubscribe in cluster mode', async t => {
168
- pubSub = new PubSub({ host: 'localhost', port: 6379 }, publisherStub, subscriberStub)
214
+ t.test('should create a Redis Cluster client when cluster config is provided', (t) => {
215
+ const config = { cluster: [{ host: '127.0.0.1', port: 6379 }] }
216
+ const pubSub = new PubSub(config)
169
217
 
170
- pubSub.isCluster = true
171
- subscriberStub.sunsubscribe = sandbox.stub().resolves()
172
- await pubSub.unsubscribe('test-channel')
173
- t.ok(subscriberStub.sunsubscribe.calledWith('test-channel'), 'sunsubscribe called')
218
+ t.ok(pubSub.publisherClient instanceof Redis.Cluster, 'publisherClient is an instance of Redis.Cluster')
219
+ t.ok(pubSub.subscriberClient instanceof Redis.Cluster, 'subscriberClient is an instance of Redis.Cluster')
174
220
  t.end()
175
221
  })
176
222
 
177
- pubSubTest.test('should broadcast message to multiple channels', async t => {
178
- pubSub = new PubSub({ host: 'localhost', port: 6379 }, publisherStub, subscriberStub)
223
+ t.test('should connect Redis Cluster clients successfully', async (t) => {
224
+ const config = { cluster: [{ host: '127.0.0.1', port: 6379 }] }
225
+ const pubSub = new PubSub(config)
226
+
227
+ sandbox.stub(pubSub.publisherClient, 'connect').resolves()
228
+ sandbox.stub(pubSub.subscriberClient, 'connect').resolves()
229
+
230
+ await pubSub.connect()
179
231
 
180
- sandbox.stub(pubSub, 'publish').resolves()
181
- await pubSub.broadcast(['a', 'b'], { foo: 'bar' })
182
- t.ok(pubSub.publish.calledTwice, 'publish called for each channel')
232
+ t.ok(pubSub.publisherClient.connect.calledOnce, 'publisherClient connect called once')
233
+ t.ok(pubSub.subscriberClient.connect.calledOnce, 'subscriberClient connect called once')
183
234
  t.end()
184
235
  })
185
236
 
186
- pubSubTest.test('should handle error on connect', async t => {
187
- pubSub = new PubSub({ host: 'localhost', port: 6379 }, publisherStub, subscriberStub)
237
+ t.test('should handle error when connecting Redis Cluster clients', async (t) => {
238
+ const config = { cluster: [{ host: '127.0.0.1', port: 6379 }] }
239
+ const pubSub = new PubSub(config)
240
+ const error = new Error('Cluster connect error')
241
+
242
+ sandbox.stub(pubSub.publisherClient, 'connect').rejects(error)
243
+ sandbox.stub(pubSub.subscriberClient, 'connect').resolves()
188
244
 
189
- publisherStub.connect.rejects(new Error('connect error'))
190
245
  try {
191
246
  await pubSub.connect()
192
- t.fail('Expected error not thrown')
247
+ t.fail('Should have thrown an error')
193
248
  } catch (err) {
194
- t.match(err.message, /connect error/, 'throws error on connect failure')
249
+ t.deepEqual(err, constructSystemExtensionError(error, '["redis"]'), 'Error thrown and rethrown correctly')
195
250
  }
196
251
  t.end()
197
252
  })
198
253
 
199
- pubSubTest.test('should handle error on disconnect', async t => {
200
- pubSub = new PubSub({ host: 'localhost', port: 6379 }, publisherStub, subscriberStub)
254
+ t.test('should not call callback if subscribedChannel does not match channel', async (t) => {
255
+ const config = {}
256
+ const pubSub = new PubSub(config)
257
+ const channel = 'test-channel'
258
+ const callback = sinon.stub()
259
+ const message = JSON.stringify({ key: 'value' })
260
+ const otherChannel = 'other-channel'
201
261
 
202
- publisherStub.quit.rejects(new Error('disconnect error'))
203
- try {
204
- await pubSub.disconnect()
205
- t.fail('Expected error not thrown')
206
- } catch (err) {
207
- t.match(err.message, /disconnect error/, 'throws error on disconnect failure')
208
- }
262
+ await pubSub.subscribe(channel, callback)
263
+ pubSub.subscriberClient.on.callArgWith(1, otherChannel, message)
264
+
265
+ t.ok(pubSub.subscriberClient.subscribe.calledWith(channel), 'subscribe called with correct channel')
266
+ t.notOk(callback.called, 'callback not called when subscribedChannel does not match channel')
209
267
  t.end()
210
268
  })
211
269
 
212
- pubSubTest.test('should handle error on publish', async t => {
213
- pubSub = new PubSub({ host: 'localhost', port: 6379 }, publisherStub, subscriberStub)
270
+ t.test('should disconnect Redis clients successfully', async (t) => {
271
+ const config = {}
272
+ const pubSub = new PubSub(config)
214
273
 
215
- publisherStub.publish.rejects(new Error('publish error'))
216
- try {
217
- await pubSub.publish('chan', { a: 1 })
218
- t.fail('Expected error not thrown')
219
- } catch (err) {
220
- t.match(err.message, /publish error/, 'throws error on publish failure')
221
- }
274
+ sandbox.stub(pubSub.publisherClient, 'quit').resolves()
275
+ sandbox.stub(pubSub.subscriberClient, 'quit').resolves()
276
+ sandbox.stub(pubSub.subscriberClient, 'removeAllListeners').resolves()
277
+
278
+ await pubSub.disconnect()
279
+
280
+ t.ok(pubSub.publisherClient.quit.calledOnce, 'publisherClient quit called once')
281
+ t.ok(pubSub.subscriberClient.quit.calledOnce, 'subscriberClient quit called once')
282
+ t.ok(pubSub.subscriberClient.removeAllListeners.calledOnce, 'subscriberClient removeAllListeners called once')
222
283
  t.end()
223
284
  })
224
285
 
225
- pubSubTest.test('should handle error on subscribe', async t => {
226
- pubSub = new PubSub({ host: 'localhost', port: 6379 }, publisherStub, subscriberStub)
286
+ t.test('should handle error when disconnecting Redis clients', async (t) => {
287
+ const config = {}
288
+ const pubSub = new PubSub(config)
289
+ const error = new Error('Disconnect error')
290
+
291
+ sandbox.stub(pubSub.publisherClient, 'quit').rejects(error)
292
+ sandbox.stub(pubSub.subscriberClient, 'quit').resolves()
227
293
 
228
- subscriberStub.subscribe.rejects(new Error('subscribe error'))
229
294
  try {
230
- await pubSub.subscribe('chan', () => {})
231
- t.fail('Expected error not thrown')
295
+ await pubSub.disconnect()
296
+ t.fail('Should have thrown an error')
232
297
  } catch (err) {
233
- t.match(err.message, /subscribe error/, 'throws error on subscribe failure')
298
+ t.deepEqual(err, constructSystemExtensionError(error, '["redis"]'), 'Error thrown and rethrown correctly')
234
299
  }
235
300
  t.end()
236
301
  })
237
302
 
238
- pubSubTest.test('should handle error on unsubscribe', async t => {
239
- pubSub = new PubSub({ host: 'localhost', port: 6379 }, publisherStub, subscriberStub)
303
+ t.test('should perform health check and return true if both clients are healthy', async (t) => {
304
+ const config = {}
305
+ const pubSub = new PubSub(config)
240
306
 
241
- subscriberStub.unsubscribe.rejects(new Error('unsubscribe error'))
242
- try {
243
- await pubSub.unsubscribe('chan')
244
- t.fail('Expected error not thrown')
245
- } catch (err) {
246
- t.match(err.message, /unsubscribe error/, 'throws error on unsubscribe failure')
247
- }
307
+ sandbox.stub(pubSub.publisherClient, 'ping').resolves('PONG')
308
+ sandbox.stub(pubSub.subscriberClient, 'ping').resolves('PONG')
309
+
310
+ const isHealthy = await pubSub.healthCheck()
311
+
312
+ t.equal(isHealthy, true, 'healthCheck returns true when both clients are healthy')
313
+ t.ok(pubSub.publisherClient.ping.calledOnce, 'publisherClient ping called once')
314
+ t.ok(pubSub.subscriberClient.ping.calledOnce, 'subscriberClient ping called once')
248
315
  t.end()
249
316
  })
250
317
 
251
- pubSubTest.test('should handle error on broadcast', async t => {
252
- pubSub = new PubSub({ host: 'localhost', port: 6379 }, publisherStub, subscriberStub)
318
+ t.test('should perform health check and return false if any client is unhealthy', async (t) => {
319
+ const config = {}
320
+ const pubSub = new PubSub(config)
253
321
 
254
- sandbox.stub(pubSub, 'publish').rejects(new Error('broadcast error'))
255
- try {
256
- await pubSub.broadcast(['a', 'b'], { foo: 'bar' })
257
- t.fail('Expected error not thrown')
258
- } catch (err) {
259
- t.match(err.message, /broadcast error/, 'throws error on broadcast failure')
260
- }
322
+ sandbox.stub(pubSub.publisherClient, 'ping').resolves('PONG')
323
+ sandbox.stub(pubSub.subscriberClient, 'ping').resolves('ERROR')
324
+
325
+ const isHealthy = await pubSub.healthCheck()
326
+
327
+ t.equal(isHealthy, false, 'healthCheck returns false when any client is unhealthy')
328
+ t.ok(pubSub.publisherClient.ping.calledOnce, 'publisherClient ping called once')
329
+ t.ok(pubSub.subscriberClient.ping.calledOnce, 'subscriberClient ping called once')
261
330
  t.end()
262
331
  })
263
332
 
264
- pubSubTest.test('should call callback with parsed message on subscribe', async t => {
265
- pubSub = new PubSub({ host: 'localhost', port: 6379 }, publisherStub, subscriberStub)
333
+ t.test('should handle error during health check and return false', async (t) => {
334
+ const config = {}
335
+ const pubSub = new PubSub(config)
336
+ const error = new Error('Health check error')
337
+
338
+ sandbox.stub(pubSub.publisherClient, 'ping').rejects(error)
339
+ sandbox.stub(pubSub.subscriberClient, 'ping').resolves('PONG')
266
340
 
267
- let received
268
- subscriberStub.subscribe.callsFake(async (channel, cb) => {
269
- cb(JSON.stringify({ test: 1 }), channel)
270
- })
271
- await pubSub.subscribe('chan', msg => { received = msg })
272
- t.same(received, { test: 1 }, 'callback called with parsed message')
341
+ const isHealthy = await pubSub.healthCheck()
342
+
343
+ t.equal(isHealthy, false, 'healthCheck returns false when an error occurs')
344
+ t.ok(pubSub.publisherClient.ping.calledOnce, 'publisherClient ping called once')
345
+ t.notOk(pubSub.subscriberClient.ping.calledOnce, 'subscriberClient ping not called once')
273
346
  t.end()
274
347
  })
275
348
 
276
- pubSubTest.test('should call callback with parsed message on ssubscribe in cluster mode', async t => {
277
- pubSub = new PubSub({ host: 'localhost', port: 6379 }, publisherStub, subscriberStub)
349
+ t.test('should return correct connection statuses for isConnected', (t) => {
350
+ const config = {}
351
+ const pubSub = new PubSub(config)
352
+
353
+ sandbox.stub(pubSub.publisherClient, 'status').value('ready')
354
+ sandbox.stub(pubSub.subscriberClient, 'status').value('ready')
355
+
356
+ const connectionStatus = pubSub.isConnected
278
357
 
279
- pubSub.isCluster = true
280
- let received
281
- subscriberStub.ssubscribe = sandbox.stub().callsFake(async (channel, cb) => {
282
- cb(JSON.stringify({ test: 2 }), channel)
283
- })
284
- await pubSub.subscribe('chan', msg => { received = msg })
285
- t.same(received, { test: 2 }, 'callback called with parsed message')
358
+ t.deepEqual(connectionStatus, { publisherConnected: true, subscriberConnected: true }, 'isConnected returns correct statuses')
286
359
  t.end()
287
360
  })
288
361
 
289
- pubSubTest.test('should not call callback if subscribedChannel does not match', async t => {
290
- pubSub = new PubSub({ host: 'localhost', port: 6379 }, publisherStub, subscriberStub)
362
+ t.test('should return false connection statuses for isConnected when clients are not connected', (t) => {
363
+ const config = {}
364
+ const pubSub = new PubSub(config)
365
+
366
+ sandbox.stub(pubSub.publisherClient, 'status').value('disconnected')
367
+ sandbox.stub(pubSub.subscriberClient, 'status').value('disconnected')
368
+
369
+ const connectionStatus = pubSub.isConnected
291
370
 
292
- let called = false
293
- subscriberStub.subscribe.callsFake(async (channel, cb) => {
294
- cb(JSON.stringify({ test: 3 }), 'other-channel')
295
- })
296
- await pubSub.subscribe('chan', () => { called = true })
297
- t.notOk(called, 'callback not called for other channel')
371
+ t.deepEqual(connectionStatus, { publisherConnected: false, subscriberConnected: false }, 'isConnected returns false statuses when clients are not connected')
298
372
  t.end()
299
373
  })
374
+ t.test('should publish a message to a channel using spublish when isCluster is true', async (t) => {
375
+ const config = { cluster: [{ host: '127.0.0.1', port: 6379 }] }
376
+ const pubSub = new PubSub(config)
377
+ const channel = 'cluster-channel'
378
+ const message = { key: 'cluster-value' }
300
379
 
301
- pubSubTest.test('should add event listeners to publisher and subscriber clients', t => {
302
- pubSub = new PubSub({ host: 'localhost', port: 6379 }, publisherStub, subscriberStub)
380
+ sandbox.stub(pubSub.publisherClient, 'spublish').resolves()
303
381
 
304
- // The event listeners are added in the constructor
305
- // We check that .on was called with the correct events for both clients
306
- const expectedEvents = ['connect', 'ready', 'end', 'error']
307
- for (const event of expectedEvents) {
308
- t.ok(
309
- publisherStub.on.calledWith(event, sinon.match.func),
310
- `publisherStub.on called with event '${event}'`
311
- )
312
- t.ok(
313
- subscriberStub.on.calledWith(event, sinon.match.func),
314
- `subscriberStub.on called with event '${event}'`
315
- )
316
- }
382
+ await pubSub.publish(channel, message)
383
+
384
+ t.ok(pubSub.publisherClient.spublish.calledWith(channel, JSON.stringify(message)), 'spublish called with correct arguments')
317
385
  t.end()
318
386
  })
319
387
 
320
- pubSubTest.test('should create publisher and subscriber clients if not provided', t => {
321
- // Arrange
322
- const config = { host: 'localhost', port: 6379 }
323
- // Stub createClient and createLogger
324
- const createClientStub = sandbox.stub(require('redis'), 'createClient').returns({
325
- connect: sandbox.stub().resolves(),
326
- quit: sandbox.stub().resolves(),
327
- ping: sandbox.stub().resolves('PONG'),
328
- publish: sandbox.stub().resolves(),
329
- isOpen: true,
330
- on: sandbox.stub().returnsThis(),
331
- removeAllListeners: sandbox.stub()
332
- })
333
- PubSub = proxyquire('../../../../src/util/redis/pubSub', {
334
- redis: {
335
- createClient: createClientStub
336
- }
337
- })
338
- // Act
388
+ t.test('should handle error when publishing a message with spublish in cluster mode', async (t) => {
389
+ const config = { cluster: [{ host: '127.0.0.1', port: 6379 }] }
339
390
  const pubSub = new PubSub(config)
340
- // Assert
341
- t.ok(createClientStub.calledTwice, 'createClient called for both publisher and subscriber')
342
- t.ok(pubSub.publisherClient, 'publisherClient is created')
343
- t.ok(pubSub.subscriberClient, 'subscriberClient is created')
344
- t.end()
345
- })
346
-
347
- pubSubTest.test('should create cluster clients if cluster config is provided and clients not provided', async t => {
348
- // Arrange
349
- const clusterConfig = { cluster: [{ host: 'c1', port: 7000 }, { host: 'c2', port: 7001 }] }
350
- const createClusterStub = sandbox.stub().callsFake(() => ({
351
- connect: sandbox.stub().resolves(),
352
- quit: sandbox.stub().resolves(),
353
- ping: sandbox.stub().resolves('PONG'),
354
- spublish: sandbox.stub().resolves(),
355
- ssubscribe: sandbox.stub().resolves(),
356
- sunsubscribe: sandbox.stub().resolves(),
357
- isOpen: true,
358
- on: sandbox.stub().returnsThis(),
359
- removeAllListeners: sandbox.stub()
360
- }))
361
-
362
- PubSub = proxyquire('../../../../src/util/redis/pubSub', {
363
- redis: {
364
- createCluster: createClusterStub
365
- }
366
- })
367
-
368
- // Act
369
- const pubSubCluster = new PubSub(clusterConfig)
370
- // Assert
371
- t.ok(createClusterStub.calledTwice, 'createCluster called for both publisher and subscriber')
372
- t.ok(pubSubCluster.isCluster, 'isCluster is true')
373
- t.end()
374
- })
375
-
376
- pubSubTest.test('should log info on publisher client connect event', t => {
377
- logStub = {
378
- info: sandbox.stub(),
379
- error: sandbox.stub(),
380
- debug: sandbox.stub()
381
- }
382
- PubSub = proxyquire('../../../../src/util/redis/pubSub', {
383
- '../createLogger': { createLogger: sandbox.stub().returns(logStub) }
384
- })
385
- pubSub = new PubSub({ host: 'localhost', port: 6379 }, publisherStub, subscriberStub)
386
- // Find the connect event handler
387
- const connectHandler = publisherStub.on.getCalls().find(call => call.args[0] === 'connect').args[1]
388
- connectHandler()
389
- t.ok(logStub.info.calledWith('Redis client connecting'), 'logs info on connect')
390
- t.end()
391
- })
392
-
393
- pubSubTest.test('should log info on publisher client ready event', t => {
394
- logStub = {
395
- info: sandbox.stub(),
396
- error: sandbox.stub(),
397
- debug: sandbox.stub()
391
+ const channel = 'cluster-channel'
392
+ const message = { key: 'cluster-value' }
393
+ const error = new Error('Cluster spublish error')
394
+
395
+ sandbox.stub(pubSub.publisherClient, 'spublish').rejects(error)
396
+
397
+ try {
398
+ await pubSub.publish(channel, message)
399
+ t.fail('Should have thrown an error')
400
+ } catch (err) {
401
+ t.deepEqual(err, constructSystemExtensionError(error, '["redis"]'), 'Error thrown and rethrown correctly')
398
402
  }
399
- PubSub = proxyquire('../../../../src/util/redis/pubSub', {
400
- '../createLogger': { createLogger: sandbox.stub().returns(logStub) }
401
- })
402
- pubSub = new PubSub({ host: 'localhost', port: 6379 }, publisherStub, subscriberStub)
403
- const readyHandler = publisherStub.on.getCalls().find(call => call.args[0] === 'ready').args[1]
404
- readyHandler()
405
- t.ok(logStub.info.calledWith('Redis client ready'), 'logs info on ready')
406
403
  t.end()
407
404
  })
408
405
 
409
- pubSubTest.test('should log info on publisher client end event', t => {
410
- logStub = {
411
- info: sandbox.stub(),
412
- error: sandbox.stub(),
413
- debug: sandbox.stub()
414
- }
415
- PubSub = proxyquire('../../../../src/util/redis/pubSub', {
416
- '../createLogger': { createLogger: sandbox.stub().returns(logStub) }
417
- })
418
- pubSub = new PubSub({ host: 'localhost', port: 6379 }, publisherStub, subscriberStub)
419
- const endHandler = publisherStub.on.getCalls().find(call => call.args[0] === 'end').args[1]
420
- endHandler()
421
- t.ok(logStub.info.calledWith('Redis client connection closed'), 'logs info on end')
406
+ t.test('should subscribe to a channel and handle smessage in cluster mode', async (t) => {
407
+ const config = { cluster: [{ host: '127.0.0.1', port: 6379 }] }
408
+ const pubSub = new PubSub(config)
409
+ const channel = 'cluster-channel'
410
+ const callback = sinon.stub()
411
+ const message = JSON.stringify({ key: 'cluster-value' })
412
+
413
+ sandbox.stub(pubSub.subscriberClient, 'ssubscribe').resolves()
414
+ pubSub.subscriberClient.on.withArgs('smessage').yields(channel, message)
415
+
416
+ await pubSub.subscribe(channel, callback)
417
+
418
+ t.ok(pubSub.subscriberClient.ssubscribe.calledWith(channel), 'ssubscribe called with correct channel')
419
+ t.ok(callback.calledWith(JSON.parse(message)), 'callback called with parsed message')
422
420
  t.end()
423
421
  })
424
422
 
425
- pubSubTest.test('should log error on publisher client error event', t => {
426
- logStub = {
427
- info: sandbox.stub(),
428
- error: sandbox.stub(),
429
- debug: sandbox.stub()
430
- }
431
- PubSub = proxyquire('../../../../src/util/redis/pubSub', {
432
- '../createLogger': { createLogger: sandbox.stub().returns(logStub) }
433
- })
434
- pubSub = new PubSub({ host: 'localhost', port: 6379 }, publisherStub, subscriberStub)
435
- const errorHandler = publisherStub.on.getCalls().find(call => call.args[0] === 'error').args[1]
436
- const err = new Error('test error')
437
- errorHandler(err)
438
- t.ok(logStub.error.calledWith('Redis client error:', err), 'logs error on error event')
439
- t.end()
440
- })
441
-
442
- pubSubTest.test('should log info on subscriber client connect event', t => {
443
- logStub = {
444
- info: sandbox.stub(),
445
- error: sandbox.stub(),
446
- debug: sandbox.stub()
447
- }
448
- PubSub = proxyquire('../../../../src/util/redis/pubSub', {
449
- '../createLogger': { createLogger: sandbox.stub().returns(logStub) }
450
- })
451
- pubSub = new PubSub({ host: 'localhost', port: 6379 }, publisherStub, subscriberStub)
452
- const connectHandler = subscriberStub.on.getCalls().find(call => call.args[0] === 'connect').args[1]
453
- connectHandler()
454
- t.ok(logStub.info.calledWith('Redis client connecting'), 'logs info on subscriber connect')
423
+ t.test('should not call callback if smessage subscribedChannel does not match channel in cluster mode', async (t) => {
424
+ const config = { cluster: [{ host: '127.0.0.1', port: 6379 }] }
425
+ const pubSub = new PubSub(config)
426
+ const channel = 'cluster-channel'
427
+ const callback = sinon.stub()
428
+ const message = JSON.stringify({ key: 'cluster-value' })
429
+ const otherChannel = 'other-cluster-channel'
430
+
431
+ sandbox.stub(pubSub.subscriberClient, 'ssubscribe').resolves()
432
+ pubSub.subscriberClient.on.withArgs('smessage').yields(otherChannel, message)
433
+
434
+ await pubSub.subscribe(channel, callback)
435
+
436
+ t.ok(pubSub.subscriberClient.ssubscribe.calledWith(channel), 'ssubscribe called with correct channel')
437
+ t.notOk(callback.called, 'callback not called when smessage channel does not match')
455
438
  t.end()
456
439
  })
457
440
 
458
- pubSubTest.test('should log info on subscriber client ready event', t => {
459
- logStub = {
460
- info: sandbox.stub(),
461
- error: sandbox.stub(),
462
- debug: sandbox.stub()
441
+ t.test('should handle error when subscribing to a channel in cluster mode', async (t) => {
442
+ const config = { cluster: [{ host: '127.0.0.1', port: 6379 }] }
443
+ const pubSub = new PubSub(config)
444
+ const channel = 'cluster-channel'
445
+ const callback = sinon.stub()
446
+ const error = new Error('Cluster subscribe error')
447
+
448
+ sandbox.stub(pubSub.subscriberClient, 'ssubscribe').rejects(error)
449
+
450
+ try {
451
+ await pubSub.subscribe(channel, callback)
452
+ t.fail('Should have thrown an error')
453
+ } catch (err) {
454
+ t.deepEqual(err, constructSystemExtensionError(error, '["redis"]'), 'Error thrown and rethrown correctly')
463
455
  }
464
- PubSub = proxyquire('../../../../src/util/redis/pubSub', {
465
- '../createLogger': { createLogger: sandbox.stub().returns(logStub) }
466
- })
467
- pubSub = new PubSub({ host: 'localhost', port: 6379 }, publisherStub, subscriberStub)
468
- const readyHandler = subscriberStub.on.getCalls().find(call => call.args[0] === 'ready').args[1]
469
- readyHandler()
470
- t.ok(logStub.info.calledWith('Redis client ready'), 'logs info on subscriber ready')
471
456
  t.end()
472
457
  })
473
458
 
474
- pubSubTest.test('should log info on subscriber client end event', t => {
475
- logStub = {
476
- info: sandbox.stub(),
477
- error: sandbox.stub(),
478
- debug: sandbox.stub()
459
+ t.test('should unsubscribe from a channel using sunsubscribe in cluster mode', async (t) => {
460
+ const config = { cluster: [{ host: '127.0.0.1', port: 6379 }] }
461
+ const pubSub = new PubSub(config)
462
+ const channel = 'cluster-channel'
463
+
464
+ sandbox.stub(pubSub.subscriberClient, 'sunsubscribe').resolves()
465
+
466
+ await pubSub.unsubscribe(channel)
467
+
468
+ t.ok(pubSub.subscriberClient.sunsubscribe.calledWith(channel), 'sunsubscribe called with correct channel')
469
+ t.end()
470
+ })
471
+
472
+ t.test('should handle error when unsubscribing from a channel in cluster mode', async (t) => {
473
+ const config = { cluster: [{ host: '127.0.0.1', port: 6379 }] }
474
+ const pubSub = new PubSub(config)
475
+ const channel = 'cluster-channel'
476
+ const error = new Error('Cluster unsubscribe error')
477
+
478
+ sandbox.stub(pubSub.subscriberClient, 'sunsubscribe').rejects(error)
479
+
480
+ try {
481
+ await pubSub.unsubscribe(channel)
482
+ t.fail('Should have thrown an error')
483
+ } catch (err) {
484
+ t.deepEqual(err, constructSystemExtensionError(error, '["redis"]'), 'Error thrown and rethrown correctly')
479
485
  }
480
- PubSub = proxyquire('../../../../src/util/redis/pubSub', {
481
- '../createLogger': { createLogger: sandbox.stub().returns(logStub) }
482
- })
483
- pubSub = new PubSub({ host: 'localhost', port: 6379 }, publisherStub, subscriberStub)
484
- const endHandler = subscriberStub.on.getCalls().find(call => call.args[0] === 'end').args[1]
485
- endHandler()
486
- t.ok(logStub.info.calledWith('Redis client connection closed'), 'logs info on subscriber end')
487
486
  t.end()
488
487
  })
489
488
 
490
- pubSubTest.test('should log error on subscriber client error event', t => {
491
- logStub = {
492
- info: sandbox.stub(),
493
- error: sandbox.stub(),
494
- debug: sandbox.stub()
489
+ t.test('should broadcast a message to multiple channels using spublish in cluster mode', async (t) => {
490
+ const config = { cluster: [{ host: '127.0.0.1', port: 6379 }] }
491
+ const pubSub = new PubSub(config)
492
+ const channels = ['cluster1', 'cluster2']
493
+ const message = { key: 'cluster-broadcast' }
494
+
495
+ sandbox.stub(pubSub.publisherClient, 'spublish').resolves()
496
+
497
+ await pubSub.broadcast(channels, message)
498
+
499
+ t.ok(pubSub.publisherClient.spublish.calledTwice, 'spublish called twice')
500
+ t.ok(pubSub.publisherClient.spublish.firstCall.calledWith(channels[0], JSON.stringify(message)), 'spublish called with first channel and message')
501
+ t.ok(pubSub.publisherClient.spublish.secondCall.calledWith(channels[1], JSON.stringify(message)), 'spublish called with second channel and message')
502
+ t.end()
503
+ })
504
+
505
+ t.test('should handle error when broadcasting a message in cluster mode', async (t) => {
506
+ const config = { cluster: [{ host: '127.0.0.1', port: 6379 }] }
507
+ const pubSub = new PubSub(config)
508
+ const channels = ['cluster1', 'cluster2']
509
+ const message = { key: 'cluster-broadcast' }
510
+ const error = new Error('Cluster broadcast error')
511
+
512
+ sandbox.stub(pubSub.publisherClient, 'spublish').onFirstCall().rejects(error)
513
+
514
+ try {
515
+ await pubSub.broadcast(channels, message)
516
+ t.fail('Should have thrown an error')
517
+ } catch (err) {
518
+ t.deepEqual(err, constructSystemExtensionError(error, '["redis"]'), 'Error thrown and rethrown correctly')
495
519
  }
496
- PubSub = proxyquire('../../../../src/util/redis/pubSub', {
497
- '../createLogger': { createLogger: sandbox.stub().returns(logStub) }
498
- })
499
- pubSub = new PubSub({ host: 'localhost', port: 6379 }, publisherStub, subscriberStub)
500
- const errorHandler = subscriberStub.on.getCalls().find(call => call.args[0] === 'error').args[1]
501
- const err = new Error('subscriber error')
502
- errorHandler(err)
503
- t.ok(logStub.error.calledWith('Redis client error:', err), 'logs error on subscriber error event')
504
520
  t.end()
505
521
  })
506
- pubSubTest.end()
522
+ t.end()
507
523
  })