@hazeljs/discovery 0.2.0-beta.15 → 0.2.0-beta.17

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.
Files changed (42) hide show
  1. package/README.md +185 -19
  2. package/dist/__tests__/consul-backend.test.d.ts +6 -0
  3. package/dist/__tests__/consul-backend.test.d.ts.map +1 -0
  4. package/dist/__tests__/consul-backend.test.js +300 -0
  5. package/dist/__tests__/kubernetes-backend.test.d.ts +6 -0
  6. package/dist/__tests__/kubernetes-backend.test.d.ts.map +1 -0
  7. package/dist/__tests__/kubernetes-backend.test.js +261 -0
  8. package/dist/__tests__/redis-backend.test.d.ts +6 -0
  9. package/dist/__tests__/redis-backend.test.d.ts.map +1 -0
  10. package/dist/__tests__/redis-backend.test.js +280 -0
  11. package/dist/backends/consul-backend.d.ts +46 -7
  12. package/dist/backends/consul-backend.d.ts.map +1 -1
  13. package/dist/backends/consul-backend.js +23 -39
  14. package/dist/backends/kubernetes-backend.d.ts +44 -6
  15. package/dist/backends/kubernetes-backend.d.ts.map +1 -1
  16. package/dist/backends/kubernetes-backend.js +11 -32
  17. package/dist/backends/memory-backend.d.ts +0 -1
  18. package/dist/backends/memory-backend.d.ts.map +1 -1
  19. package/dist/backends/memory-backend.js +3 -32
  20. package/dist/backends/redis-backend.d.ts +11 -6
  21. package/dist/backends/redis-backend.d.ts.map +1 -1
  22. package/dist/backends/redis-backend.js +66 -46
  23. package/dist/client/discovery-client.d.ts +6 -4
  24. package/dist/client/discovery-client.d.ts.map +1 -1
  25. package/dist/client/discovery-client.js +30 -30
  26. package/dist/client/service-client.d.ts.map +1 -1
  27. package/dist/client/service-client.js +82 -17
  28. package/dist/index.d.ts +4 -1
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +8 -1
  31. package/dist/registry/service-registry.d.ts.map +1 -1
  32. package/dist/registry/service-registry.js +13 -2
  33. package/dist/utils/filter.d.ts +10 -0
  34. package/dist/utils/filter.d.ts.map +1 -0
  35. package/dist/utils/filter.js +39 -0
  36. package/dist/utils/logger.d.ts +21 -0
  37. package/dist/utils/logger.d.ts.map +1 -0
  38. package/dist/utils/logger.js +34 -0
  39. package/dist/utils/validation.d.ts +36 -0
  40. package/dist/utils/validation.d.ts.map +1 -0
  41. package/dist/utils/validation.js +109 -0
  42. package/package.json +3 -3
package/README.md CHANGED
@@ -7,14 +7,18 @@ Service Discovery and Registry for HazelJS microservices - inspired by Netflix E
7
7
 
8
8
  ## Features
9
9
 
10
- - 🔍 **Service Registration & Discovery** - Automatic service registration with health checks
11
- - ⚖️ **Load Balancing** - 6 built-in strategies (Round Robin, Random, Least Connections, etc.)
12
- - 🏥 **Health Checks** - Automatic health monitoring with heartbeat
13
- - 🎯 **Service Filtering** - Filter by zone, tags, metadata, and status
14
- - 💾 **Multiple Backends** - Memory (dev), Redis, Consul, etcd, Kubernetes
15
- - 🎨 **Decorator Support** - Clean integration with HazelJS apps
16
- - 📊 **Caching** - Built-in service discovery caching
17
- - 🔄 **Auto-Cleanup** - Automatic removal of expired instances
10
+ - **Service Registration & Discovery** - Automatic service registration with health checks
11
+ - **Load Balancing** - 6 built-in strategies (Round Robin, Random, Least Connections, Weighted Round Robin, IP Hash, Zone Aware)
12
+ - **Health Checks** - Automatic health monitoring with heartbeat
13
+ - **Service Filtering** - Filter by zone, tags, metadata, and status
14
+ - **Multiple Backends** - Memory (dev), Redis, Consul, Kubernetes
15
+ - **Decorator Support** - Clean integration with HazelJS apps
16
+ - **Caching** - Built-in service discovery caching with auto-refresh
17
+ - **Auto-Cleanup** - Automatic removal of expired instances
18
+ - **Smart Retries** - Only retries on transient/network errors, not client errors
19
+ - **Pluggable Logging** - Bring your own logger or use the built-in console logger
20
+ - **Config Validation** - Runtime validation of all configuration objects
21
+ - **Graceful Shutdown** - Proper cleanup of intervals, connections, and resources
18
22
 
19
23
  ## Installation
20
24
 
@@ -22,6 +26,21 @@ Service Discovery and Registry for HazelJS microservices - inspired by Netflix E
22
26
  npm install @hazeljs/discovery
23
27
  ```
24
28
 
29
+ ### Optional peer dependencies
30
+
31
+ Install the backend you need:
32
+
33
+ ```bash
34
+ # Redis backend
35
+ npm install ioredis
36
+
37
+ # Consul backend
38
+ npm install consul
39
+
40
+ # Kubernetes backend
41
+ npm install @kubernetes/client-node
42
+ ```
43
+
25
44
  ## Quick Start
26
45
 
27
46
  ### 1. Register a Service
@@ -41,6 +60,9 @@ const registry = new ServiceRegistry({
41
60
  });
42
61
 
43
62
  await registry.register();
63
+
64
+ // On shutdown
65
+ await registry.deregister();
44
66
  ```
45
67
 
46
68
  ### 2. Discover Services
@@ -51,6 +73,7 @@ import { DiscoveryClient } from '@hazeljs/discovery';
51
73
  const client = new DiscoveryClient({
52
74
  cacheEnabled: true,
53
75
  cacheTTL: 30000,
76
+ refreshInterval: 15000, // auto-refresh cache every 15s
54
77
  });
55
78
 
56
79
  // Get all instances
@@ -58,6 +81,9 @@ const instances = await client.getInstances('user-service');
58
81
 
59
82
  // Get one instance with load balancing
60
83
  const instance = await client.getInstance('user-service', 'round-robin');
84
+
85
+ // On shutdown
86
+ client.close();
61
87
  ```
62
88
 
63
89
  ### 3. Call Services
@@ -70,10 +96,12 @@ const serviceClient = new ServiceClient(discoveryClient, {
70
96
  loadBalancingStrategy: 'round-robin',
71
97
  timeout: 5000,
72
98
  retries: 3,
99
+ retryDelay: 1000,
73
100
  });
74
101
 
75
- // Automatic service discovery + load balancing
102
+ // Automatic service discovery + load balancing + smart retries
76
103
  const user = await serviceClient.get('/users/123');
104
+ const created = await serviceClient.post('/users', { name: 'John' });
77
105
  ```
78
106
 
79
107
  ### 4. With HazelJS Decorators
@@ -105,36 +133,44 @@ export class OrderService {
105
133
  ## Load Balancing Strategies
106
134
 
107
135
  ### Round Robin
136
+
108
137
  ```typescript
109
138
  const instance = await client.getInstance('service-name', 'round-robin');
110
139
  ```
111
140
 
112
141
  ### Random
142
+
113
143
  ```typescript
114
144
  const instance = await client.getInstance('service-name', 'random');
115
145
  ```
116
146
 
117
147
  ### Least Connections
148
+
149
+ Tracks active connections per instance. When used with `ServiceClient`, connection counts are automatically incremented/decremented on each request.
150
+
118
151
  ```typescript
119
152
  const instance = await client.getInstance('service-name', 'least-connections');
120
153
  ```
121
154
 
122
155
  ### Weighted Round Robin
156
+
123
157
  ```typescript
124
158
  // Set weight in service metadata
125
159
  const registry = new ServiceRegistry({
126
160
  name: 'api-service',
161
+ port: 3000,
127
162
  metadata: { weight: 5 }, // Higher weight = more traffic
128
- // ...
129
163
  });
130
164
  ```
131
165
 
132
166
  ### IP Hash (Sticky Sessions)
167
+
133
168
  ```typescript
134
169
  const instance = await client.getInstance('service-name', 'ip-hash');
135
170
  ```
136
171
 
137
172
  ### Zone Aware
173
+
138
174
  ```typescript
139
175
  const factory = client.getLoadBalancerFactory();
140
176
  const strategy = factory.create('zone-aware', { zone: 'us-east-1' });
@@ -143,6 +179,8 @@ const strategy = factory.create('zone-aware', { zone: 'us-east-1' });
143
179
  ## Service Filtering
144
180
 
145
181
  ```typescript
182
+ import { ServiceStatus } from '@hazeljs/discovery';
183
+
146
184
  const instances = await client.getInstances('user-service', {
147
185
  zone: 'us-east-1',
148
186
  status: ServiceStatus.UP,
@@ -151,17 +189,35 @@ const instances = await client.getInstances('user-service', {
151
189
  });
152
190
  ```
153
191
 
192
+ The `applyServiceFilter` utility is also exported for use in custom backends or application code:
193
+
194
+ ```typescript
195
+ import { applyServiceFilter } from '@hazeljs/discovery';
196
+
197
+ const filtered = applyServiceFilter(instances, { zone: 'us-east-1' });
198
+ ```
199
+
154
200
  ## Registry Backends
155
201
 
156
202
  ### Memory (Development)
203
+
204
+ The default backend. Stores everything in-process memory -- suitable for development and testing.
205
+
157
206
  ```typescript
158
207
  import { MemoryRegistryBackend } from '@hazeljs/discovery';
159
208
 
160
- const backend = new MemoryRegistryBackend();
209
+ const backend = new MemoryRegistryBackend(90000); // optional expiration in ms
161
210
  const registry = new ServiceRegistry(config, backend);
162
211
  ```
163
212
 
164
213
  ### Redis (Production)
214
+
215
+ Distributed registry using Redis with TTL-based expiration. Uses `SCAN` (not `KEYS`) for production safety and `MGET` for efficient batch lookups. Includes connection error handling with automatic reconnection support.
216
+
217
+ ```bash
218
+ npm install ioredis
219
+ ```
220
+
165
221
  ```typescript
166
222
  import Redis from 'ioredis';
167
223
  import { RedisRegistryBackend } from '@hazeljs/discovery';
@@ -173,14 +229,24 @@ const redis = new Redis({
173
229
  });
174
230
 
175
231
  const backend = new RedisRegistryBackend(redis, {
176
- keyPrefix: 'myapp:discovery:',
177
- ttl: 90, // seconds
232
+ keyPrefix: 'myapp:discovery:', // default: 'hazeljs:discovery:'
233
+ ttl: 90, // seconds, default: 90
178
234
  });
179
235
 
180
236
  const registry = new ServiceRegistry(config, backend);
237
+
238
+ // On shutdown
239
+ await backend.close();
181
240
  ```
182
241
 
183
242
  ### Consul
243
+
244
+ Integrates with HashiCorp Consul using TTL-based health checks.
245
+
246
+ ```bash
247
+ npm install consul
248
+ ```
249
+
184
250
  ```typescript
185
251
  import Consul from 'consul';
186
252
  import { ConsulRegistryBackend } from '@hazeljs/discovery';
@@ -191,14 +257,24 @@ const consul = new Consul({
191
257
  });
192
258
 
193
259
  const backend = new ConsulRegistryBackend(consul, {
194
- ttl: '30s',
260
+ ttl: '30s', // TTL check interval (supports "30s", "5m", "1h")
195
261
  datacenter: 'dc1',
196
262
  });
197
263
 
198
264
  const registry = new ServiceRegistry(config, backend);
265
+
266
+ // On shutdown
267
+ await backend.close();
199
268
  ```
200
269
 
201
270
  ### Kubernetes
271
+
272
+ Read-only discovery backend that integrates with Kubernetes Endpoints API. Registration, deregistration, heartbeat, and status updates are no-ops since Kubernetes manages these through its own primitives (Services, Endpoints, probes).
273
+
274
+ ```bash
275
+ npm install @kubernetes/client-node
276
+ ```
277
+
202
278
  ```typescript
203
279
  import { KubeConfig } from '@kubernetes/client-node';
204
280
  import { KubernetesRegistryBackend } from '@hazeljs/discovery';
@@ -211,11 +287,66 @@ const backend = new KubernetesRegistryBackend(kubeConfig, {
211
287
  labelSelector: 'app.kubernetes.io/managed-by=hazeljs',
212
288
  });
213
289
 
214
- // In Kubernetes, service registration is handled by the platform
215
290
  // Use the backend for service discovery only
216
291
  const client = new DiscoveryClient({}, backend);
217
292
  ```
218
293
 
294
+ ## Smart Retry Logic
295
+
296
+ `ServiceClient` only retries on transient errors. Client errors (4xx) are thrown immediately without wasting retries:
297
+
298
+ | Error Type | Retried? |
299
+ |---|---|
300
+ | Network errors (ECONNREFUSED, timeout) | Yes |
301
+ | 502 Bad Gateway | Yes |
302
+ | 503 Service Unavailable | Yes |
303
+ | 504 Gateway Timeout | Yes |
304
+ | 408 Request Timeout | Yes |
305
+ | 429 Too Many Requests | Yes |
306
+ | 400 Bad Request | No |
307
+ | 401 Unauthorized | No |
308
+ | 403 Forbidden | No |
309
+ | 404 Not Found | No |
310
+ | Other 4xx | No |
311
+
312
+ ## Custom Logging
313
+
314
+ By default, the package logs to the console with a `[discovery]` prefix. You can plug in your own logger (e.g., Winston, Pino, Bunyan):
315
+
316
+ ```typescript
317
+ import { DiscoveryLogger } from '@hazeljs/discovery';
318
+
319
+ DiscoveryLogger.setLogger({
320
+ debug: (msg, ...args) => myLogger.debug(msg, ...args),
321
+ info: (msg, ...args) => myLogger.info(msg, ...args),
322
+ warn: (msg, ...args) => myLogger.warn(msg, ...args),
323
+ error: (msg, ...args) => myLogger.error(msg, ...args),
324
+ });
325
+
326
+ // Reset to default console logger
327
+ DiscoveryLogger.resetLogger();
328
+ ```
329
+
330
+ ## Config Validation
331
+
332
+ All configuration objects are validated at construction time. Invalid configs throw a `ConfigValidationError` with a descriptive message:
333
+
334
+ ```typescript
335
+ import { ServiceRegistry, ConfigValidationError } from '@hazeljs/discovery';
336
+
337
+ try {
338
+ const registry = new ServiceRegistry({
339
+ name: '', // invalid: empty string
340
+ port: -1, // invalid: negative port
341
+ });
342
+ } catch (error) {
343
+ if (error instanceof ConfigValidationError) {
344
+ console.error(error.message);
345
+ // => 'ServiceRegistryConfig: "name" is required and must be a non-empty string'
346
+ }
347
+ }
348
+ ```
349
+
219
350
  ## API Reference
220
351
 
221
352
  ### ServiceRegistry
@@ -239,6 +370,8 @@ class DiscoveryClient {
239
370
  getInstance(serviceName: string, strategy?: string, filter?: ServiceFilter): Promise<ServiceInstance | null>;
240
371
  getAllServices(): Promise<string[]>;
241
372
  clearCache(serviceName?: string): void;
373
+ getLoadBalancerFactory(): LoadBalancerFactory;
374
+ close(): void;
242
375
  }
243
376
  ```
244
377
 
@@ -248,10 +381,41 @@ class DiscoveryClient {
248
381
  class ServiceClient {
249
382
  constructor(discoveryClient: DiscoveryClient, config: ServiceClientConfig);
250
383
  get<T>(path: string, config?: AxiosRequestConfig): Promise<AxiosResponse<T>>;
251
- post<T>(path: string, data?: any, config?: AxiosRequestConfig): Promise<AxiosResponse<T>>;
252
- put<T>(path: string, data?: any, config?: AxiosRequestConfig): Promise<AxiosResponse<T>>;
384
+ post<T>(path: string, data?: unknown, config?: AxiosRequestConfig): Promise<AxiosResponse<T>>;
385
+ put<T>(path: string, data?: unknown, config?: AxiosRequestConfig): Promise<AxiosResponse<T>>;
253
386
  delete<T>(path: string, config?: AxiosRequestConfig): Promise<AxiosResponse<T>>;
254
- patch<T>(path: string, data?: any, config?: AxiosRequestConfig): Promise<AxiosResponse<T>>;
387
+ patch<T>(path: string, data?: unknown, config?: AxiosRequestConfig): Promise<AxiosResponse<T>>;
388
+ }
389
+ ```
390
+
391
+ ### Configuration Types
392
+
393
+ ```typescript
394
+ interface ServiceRegistryConfig {
395
+ name: string;
396
+ port: number;
397
+ host?: string;
398
+ protocol?: 'http' | 'https' | 'grpc';
399
+ healthCheckPath?: string; // default: '/health'
400
+ healthCheckInterval?: number; // default: 30000 (ms)
401
+ metadata?: Record<string, unknown>;
402
+ zone?: string;
403
+ tags?: string[];
404
+ }
405
+
406
+ interface DiscoveryClientConfig {
407
+ cacheEnabled?: boolean;
408
+ cacheTTL?: number; // default: 30000 (ms)
409
+ refreshInterval?: number; // auto-refresh cache interval (ms)
410
+ }
411
+
412
+ interface ServiceClientConfig {
413
+ serviceName: string;
414
+ loadBalancingStrategy?: string; // default: 'round-robin'
415
+ filter?: ServiceFilter;
416
+ timeout?: number; // default: 5000 (ms)
417
+ retries?: number; // default: 3
418
+ retryDelay?: number; // default: 1000 (ms)
255
419
  }
256
420
  ```
257
421
 
@@ -265,13 +429,15 @@ See the [examples](./examples) directory for complete working examples.
265
429
  npm test
266
430
  ```
267
431
 
432
+ The package includes 145+ unit tests across 9 test suites with 85%+ code coverage.
433
+
268
434
  ## Contributing
269
435
 
270
436
  Contributions are welcome! Please read our [Contributing Guide](../../CONTRIBUTING.md) for details.
271
437
 
272
438
  ## License
273
439
 
274
- MIT © [HazelJS](https://hazeljs.com)
440
+ MIT &copy; [HazelJS](https://hazeljs.com)
275
441
 
276
442
  ## Links
277
443
 
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Consul Backend Tests
3
+ * Uses mocked Consul client
4
+ */
5
+ export {};
6
+ //# sourceMappingURL=consul-backend.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"consul-backend.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/consul-backend.test.ts"],"names":[],"mappings":"AAAA;;;GAGG"}
@@ -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,6 @@
1
+ /**
2
+ * Kubernetes Backend Tests
3
+ * Uses mocked @kubernetes/client-node API
4
+ */
5
+ export {};
6
+ //# sourceMappingURL=kubernetes-backend.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"kubernetes-backend.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/kubernetes-backend.test.ts"],"names":[],"mappings":"AAAA;;;GAGG"}