@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.
Files changed (78) hide show
  1. package/LICENSE +192 -0
  2. package/README.md +450 -0
  3. package/dist/__tests__/consul-backend.test.d.ts +6 -0
  4. package/dist/__tests__/consul-backend.test.d.ts.map +1 -0
  5. package/dist/__tests__/consul-backend.test.js +300 -0
  6. package/dist/__tests__/decorators.test.d.ts +5 -0
  7. package/dist/__tests__/decorators.test.d.ts.map +1 -0
  8. package/dist/__tests__/decorators.test.js +72 -0
  9. package/dist/__tests__/discovery-client.test.d.ts +5 -0
  10. package/dist/__tests__/discovery-client.test.d.ts.map +1 -0
  11. package/dist/__tests__/discovery-client.test.js +142 -0
  12. package/dist/__tests__/kubernetes-backend.test.d.ts +6 -0
  13. package/dist/__tests__/kubernetes-backend.test.d.ts.map +1 -0
  14. package/dist/__tests__/kubernetes-backend.test.js +261 -0
  15. package/dist/__tests__/load-balancer-strategies.test.d.ts +5 -0
  16. package/dist/__tests__/load-balancer-strategies.test.d.ts.map +1 -0
  17. package/dist/__tests__/load-balancer-strategies.test.js +234 -0
  18. package/dist/__tests__/memory-backend.test.d.ts +5 -0
  19. package/dist/__tests__/memory-backend.test.d.ts.map +1 -0
  20. package/dist/__tests__/memory-backend.test.js +246 -0
  21. package/dist/__tests__/redis-backend.test.d.ts +6 -0
  22. package/dist/__tests__/redis-backend.test.d.ts.map +1 -0
  23. package/dist/__tests__/redis-backend.test.js +280 -0
  24. package/dist/__tests__/service-client.test.d.ts +5 -0
  25. package/dist/__tests__/service-client.test.d.ts.map +1 -0
  26. package/dist/__tests__/service-client.test.js +216 -0
  27. package/dist/__tests__/service-registry.test.d.ts +5 -0
  28. package/dist/__tests__/service-registry.test.d.ts.map +1 -0
  29. package/dist/__tests__/service-registry.test.js +65 -0
  30. package/dist/backends/consul-backend.d.ts +115 -0
  31. package/dist/backends/consul-backend.d.ts.map +1 -0
  32. package/dist/backends/consul-backend.js +259 -0
  33. package/dist/backends/kubernetes-backend.d.ts +103 -0
  34. package/dist/backends/kubernetes-backend.d.ts.map +1 -0
  35. package/dist/backends/kubernetes-backend.js +153 -0
  36. package/dist/backends/memory-backend.d.ts +21 -0
  37. package/dist/backends/memory-backend.d.ts.map +1 -0
  38. package/dist/backends/memory-backend.js +86 -0
  39. package/dist/backends/redis-backend.d.ts +76 -0
  40. package/dist/backends/redis-backend.d.ts.map +1 -0
  41. package/dist/backends/redis-backend.js +220 -0
  42. package/dist/backends/registry-backend.d.ts +39 -0
  43. package/dist/backends/registry-backend.d.ts.map +1 -0
  44. package/dist/backends/registry-backend.js +5 -0
  45. package/dist/client/discovery-client.d.ts +49 -0
  46. package/dist/client/discovery-client.d.ts.map +1 -0
  47. package/dist/client/discovery-client.js +123 -0
  48. package/dist/client/service-client.d.ts +48 -0
  49. package/dist/client/service-client.d.ts.map +1 -0
  50. package/dist/client/service-client.js +155 -0
  51. package/dist/decorators/inject-service-client.decorator.d.ts +16 -0
  52. package/dist/decorators/inject-service-client.decorator.d.ts.map +1 -0
  53. package/dist/decorators/inject-service-client.decorator.js +24 -0
  54. package/dist/decorators/service-registry.decorator.d.ts +11 -0
  55. package/dist/decorators/service-registry.decorator.d.ts.map +1 -0
  56. package/dist/decorators/service-registry.decorator.js +20 -0
  57. package/dist/index.d.ts +21 -0
  58. package/dist/index.d.ts.map +1 -0
  59. package/dist/index.js +51 -0
  60. package/dist/load-balancer/strategies.d.ts +82 -0
  61. package/dist/load-balancer/strategies.d.ts.map +1 -0
  62. package/dist/load-balancer/strategies.js +209 -0
  63. package/dist/registry/service-registry.d.ts +51 -0
  64. package/dist/registry/service-registry.d.ts.map +1 -0
  65. package/dist/registry/service-registry.js +159 -0
  66. package/dist/types/index.d.ts +61 -0
  67. package/dist/types/index.d.ts.map +1 -0
  68. package/dist/types/index.js +14 -0
  69. package/dist/utils/filter.d.ts +10 -0
  70. package/dist/utils/filter.d.ts.map +1 -0
  71. package/dist/utils/filter.js +39 -0
  72. package/dist/utils/logger.d.ts +21 -0
  73. package/dist/utils/logger.d.ts.map +1 -0
  74. package/dist/utils/logger.js +34 -0
  75. package/dist/utils/validation.d.ts +36 -0
  76. package/dist/utils/validation.d.ts.map +1 -0
  77. package/dist/utils/validation.js +109 -0
  78. package/package.json +81 -0
@@ -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,5 @@
1
+ /**
2
+ * Load Balancer Strategies Tests
3
+ */
4
+ export {};
5
+ //# sourceMappingURL=load-balancer-strategies.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"load-balancer-strategies.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/load-balancer-strategies.test.ts"],"names":[],"mappings":"AAAA;;GAEG"}
@@ -0,0 +1,234 @@
1
+ "use strict";
2
+ /**
3
+ * Load Balancer Strategies Tests
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const strategies_1 = require("../load-balancer/strategies");
7
+ const types_1 = require("../types");
8
+ describe('Load Balancer Strategies', () => {
9
+ const createInstance = (id, status = types_1.ServiceStatus.UP) => ({
10
+ id,
11
+ name: 'test-service',
12
+ host: 'localhost',
13
+ port: 3000,
14
+ status,
15
+ lastHeartbeat: new Date(),
16
+ registeredAt: new Date(),
17
+ });
18
+ describe('RoundRobinStrategy', () => {
19
+ it('should cycle through instances in order', () => {
20
+ const strategy = new strategies_1.RoundRobinStrategy();
21
+ const instances = [createInstance('1'), createInstance('2'), createInstance('3')];
22
+ expect(strategy.choose(instances)?.id).toBe('1');
23
+ expect(strategy.choose(instances)?.id).toBe('2');
24
+ expect(strategy.choose(instances)?.id).toBe('3');
25
+ expect(strategy.choose(instances)?.id).toBe('1'); // Cycles back
26
+ });
27
+ it('should return null when no healthy instances', () => {
28
+ const strategy = new strategies_1.RoundRobinStrategy();
29
+ const instances = [
30
+ createInstance('1', types_1.ServiceStatus.DOWN),
31
+ createInstance('2', types_1.ServiceStatus.DOWN),
32
+ ];
33
+ expect(strategy.choose(instances)).toBeNull();
34
+ });
35
+ it('should filter out unhealthy instances', () => {
36
+ const strategy = new strategies_1.RoundRobinStrategy();
37
+ const instances = [
38
+ createInstance('1', types_1.ServiceStatus.UP),
39
+ createInstance('2', types_1.ServiceStatus.DOWN),
40
+ createInstance('3', types_1.ServiceStatus.UP),
41
+ ];
42
+ const first = strategy.choose(instances);
43
+ const second = strategy.choose(instances);
44
+ expect([first?.id, second?.id]).toEqual(['1', '3']);
45
+ });
46
+ });
47
+ describe('RandomStrategy', () => {
48
+ it('should return a random instance', () => {
49
+ const strategy = new strategies_1.RandomStrategy();
50
+ const instances = [createInstance('1'), createInstance('2'), createInstance('3')];
51
+ const selected = strategy.choose(instances);
52
+ expect(selected).toBeDefined();
53
+ expect(['1', '2', '3']).toContain(selected?.id);
54
+ });
55
+ it('should return null when no healthy instances', () => {
56
+ const strategy = new strategies_1.RandomStrategy();
57
+ const instances = [
58
+ createInstance('1', types_1.ServiceStatus.DOWN),
59
+ createInstance('2', types_1.ServiceStatus.DOWN),
60
+ ];
61
+ expect(strategy.choose(instances)).toBeNull();
62
+ });
63
+ });
64
+ describe('LeastConnectionsStrategy', () => {
65
+ it('should select instance with least connections', () => {
66
+ const strategy = new strategies_1.LeastConnectionsStrategy();
67
+ const instances = [createInstance('1'), createInstance('2'), createInstance('3')];
68
+ strategy.incrementConnections('1');
69
+ strategy.incrementConnections('1');
70
+ strategy.incrementConnections('2');
71
+ const selected = strategy.choose(instances);
72
+ expect(selected?.id).toBe('3');
73
+ });
74
+ it('should handle connection increments and decrements', () => {
75
+ const strategy = new strategies_1.LeastConnectionsStrategy();
76
+ const instances = [createInstance('1')];
77
+ strategy.incrementConnections('1');
78
+ strategy.incrementConnections('1');
79
+ expect(strategy.choose(instances)?.id).toBe('1');
80
+ strategy.decrementConnections('1');
81
+ strategy.decrementConnections('1');
82
+ expect(strategy.choose(instances)?.id).toBe('1');
83
+ });
84
+ it('should not allow negative connections', () => {
85
+ const strategy = new strategies_1.LeastConnectionsStrategy();
86
+ const instances = [createInstance('1')];
87
+ strategy.decrementConnections('1');
88
+ strategy.decrementConnections('1');
89
+ expect(strategy.choose(instances)?.id).toBe('1');
90
+ });
91
+ it('should return null when no healthy instances', () => {
92
+ const strategy = new strategies_1.LeastConnectionsStrategy();
93
+ const instances = [
94
+ createInstance('1', types_1.ServiceStatus.DOWN),
95
+ createInstance('2', types_1.ServiceStatus.DOWN),
96
+ ];
97
+ expect(strategy.choose(instances)).toBeNull();
98
+ });
99
+ });
100
+ describe('WeightedRoundRobinStrategy', () => {
101
+ it('should select instances based on weight', () => {
102
+ const strategy = new strategies_1.WeightedRoundRobinStrategy();
103
+ const instances = [
104
+ { ...createInstance('1'), metadata: { weight: 2 } },
105
+ { ...createInstance('2'), metadata: { weight: 1 } },
106
+ ];
107
+ const results = [];
108
+ for (let i = 0; i < 6; i++) {
109
+ const selected = strategy.choose(instances);
110
+ if (selected)
111
+ results.push(selected.id);
112
+ }
113
+ // Should have more '1' than '2' due to weight
114
+ const count1 = results.filter((id) => id === '1').length;
115
+ const count2 = results.filter((id) => id === '2').length;
116
+ expect(count1).toBeGreaterThan(count2);
117
+ });
118
+ it('should default to weight 1 when weight is missing', () => {
119
+ const strategy = new strategies_1.WeightedRoundRobinStrategy();
120
+ const instances = [createInstance('1'), createInstance('2')];
121
+ const selected = strategy.choose(instances);
122
+ expect(selected).toBeDefined();
123
+ });
124
+ it('should handle invalid weight values', () => {
125
+ const strategy = new strategies_1.WeightedRoundRobinStrategy();
126
+ const instances = [
127
+ { ...createInstance('1'), metadata: { weight: 'invalid' } },
128
+ { ...createInstance('2'), metadata: { weight: -1 } },
129
+ ];
130
+ const selected = strategy.choose(instances);
131
+ expect(selected).toBeDefined();
132
+ });
133
+ it('should return null when no healthy instances', () => {
134
+ const strategy = new strategies_1.WeightedRoundRobinStrategy();
135
+ const instances = [
136
+ createInstance('1', types_1.ServiceStatus.DOWN),
137
+ createInstance('2', types_1.ServiceStatus.DOWN),
138
+ ];
139
+ expect(strategy.choose(instances)).toBeNull();
140
+ });
141
+ });
142
+ describe('IPHashStrategy', () => {
143
+ it('should return same instance for same IP', () => {
144
+ const strategy = new strategies_1.IPHashStrategy();
145
+ const instances = [createInstance('1'), createInstance('2'), createInstance('3')];
146
+ const ip = '192.168.1.1';
147
+ const first = strategy.choose(instances, ip);
148
+ const second = strategy.choose(instances, ip);
149
+ expect(first?.id).toBe(second?.id);
150
+ });
151
+ it('should return first instance when no IP provided', () => {
152
+ const strategy = new strategies_1.IPHashStrategy();
153
+ const instances = [createInstance('1'), createInstance('2')];
154
+ const selected = strategy.choose(instances);
155
+ expect(selected?.id).toBe('1');
156
+ });
157
+ it('should return null when no healthy instances', () => {
158
+ const strategy = new strategies_1.IPHashStrategy();
159
+ const instances = [
160
+ createInstance('1', types_1.ServiceStatus.DOWN),
161
+ createInstance('2', types_1.ServiceStatus.DOWN),
162
+ ];
163
+ expect(strategy.choose(instances, '192.168.1.1')).toBeNull();
164
+ });
165
+ });
166
+ describe('ZoneAwareStrategy', () => {
167
+ it('should prefer instances in preferred zone', () => {
168
+ const strategy = new strategies_1.ZoneAwareStrategy('us-east-1');
169
+ const instances = [
170
+ { ...createInstance('1'), zone: 'us-east-1' },
171
+ { ...createInstance('2'), zone: 'us-west-1' },
172
+ { ...createInstance('3'), zone: 'us-east-1' },
173
+ ];
174
+ const selected = strategy.choose(instances);
175
+ expect(selected?.zone).toBe('us-east-1');
176
+ expect(['1', '3']).toContain(selected?.id);
177
+ });
178
+ it('should fallback to any zone when preferred zone not available', () => {
179
+ const strategy = new strategies_1.ZoneAwareStrategy('us-east-1');
180
+ const instances = [
181
+ { ...createInstance('1'), zone: 'us-west-1' },
182
+ { ...createInstance('2'), zone: 'eu-west-1' },
183
+ ];
184
+ const selected = strategy.choose(instances);
185
+ expect(selected).toBeDefined();
186
+ expect(['1', '2']).toContain(selected?.id);
187
+ });
188
+ it('should work without preferred zone', () => {
189
+ const strategy = new strategies_1.ZoneAwareStrategy();
190
+ const instances = [createInstance('1'), createInstance('2')];
191
+ const selected = strategy.choose(instances);
192
+ expect(selected).toBeDefined();
193
+ });
194
+ it('should return null when no healthy instances', () => {
195
+ const strategy = new strategies_1.ZoneAwareStrategy('us-east-1');
196
+ const instances = [
197
+ createInstance('1', types_1.ServiceStatus.DOWN),
198
+ createInstance('2', types_1.ServiceStatus.DOWN),
199
+ ];
200
+ expect(strategy.choose(instances)).toBeNull();
201
+ });
202
+ });
203
+ describe('LoadBalancerFactory', () => {
204
+ it('should register and retrieve strategies', () => {
205
+ const factory = new strategies_1.LoadBalancerFactory();
206
+ const customStrategy = new strategies_1.RoundRobinStrategy();
207
+ customStrategy.name = 'custom';
208
+ factory.register(customStrategy);
209
+ expect(factory.get('custom')).toBe(customStrategy);
210
+ });
211
+ it('should create default strategies', () => {
212
+ const factory = new strategies_1.LoadBalancerFactory();
213
+ expect(factory.get('round-robin')).toBeDefined();
214
+ expect(factory.get('random')).toBeDefined();
215
+ expect(factory.get('least-connections')).toBeDefined();
216
+ expect(factory.get('weighted-round-robin')).toBeDefined();
217
+ expect(factory.get('ip-hash')).toBeDefined();
218
+ });
219
+ it('should create zone-aware strategy with options', () => {
220
+ const factory = new strategies_1.LoadBalancerFactory();
221
+ const strategy = factory.create('zone-aware', { zone: 'us-east-1' });
222
+ expect(strategy).toBeInstanceOf(strategies_1.ZoneAwareStrategy);
223
+ expect(strategy.name).toBe('zone-aware');
224
+ });
225
+ it('should throw error for unknown strategy', () => {
226
+ const factory = new strategies_1.LoadBalancerFactory();
227
+ expect(() => factory.create('unknown-strategy')).toThrow("Load balancing strategy 'unknown-strategy' not found");
228
+ });
229
+ it('should return undefined for non-existent strategy', () => {
230
+ const factory = new strategies_1.LoadBalancerFactory();
231
+ expect(factory.get('non-existent')).toBeUndefined();
232
+ });
233
+ });
234
+ });
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Memory Backend Tests
3
+ */
4
+ export {};
5
+ //# sourceMappingURL=memory-backend.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory-backend.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/memory-backend.test.ts"],"names":[],"mappings":"AAAA;;GAEG"}