@hazeljs/discovery 0.2.0-alpha.1
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/LICENSE +192 -0
- package/README.md +450 -0
- package/dist/__tests__/consul-backend.test.d.ts +6 -0
- package/dist/__tests__/consul-backend.test.d.ts.map +1 -0
- package/dist/__tests__/consul-backend.test.js +300 -0
- package/dist/__tests__/decorators.test.d.ts +5 -0
- package/dist/__tests__/decorators.test.d.ts.map +1 -0
- package/dist/__tests__/decorators.test.js +72 -0
- package/dist/__tests__/discovery-client.test.d.ts +5 -0
- package/dist/__tests__/discovery-client.test.d.ts.map +1 -0
- package/dist/__tests__/discovery-client.test.js +142 -0
- package/dist/__tests__/kubernetes-backend.test.d.ts +6 -0
- package/dist/__tests__/kubernetes-backend.test.d.ts.map +1 -0
- package/dist/__tests__/kubernetes-backend.test.js +261 -0
- package/dist/__tests__/load-balancer-strategies.test.d.ts +5 -0
- package/dist/__tests__/load-balancer-strategies.test.d.ts.map +1 -0
- package/dist/__tests__/load-balancer-strategies.test.js +234 -0
- package/dist/__tests__/memory-backend.test.d.ts +5 -0
- package/dist/__tests__/memory-backend.test.d.ts.map +1 -0
- package/dist/__tests__/memory-backend.test.js +246 -0
- package/dist/__tests__/redis-backend.test.d.ts +6 -0
- package/dist/__tests__/redis-backend.test.d.ts.map +1 -0
- package/dist/__tests__/redis-backend.test.js +280 -0
- package/dist/__tests__/service-client.test.d.ts +5 -0
- package/dist/__tests__/service-client.test.d.ts.map +1 -0
- package/dist/__tests__/service-client.test.js +216 -0
- package/dist/__tests__/service-registry.test.d.ts +5 -0
- package/dist/__tests__/service-registry.test.d.ts.map +1 -0
- package/dist/__tests__/service-registry.test.js +65 -0
- package/dist/backends/consul-backend.d.ts +115 -0
- package/dist/backends/consul-backend.d.ts.map +1 -0
- package/dist/backends/consul-backend.js +259 -0
- package/dist/backends/kubernetes-backend.d.ts +103 -0
- package/dist/backends/kubernetes-backend.d.ts.map +1 -0
- package/dist/backends/kubernetes-backend.js +153 -0
- package/dist/backends/memory-backend.d.ts +21 -0
- package/dist/backends/memory-backend.d.ts.map +1 -0
- package/dist/backends/memory-backend.js +86 -0
- package/dist/backends/redis-backend.d.ts +76 -0
- package/dist/backends/redis-backend.d.ts.map +1 -0
- package/dist/backends/redis-backend.js +220 -0
- package/dist/backends/registry-backend.d.ts +39 -0
- package/dist/backends/registry-backend.d.ts.map +1 -0
- package/dist/backends/registry-backend.js +5 -0
- package/dist/client/discovery-client.d.ts +49 -0
- package/dist/client/discovery-client.d.ts.map +1 -0
- package/dist/client/discovery-client.js +123 -0
- package/dist/client/service-client.d.ts +48 -0
- package/dist/client/service-client.d.ts.map +1 -0
- package/dist/client/service-client.js +155 -0
- package/dist/decorators/inject-service-client.decorator.d.ts +16 -0
- package/dist/decorators/inject-service-client.decorator.d.ts.map +1 -0
- package/dist/decorators/inject-service-client.decorator.js +24 -0
- package/dist/decorators/service-registry.decorator.d.ts +11 -0
- package/dist/decorators/service-registry.decorator.d.ts.map +1 -0
- package/dist/decorators/service-registry.decorator.js +20 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +51 -0
- package/dist/load-balancer/strategies.d.ts +82 -0
- package/dist/load-balancer/strategies.d.ts.map +1 -0
- package/dist/load-balancer/strategies.js +209 -0
- package/dist/registry/service-registry.d.ts +51 -0
- package/dist/registry/service-registry.d.ts.map +1 -0
- package/dist/registry/service-registry.js +159 -0
- package/dist/types/index.d.ts +61 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +14 -0
- package/dist/utils/filter.d.ts +10 -0
- package/dist/utils/filter.d.ts.map +1 -0
- package/dist/utils/filter.js +39 -0
- package/dist/utils/logger.d.ts +21 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +34 -0
- package/dist/utils/validation.d.ts +36 -0
- package/dist/utils/validation.d.ts.map +1 -0
- package/dist/utils/validation.js +109 -0
- package/package.json +81 -0
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Memory Backend Tests
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const memory_backend_1 = require("../backends/memory-backend");
|
|
7
|
+
const types_1 = require("../types");
|
|
8
|
+
describe('MemoryRegistryBackend', () => {
|
|
9
|
+
let backend;
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
backend = new memory_backend_1.MemoryRegistryBackend();
|
|
12
|
+
});
|
|
13
|
+
const createInstance = (id, name = 'test-service') => ({
|
|
14
|
+
id,
|
|
15
|
+
name,
|
|
16
|
+
host: 'localhost',
|
|
17
|
+
port: 3000,
|
|
18
|
+
status: types_1.ServiceStatus.UP,
|
|
19
|
+
lastHeartbeat: new Date(),
|
|
20
|
+
registeredAt: new Date(),
|
|
21
|
+
});
|
|
22
|
+
describe('register', () => {
|
|
23
|
+
it('should register a service instance', async () => {
|
|
24
|
+
const instance = createInstance('1');
|
|
25
|
+
await backend.register(instance);
|
|
26
|
+
const retrieved = await backend.getInstance('1');
|
|
27
|
+
expect(retrieved).toEqual(instance);
|
|
28
|
+
});
|
|
29
|
+
it('should index instances by service name', async () => {
|
|
30
|
+
const instance1 = createInstance('1', 'service-a');
|
|
31
|
+
const instance2 = createInstance('2', 'service-a');
|
|
32
|
+
const instance3 = createInstance('3', 'service-b');
|
|
33
|
+
await backend.register(instance1);
|
|
34
|
+
await backend.register(instance2);
|
|
35
|
+
await backend.register(instance3);
|
|
36
|
+
const instancesA = await backend.getInstances('service-a');
|
|
37
|
+
const instancesB = await backend.getInstances('service-b');
|
|
38
|
+
expect(instancesA).toHaveLength(2);
|
|
39
|
+
expect(instancesB).toHaveLength(1);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
describe('deregister', () => {
|
|
43
|
+
it('should remove a registered instance', async () => {
|
|
44
|
+
const instance = createInstance('1');
|
|
45
|
+
await backend.register(instance);
|
|
46
|
+
await backend.deregister('1');
|
|
47
|
+
const retrieved = await backend.getInstance('1');
|
|
48
|
+
expect(retrieved).toBeNull();
|
|
49
|
+
});
|
|
50
|
+
it('should clean up service index when last instance is removed', async () => {
|
|
51
|
+
const instance = createInstance('1', 'service-a');
|
|
52
|
+
await backend.register(instance);
|
|
53
|
+
const servicesBefore = await backend.getAllServices();
|
|
54
|
+
expect(servicesBefore).toContain('service-a');
|
|
55
|
+
await backend.deregister('1');
|
|
56
|
+
const servicesAfter = await backend.getAllServices();
|
|
57
|
+
expect(servicesAfter).not.toContain('service-a');
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
describe('heartbeat', () => {
|
|
61
|
+
it('should update lastHeartbeat and set status to UP', async () => {
|
|
62
|
+
const instance = createInstance('1');
|
|
63
|
+
instance.status = types_1.ServiceStatus.DOWN;
|
|
64
|
+
await backend.register(instance);
|
|
65
|
+
const before = await backend.getInstance('1');
|
|
66
|
+
const beforeTime = before.lastHeartbeat.getTime();
|
|
67
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
68
|
+
await backend.heartbeat('1');
|
|
69
|
+
const after = await backend.getInstance('1');
|
|
70
|
+
expect(after.lastHeartbeat.getTime()).toBeGreaterThan(beforeTime);
|
|
71
|
+
expect(after.status).toBe(types_1.ServiceStatus.UP);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
describe('getInstances', () => {
|
|
75
|
+
it('should return all instances for a service', async () => {
|
|
76
|
+
const instance1 = createInstance('1', 'service-a');
|
|
77
|
+
const instance2 = createInstance('2', 'service-a');
|
|
78
|
+
const instance3 = createInstance('3', 'service-b');
|
|
79
|
+
await backend.register(instance1);
|
|
80
|
+
await backend.register(instance2);
|
|
81
|
+
await backend.register(instance3);
|
|
82
|
+
const instances = await backend.getInstances('service-a');
|
|
83
|
+
expect(instances).toHaveLength(2);
|
|
84
|
+
expect(instances.map((i) => i.id)).toEqual(['1', '2']);
|
|
85
|
+
});
|
|
86
|
+
it('should return empty array for non-existent service', async () => {
|
|
87
|
+
const instances = await backend.getInstances('non-existent');
|
|
88
|
+
expect(instances).toEqual([]);
|
|
89
|
+
});
|
|
90
|
+
it('should filter by zone', async () => {
|
|
91
|
+
const instance1 = { ...createInstance('1'), zone: 'us-east-1' };
|
|
92
|
+
const instance2 = { ...createInstance('2'), zone: 'us-west-1' };
|
|
93
|
+
const instance3 = { ...createInstance('3'), zone: 'us-east-1' };
|
|
94
|
+
await backend.register(instance1);
|
|
95
|
+
await backend.register(instance2);
|
|
96
|
+
await backend.register(instance3);
|
|
97
|
+
const instances = await backend.getInstances('test-service', {
|
|
98
|
+
zone: 'us-east-1',
|
|
99
|
+
});
|
|
100
|
+
expect(instances).toHaveLength(2);
|
|
101
|
+
expect(instances.every((i) => i.zone === 'us-east-1')).toBe(true);
|
|
102
|
+
});
|
|
103
|
+
it('should filter by status', async () => {
|
|
104
|
+
const instance1 = { ...createInstance('1'), status: types_1.ServiceStatus.UP };
|
|
105
|
+
const instance2 = { ...createInstance('2'), status: types_1.ServiceStatus.DOWN };
|
|
106
|
+
const instance3 = { ...createInstance('3'), status: types_1.ServiceStatus.UP };
|
|
107
|
+
await backend.register(instance1);
|
|
108
|
+
await backend.register(instance2);
|
|
109
|
+
await backend.register(instance3);
|
|
110
|
+
const instances = await backend.getInstances('test-service', {
|
|
111
|
+
status: types_1.ServiceStatus.UP,
|
|
112
|
+
});
|
|
113
|
+
expect(instances).toHaveLength(2);
|
|
114
|
+
expect(instances.every((i) => i.status === types_1.ServiceStatus.UP)).toBe(true);
|
|
115
|
+
});
|
|
116
|
+
it('should filter by tags', async () => {
|
|
117
|
+
const instance1 = { ...createInstance('1'), tags: ['web', 'api'] };
|
|
118
|
+
const instance2 = { ...createInstance('2'), tags: ['api'] };
|
|
119
|
+
const instance3 = { ...createInstance('3'), tags: ['web'] };
|
|
120
|
+
await backend.register(instance1);
|
|
121
|
+
await backend.register(instance2);
|
|
122
|
+
await backend.register(instance3);
|
|
123
|
+
const instances = await backend.getInstances('test-service', {
|
|
124
|
+
tags: ['web', 'api'],
|
|
125
|
+
});
|
|
126
|
+
expect(instances).toHaveLength(1);
|
|
127
|
+
expect(instances[0].id).toBe('1');
|
|
128
|
+
});
|
|
129
|
+
it('should filter by metadata', async () => {
|
|
130
|
+
const instance1 = {
|
|
131
|
+
...createInstance('1'),
|
|
132
|
+
metadata: { version: '1.0.0', env: 'prod' },
|
|
133
|
+
};
|
|
134
|
+
const instance2 = {
|
|
135
|
+
...createInstance('2'),
|
|
136
|
+
metadata: { version: '2.0.0', env: 'prod' },
|
|
137
|
+
};
|
|
138
|
+
const instance3 = {
|
|
139
|
+
...createInstance('3'),
|
|
140
|
+
metadata: { version: '1.0.0', env: 'dev' },
|
|
141
|
+
};
|
|
142
|
+
await backend.register(instance1);
|
|
143
|
+
await backend.register(instance2);
|
|
144
|
+
await backend.register(instance3);
|
|
145
|
+
const instances = await backend.getInstances('test-service', {
|
|
146
|
+
metadata: { version: '1.0.0' },
|
|
147
|
+
});
|
|
148
|
+
expect(instances).toHaveLength(2);
|
|
149
|
+
expect(instances.every((i) => i.metadata?.version === '1.0.0')).toBe(true);
|
|
150
|
+
});
|
|
151
|
+
it('should combine multiple filters', async () => {
|
|
152
|
+
const instance1 = {
|
|
153
|
+
...createInstance('1'),
|
|
154
|
+
zone: 'us-east-1',
|
|
155
|
+
status: types_1.ServiceStatus.UP,
|
|
156
|
+
tags: ['web'],
|
|
157
|
+
};
|
|
158
|
+
const instance2 = {
|
|
159
|
+
...createInstance('2'),
|
|
160
|
+
zone: 'us-east-1',
|
|
161
|
+
status: types_1.ServiceStatus.DOWN,
|
|
162
|
+
tags: ['web'],
|
|
163
|
+
};
|
|
164
|
+
const instance3 = {
|
|
165
|
+
...createInstance('3'),
|
|
166
|
+
zone: 'us-west-1',
|
|
167
|
+
status: types_1.ServiceStatus.UP,
|
|
168
|
+
tags: ['web'],
|
|
169
|
+
};
|
|
170
|
+
await backend.register(instance1);
|
|
171
|
+
await backend.register(instance2);
|
|
172
|
+
await backend.register(instance3);
|
|
173
|
+
const instances = await backend.getInstances('test-service', {
|
|
174
|
+
zone: 'us-east-1',
|
|
175
|
+
status: types_1.ServiceStatus.UP,
|
|
176
|
+
tags: ['web'],
|
|
177
|
+
});
|
|
178
|
+
expect(instances).toHaveLength(1);
|
|
179
|
+
expect(instances[0].id).toBe('1');
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
describe('getInstance', () => {
|
|
183
|
+
it('should return instance by ID', async () => {
|
|
184
|
+
const instance = createInstance('1');
|
|
185
|
+
await backend.register(instance);
|
|
186
|
+
const retrieved = await backend.getInstance('1');
|
|
187
|
+
expect(retrieved).toEqual(instance);
|
|
188
|
+
});
|
|
189
|
+
it('should return null for non-existent instance', async () => {
|
|
190
|
+
const retrieved = await backend.getInstance('non-existent');
|
|
191
|
+
expect(retrieved).toBeNull();
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
describe('getAllServices', () => {
|
|
195
|
+
it('should return all registered service names', async () => {
|
|
196
|
+
await backend.register(createInstance('1', 'service-a'));
|
|
197
|
+
await backend.register(createInstance('2', 'service-a'));
|
|
198
|
+
await backend.register(createInstance('3', 'service-b'));
|
|
199
|
+
const services = await backend.getAllServices();
|
|
200
|
+
expect(services.sort()).toEqual(['service-a', 'service-b']);
|
|
201
|
+
});
|
|
202
|
+
it('should return empty array when no services registered', async () => {
|
|
203
|
+
const services = await backend.getAllServices();
|
|
204
|
+
expect(services).toEqual([]);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
describe('updateStatus', () => {
|
|
208
|
+
it('should update instance status', async () => {
|
|
209
|
+
const instance = createInstance('1');
|
|
210
|
+
await backend.register(instance);
|
|
211
|
+
await backend.updateStatus('1', types_1.ServiceStatus.DOWN);
|
|
212
|
+
const updated = await backend.getInstance('1');
|
|
213
|
+
expect(updated.status).toBe(types_1.ServiceStatus.DOWN);
|
|
214
|
+
await backend.updateStatus('1', types_1.ServiceStatus.UP);
|
|
215
|
+
const updated2 = await backend.getInstance('1');
|
|
216
|
+
expect(updated2.status).toBe(types_1.ServiceStatus.UP);
|
|
217
|
+
});
|
|
218
|
+
it('should not throw error for non-existent instance', async () => {
|
|
219
|
+
await expect(backend.updateStatus('non-existent', types_1.ServiceStatus.DOWN)).resolves.not.toThrow();
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
describe('cleanup', () => {
|
|
223
|
+
it('should remove expired instances', async () => {
|
|
224
|
+
const backend = new memory_backend_1.MemoryRegistryBackend(100); // 100ms expiration
|
|
225
|
+
const instance = createInstance('1');
|
|
226
|
+
await backend.register(instance);
|
|
227
|
+
// Wait for expiration
|
|
228
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
229
|
+
await backend.cleanup();
|
|
230
|
+
const retrieved = await backend.getInstance('1');
|
|
231
|
+
expect(retrieved).toBeNull();
|
|
232
|
+
});
|
|
233
|
+
it('should not remove active instances', async () => {
|
|
234
|
+
const backend = new memory_backend_1.MemoryRegistryBackend(100);
|
|
235
|
+
const instance = createInstance('1');
|
|
236
|
+
await backend.register(instance);
|
|
237
|
+
// Update heartbeat before expiration
|
|
238
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
239
|
+
await backend.heartbeat('1');
|
|
240
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
241
|
+
await backend.cleanup();
|
|
242
|
+
const retrieved = await backend.getInstance('1');
|
|
243
|
+
expect(retrieved).toBeDefined();
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redis-backend.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/redis-backend.test.ts"],"names":[],"mappings":"AAAA;;;GAGG"}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Redis Backend Tests
|
|
4
|
+
* Uses mocked ioredis client
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
const redis_backend_1 = require("../backends/redis-backend");
|
|
8
|
+
const types_1 = require("../types");
|
|
9
|
+
const logger_1 = require("../utils/logger");
|
|
10
|
+
// Suppress console logs during tests
|
|
11
|
+
beforeAll(() => {
|
|
12
|
+
logger_1.DiscoveryLogger.setLogger({
|
|
13
|
+
debug: jest.fn(),
|
|
14
|
+
info: jest.fn(),
|
|
15
|
+
warn: jest.fn(),
|
|
16
|
+
error: jest.fn(),
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
afterAll(() => {
|
|
20
|
+
logger_1.DiscoveryLogger.resetLogger();
|
|
21
|
+
});
|
|
22
|
+
/** Helper to create a mock Redis instance */
|
|
23
|
+
function createMockRedis() {
|
|
24
|
+
const store = new Map();
|
|
25
|
+
const sets = new Map();
|
|
26
|
+
const ttls = new Map();
|
|
27
|
+
const listeners = new Map();
|
|
28
|
+
const mock = {
|
|
29
|
+
on: jest.fn((event, handler) => {
|
|
30
|
+
if (!listeners.has(event))
|
|
31
|
+
listeners.set(event, []);
|
|
32
|
+
listeners.get(event).push(handler);
|
|
33
|
+
}),
|
|
34
|
+
setex: jest.fn(async (key, ttl, value) => {
|
|
35
|
+
store.set(key, value);
|
|
36
|
+
ttls.set(key, ttl);
|
|
37
|
+
}),
|
|
38
|
+
get: jest.fn(async (key) => store.get(key) || null),
|
|
39
|
+
del: jest.fn(async (key) => {
|
|
40
|
+
store.delete(key);
|
|
41
|
+
ttls.delete(key);
|
|
42
|
+
return 1;
|
|
43
|
+
}),
|
|
44
|
+
mget: jest.fn(async (...keys) => {
|
|
45
|
+
// mget can receive keys as individual args or a single array
|
|
46
|
+
const flatKeys = keys.flat();
|
|
47
|
+
return flatKeys.map((k) => store.get(k) || null);
|
|
48
|
+
}),
|
|
49
|
+
sadd: jest.fn(async (key, member) => {
|
|
50
|
+
if (!sets.has(key))
|
|
51
|
+
sets.set(key, new Set());
|
|
52
|
+
sets.get(key).add(member);
|
|
53
|
+
return 1;
|
|
54
|
+
}),
|
|
55
|
+
srem: jest.fn(async (key, member) => {
|
|
56
|
+
sets.get(key)?.delete(member);
|
|
57
|
+
return 1;
|
|
58
|
+
}),
|
|
59
|
+
smembers: jest.fn(async (key) => {
|
|
60
|
+
return Array.from(sets.get(key) || []);
|
|
61
|
+
}),
|
|
62
|
+
scard: jest.fn(async (key) => {
|
|
63
|
+
return sets.get(key)?.size || 0;
|
|
64
|
+
}),
|
|
65
|
+
exists: jest.fn(async (key) => {
|
|
66
|
+
return store.has(key) ? 1 : 0;
|
|
67
|
+
}),
|
|
68
|
+
expire: jest.fn(async () => 1),
|
|
69
|
+
scan: jest.fn(async (_cursor, _match, pattern) => {
|
|
70
|
+
const keys = Array.from(sets.keys()).filter((k) => {
|
|
71
|
+
// Simple glob match: convert "prefix*" to regex
|
|
72
|
+
const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
|
|
73
|
+
return regex.test(k);
|
|
74
|
+
});
|
|
75
|
+
return ['0', keys];
|
|
76
|
+
}),
|
|
77
|
+
quit: jest.fn(async () => 'OK'),
|
|
78
|
+
// Helper to emit events during tests
|
|
79
|
+
_emit: (event, ...args) => {
|
|
80
|
+
const handlers = listeners.get(event) || [];
|
|
81
|
+
handlers.forEach((h) => h(...args));
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
return mock;
|
|
85
|
+
}
|
|
86
|
+
function createInstance(id, name = 'test-service', overrides = {}) {
|
|
87
|
+
return {
|
|
88
|
+
id,
|
|
89
|
+
name,
|
|
90
|
+
host: 'localhost',
|
|
91
|
+
port: 3000,
|
|
92
|
+
status: types_1.ServiceStatus.UP,
|
|
93
|
+
lastHeartbeat: new Date('2025-01-01T00:00:00Z'),
|
|
94
|
+
registeredAt: new Date('2025-01-01T00:00:00Z'),
|
|
95
|
+
...overrides,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
describe('RedisRegistryBackend', () => {
|
|
99
|
+
let redis;
|
|
100
|
+
let backend;
|
|
101
|
+
beforeEach(() => {
|
|
102
|
+
redis = createMockRedis();
|
|
103
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
104
|
+
backend = new redis_backend_1.RedisRegistryBackend(redis);
|
|
105
|
+
});
|
|
106
|
+
describe('constructor', () => {
|
|
107
|
+
it('should use default config values', () => {
|
|
108
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
109
|
+
const b = new redis_backend_1.RedisRegistryBackend(redis);
|
|
110
|
+
expect(b).toBeDefined();
|
|
111
|
+
});
|
|
112
|
+
it('should accept custom config', () => {
|
|
113
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
114
|
+
const b = new redis_backend_1.RedisRegistryBackend(redis, {
|
|
115
|
+
keyPrefix: 'custom:',
|
|
116
|
+
ttl: 120,
|
|
117
|
+
});
|
|
118
|
+
expect(b).toBeDefined();
|
|
119
|
+
});
|
|
120
|
+
it('should throw on invalid TTL', () => {
|
|
121
|
+
expect(() => {
|
|
122
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
123
|
+
new redis_backend_1.RedisRegistryBackend(redis, { ttl: -5 });
|
|
124
|
+
}).toThrow();
|
|
125
|
+
});
|
|
126
|
+
it('should register connection event handlers', () => {
|
|
127
|
+
expect(redis.on).toHaveBeenCalledWith('error', expect.any(Function));
|
|
128
|
+
expect(redis.on).toHaveBeenCalledWith('connect', expect.any(Function));
|
|
129
|
+
expect(redis.on).toHaveBeenCalledWith('reconnecting', expect.any(Function));
|
|
130
|
+
expect(redis.on).toHaveBeenCalledWith('close', expect.any(Function));
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
describe('register', () => {
|
|
134
|
+
it('should store the instance with TTL', async () => {
|
|
135
|
+
const instance = createInstance('1');
|
|
136
|
+
await backend.register(instance);
|
|
137
|
+
expect(redis.setex).toHaveBeenCalledWith('hazeljs:discovery:instance:1', 90, JSON.stringify(instance));
|
|
138
|
+
});
|
|
139
|
+
it('should add instance to service set', async () => {
|
|
140
|
+
const instance = createInstance('1', 'my-service');
|
|
141
|
+
await backend.register(instance);
|
|
142
|
+
expect(redis.sadd).toHaveBeenCalledWith('hazeljs:discovery:service:my-service', '1');
|
|
143
|
+
});
|
|
144
|
+
it('should set expiration on service set', async () => {
|
|
145
|
+
const instance = createInstance('1');
|
|
146
|
+
await backend.register(instance);
|
|
147
|
+
expect(redis.expire).toHaveBeenCalledWith('hazeljs:discovery:service:test-service', 180);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
describe('deregister', () => {
|
|
151
|
+
it('should remove instance from Redis', async () => {
|
|
152
|
+
const instance = createInstance('1');
|
|
153
|
+
await backend.register(instance);
|
|
154
|
+
await backend.deregister('1');
|
|
155
|
+
expect(redis.srem).toHaveBeenCalledWith('hazeljs:discovery:service:test-service', '1');
|
|
156
|
+
expect(redis.del).toHaveBeenCalledWith('hazeljs:discovery:instance:1');
|
|
157
|
+
});
|
|
158
|
+
it('should do nothing if instance does not exist', async () => {
|
|
159
|
+
await backend.deregister('non-existent');
|
|
160
|
+
expect(redis.del).not.toHaveBeenCalled();
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
describe('heartbeat', () => {
|
|
164
|
+
it('should update heartbeat timestamp and status', async () => {
|
|
165
|
+
const instance = createInstance('1');
|
|
166
|
+
instance.status = types_1.ServiceStatus.DOWN;
|
|
167
|
+
await backend.register(instance);
|
|
168
|
+
await backend.heartbeat('1');
|
|
169
|
+
// Should have been called again with updated data
|
|
170
|
+
expect(redis.setex).toHaveBeenCalledTimes(2);
|
|
171
|
+
const lastCall = redis.setex.mock.calls[1];
|
|
172
|
+
const storedInstance = JSON.parse(lastCall[2]);
|
|
173
|
+
expect(storedInstance.status).toBe(types_1.ServiceStatus.UP);
|
|
174
|
+
});
|
|
175
|
+
it('should do nothing if instance does not exist', async () => {
|
|
176
|
+
await backend.heartbeat('non-existent');
|
|
177
|
+
// setex should not be called
|
|
178
|
+
expect(redis.setex).not.toHaveBeenCalled();
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
describe('getInstances', () => {
|
|
182
|
+
it('should return all instances for a service using MGET', async () => {
|
|
183
|
+
const i1 = createInstance('1', 'svc');
|
|
184
|
+
const i2 = createInstance('2', 'svc');
|
|
185
|
+
await backend.register(i1);
|
|
186
|
+
await backend.register(i2);
|
|
187
|
+
const instances = await backend.getInstances('svc');
|
|
188
|
+
expect(instances).toHaveLength(2);
|
|
189
|
+
expect(redis.mget).toHaveBeenCalled();
|
|
190
|
+
});
|
|
191
|
+
it('should return empty array for non-existent service', async () => {
|
|
192
|
+
const instances = await backend.getInstances('non-existent');
|
|
193
|
+
expect(instances).toEqual([]);
|
|
194
|
+
});
|
|
195
|
+
it('should apply filter', async () => {
|
|
196
|
+
const i1 = createInstance('1', 'svc', { zone: 'us-east-1' });
|
|
197
|
+
const i2 = createInstance('2', 'svc', { zone: 'us-west-1' });
|
|
198
|
+
await backend.register(i1);
|
|
199
|
+
await backend.register(i2);
|
|
200
|
+
const instances = await backend.getInstances('svc', { zone: 'us-east-1' });
|
|
201
|
+
expect(instances).toHaveLength(1);
|
|
202
|
+
expect(instances[0].zone).toBe('us-east-1');
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
describe('getInstance', () => {
|
|
206
|
+
it('should return a specific instance', async () => {
|
|
207
|
+
const instance = createInstance('1');
|
|
208
|
+
await backend.register(instance);
|
|
209
|
+
const retrieved = await backend.getInstance('1');
|
|
210
|
+
expect(retrieved).toBeDefined();
|
|
211
|
+
expect(retrieved.id).toBe('1');
|
|
212
|
+
});
|
|
213
|
+
it('should return null for non-existent instance', async () => {
|
|
214
|
+
const result = await backend.getInstance('non-existent');
|
|
215
|
+
expect(result).toBeNull();
|
|
216
|
+
});
|
|
217
|
+
it('should convert dates back from strings', async () => {
|
|
218
|
+
const instance = createInstance('1');
|
|
219
|
+
await backend.register(instance);
|
|
220
|
+
const retrieved = await backend.getInstance('1');
|
|
221
|
+
expect(retrieved.lastHeartbeat).toBeInstanceOf(Date);
|
|
222
|
+
expect(retrieved.registeredAt).toBeInstanceOf(Date);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
describe('getAllServices', () => {
|
|
226
|
+
it('should use SCAN instead of KEYS', async () => {
|
|
227
|
+
const i1 = createInstance('1', 'svc-a');
|
|
228
|
+
const i2 = createInstance('2', 'svc-b');
|
|
229
|
+
await backend.register(i1);
|
|
230
|
+
await backend.register(i2);
|
|
231
|
+
const services = await backend.getAllServices();
|
|
232
|
+
expect(redis.scan).toHaveBeenCalled();
|
|
233
|
+
expect(services).toHaveLength(2);
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
describe('updateStatus', () => {
|
|
237
|
+
it('should update the status of an instance', async () => {
|
|
238
|
+
const instance = createInstance('1');
|
|
239
|
+
await backend.register(instance);
|
|
240
|
+
await backend.updateStatus('1', types_1.ServiceStatus.DOWN);
|
|
241
|
+
const retrieved = await backend.getInstance('1');
|
|
242
|
+
expect(retrieved.status).toBe(types_1.ServiceStatus.DOWN);
|
|
243
|
+
});
|
|
244
|
+
it('should do nothing for non-existent instance', async () => {
|
|
245
|
+
await backend.updateStatus('non-existent', types_1.ServiceStatus.DOWN);
|
|
246
|
+
expect(redis.setex).not.toHaveBeenCalled();
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
describe('cleanup', () => {
|
|
250
|
+
it('should remove stale entries from service sets', async () => {
|
|
251
|
+
const instance = createInstance('1', 'svc');
|
|
252
|
+
await backend.register(instance);
|
|
253
|
+
// Simulate the instance key expiring in Redis
|
|
254
|
+
redis.exists.mockResolvedValueOnce(0);
|
|
255
|
+
await backend.cleanup();
|
|
256
|
+
expect(redis.srem).toHaveBeenCalledWith('hazeljs:discovery:service:svc', '1');
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
describe('connection error handling', () => {
|
|
260
|
+
it('should track connection state via events', () => {
|
|
261
|
+
redis._emit('close');
|
|
262
|
+
// Backend should mark as disconnected
|
|
263
|
+
// Next operation should throw
|
|
264
|
+
expect(backend.getInstances('svc')).rejects.toThrow('Redis backend is not connected');
|
|
265
|
+
});
|
|
266
|
+
it('should recover on connect event', async () => {
|
|
267
|
+
redis._emit('close');
|
|
268
|
+
redis._emit('connect');
|
|
269
|
+
// Should work again
|
|
270
|
+
const result = await backend.getInstances('svc');
|
|
271
|
+
expect(result).toEqual([]);
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
describe('close', () => {
|
|
275
|
+
it('should call redis.quit()', async () => {
|
|
276
|
+
await backend.close();
|
|
277
|
+
expect(redis.quit).toHaveBeenCalled();
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service-client.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/service-client.test.ts"],"names":[],"mappings":"AAAA;;GAEG"}
|