@hazeljs/discovery 0.2.0-beta.8 → 0.2.0-beta.80
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 -21
- package/README.md +190 -21
- 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__/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__/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.js +2 -1
- package/dist/backends/consul-backend.d.ts +46 -7
- package/dist/backends/consul-backend.d.ts.map +1 -1
- package/dist/backends/consul-backend.js +23 -39
- package/dist/backends/kubernetes-backend.d.ts +44 -6
- package/dist/backends/kubernetes-backend.d.ts.map +1 -1
- package/dist/backends/kubernetes-backend.js +11 -32
- package/dist/backends/memory-backend.d.ts +0 -1
- package/dist/backends/memory-backend.d.ts.map +1 -1
- package/dist/backends/memory-backend.js +3 -32
- package/dist/backends/redis-backend.d.ts +11 -6
- package/dist/backends/redis-backend.d.ts.map +1 -1
- package/dist/backends/redis-backend.js +66 -46
- package/dist/client/discovery-client.d.ts +6 -4
- package/dist/client/discovery-client.d.ts.map +1 -1
- package/dist/client/discovery-client.js +30 -30
- package/dist/client/service-client.d.ts +4 -8
- package/dist/client/service-client.d.ts.map +1 -1
- package/dist/client/service-client.js +94 -34
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -1
- package/dist/registry/service-registry.d.ts.map +1 -1
- package/dist/registry/service-registry.js +13 -2
- 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 +7 -5
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Consul Backend Tests
|
|
4
|
+
* Uses mocked Consul client
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
const consul_backend_1 = require("../backends/consul-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
|
+
function createMockConsul() {
|
|
23
|
+
return {
|
|
24
|
+
agent: {
|
|
25
|
+
service: {
|
|
26
|
+
register: jest.fn().mockResolvedValue(undefined),
|
|
27
|
+
deregister: jest.fn().mockResolvedValue(undefined),
|
|
28
|
+
list: jest.fn().mockResolvedValue({}),
|
|
29
|
+
},
|
|
30
|
+
check: {
|
|
31
|
+
pass: jest.fn().mockResolvedValue(undefined),
|
|
32
|
+
fail: jest.fn().mockResolvedValue(undefined),
|
|
33
|
+
warn: jest.fn().mockResolvedValue(undefined),
|
|
34
|
+
list: jest.fn().mockResolvedValue({}),
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
health: {
|
|
38
|
+
service: jest.fn().mockResolvedValue([]),
|
|
39
|
+
},
|
|
40
|
+
catalog: {
|
|
41
|
+
service: {
|
|
42
|
+
list: jest.fn().mockResolvedValue({}),
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function createInstance(id, name = 'test-service', overrides = {}) {
|
|
48
|
+
return {
|
|
49
|
+
id,
|
|
50
|
+
name,
|
|
51
|
+
host: 'localhost',
|
|
52
|
+
port: 3000,
|
|
53
|
+
status: types_1.ServiceStatus.UP,
|
|
54
|
+
lastHeartbeat: new Date('2025-01-01T00:00:00Z'),
|
|
55
|
+
registeredAt: new Date('2025-01-01T00:00:00Z'),
|
|
56
|
+
tags: ['web'],
|
|
57
|
+
metadata: { version: '1.0' },
|
|
58
|
+
zone: 'us-east-1',
|
|
59
|
+
...overrides,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
describe('ConsulRegistryBackend', () => {
|
|
63
|
+
let consul;
|
|
64
|
+
let backend;
|
|
65
|
+
beforeEach(() => {
|
|
66
|
+
jest.useFakeTimers();
|
|
67
|
+
consul = createMockConsul();
|
|
68
|
+
backend = new consul_backend_1.ConsulRegistryBackend(consul);
|
|
69
|
+
});
|
|
70
|
+
afterEach(async () => {
|
|
71
|
+
await backend.close();
|
|
72
|
+
jest.useRealTimers();
|
|
73
|
+
});
|
|
74
|
+
describe('constructor', () => {
|
|
75
|
+
it('should use default TTL of 30s', () => {
|
|
76
|
+
expect(backend).toBeDefined();
|
|
77
|
+
});
|
|
78
|
+
it('should accept custom config', () => {
|
|
79
|
+
const b = new consul_backend_1.ConsulRegistryBackend(consul, { ttl: '60s' });
|
|
80
|
+
expect(b).toBeDefined();
|
|
81
|
+
});
|
|
82
|
+
it('should throw on invalid TTL format', () => {
|
|
83
|
+
expect(() => {
|
|
84
|
+
new consul_backend_1.ConsulRegistryBackend(consul, { ttl: 'invalid' });
|
|
85
|
+
}).toThrow();
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
describe('register', () => {
|
|
89
|
+
it('should call consul agent service register', async () => {
|
|
90
|
+
const instance = createInstance('svc-1', 'my-service');
|
|
91
|
+
await backend.register(instance);
|
|
92
|
+
expect(consul.agent.service.register).toHaveBeenCalledWith(expect.objectContaining({
|
|
93
|
+
id: 'svc-1',
|
|
94
|
+
name: 'my-service',
|
|
95
|
+
address: 'localhost',
|
|
96
|
+
port: 3000,
|
|
97
|
+
}));
|
|
98
|
+
});
|
|
99
|
+
it('should start TTL check interval', async () => {
|
|
100
|
+
const instance = createInstance('svc-1');
|
|
101
|
+
await backend.register(instance);
|
|
102
|
+
// Advance timers past the TTL check interval (2/3 of 30s = 20s)
|
|
103
|
+
jest.advanceTimersByTime(21000);
|
|
104
|
+
expect(consul.agent.check.pass).toHaveBeenCalledWith('service:svc-1');
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
describe('deregister', () => {
|
|
108
|
+
it('should call consul agent service deregister', async () => {
|
|
109
|
+
const instance = createInstance('svc-1');
|
|
110
|
+
await backend.register(instance);
|
|
111
|
+
await backend.deregister('svc-1');
|
|
112
|
+
expect(consul.agent.service.deregister).toHaveBeenCalledWith('svc-1');
|
|
113
|
+
});
|
|
114
|
+
it('should stop TTL check interval', async () => {
|
|
115
|
+
const instance = createInstance('svc-1');
|
|
116
|
+
await backend.register(instance);
|
|
117
|
+
await backend.deregister('svc-1');
|
|
118
|
+
jest.clearAllMocks();
|
|
119
|
+
jest.advanceTimersByTime(30000);
|
|
120
|
+
// TTL check should NOT have been called after deregister
|
|
121
|
+
expect(consul.agent.check.pass).not.toHaveBeenCalled();
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
describe('heartbeat', () => {
|
|
125
|
+
it('should pass the TTL check', async () => {
|
|
126
|
+
await backend.heartbeat('svc-1');
|
|
127
|
+
expect(consul.agent.check.pass).toHaveBeenCalledWith('service:svc-1');
|
|
128
|
+
});
|
|
129
|
+
it('should not throw when consul fails', async () => {
|
|
130
|
+
consul.agent.check.pass.mockRejectedValueOnce(new Error('Consul error'));
|
|
131
|
+
await expect(backend.heartbeat('svc-1')).resolves.not.toThrow();
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
describe('getInstances', () => {
|
|
135
|
+
it('should return instances from consul health service', async () => {
|
|
136
|
+
consul.health.service.mockResolvedValueOnce([
|
|
137
|
+
{
|
|
138
|
+
Service: {
|
|
139
|
+
ID: 'svc-1',
|
|
140
|
+
Service: 'my-service',
|
|
141
|
+
Address: '10.0.0.1',
|
|
142
|
+
Port: 8080,
|
|
143
|
+
Meta: { zone: 'us-east-1', registeredAt: '2025-01-01T00:00:00Z' },
|
|
144
|
+
Tags: ['web'],
|
|
145
|
+
},
|
|
146
|
+
Checks: [{ Status: 'passing' }],
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
Service: {
|
|
150
|
+
ID: 'svc-2',
|
|
151
|
+
Service: 'my-service',
|
|
152
|
+
Address: '10.0.0.2',
|
|
153
|
+
Port: 8080,
|
|
154
|
+
Meta: { zone: 'us-west-1' },
|
|
155
|
+
Tags: ['api'],
|
|
156
|
+
},
|
|
157
|
+
Checks: [{ Status: 'passing' }],
|
|
158
|
+
},
|
|
159
|
+
]);
|
|
160
|
+
const instances = await backend.getInstances('my-service');
|
|
161
|
+
expect(instances).toHaveLength(2);
|
|
162
|
+
expect(instances[0].id).toBe('svc-1');
|
|
163
|
+
expect(instances[0].host).toBe('10.0.0.1');
|
|
164
|
+
});
|
|
165
|
+
it('should set status based on check results', async () => {
|
|
166
|
+
consul.health.service.mockResolvedValueOnce([
|
|
167
|
+
{
|
|
168
|
+
Service: {
|
|
169
|
+
ID: 'svc-1',
|
|
170
|
+
Service: 'my-service',
|
|
171
|
+
Address: '10.0.0.1',
|
|
172
|
+
Port: 8080,
|
|
173
|
+
},
|
|
174
|
+
Checks: [{ Status: 'critical' }],
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
Service: {
|
|
178
|
+
ID: 'svc-2',
|
|
179
|
+
Service: 'my-service',
|
|
180
|
+
Address: '10.0.0.2',
|
|
181
|
+
Port: 8080,
|
|
182
|
+
},
|
|
183
|
+
Checks: [{ Status: 'warning' }],
|
|
184
|
+
},
|
|
185
|
+
]);
|
|
186
|
+
const instances = await backend.getInstances('my-service');
|
|
187
|
+
expect(instances[0].status).toBe(types_1.ServiceStatus.DOWN);
|
|
188
|
+
expect(instances[1].status).toBe(types_1.ServiceStatus.STARTING);
|
|
189
|
+
});
|
|
190
|
+
it('should filter instances', async () => {
|
|
191
|
+
consul.health.service.mockResolvedValueOnce([
|
|
192
|
+
{
|
|
193
|
+
Service: {
|
|
194
|
+
ID: 'svc-1',
|
|
195
|
+
Service: 'my-service',
|
|
196
|
+
Address: '10.0.0.1',
|
|
197
|
+
Port: 8080,
|
|
198
|
+
Meta: { zone: 'us-east-1' },
|
|
199
|
+
},
|
|
200
|
+
Checks: [],
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
Service: {
|
|
204
|
+
ID: 'svc-2',
|
|
205
|
+
Service: 'my-service',
|
|
206
|
+
Address: '10.0.0.2',
|
|
207
|
+
Port: 8080,
|
|
208
|
+
Meta: { zone: 'us-west-1' },
|
|
209
|
+
},
|
|
210
|
+
Checks: [],
|
|
211
|
+
},
|
|
212
|
+
]);
|
|
213
|
+
const instances = await backend.getInstances('my-service', { zone: 'us-east-1' });
|
|
214
|
+
expect(instances).toHaveLength(1);
|
|
215
|
+
expect(instances[0].id).toBe('svc-1');
|
|
216
|
+
});
|
|
217
|
+
it('should return empty array on error', async () => {
|
|
218
|
+
consul.health.service.mockRejectedValueOnce(new Error('Consul error'));
|
|
219
|
+
const instances = await backend.getInstances('my-service');
|
|
220
|
+
expect(instances).toEqual([]);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
describe('getInstance', () => {
|
|
224
|
+
it('should return a specific instance', async () => {
|
|
225
|
+
consul.agent.service.list.mockResolvedValueOnce({
|
|
226
|
+
'svc-1': {
|
|
227
|
+
ID: 'svc-1',
|
|
228
|
+
Service: 'my-service',
|
|
229
|
+
Address: '10.0.0.1',
|
|
230
|
+
Port: 8080,
|
|
231
|
+
Meta: { zone: 'us-east-1' },
|
|
232
|
+
Tags: ['web'],
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
consul.agent.check.list.mockResolvedValueOnce({
|
|
236
|
+
'service:svc-1': { Status: 'passing' },
|
|
237
|
+
});
|
|
238
|
+
const instance = await backend.getInstance('svc-1');
|
|
239
|
+
expect(instance).toBeDefined();
|
|
240
|
+
expect(instance.id).toBe('svc-1');
|
|
241
|
+
expect(instance.status).toBe(types_1.ServiceStatus.UP);
|
|
242
|
+
});
|
|
243
|
+
it('should return null for non-existent instance', async () => {
|
|
244
|
+
consul.agent.service.list.mockResolvedValueOnce({});
|
|
245
|
+
const instance = await backend.getInstance('non-existent');
|
|
246
|
+
expect(instance).toBeNull();
|
|
247
|
+
});
|
|
248
|
+
it('should return null on error', async () => {
|
|
249
|
+
consul.agent.service.list.mockRejectedValueOnce(new Error('fail'));
|
|
250
|
+
const instance = await backend.getInstance('svc-1');
|
|
251
|
+
expect(instance).toBeNull();
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
describe('getAllServices', () => {
|
|
255
|
+
it('should return service names from catalog', async () => {
|
|
256
|
+
consul.catalog.service.list.mockResolvedValueOnce({
|
|
257
|
+
'service-a': [],
|
|
258
|
+
'service-b': [],
|
|
259
|
+
consul: [],
|
|
260
|
+
});
|
|
261
|
+
const services = await backend.getAllServices();
|
|
262
|
+
expect(services).toEqual(['service-a', 'service-b', 'consul']);
|
|
263
|
+
});
|
|
264
|
+
it('should return empty array on error', async () => {
|
|
265
|
+
consul.catalog.service.list.mockRejectedValueOnce(new Error('fail'));
|
|
266
|
+
const services = await backend.getAllServices();
|
|
267
|
+
expect(services).toEqual([]);
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
describe('updateStatus', () => {
|
|
271
|
+
it('should pass check for UP status', async () => {
|
|
272
|
+
await backend.updateStatus('svc-1', types_1.ServiceStatus.UP);
|
|
273
|
+
expect(consul.agent.check.pass).toHaveBeenCalledWith('service:svc-1');
|
|
274
|
+
});
|
|
275
|
+
it('should fail check for DOWN status', async () => {
|
|
276
|
+
await backend.updateStatus('svc-1', types_1.ServiceStatus.DOWN);
|
|
277
|
+
expect(consul.agent.check.fail).toHaveBeenCalledWith('service:svc-1');
|
|
278
|
+
});
|
|
279
|
+
it('should warn check for STARTING status', async () => {
|
|
280
|
+
await backend.updateStatus('svc-1', types_1.ServiceStatus.STARTING);
|
|
281
|
+
expect(consul.agent.check.warn).toHaveBeenCalledWith('service:svc-1');
|
|
282
|
+
});
|
|
283
|
+
it('should not throw on error', async () => {
|
|
284
|
+
consul.agent.check.pass.mockRejectedValueOnce(new Error('fail'));
|
|
285
|
+
await expect(backend.updateStatus('svc-1', types_1.ServiceStatus.UP)).resolves.not.toThrow();
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
describe('close', () => {
|
|
289
|
+
it('should stop all TTL check intervals', async () => {
|
|
290
|
+
const i1 = createInstance('svc-1');
|
|
291
|
+
const i2 = createInstance('svc-2');
|
|
292
|
+
await backend.register(i1);
|
|
293
|
+
await backend.register(i2);
|
|
294
|
+
await backend.close();
|
|
295
|
+
jest.clearAllMocks();
|
|
296
|
+
jest.advanceTimersByTime(60000);
|
|
297
|
+
expect(consul.agent.check.pass).not.toHaveBeenCalled();
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"kubernetes-backend.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/kubernetes-backend.test.ts"],"names":[],"mappings":"AAAA;;;GAGG"}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Kubernetes Backend Tests
|
|
4
|
+
* Uses mocked @kubernetes/client-node API
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
const kubernetes_backend_1 = require("../backends/kubernetes-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
|
+
// Mock @kubernetes/client-node (optional peer dependency)
|
|
23
|
+
jest.mock('@kubernetes/client-node', () => ({
|
|
24
|
+
CoreV1Api: class MockCoreV1Api {
|
|
25
|
+
},
|
|
26
|
+
}), { virtual: true });
|
|
27
|
+
function createMockK8sApi() {
|
|
28
|
+
return {
|
|
29
|
+
readNamespacedEndpoints: jest.fn(),
|
|
30
|
+
listNamespacedService: jest.fn(),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function createMockKubeConfig(api) {
|
|
34
|
+
return {
|
|
35
|
+
makeApiClient: jest.fn(() => api),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
describe('KubernetesRegistryBackend', () => {
|
|
39
|
+
let k8sApi;
|
|
40
|
+
let backend;
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
k8sApi = createMockK8sApi();
|
|
43
|
+
const kubeConfig = createMockKubeConfig(k8sApi);
|
|
44
|
+
backend = new kubernetes_backend_1.KubernetesRegistryBackend(kubeConfig);
|
|
45
|
+
});
|
|
46
|
+
describe('constructor', () => {
|
|
47
|
+
it('should create with default config', () => {
|
|
48
|
+
expect(backend).toBeDefined();
|
|
49
|
+
});
|
|
50
|
+
it('should accept custom namespace and label selector', () => {
|
|
51
|
+
const api = createMockK8sApi();
|
|
52
|
+
const config = createMockKubeConfig(api);
|
|
53
|
+
const b = new kubernetes_backend_1.KubernetesRegistryBackend(config, {
|
|
54
|
+
namespace: 'production',
|
|
55
|
+
labelSelector: 'app=myapp',
|
|
56
|
+
});
|
|
57
|
+
expect(b).toBeDefined();
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
describe('register / deregister / heartbeat (no-ops)', () => {
|
|
61
|
+
it('register should be a no-op', async () => {
|
|
62
|
+
await expect(backend.register({
|
|
63
|
+
id: 'svc-1',
|
|
64
|
+
name: 'test',
|
|
65
|
+
host: 'localhost',
|
|
66
|
+
port: 3000,
|
|
67
|
+
status: types_1.ServiceStatus.UP,
|
|
68
|
+
lastHeartbeat: new Date(),
|
|
69
|
+
registeredAt: new Date(),
|
|
70
|
+
})).resolves.not.toThrow();
|
|
71
|
+
});
|
|
72
|
+
it('deregister should be a no-op', async () => {
|
|
73
|
+
await expect(backend.deregister('svc-1')).resolves.not.toThrow();
|
|
74
|
+
});
|
|
75
|
+
it('heartbeat should be a no-op', async () => {
|
|
76
|
+
await expect(backend.heartbeat('svc-1')).resolves.not.toThrow();
|
|
77
|
+
});
|
|
78
|
+
it('updateStatus should be a no-op', async () => {
|
|
79
|
+
await expect(backend.updateStatus('svc-1', types_1.ServiceStatus.DOWN)).resolves.not.toThrow();
|
|
80
|
+
});
|
|
81
|
+
it('cleanup should be a no-op', async () => {
|
|
82
|
+
await expect(backend.cleanup()).resolves.not.toThrow();
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
describe('getInstances', () => {
|
|
86
|
+
it('should return instances from endpoints', async () => {
|
|
87
|
+
k8sApi.readNamespacedEndpoints.mockResolvedValueOnce({
|
|
88
|
+
body: {
|
|
89
|
+
metadata: {
|
|
90
|
+
annotations: {},
|
|
91
|
+
labels: { app: 'my-service' },
|
|
92
|
+
creationTimestamp: '2025-01-01T00:00:00Z',
|
|
93
|
+
},
|
|
94
|
+
subsets: [
|
|
95
|
+
{
|
|
96
|
+
addresses: [
|
|
97
|
+
{ ip: '10.0.0.1', targetRef: { name: 'pod-1' }, nodeName: 'node-1' },
|
|
98
|
+
{ ip: '10.0.0.2', targetRef: { name: 'pod-2' }, nodeName: 'node-2' },
|
|
99
|
+
],
|
|
100
|
+
ports: [{ port: 8080 }],
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
const instances = await backend.getInstances('my-service');
|
|
106
|
+
expect(instances).toHaveLength(2);
|
|
107
|
+
expect(instances[0].id).toBe('my-service:10.0.0.1:8080');
|
|
108
|
+
expect(instances[0].host).toBe('10.0.0.1');
|
|
109
|
+
expect(instances[0].port).toBe(8080);
|
|
110
|
+
expect(instances[0].status).toBe(types_1.ServiceStatus.UP);
|
|
111
|
+
expect(instances[0].metadata?.podName).toBe('pod-1');
|
|
112
|
+
expect(instances[0].metadata?.nodeName).toBe('node-1');
|
|
113
|
+
});
|
|
114
|
+
it('should mark notReadyAddresses as STARTING', async () => {
|
|
115
|
+
k8sApi.readNamespacedEndpoints.mockResolvedValueOnce({
|
|
116
|
+
body: {
|
|
117
|
+
metadata: { labels: {} },
|
|
118
|
+
subsets: [
|
|
119
|
+
{
|
|
120
|
+
addresses: [],
|
|
121
|
+
notReadyAddresses: [{ ip: '10.0.0.3' }],
|
|
122
|
+
ports: [{ port: 8080 }],
|
|
123
|
+
},
|
|
124
|
+
],
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
const instances = await backend.getInstances('my-service');
|
|
128
|
+
expect(instances).toHaveLength(1);
|
|
129
|
+
expect(instances[0].status).toBe(types_1.ServiceStatus.STARTING);
|
|
130
|
+
});
|
|
131
|
+
it('should handle multiple ports per address', async () => {
|
|
132
|
+
k8sApi.readNamespacedEndpoints.mockResolvedValueOnce({
|
|
133
|
+
body: {
|
|
134
|
+
metadata: { labels: {} },
|
|
135
|
+
subsets: [
|
|
136
|
+
{
|
|
137
|
+
addresses: [{ ip: '10.0.0.1' }],
|
|
138
|
+
ports: [{ port: 8080 }, { port: 8443 }],
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
const instances = await backend.getInstances('my-service');
|
|
144
|
+
expect(instances).toHaveLength(2);
|
|
145
|
+
expect(instances[0].port).toBe(8080);
|
|
146
|
+
expect(instances[1].port).toBe(8443);
|
|
147
|
+
});
|
|
148
|
+
it('should return empty array when no subsets', async () => {
|
|
149
|
+
k8sApi.readNamespacedEndpoints.mockResolvedValueOnce({
|
|
150
|
+
body: {
|
|
151
|
+
metadata: {},
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
const instances = await backend.getInstances('my-service');
|
|
155
|
+
expect(instances).toEqual([]);
|
|
156
|
+
});
|
|
157
|
+
it('should return empty array on API error', async () => {
|
|
158
|
+
k8sApi.readNamespacedEndpoints.mockRejectedValueOnce(new Error('Not found'));
|
|
159
|
+
const instances = await backend.getInstances('non-existent');
|
|
160
|
+
expect(instances).toEqual([]);
|
|
161
|
+
});
|
|
162
|
+
it('should apply filters', async () => {
|
|
163
|
+
k8sApi.readNamespacedEndpoints.mockResolvedValueOnce({
|
|
164
|
+
body: {
|
|
165
|
+
metadata: {
|
|
166
|
+
labels: {
|
|
167
|
+
'topology.kubernetes.io/zone': 'us-east-1a',
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
subsets: [
|
|
171
|
+
{
|
|
172
|
+
addresses: [{ ip: '10.0.0.1' }, { ip: '10.0.0.2' }],
|
|
173
|
+
ports: [{ port: 8080 }],
|
|
174
|
+
},
|
|
175
|
+
],
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
const instances = await backend.getInstances('my-service', {
|
|
179
|
+
status: types_1.ServiceStatus.UP,
|
|
180
|
+
});
|
|
181
|
+
expect(instances).toHaveLength(2);
|
|
182
|
+
});
|
|
183
|
+
it('should extract zone from labels', async () => {
|
|
184
|
+
k8sApi.readNamespacedEndpoints.mockResolvedValueOnce({
|
|
185
|
+
body: {
|
|
186
|
+
metadata: {
|
|
187
|
+
labels: {
|
|
188
|
+
'topology.kubernetes.io/zone': 'us-west-2a',
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
subsets: [
|
|
192
|
+
{
|
|
193
|
+
addresses: [{ ip: '10.0.0.1' }],
|
|
194
|
+
ports: [{ port: 8080 }],
|
|
195
|
+
},
|
|
196
|
+
],
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
const instances = await backend.getInstances('my-service');
|
|
200
|
+
expect(instances[0].zone).toBe('us-west-2a');
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
describe('getInstance', () => {
|
|
204
|
+
it('should find instance by ID', async () => {
|
|
205
|
+
k8sApi.readNamespacedEndpoints.mockResolvedValueOnce({
|
|
206
|
+
body: {
|
|
207
|
+
metadata: { labels: {} },
|
|
208
|
+
subsets: [
|
|
209
|
+
{
|
|
210
|
+
addresses: [{ ip: '10.0.0.1' }, { ip: '10.0.0.2' }],
|
|
211
|
+
ports: [{ port: 8080 }],
|
|
212
|
+
},
|
|
213
|
+
],
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
const instance = await backend.getInstance('my-service:10.0.0.2:8080');
|
|
217
|
+
expect(instance).toBeDefined();
|
|
218
|
+
expect(instance.host).toBe('10.0.0.2');
|
|
219
|
+
});
|
|
220
|
+
it('should return null for non-existent instance', async () => {
|
|
221
|
+
k8sApi.readNamespacedEndpoints.mockResolvedValueOnce({
|
|
222
|
+
body: {
|
|
223
|
+
metadata: { labels: {} },
|
|
224
|
+
subsets: [
|
|
225
|
+
{
|
|
226
|
+
addresses: [{ ip: '10.0.0.1' }],
|
|
227
|
+
ports: [{ port: 8080 }],
|
|
228
|
+
},
|
|
229
|
+
],
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
const instance = await backend.getInstance('my-service:10.0.0.99:8080');
|
|
233
|
+
expect(instance).toBeNull();
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
describe('getAllServices', () => {
|
|
237
|
+
it('should return service names from namespace', async () => {
|
|
238
|
+
k8sApi.listNamespacedService.mockResolvedValueOnce({
|
|
239
|
+
body: {
|
|
240
|
+
items: [{ metadata: { name: 'svc-a' } }, { metadata: { name: 'svc-b' } }],
|
|
241
|
+
},
|
|
242
|
+
});
|
|
243
|
+
const services = await backend.getAllServices();
|
|
244
|
+
expect(services).toEqual(['svc-a', 'svc-b']);
|
|
245
|
+
});
|
|
246
|
+
it('should return empty array on error', async () => {
|
|
247
|
+
k8sApi.listNamespacedService.mockRejectedValueOnce(new Error('Forbidden'));
|
|
248
|
+
const services = await backend.getAllServices();
|
|
249
|
+
expect(services).toEqual([]);
|
|
250
|
+
});
|
|
251
|
+
it('should handle missing metadata', async () => {
|
|
252
|
+
k8sApi.listNamespacedService.mockResolvedValueOnce({
|
|
253
|
+
body: {
|
|
254
|
+
items: [{ metadata: {} }, {}],
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
const services = await backend.getAllServices();
|
|
258
|
+
expect(services).toEqual(['', '']);
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redis-backend.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/redis-backend.test.ts"],"names":[],"mappings":"AAAA;;;GAGG"}
|