@onlineapps/conn-orch-validator 2.0.5 → 2.0.7
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/README.md +30 -1
- package/TESTING_STRATEGY.md +0 -0
- package/docs/DESIGN.md +0 -0
- package/examples/service-wrapper-usage.js +0 -0
- package/examples/three-tier-testing.js +0 -0
- package/jest.config.js +0 -0
- package/onlineapps-conn-e2e-testing-1.0.0.tgz +0 -0
- package/package.json +1 -1
- package/src/CookbookTestRunner.js +4 -4
- package/src/CookbookTestUtils.js +0 -0
- package/src/ServiceReadinessValidator.js +0 -0
- package/src/ServiceTestHarness.js +0 -0
- package/src/ServiceValidator.js +0 -0
- package/src/TestOrchestrator.js +0 -0
- package/src/ValidationOrchestrator.js +31 -33
- package/src/WorkflowTestRunner.js +0 -0
- package/src/helpers/README.md +0 -0
- package/src/helpers/createPreValidationTests.js +0 -0
- package/src/helpers/createServiceReadinessTests.js +0 -0
- package/src/index.js +0 -0
- package/src/mocks/MockMQClient.js +0 -0
- package/src/mocks/MockRegistry.js +0 -0
- package/src/mocks/MockStorage.js +0 -0
- package/src/validators/ServiceStructureValidator.js +0 -0
- package/src/validators/ValidationProofGenerator.js +0 -0
- package/test-mq-flow.js +0 -0
- package/tests/component/testing-framework-integration.test.js +0 -313
- package/tests/integration/ServiceReadiness.test.js +0 -265
- package/tests/monitoring-e2e.test.js +0 -315
- package/tests/run-example.js +0 -257
- package/tests/unit/CookbookTestRunner.test.js +0 -353
- package/tests/unit/MockMQClient.test.js +0 -190
- package/tests/unit/MockRegistry.test.js +0 -233
- package/tests/unit/MockStorage.test.js +0 -257
- package/tests/unit/ServiceValidator.test.js +0 -429
- package/tests/unit/WorkflowTestRunner.test.js +0 -546
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const MockMQClient = require('../../src/mocks/MockMQClient');
|
|
4
|
-
|
|
5
|
-
describe('MockMQClient @unit', () => {
|
|
6
|
-
let mqClient;
|
|
7
|
-
|
|
8
|
-
beforeEach(() => {
|
|
9
|
-
mqClient = new MockMQClient();
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
describe('Connection Management', () => {
|
|
13
|
-
it('should connect successfully', async () => {
|
|
14
|
-
expect(mqClient.isConnected).toBe(false);
|
|
15
|
-
await mqClient.connect();
|
|
16
|
-
expect(mqClient.isConnected).toBe(true);
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it('should disconnect successfully', async () => {
|
|
20
|
-
await mqClient.connect();
|
|
21
|
-
expect(mqClient.isConnected).toBe(true);
|
|
22
|
-
await mqClient.disconnect();
|
|
23
|
-
expect(mqClient.isConnected).toBe(false);
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it('should clear consumers on disconnect', async () => {
|
|
27
|
-
await mqClient.connect();
|
|
28
|
-
await mqClient.consume('test-queue', jest.fn());
|
|
29
|
-
expect(Object.keys(mqClient.consumers)).toHaveLength(1);
|
|
30
|
-
await mqClient.disconnect();
|
|
31
|
-
expect(Object.keys(mqClient.consumers)).toHaveLength(0);
|
|
32
|
-
});
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
describe('Publishing Messages', () => {
|
|
36
|
-
beforeEach(async () => {
|
|
37
|
-
await mqClient.connect();
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it('should publish message to queue', async () => {
|
|
41
|
-
const message = { test: 'data' };
|
|
42
|
-
await mqClient.publish('test-queue', message);
|
|
43
|
-
|
|
44
|
-
const messages = mqClient.getMessages('test-queue');
|
|
45
|
-
expect(messages).toHaveLength(1);
|
|
46
|
-
expect(messages[0]).toEqual(message);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it('should track published messages', async () => {
|
|
50
|
-
await mqClient.publish('queue1', { msg: 1 });
|
|
51
|
-
await mqClient.publish('queue2', { msg: 2 });
|
|
52
|
-
|
|
53
|
-
const allPublished = mqClient.getPublishedMessages();
|
|
54
|
-
expect(allPublished).toHaveLength(2);
|
|
55
|
-
|
|
56
|
-
const queue1Messages = mqClient.getPublishedMessages('queue1');
|
|
57
|
-
expect(queue1Messages).toHaveLength(1);
|
|
58
|
-
expect(queue1Messages[0].message).toEqual({ msg: 1 });
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it('should throw error when not connected', async () => {
|
|
62
|
-
await mqClient.disconnect();
|
|
63
|
-
await expect(mqClient.publish('test', {})).rejects.toThrow('Not connected to MQ');
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it('should include message options', async () => {
|
|
67
|
-
const options = { priority: 1, persistent: true };
|
|
68
|
-
await mqClient.publish('test-queue', { test: 'data' }, options);
|
|
69
|
-
|
|
70
|
-
const published = mqClient.getPublishedMessages()[0];
|
|
71
|
-
expect(published.options).toEqual(options);
|
|
72
|
-
});
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
describe('Consuming Messages', () => {
|
|
76
|
-
let callback;
|
|
77
|
-
|
|
78
|
-
beforeEach(async () => {
|
|
79
|
-
callback = jest.fn();
|
|
80
|
-
await mqClient.connect();
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('should register consumer', async () => {
|
|
84
|
-
await mqClient.consume('test-queue', callback);
|
|
85
|
-
expect(mqClient.consumers['test-queue']).toBeDefined();
|
|
86
|
-
expect(mqClient.consumers['test-queue'].callback).toBe(callback);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it('should process existing messages when consuming', async () => {
|
|
90
|
-
await mqClient.publish('test-queue', { msg: 1 });
|
|
91
|
-
await mqClient.publish('test-queue', { msg: 2 });
|
|
92
|
-
|
|
93
|
-
await mqClient.consume('test-queue', callback);
|
|
94
|
-
|
|
95
|
-
// Wait for async processing
|
|
96
|
-
await new Promise(resolve => setTimeout(resolve, 10));
|
|
97
|
-
|
|
98
|
-
expect(callback).toHaveBeenCalledTimes(2);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it('should trigger consumer on new message', async () => {
|
|
102
|
-
await mqClient.consume('test-queue', callback);
|
|
103
|
-
await mqClient.publish('test-queue', { test: 'data' });
|
|
104
|
-
|
|
105
|
-
// Wait for async processing
|
|
106
|
-
await new Promise(resolve => setTimeout(resolve, 10));
|
|
107
|
-
|
|
108
|
-
expect(callback).toHaveBeenCalledTimes(1);
|
|
109
|
-
const call = callback.mock.calls[0][0];
|
|
110
|
-
expect(JSON.parse(call.content.toString())).toEqual({ test: 'data' });
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it('should throw error when not connected', async () => {
|
|
114
|
-
await mqClient.disconnect();
|
|
115
|
-
await expect(mqClient.consume('test', jest.fn())).rejects.toThrow('Not connected to MQ');
|
|
116
|
-
});
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
describe('Message Acknowledgement', () => {
|
|
120
|
-
let message;
|
|
121
|
-
|
|
122
|
-
beforeEach(async () => {
|
|
123
|
-
await mqClient.connect();
|
|
124
|
-
message = {
|
|
125
|
-
content: Buffer.from(JSON.stringify({ test: 'data' })),
|
|
126
|
-
fields: { routingKey: 'test-queue' }
|
|
127
|
-
};
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
it('should acknowledge message', async () => {
|
|
131
|
-
await mqClient.ack(message);
|
|
132
|
-
expect(mqClient.acknowledgedMessages).toHaveLength(1);
|
|
133
|
-
expect(mqClient.acknowledgedMessages[0].message).toBe(message);
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
it('should reject message without requeue', async () => {
|
|
137
|
-
await mqClient.nack(message, false, false);
|
|
138
|
-
expect(mqClient.rejectedMessages).toHaveLength(1);
|
|
139
|
-
expect(mqClient.rejectedMessages[0].requeue).toBe(false);
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
it('should reject and requeue message', async () => {
|
|
143
|
-
await mqClient.nack(message, false, true);
|
|
144
|
-
expect(mqClient.rejectedMessages).toHaveLength(1);
|
|
145
|
-
expect(mqClient.rejectedMessages[0].requeue).toBe(true);
|
|
146
|
-
|
|
147
|
-
const messages = mqClient.getMessages('test-queue');
|
|
148
|
-
expect(messages).toHaveLength(1);
|
|
149
|
-
});
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
describe('Statistics and Clearing', () => {
|
|
153
|
-
beforeEach(async () => {
|
|
154
|
-
await mqClient.connect();
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
it('should return statistics', async () => {
|
|
158
|
-
await mqClient.publish('queue1', { msg: 1 });
|
|
159
|
-
await mqClient.publish('queue2', { msg: 2 });
|
|
160
|
-
await mqClient.consume('queue1', jest.fn());
|
|
161
|
-
|
|
162
|
-
const stats = mqClient.getStats();
|
|
163
|
-
expect(stats.queues).toEqual(['queue1', 'queue2']);
|
|
164
|
-
expect(stats.messageCount).toBe(2);
|
|
165
|
-
expect(stats.published).toBe(2);
|
|
166
|
-
expect(stats.consumers).toEqual(['queue1']);
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
it('should clear all data', async () => {
|
|
170
|
-
await mqClient.publish('test', { msg: 1 });
|
|
171
|
-
await mqClient.ack({ test: 'message' });
|
|
172
|
-
|
|
173
|
-
mqClient.clear();
|
|
174
|
-
|
|
175
|
-
expect(mqClient.getMessages('test')).toHaveLength(0);
|
|
176
|
-
expect(mqClient.publishedMessages).toHaveLength(0);
|
|
177
|
-
expect(mqClient.acknowledgedMessages).toHaveLength(0);
|
|
178
|
-
expect(mqClient.rejectedMessages).toHaveLength(0);
|
|
179
|
-
});
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
describe('Delay Simulation', () => {
|
|
183
|
-
it('should simulate delay', async () => {
|
|
184
|
-
const start = Date.now();
|
|
185
|
-
await mqClient.simulateDelay(50);
|
|
186
|
-
const duration = Date.now() - start;
|
|
187
|
-
expect(duration).toBeGreaterThanOrEqual(50);
|
|
188
|
-
});
|
|
189
|
-
});
|
|
190
|
-
});
|
|
@@ -1,233 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const MockRegistry = require('../../src/mocks/MockRegistry');
|
|
4
|
-
|
|
5
|
-
describe('MockRegistry @unit', () => {
|
|
6
|
-
let registry;
|
|
7
|
-
|
|
8
|
-
beforeEach(() => {
|
|
9
|
-
registry = new MockRegistry();
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
describe('Service Registration', () => {
|
|
13
|
-
it('should register service successfully', async () => {
|
|
14
|
-
const serviceData = {
|
|
15
|
-
name: 'test-service',
|
|
16
|
-
version: '1.0.0',
|
|
17
|
-
url: 'http://localhost:3000',
|
|
18
|
-
healthCheck: '/health',
|
|
19
|
-
openapi: { paths: {} }
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
const result = await registry.register(serviceData);
|
|
23
|
-
|
|
24
|
-
expect(result.success).toBe(true);
|
|
25
|
-
expect(result.certificate).toBeDefined();
|
|
26
|
-
expect(result.certificate.serviceId).toBe('test-service');
|
|
27
|
-
|
|
28
|
-
const service = registry.getService('test-service');
|
|
29
|
-
expect(service).toBeDefined();
|
|
30
|
-
expect(service.name).toBe('test-service');
|
|
31
|
-
expect(service.version).toBe('1.0.0');
|
|
32
|
-
expect(service.status).toBe('active');
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it('should use defaults for optional fields', async () => {
|
|
36
|
-
const result = await registry.register({ name: 'minimal-service' });
|
|
37
|
-
|
|
38
|
-
const service = registry.getService('minimal-service');
|
|
39
|
-
expect(service.version).toBe('1.0.0');
|
|
40
|
-
expect(service.url).toBe('http://localhost:3000');
|
|
41
|
-
expect(service.healthCheck).toBe('/health');
|
|
42
|
-
expect(service.openapi).toEqual({});
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it('should throw error if name not provided', async () => {
|
|
46
|
-
await expect(registry.register({})).rejects.toThrow('Service name is required');
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it('should issue certificate on registration', async () => {
|
|
50
|
-
await registry.register({ name: 'cert-service' });
|
|
51
|
-
|
|
52
|
-
const cert = registry.getCertificate('cert-service');
|
|
53
|
-
expect(cert).toBeDefined();
|
|
54
|
-
expect(cert.serviceId).toBe('cert-service');
|
|
55
|
-
expect(cert.fingerprint).toContain('mock-cert-cert-service');
|
|
56
|
-
expect(cert.expiresAt).toBeGreaterThan(Date.now());
|
|
57
|
-
});
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
describe('Service Unregistration', () => {
|
|
61
|
-
beforeEach(async () => {
|
|
62
|
-
await registry.register({ name: 'test-service' });
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it('should unregister service', async () => {
|
|
66
|
-
const result = await registry.unregister('test-service');
|
|
67
|
-
|
|
68
|
-
expect(result.success).toBe(true);
|
|
69
|
-
expect(registry.getService('test-service')).toBeNull();
|
|
70
|
-
expect(registry.getCertificate('test-service')).toBeNull();
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it('should remove heartbeats on unregister', async () => {
|
|
74
|
-
await registry.heartbeat('test-service');
|
|
75
|
-
await registry.unregister('test-service');
|
|
76
|
-
|
|
77
|
-
expect(registry.heartbeats['test-service']).toBeUndefined();
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
describe('Heartbeat Management', () => {
|
|
82
|
-
beforeEach(async () => {
|
|
83
|
-
await registry.register({ name: 'test-service' });
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it('should send heartbeat successfully', async () => {
|
|
87
|
-
const result = await registry.heartbeat('test-service');
|
|
88
|
-
|
|
89
|
-
expect(result.success).toBe(true);
|
|
90
|
-
expect(registry.heartbeats['test-service']).toBeDefined();
|
|
91
|
-
expect(registry.heartbeats['test-service'].status).toBe('healthy');
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it('should throw error for unregistered service', async () => {
|
|
95
|
-
await expect(registry.heartbeat('unknown-service'))
|
|
96
|
-
.rejects.toThrow('Service unknown-service not registered');
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it('should update heartbeat timestamp', async () => {
|
|
100
|
-
await registry.heartbeat('test-service');
|
|
101
|
-
const firstTimestamp = registry.heartbeats['test-service'].timestamp;
|
|
102
|
-
|
|
103
|
-
await new Promise(resolve => setTimeout(resolve, 10));
|
|
104
|
-
|
|
105
|
-
await registry.heartbeat('test-service');
|
|
106
|
-
const secondTimestamp = registry.heartbeats['test-service'].timestamp;
|
|
107
|
-
|
|
108
|
-
expect(secondTimestamp).toBeGreaterThan(firstTimestamp);
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
describe('Service Discovery', () => {
|
|
113
|
-
beforeEach(async () => {
|
|
114
|
-
await registry.register({
|
|
115
|
-
name: 'service-v1',
|
|
116
|
-
version: '1.0.0',
|
|
117
|
-
status: 'active'
|
|
118
|
-
});
|
|
119
|
-
await registry.register({
|
|
120
|
-
name: 'service-v2',
|
|
121
|
-
version: '2.0.0',
|
|
122
|
-
status: 'active'
|
|
123
|
-
});
|
|
124
|
-
await registry.register({
|
|
125
|
-
name: 'inactive-service',
|
|
126
|
-
version: '1.0.0'
|
|
127
|
-
});
|
|
128
|
-
registry.services['inactive-service'].status = 'inactive';
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it('should get all services', () => {
|
|
132
|
-
const services = registry.getServices();
|
|
133
|
-
expect(services).toHaveLength(3);
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
it('should discover by name', () => {
|
|
137
|
-
const results = registry.discover({ name: 'service' });
|
|
138
|
-
expect(results).toHaveLength(3);
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
it('should discover by version', () => {
|
|
142
|
-
const results = registry.discover({ version: '1.0.0' });
|
|
143
|
-
expect(results).toHaveLength(2);
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
it('should discover by status', () => {
|
|
147
|
-
const results = registry.discover({ status: 'active' });
|
|
148
|
-
expect(results).toHaveLength(2);
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
it('should combine discovery filters', () => {
|
|
152
|
-
const results = registry.discover({
|
|
153
|
-
version: '1.0.0',
|
|
154
|
-
status: 'active'
|
|
155
|
-
});
|
|
156
|
-
expect(results).toHaveLength(1);
|
|
157
|
-
expect(results[0].name).toBe('service-v1');
|
|
158
|
-
});
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
describe('Validation', () => {
|
|
162
|
-
beforeEach(async () => {
|
|
163
|
-
await registry.register({ name: 'test-service' });
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
it('should validate service', async () => {
|
|
167
|
-
const validationData = {
|
|
168
|
-
cookbook: { version: '1.0.0' },
|
|
169
|
-
testResults: { passed: 10, failed: 0 }
|
|
170
|
-
};
|
|
171
|
-
|
|
172
|
-
const result = await registry.validate('test-service', validationData);
|
|
173
|
-
|
|
174
|
-
expect(result.success).toBe(true);
|
|
175
|
-
expect(result.token).toContain('validation-token-test-service');
|
|
176
|
-
|
|
177
|
-
const validationResults = registry.getValidationResults('test-service');
|
|
178
|
-
expect(validationResults).toBeDefined();
|
|
179
|
-
expect(validationResults.passed).toBe(true);
|
|
180
|
-
expect(validationResults.cookbook).toEqual(validationData.cookbook);
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
it('should return null for non-validated service', () => {
|
|
184
|
-
const results = registry.getValidationResults('non-validated');
|
|
185
|
-
expect(results).toBeNull();
|
|
186
|
-
});
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
describe('Statistics', () => {
|
|
190
|
-
beforeEach(async () => {
|
|
191
|
-
await registry.register({ name: 'service1' });
|
|
192
|
-
await registry.register({ name: 'service2' });
|
|
193
|
-
await registry.heartbeat('service1');
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
it('should return registry statistics', () => {
|
|
197
|
-
const stats = registry.getStats();
|
|
198
|
-
|
|
199
|
-
expect(stats.serviceCount).toBe(2);
|
|
200
|
-
expect(stats.activeServices).toBe(2);
|
|
201
|
-
expect(stats.certificates).toBe(2);
|
|
202
|
-
expect(stats.recentHeartbeats).toBe(1);
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
it('should count only recent heartbeats', async () => {
|
|
206
|
-
// Add old heartbeat
|
|
207
|
-
registry.heartbeats['service2'] = {
|
|
208
|
-
timestamp: Date.now() - 120000, // 2 minutes ago
|
|
209
|
-
status: 'healthy'
|
|
210
|
-
};
|
|
211
|
-
|
|
212
|
-
const stats = registry.getStats();
|
|
213
|
-
expect(stats.recentHeartbeats).toBe(1); // Only service1 is recent
|
|
214
|
-
});
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
describe('Clear Data', () => {
|
|
218
|
-
beforeEach(async () => {
|
|
219
|
-
await registry.register({ name: 'test-service' });
|
|
220
|
-
await registry.heartbeat('test-service');
|
|
221
|
-
await registry.validate('test-service', { test: 'data' });
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
it('should clear all data', () => {
|
|
225
|
-
registry.clear();
|
|
226
|
-
|
|
227
|
-
expect(Object.keys(registry.services)).toHaveLength(0);
|
|
228
|
-
expect(Object.keys(registry.certificates)).toHaveLength(0);
|
|
229
|
-
expect(Object.keys(registry.heartbeats)).toHaveLength(0);
|
|
230
|
-
expect(Object.keys(registry.validationResults)).toHaveLength(0);
|
|
231
|
-
});
|
|
232
|
-
});
|
|
233
|
-
});
|
|
@@ -1,257 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const MockStorage = require('../../src/mocks/MockStorage');
|
|
4
|
-
|
|
5
|
-
describe('MockStorage @unit', () => {
|
|
6
|
-
let storage;
|
|
7
|
-
|
|
8
|
-
beforeEach(() => {
|
|
9
|
-
storage = new MockStorage();
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
describe('Bucket Management', () => {
|
|
13
|
-
it('should create bucket', async () => {
|
|
14
|
-
const result = await storage.createBucket('test-bucket');
|
|
15
|
-
expect(result.success).toBe(true);
|
|
16
|
-
expect(storage.buckets['test-bucket']).toBeDefined();
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it('should not error on duplicate bucket creation', async () => {
|
|
20
|
-
await storage.createBucket('test-bucket');
|
|
21
|
-
const result = await storage.createBucket('test-bucket');
|
|
22
|
-
expect(result.success).toBe(true);
|
|
23
|
-
});
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
describe('Object Storage', () => {
|
|
27
|
-
it('should put object in bucket', async () => {
|
|
28
|
-
const content = { test: 'data' };
|
|
29
|
-
const result = await storage.put('test-bucket', 'test-key', content);
|
|
30
|
-
|
|
31
|
-
expect(result.success).toBe(true);
|
|
32
|
-
expect(result.etag).toContain('mock-etag');
|
|
33
|
-
expect(result.location).toBe('test-bucket/test-key');
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it('should auto-create bucket if not exists', async () => {
|
|
37
|
-
await storage.put('new-bucket', 'key', 'content');
|
|
38
|
-
expect(storage.buckets['new-bucket']).toBeDefined();
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it('should store string content', async () => {
|
|
42
|
-
await storage.put('bucket', 'key', 'string content');
|
|
43
|
-
const obj = await storage.get('bucket', 'key');
|
|
44
|
-
expect(obj.content).toBe('string content');
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it('should store object as JSON', async () => {
|
|
48
|
-
const data = { nested: { value: 123 } };
|
|
49
|
-
await storage.put('bucket', 'key', data);
|
|
50
|
-
const obj = await storage.get('bucket', 'key');
|
|
51
|
-
expect(obj.content).toEqual(data);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it('should store metadata', async () => {
|
|
55
|
-
const metadata = { author: 'test', version: '1.0' };
|
|
56
|
-
await storage.put('bucket', 'key', 'content', {
|
|
57
|
-
metadata,
|
|
58
|
-
contentType: 'text/plain'
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
const obj = await storage.get('bucket', 'key');
|
|
62
|
-
expect(obj.metadata).toEqual(metadata);
|
|
63
|
-
expect(obj.contentType).toBe('text/plain');
|
|
64
|
-
});
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
describe('Object Retrieval', () => {
|
|
68
|
-
beforeEach(async () => {
|
|
69
|
-
await storage.put('bucket', 'key1', { data: 'test' });
|
|
70
|
-
await storage.put('bucket', 'key2', 'text content', {
|
|
71
|
-
contentType: 'text/plain'
|
|
72
|
-
});
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it('should get object from bucket', async () => {
|
|
76
|
-
const obj = await storage.get('bucket', 'key1');
|
|
77
|
-
expect(obj.content).toEqual({ data: 'test' });
|
|
78
|
-
expect(obj.contentType).toBe('application/json');
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it('should throw error for non-existent object', async () => {
|
|
82
|
-
await expect(storage.get('bucket', 'non-existent'))
|
|
83
|
-
.rejects.toThrow('Object not found: bucket/non-existent');
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it('should check object existence', async () => {
|
|
87
|
-
expect(await storage.exists('bucket', 'key1')).toBe(true);
|
|
88
|
-
expect(await storage.exists('bucket', 'non-existent')).toBe(false);
|
|
89
|
-
expect(await storage.exists('non-bucket', 'key')).toBe(false);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it('should get object metadata', async () => {
|
|
93
|
-
const metadata = await storage.getMetadata('bucket', 'key1');
|
|
94
|
-
expect(metadata.size).toBeGreaterThan(0);
|
|
95
|
-
expect(metadata.lastModified).toBeDefined();
|
|
96
|
-
expect(metadata.contentType).toBe('application/json');
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it('should throw error for non-existent metadata', async () => {
|
|
100
|
-
await expect(storage.getMetadata('bucket', 'non-existent'))
|
|
101
|
-
.rejects.toThrow('Metadata not found: bucket/non-existent');
|
|
102
|
-
});
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
describe('Object Deletion', () => {
|
|
106
|
-
beforeEach(async () => {
|
|
107
|
-
await storage.put('bucket', 'key', 'content');
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it('should delete object', async () => {
|
|
111
|
-
expect(await storage.exists('bucket', 'key')).toBe(true);
|
|
112
|
-
|
|
113
|
-
const result = await storage.delete('bucket', 'key');
|
|
114
|
-
expect(result.success).toBe(true);
|
|
115
|
-
|
|
116
|
-
expect(await storage.exists('bucket', 'key')).toBe(false);
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it('should not error on deleting non-existent object', async () => {
|
|
120
|
-
const result = await storage.delete('bucket', 'non-existent');
|
|
121
|
-
expect(result.success).toBe(true);
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it('should remove metadata on delete', async () => {
|
|
125
|
-
await storage.delete('bucket', 'key');
|
|
126
|
-
await expect(storage.getMetadata('bucket', 'key'))
|
|
127
|
-
.rejects.toThrow('Metadata not found');
|
|
128
|
-
});
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
describe('Object Listing', () => {
|
|
132
|
-
beforeEach(async () => {
|
|
133
|
-
await storage.put('bucket', 'file1.txt', 'content1');
|
|
134
|
-
await storage.put('bucket', 'file2.txt', 'content2');
|
|
135
|
-
await storage.put('bucket', 'folder/file3.txt', 'content3');
|
|
136
|
-
await storage.put('bucket', 'folder/file4.txt', 'content4');
|
|
137
|
-
await storage.put('other-bucket', 'file5.txt', 'content5');
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
it('should list all objects in bucket', async () => {
|
|
141
|
-
const result = await storage.list('bucket');
|
|
142
|
-
expect(result.objects).toHaveLength(4);
|
|
143
|
-
expect(result.objects[0].key).toBe('file1.txt');
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
it('should list with prefix', async () => {
|
|
147
|
-
const result = await storage.list('bucket', 'folder/');
|
|
148
|
-
expect(result.objects).toHaveLength(2);
|
|
149
|
-
expect(result.objects.every(o => o.key.startsWith('folder/'))).toBe(true);
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
it('should respect limit', async () => {
|
|
153
|
-
const result = await storage.list('bucket', '', 2);
|
|
154
|
-
expect(result.objects).toHaveLength(2);
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
it('should return empty list for non-existent bucket', async () => {
|
|
158
|
-
const result = await storage.list('non-existent');
|
|
159
|
-
expect(result.objects).toHaveLength(0);
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
it('should include size and timestamp in listing', async () => {
|
|
163
|
-
const result = await storage.list('bucket');
|
|
164
|
-
const obj = result.objects[0];
|
|
165
|
-
expect(obj.size).toBeGreaterThan(0);
|
|
166
|
-
expect(obj.lastModified).toBeDefined();
|
|
167
|
-
});
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
describe('Object Copying', () => {
|
|
171
|
-
beforeEach(async () => {
|
|
172
|
-
await storage.put('source-bucket', 'source-key', { data: 'test' }, {
|
|
173
|
-
metadata: { original: true },
|
|
174
|
-
contentType: 'application/json'
|
|
175
|
-
});
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
it('should copy object to same bucket', async () => {
|
|
179
|
-
await storage.copy('source-bucket', 'source-key', 'source-bucket', 'copy-key');
|
|
180
|
-
|
|
181
|
-
const copy = await storage.get('source-bucket', 'copy-key');
|
|
182
|
-
expect(copy.content).toEqual({ data: 'test' });
|
|
183
|
-
expect(copy.metadata).toEqual({ original: true });
|
|
184
|
-
expect(copy.contentType).toBe('application/json');
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
it('should copy object to different bucket', async () => {
|
|
188
|
-
await storage.copy('source-bucket', 'source-key', 'dest-bucket', 'dest-key');
|
|
189
|
-
|
|
190
|
-
const copy = await storage.get('dest-bucket', 'dest-key');
|
|
191
|
-
expect(copy.content).toEqual({ data: 'test' });
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
it('should throw error copying non-existent object', async () => {
|
|
195
|
-
await expect(storage.copy('source-bucket', 'non-existent', 'dest', 'key'))
|
|
196
|
-
.rejects.toThrow('Object not found');
|
|
197
|
-
});
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
describe('Presigned URLs', () => {
|
|
201
|
-
it('should generate presigned URL', async () => {
|
|
202
|
-
const result = await storage.getPresignedUrl('bucket', 'key');
|
|
203
|
-
|
|
204
|
-
expect(result.url).toContain('http://mock-storage/bucket/key');
|
|
205
|
-
expect(result.url).toContain('token=mock-token');
|
|
206
|
-
expect(result.expiresAt).toBeGreaterThan(Date.now());
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
it('should respect custom expiration', async () => {
|
|
210
|
-
const expiresIn = 7200; // 2 hours
|
|
211
|
-
const result = await storage.getPresignedUrl('bucket', 'key', expiresIn);
|
|
212
|
-
|
|
213
|
-
const expectedExpiry = Date.now() + (expiresIn * 1000);
|
|
214
|
-
expect(result.expiresAt).toBeCloseTo(expectedExpiry, -3);
|
|
215
|
-
});
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
describe('Statistics', () => {
|
|
219
|
-
beforeEach(async () => {
|
|
220
|
-
await storage.put('bucket1', 'key1', 'content1');
|
|
221
|
-
await storage.put('bucket1', 'key2', 'content2');
|
|
222
|
-
await storage.put('bucket2', 'key3', 'content3');
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
it('should return storage statistics', () => {
|
|
226
|
-
const stats = storage.getStats();
|
|
227
|
-
|
|
228
|
-
expect(stats.bucketCount).toBe(2);
|
|
229
|
-
expect(stats.totalObjects).toBe(3);
|
|
230
|
-
expect(stats.totalSize).toBeGreaterThan(0);
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
it('should calculate total size correctly', () => {
|
|
234
|
-
const stats = storage.getStats();
|
|
235
|
-
const expectedSize = 'content1'.length + 'content2'.length + 'content3'.length;
|
|
236
|
-
expect(stats.totalSize).toBe(expectedSize);
|
|
237
|
-
});
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
describe('Clear Data', () => {
|
|
241
|
-
beforeEach(async () => {
|
|
242
|
-
await storage.put('bucket1', 'key1', 'content');
|
|
243
|
-
await storage.put('bucket2', 'key2', 'content');
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
it('should clear all data', () => {
|
|
247
|
-
storage.clear();
|
|
248
|
-
|
|
249
|
-
expect(Object.keys(storage.buckets)).toHaveLength(0);
|
|
250
|
-
expect(Object.keys(storage.metadata)).toHaveLength(0);
|
|
251
|
-
|
|
252
|
-
const stats = storage.getStats();
|
|
253
|
-
expect(stats.bucketCount).toBe(0);
|
|
254
|
-
expect(stats.totalObjects).toBe(0);
|
|
255
|
-
});
|
|
256
|
-
});
|
|
257
|
-
});
|