@onlineapps/conn-orch-validator 2.0.0
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 +78 -0
- package/TESTING_STRATEGY.md +92 -0
- package/docs/DESIGN.md +134 -0
- package/examples/service-wrapper-usage.js +250 -0
- package/examples/three-tier-testing.js +144 -0
- package/jest.config.js +23 -0
- package/onlineapps-conn-e2e-testing-1.0.0.tgz +0 -0
- package/package.json +43 -0
- package/src/CookbookTestRunner.js +434 -0
- package/src/CookbookTestUtils.js +237 -0
- package/src/ServiceReadinessValidator.js +430 -0
- package/src/ServiceTestHarness.js +256 -0
- package/src/ServiceValidator.js +387 -0
- package/src/TestOrchestrator.js +727 -0
- package/src/ValidationOrchestrator.js +506 -0
- package/src/WorkflowTestRunner.js +396 -0
- package/src/helpers/README.md +235 -0
- package/src/helpers/createPreValidationTests.js +321 -0
- package/src/helpers/createServiceReadinessTests.js +245 -0
- package/src/index.js +62 -0
- package/src/mocks/MockMQClient.js +176 -0
- package/src/mocks/MockRegistry.js +164 -0
- package/src/mocks/MockStorage.js +186 -0
- package/src/validators/ServiceStructureValidator.js +487 -0
- package/src/validators/ValidationProofGenerator.js +79 -0
- package/test-mq-flow.js +72 -0
- package/test-orchestrator.js +95 -0
- package/tests/component/testing-framework-integration.test.js +313 -0
- package/tests/integration/ServiceReadiness.test.js +265 -0
- package/tests/monitoring-e2e.test.js +315 -0
- package/tests/run-example.js +257 -0
- package/tests/unit/CookbookTestRunner.test.js +353 -0
- package/tests/unit/MockMQClient.test.js +190 -0
- package/tests/unit/MockRegistry.test.js +233 -0
- package/tests/unit/MockStorage.test.js +257 -0
- package/tests/unit/ServiceValidator.test.js +429 -0
- package/tests/unit/WorkflowTestRunner.test.js +546 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MockMQClient - Simulates RabbitMQ for testing
|
|
5
|
+
* Provides in-memory message queue simulation
|
|
6
|
+
*/
|
|
7
|
+
class MockMQClient {
|
|
8
|
+
constructor() {
|
|
9
|
+
this.queues = {};
|
|
10
|
+
this.consumers = {};
|
|
11
|
+
this.isConnected = false;
|
|
12
|
+
this.publishedMessages = [];
|
|
13
|
+
this.acknowledgedMessages = [];
|
|
14
|
+
this.rejectedMessages = [];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Simulate connection
|
|
19
|
+
*/
|
|
20
|
+
async connect() {
|
|
21
|
+
this.isConnected = true;
|
|
22
|
+
return Promise.resolve();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Simulate disconnection
|
|
27
|
+
*/
|
|
28
|
+
async disconnect() {
|
|
29
|
+
this.isConnected = false;
|
|
30
|
+
this.consumers = {};
|
|
31
|
+
return Promise.resolve();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Publish message to queue
|
|
36
|
+
*/
|
|
37
|
+
async publish(queue, message, options = {}) {
|
|
38
|
+
if (!this.isConnected) {
|
|
39
|
+
throw new Error('Not connected to MQ');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!this.queues[queue]) {
|
|
43
|
+
this.queues[queue] = [];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const messageWrapper = {
|
|
47
|
+
content: Buffer.from(JSON.stringify(message)),
|
|
48
|
+
fields: { routingKey: queue },
|
|
49
|
+
properties: options,
|
|
50
|
+
timestamp: Date.now()
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
this.queues[queue].push(messageWrapper);
|
|
54
|
+
this.publishedMessages.push({ queue, message, options, timestamp: Date.now() });
|
|
55
|
+
|
|
56
|
+
// Trigger consumers if any
|
|
57
|
+
if (this.consumers[queue]) {
|
|
58
|
+
const consumer = this.consumers[queue];
|
|
59
|
+
setImmediate(() => {
|
|
60
|
+
consumer.callback(messageWrapper);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return Promise.resolve();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Consume messages from queue
|
|
69
|
+
*/
|
|
70
|
+
async consume(queue, callback, options = {}) {
|
|
71
|
+
if (!this.isConnected) {
|
|
72
|
+
throw new Error('Not connected to MQ');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
this.consumers[queue] = {
|
|
76
|
+
callback,
|
|
77
|
+
options
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// Process existing messages in queue
|
|
81
|
+
if (this.queues[queue]) {
|
|
82
|
+
const messages = [...this.queues[queue]];
|
|
83
|
+
this.queues[queue] = [];
|
|
84
|
+
|
|
85
|
+
messages.forEach(msg => {
|
|
86
|
+
setImmediate(() => callback(msg));
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return Promise.resolve();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Acknowledge message
|
|
95
|
+
*/
|
|
96
|
+
async ack(message) {
|
|
97
|
+
this.acknowledgedMessages.push({
|
|
98
|
+
message,
|
|
99
|
+
timestamp: Date.now()
|
|
100
|
+
});
|
|
101
|
+
return Promise.resolve();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Reject message
|
|
106
|
+
*/
|
|
107
|
+
async nack(message, allUpTo = false, requeue = false) {
|
|
108
|
+
this.rejectedMessages.push({
|
|
109
|
+
message,
|
|
110
|
+
allUpTo,
|
|
111
|
+
requeue,
|
|
112
|
+
timestamp: Date.now()
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
if (requeue && message.fields) {
|
|
116
|
+
const queue = message.fields.routingKey;
|
|
117
|
+
if (this.queues[queue]) {
|
|
118
|
+
this.queues[queue].push(message);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return Promise.resolve();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Get messages from queue (for testing)
|
|
127
|
+
*/
|
|
128
|
+
getMessages(queue) {
|
|
129
|
+
return (this.queues[queue] || []).map(msg =>
|
|
130
|
+
JSON.parse(msg.content.toString())
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Get all published messages (for testing)
|
|
136
|
+
*/
|
|
137
|
+
getPublishedMessages(queue = null) {
|
|
138
|
+
if (queue) {
|
|
139
|
+
return this.publishedMessages.filter(m => m.queue === queue);
|
|
140
|
+
}
|
|
141
|
+
return this.publishedMessages;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Clear all data (for testing)
|
|
146
|
+
*/
|
|
147
|
+
clear() {
|
|
148
|
+
this.queues = {};
|
|
149
|
+
this.publishedMessages = [];
|
|
150
|
+
this.acknowledgedMessages = [];
|
|
151
|
+
this.rejectedMessages = [];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Get statistics (for testing)
|
|
156
|
+
*/
|
|
157
|
+
getStats() {
|
|
158
|
+
return {
|
|
159
|
+
queues: Object.keys(this.queues),
|
|
160
|
+
messageCount: Object.values(this.queues).reduce((sum, q) => sum + q.length, 0),
|
|
161
|
+
published: this.publishedMessages.length,
|
|
162
|
+
acknowledged: this.acknowledgedMessages.length,
|
|
163
|
+
rejected: this.rejectedMessages.length,
|
|
164
|
+
consumers: Object.keys(this.consumers)
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Simulate message processing delay
|
|
170
|
+
*/
|
|
171
|
+
async simulateDelay(ms) {
|
|
172
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
module.exports = MockMQClient;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MockRegistry - Simulates service registry for testing
|
|
5
|
+
*/
|
|
6
|
+
class MockRegistry {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.services = {};
|
|
9
|
+
this.certificates = {};
|
|
10
|
+
this.heartbeats = {};
|
|
11
|
+
this.validationResults = {};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Register a service
|
|
16
|
+
*/
|
|
17
|
+
async register(serviceData) {
|
|
18
|
+
const { name, version, url, healthCheck, openapi } = serviceData;
|
|
19
|
+
|
|
20
|
+
if (!name) {
|
|
21
|
+
throw new Error('Service name is required');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
this.services[name] = {
|
|
25
|
+
name,
|
|
26
|
+
version: version || '1.0.0',
|
|
27
|
+
url: url || `http://localhost:3000`,
|
|
28
|
+
healthCheck: healthCheck || '/health',
|
|
29
|
+
openapi: openapi || {},
|
|
30
|
+
registeredAt: Date.now(),
|
|
31
|
+
status: 'active'
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Issue mock certificate
|
|
35
|
+
this.certificates[name] = {
|
|
36
|
+
serviceId: name,
|
|
37
|
+
issuedAt: Date.now(),
|
|
38
|
+
expiresAt: Date.now() + 86400000, // 24 hours
|
|
39
|
+
fingerprint: `mock-cert-${name}-${Date.now()}`
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
success: true,
|
|
44
|
+
certificate: this.certificates[name]
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Unregister a service
|
|
50
|
+
*/
|
|
51
|
+
async unregister(serviceName) {
|
|
52
|
+
delete this.services[serviceName];
|
|
53
|
+
delete this.certificates[serviceName];
|
|
54
|
+
delete this.heartbeats[serviceName];
|
|
55
|
+
return { success: true };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Send heartbeat
|
|
60
|
+
*/
|
|
61
|
+
async heartbeat(serviceName) {
|
|
62
|
+
if (!this.services[serviceName]) {
|
|
63
|
+
throw new Error(`Service ${serviceName} not registered`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
this.heartbeats[serviceName] = {
|
|
67
|
+
timestamp: Date.now(),
|
|
68
|
+
status: 'healthy'
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
return { success: true };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Get service by name
|
|
76
|
+
*/
|
|
77
|
+
getService(serviceName) {
|
|
78
|
+
return this.services[serviceName] || null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Get all services
|
|
83
|
+
*/
|
|
84
|
+
getServices() {
|
|
85
|
+
return Object.values(this.services);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Get certificate for service
|
|
90
|
+
*/
|
|
91
|
+
getCertificate(serviceName) {
|
|
92
|
+
return this.certificates[serviceName] || null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Simulate validation
|
|
97
|
+
*/
|
|
98
|
+
async validate(serviceName, validationData) {
|
|
99
|
+
this.validationResults[serviceName] = {
|
|
100
|
+
...validationData,
|
|
101
|
+
timestamp: Date.now(),
|
|
102
|
+
passed: true
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
success: true,
|
|
107
|
+
token: `validation-token-${serviceName}-${Date.now()}`
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Get validation results
|
|
113
|
+
*/
|
|
114
|
+
getValidationResults(serviceName) {
|
|
115
|
+
return this.validationResults[serviceName] || null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Clear all data
|
|
120
|
+
*/
|
|
121
|
+
clear() {
|
|
122
|
+
this.services = {};
|
|
123
|
+
this.certificates = {};
|
|
124
|
+
this.heartbeats = {};
|
|
125
|
+
this.validationResults = {};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Get registry stats
|
|
130
|
+
*/
|
|
131
|
+
getStats() {
|
|
132
|
+
return {
|
|
133
|
+
serviceCount: Object.keys(this.services).length,
|
|
134
|
+
activeServices: Object.values(this.services).filter(s => s.status === 'active').length,
|
|
135
|
+
certificates: Object.keys(this.certificates).length,
|
|
136
|
+
recentHeartbeats: Object.values(this.heartbeats).filter(
|
|
137
|
+
h => Date.now() - h.timestamp < 60000
|
|
138
|
+
).length
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Simulate service discovery
|
|
144
|
+
*/
|
|
145
|
+
discover(query = {}) {
|
|
146
|
+
let results = Object.values(this.services);
|
|
147
|
+
|
|
148
|
+
if (query.name) {
|
|
149
|
+
results = results.filter(s => s.name.includes(query.name));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (query.version) {
|
|
153
|
+
results = results.filter(s => s.version === query.version);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (query.status) {
|
|
157
|
+
results = results.filter(s => s.status === query.status);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return results;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
module.exports = MockRegistry;
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MockStorage - Simulates MinIO/S3 storage for testing
|
|
5
|
+
*/
|
|
6
|
+
class MockStorage {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.buckets = {};
|
|
9
|
+
this.metadata = {};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Create bucket
|
|
14
|
+
*/
|
|
15
|
+
async createBucket(bucketName) {
|
|
16
|
+
if (!this.buckets[bucketName]) {
|
|
17
|
+
this.buckets[bucketName] = {};
|
|
18
|
+
}
|
|
19
|
+
return { success: true };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Put object
|
|
24
|
+
*/
|
|
25
|
+
async put(bucket, key, content, options = {}) {
|
|
26
|
+
if (!this.buckets[bucket]) {
|
|
27
|
+
await this.createBucket(bucket);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const data = typeof content === 'object' ? JSON.stringify(content) : content;
|
|
31
|
+
|
|
32
|
+
this.buckets[bucket][key] = {
|
|
33
|
+
content: data,
|
|
34
|
+
contentType: options.contentType || 'application/json',
|
|
35
|
+
metadata: options.metadata || {},
|
|
36
|
+
timestamp: Date.now(),
|
|
37
|
+
size: data.length
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Store metadata separately for querying
|
|
41
|
+
if (!this.metadata[bucket]) {
|
|
42
|
+
this.metadata[bucket] = {};
|
|
43
|
+
}
|
|
44
|
+
this.metadata[bucket][key] = {
|
|
45
|
+
size: data.length,
|
|
46
|
+
lastModified: Date.now(),
|
|
47
|
+
contentType: options.contentType || 'application/json'
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
success: true,
|
|
52
|
+
etag: `mock-etag-${Date.now()}`,
|
|
53
|
+
location: `${bucket}/${key}`
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Get object
|
|
59
|
+
*/
|
|
60
|
+
async get(bucket, key) {
|
|
61
|
+
if (!this.buckets[bucket] || !this.buckets[bucket][key]) {
|
|
62
|
+
throw new Error(`Object not found: ${bucket}/${key}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const obj = this.buckets[bucket][key];
|
|
66
|
+
let content = obj.content;
|
|
67
|
+
|
|
68
|
+
// Try to parse JSON if content type suggests it
|
|
69
|
+
if (obj.contentType === 'application/json' && typeof content === 'string') {
|
|
70
|
+
try {
|
|
71
|
+
content = JSON.parse(content);
|
|
72
|
+
} catch (e) {
|
|
73
|
+
// Keep as string if parsing fails
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
content,
|
|
79
|
+
metadata: obj.metadata,
|
|
80
|
+
contentType: obj.contentType,
|
|
81
|
+
size: obj.size,
|
|
82
|
+
lastModified: obj.timestamp
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Delete object
|
|
88
|
+
*/
|
|
89
|
+
async delete(bucket, key) {
|
|
90
|
+
if (this.buckets[bucket]) {
|
|
91
|
+
delete this.buckets[bucket][key];
|
|
92
|
+
if (this.metadata[bucket]) {
|
|
93
|
+
delete this.metadata[bucket][key];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return { success: true };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* List objects in bucket
|
|
101
|
+
*/
|
|
102
|
+
async list(bucket, prefix = '', limit = 1000) {
|
|
103
|
+
if (!this.buckets[bucket]) {
|
|
104
|
+
return { objects: [] };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const objects = Object.keys(this.buckets[bucket])
|
|
108
|
+
.filter(key => key.startsWith(prefix))
|
|
109
|
+
.slice(0, limit)
|
|
110
|
+
.map(key => ({
|
|
111
|
+
key,
|
|
112
|
+
size: this.buckets[bucket][key].size,
|
|
113
|
+
lastModified: this.buckets[bucket][key].timestamp
|
|
114
|
+
}));
|
|
115
|
+
|
|
116
|
+
return { objects };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Check if object exists
|
|
121
|
+
*/
|
|
122
|
+
async exists(bucket, key) {
|
|
123
|
+
return !!(this.buckets[bucket] && this.buckets[bucket][key]);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Get object metadata
|
|
128
|
+
*/
|
|
129
|
+
async getMetadata(bucket, key) {
|
|
130
|
+
if (!this.metadata[bucket] || !this.metadata[bucket][key]) {
|
|
131
|
+
throw new Error(`Metadata not found: ${bucket}/${key}`);
|
|
132
|
+
}
|
|
133
|
+
return this.metadata[bucket][key];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Generate presigned URL (mock)
|
|
138
|
+
*/
|
|
139
|
+
async getPresignedUrl(bucket, key, expiresIn = 3600) {
|
|
140
|
+
return {
|
|
141
|
+
url: `http://mock-storage/${bucket}/${key}?token=mock-token-${Date.now()}`,
|
|
142
|
+
expiresAt: Date.now() + (expiresIn * 1000)
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Clear all data
|
|
148
|
+
*/
|
|
149
|
+
clear() {
|
|
150
|
+
this.buckets = {};
|
|
151
|
+
this.metadata = {};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Get storage stats
|
|
156
|
+
*/
|
|
157
|
+
getStats() {
|
|
158
|
+
const stats = {
|
|
159
|
+
bucketCount: Object.keys(this.buckets).length,
|
|
160
|
+
totalObjects: 0,
|
|
161
|
+
totalSize: 0
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
Object.values(this.buckets).forEach(bucket => {
|
|
165
|
+
Object.values(bucket).forEach(obj => {
|
|
166
|
+
stats.totalObjects++;
|
|
167
|
+
stats.totalSize += obj.size;
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
return stats;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Copy object
|
|
176
|
+
*/
|
|
177
|
+
async copy(sourceBucket, sourceKey, destBucket, destKey) {
|
|
178
|
+
const source = await this.get(sourceBucket, sourceKey);
|
|
179
|
+
return await this.put(destBucket, destKey, source.content, {
|
|
180
|
+
contentType: source.contentType,
|
|
181
|
+
metadata: source.metadata
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
module.exports = MockStorage;
|