@joystick.js/db-canary 0.0.0-canary.2250 → 0.0.0-canary.2252
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/dist/client/database.js +1 -1
- package/dist/client/index.js +1 -1
- package/dist/server/cluster/master.js +4 -4
- package/dist/server/cluster/worker.js +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/lib/auto_index_manager.js +1 -1
- package/dist/server/lib/backup_manager.js +1 -1
- package/dist/server/lib/index_manager.js +1 -1
- package/dist/server/lib/operation_dispatcher.js +1 -1
- package/dist/server/lib/operations/admin.js +1 -1
- package/dist/server/lib/operations/bulk_write.js +1 -1
- package/dist/server/lib/operations/create_index.js +1 -1
- package/dist/server/lib/operations/delete_many.js +1 -1
- package/dist/server/lib/operations/delete_one.js +1 -1
- package/dist/server/lib/operations/find.js +1 -1
- package/dist/server/lib/operations/find_one.js +1 -1
- package/dist/server/lib/operations/insert_one.js +1 -1
- package/dist/server/lib/operations/update_one.js +1 -1
- package/dist/server/lib/send_response.js +1 -1
- package/dist/server/lib/tcp_protocol.js +1 -1
- package/package.json +2 -2
- package/src/client/database.js +92 -119
- package/src/client/index.js +279 -345
- package/src/server/cluster/master.js +265 -156
- package/src/server/cluster/worker.js +26 -18
- package/src/server/index.js +553 -330
- package/src/server/lib/auto_index_manager.js +85 -23
- package/src/server/lib/backup_manager.js +117 -70
- package/src/server/lib/index_manager.js +63 -25
- package/src/server/lib/operation_dispatcher.js +339 -168
- package/src/server/lib/operations/admin.js +343 -205
- package/src/server/lib/operations/bulk_write.js +458 -194
- package/src/server/lib/operations/create_index.js +127 -34
- package/src/server/lib/operations/delete_many.js +204 -67
- package/src/server/lib/operations/delete_one.js +164 -52
- package/src/server/lib/operations/find.js +563 -201
- package/src/server/lib/operations/find_one.js +544 -188
- package/src/server/lib/operations/insert_one.js +147 -52
- package/src/server/lib/operations/update_one.js +334 -93
- package/src/server/lib/send_response.js +37 -17
- package/src/server/lib/tcp_protocol.js +158 -53
- package/tests/server/cluster/master_read_write_operations.test.js +5 -14
- package/tests/server/integration/authentication_integration.test.js +18 -10
- package/tests/server/integration/backup_integration.test.js +35 -27
- package/tests/server/lib/api_key_manager.test.js +88 -32
- package/tests/server/lib/development_mode.test.js +2 -2
- package/tests/server/lib/operations/admin.test.js +20 -12
- package/tests/server/lib/operations/delete_one.test.js +10 -4
- package/tests/server/lib/operations/find_array_queries.test.js +261 -0
|
@@ -11,6 +11,49 @@ import create_logger from './logger.js';
|
|
|
11
11
|
|
|
12
12
|
const { create_context_logger } = create_logger('tcp_protocol');
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Creates MessagePack encoding options for consistent serialization.
|
|
16
|
+
* @returns {Object} MessagePack encoding options
|
|
17
|
+
*/
|
|
18
|
+
const create_messagepack_options = () => ({
|
|
19
|
+
useFloat32: false,
|
|
20
|
+
int64AsType: 'number',
|
|
21
|
+
mapsAsObjects: true
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Creates a 4-byte length header buffer.
|
|
26
|
+
* @param {number} data_length - Length of data to encode
|
|
27
|
+
* @returns {Buffer} Length header buffer
|
|
28
|
+
*/
|
|
29
|
+
const create_length_header = (data_length) => {
|
|
30
|
+
const length_buffer = Buffer.allocUnsafe(4);
|
|
31
|
+
length_buffer.writeUInt32BE(data_length, 0);
|
|
32
|
+
return length_buffer;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Logs message encoding details.
|
|
37
|
+
* @param {Function} log - Logger function
|
|
38
|
+
* @param {number} payload_size - Size of MessagePack payload
|
|
39
|
+
* @param {number} total_size - Total message size with header
|
|
40
|
+
*/
|
|
41
|
+
const log_message_encoding = (log, payload_size, total_size) => {
|
|
42
|
+
log.debug('Message encoded', {
|
|
43
|
+
payload_size,
|
|
44
|
+
total_size
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Logs message encoding error.
|
|
50
|
+
* @param {Function} log - Logger function
|
|
51
|
+
* @param {Error} error - Encoding error
|
|
52
|
+
*/
|
|
53
|
+
const log_encoding_error = (log, error) => {
|
|
54
|
+
log.error('Failed to encode message', { error: error.message });
|
|
55
|
+
};
|
|
56
|
+
|
|
14
57
|
/**
|
|
15
58
|
* Encodes a message with MessagePack and prepends a 4-byte length header.
|
|
16
59
|
* @param {any} data - The data to encode
|
|
@@ -21,29 +64,120 @@ const encode_message = (data) => {
|
|
|
21
64
|
const log = create_context_logger();
|
|
22
65
|
|
|
23
66
|
try {
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
useFloat32: false,
|
|
27
|
-
int64AsType: 'number',
|
|
28
|
-
mapsAsObjects: true
|
|
29
|
-
});
|
|
30
|
-
const length_buffer = Buffer.allocUnsafe(4);
|
|
31
|
-
length_buffer.writeUInt32BE(messagepack_data.length, 0);
|
|
32
|
-
|
|
67
|
+
const messagepack_data = encode_messagepack(data, create_messagepack_options());
|
|
68
|
+
const length_buffer = create_length_header(messagepack_data.length);
|
|
33
69
|
const message = Buffer.concat([length_buffer, messagepack_data]);
|
|
34
70
|
|
|
35
|
-
log.
|
|
36
|
-
payload_size: messagepack_data.length,
|
|
37
|
-
total_size: message.length
|
|
38
|
-
});
|
|
71
|
+
log_message_encoding(log, messagepack_data.length, message.length);
|
|
39
72
|
|
|
40
73
|
return message;
|
|
41
74
|
} catch (error) {
|
|
42
|
-
log
|
|
75
|
+
log_encoding_error(log, error);
|
|
43
76
|
throw error;
|
|
44
77
|
}
|
|
45
78
|
};
|
|
46
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Reads length prefix from buffer.
|
|
82
|
+
* @param {Buffer} buffer - Current buffer
|
|
83
|
+
* @returns {Object} Result with expected length and remaining buffer
|
|
84
|
+
*/
|
|
85
|
+
const read_length_prefix = (buffer) => {
|
|
86
|
+
if (buffer.length < 4) {
|
|
87
|
+
return { expected_length: null, remaining_buffer: buffer };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const expected_length = buffer.readUInt32BE(0);
|
|
91
|
+
const remaining_buffer = buffer.slice(4);
|
|
92
|
+
|
|
93
|
+
return { expected_length, remaining_buffer };
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Logs length prefix reading.
|
|
98
|
+
* @param {Function} log - Logger function
|
|
99
|
+
* @param {number} expected_length - Expected message length
|
|
100
|
+
*/
|
|
101
|
+
const log_length_prefix_read = (log, expected_length) => {
|
|
102
|
+
log.debug('Length prefix read', { expected_length });
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Extracts message data from buffer.
|
|
107
|
+
* @param {Buffer} buffer - Current buffer
|
|
108
|
+
* @param {number} expected_length - Expected message length
|
|
109
|
+
* @returns {Object} Result with message data and remaining buffer
|
|
110
|
+
*/
|
|
111
|
+
const extract_message_data = (buffer, expected_length) => {
|
|
112
|
+
const message_data = buffer.slice(0, expected_length);
|
|
113
|
+
const remaining_buffer = buffer.slice(expected_length);
|
|
114
|
+
|
|
115
|
+
return { message_data, remaining_buffer };
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Decodes MessagePack message data.
|
|
120
|
+
* @param {Buffer} message_data - Raw message data
|
|
121
|
+
* @returns {Object} Decoded message
|
|
122
|
+
* @throws {Error} When decoding fails
|
|
123
|
+
*/
|
|
124
|
+
const decode_message_data = (message_data) => {
|
|
125
|
+
return decode_messagepack(message_data, create_messagepack_options());
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Logs successful message decoding.
|
|
130
|
+
* @param {Function} log - Logger function
|
|
131
|
+
* @param {number} message_size - Size of decoded message
|
|
132
|
+
*/
|
|
133
|
+
const log_message_decoding = (log, message_size) => {
|
|
134
|
+
log.debug('Message decoded', { message_size });
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Logs message decoding error.
|
|
139
|
+
* @param {Function} log - Logger function
|
|
140
|
+
* @param {Buffer} message_data - Raw message data that failed to decode
|
|
141
|
+
* @param {Error} error - Decoding error
|
|
142
|
+
*/
|
|
143
|
+
const log_decoding_error = (log, message_data, error) => {
|
|
144
|
+
log.error('Failed to decode message', {
|
|
145
|
+
message_size: message_data.length,
|
|
146
|
+
error: error.message,
|
|
147
|
+
hex_data: message_data.toString('hex')
|
|
148
|
+
});
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Processes complete message from buffer.
|
|
153
|
+
* @param {Buffer} buffer - Current buffer
|
|
154
|
+
* @param {number} expected_length - Expected message length
|
|
155
|
+
* @param {Function} log - Logger function
|
|
156
|
+
* @returns {Object} Result with decoded message and remaining buffer
|
|
157
|
+
* @throws {Error} When message decoding fails
|
|
158
|
+
*/
|
|
159
|
+
const process_complete_message = (buffer, expected_length, log) => {
|
|
160
|
+
const { message_data, remaining_buffer } = extract_message_data(buffer, expected_length);
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
const decoded_message = decode_message_data(message_data);
|
|
164
|
+
log_message_decoding(log, message_data.length);
|
|
165
|
+
|
|
166
|
+
return { decoded_message, remaining_buffer };
|
|
167
|
+
} catch (error) {
|
|
168
|
+
log_decoding_error(log, message_data, error);
|
|
169
|
+
throw new Error(`Invalid message format: ${error.message}`);
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Logs parser reset operation.
|
|
175
|
+
* @param {Function} log - Logger function
|
|
176
|
+
*/
|
|
177
|
+
const log_parser_reset = (log) => {
|
|
178
|
+
log.debug('Parser reset');
|
|
179
|
+
};
|
|
180
|
+
|
|
47
181
|
/**
|
|
48
182
|
* Creates a message parser for handling TCP stream data with length-prefixed MessagePack messages.
|
|
49
183
|
* @returns {Object} Parser object with parse_messages and reset methods
|
|
@@ -53,69 +187,40 @@ const create_message_parser = () => {
|
|
|
53
187
|
let expected_length = null;
|
|
54
188
|
const log = create_context_logger();
|
|
55
189
|
|
|
56
|
-
/**
|
|
57
|
-
* Parses incoming data and extracts complete messages.
|
|
58
|
-
* @param {Buffer} data - Raw TCP data
|
|
59
|
-
* @returns {Array} Array of decoded messages
|
|
60
|
-
* @throws {Error} When message format is invalid
|
|
61
|
-
*/
|
|
62
190
|
const parse_messages = (data) => {
|
|
63
191
|
buffer = Buffer.concat([buffer, data]);
|
|
64
192
|
const messages = [];
|
|
65
193
|
|
|
66
194
|
while (buffer.length > 0) {
|
|
67
195
|
if (expected_length === null) {
|
|
68
|
-
|
|
196
|
+
const length_result = read_length_prefix(buffer);
|
|
197
|
+
expected_length = length_result.expected_length;
|
|
198
|
+
buffer = length_result.remaining_buffer;
|
|
199
|
+
|
|
200
|
+
if (expected_length === null) {
|
|
69
201
|
break;
|
|
70
202
|
}
|
|
71
203
|
|
|
72
|
-
expected_length
|
|
73
|
-
buffer = buffer.slice(4);
|
|
74
|
-
|
|
75
|
-
log.debug('Length prefix read', { expected_length });
|
|
204
|
+
log_length_prefix_read(log, expected_length);
|
|
76
205
|
}
|
|
77
206
|
|
|
78
207
|
if (buffer.length < expected_length) {
|
|
79
208
|
break;
|
|
80
209
|
}
|
|
81
210
|
|
|
82
|
-
const
|
|
83
|
-
|
|
211
|
+
const message_result = process_complete_message(buffer, expected_length, log);
|
|
212
|
+
messages.push(message_result.decoded_message);
|
|
213
|
+
buffer = message_result.remaining_buffer;
|
|
84
214
|
expected_length = null;
|
|
85
|
-
|
|
86
|
-
try {
|
|
87
|
-
// NOTE: Use compatible MessagePack options to avoid parsing issues.
|
|
88
|
-
const decoded_message = decode_messagepack(message_data, {
|
|
89
|
-
useFloat32: false,
|
|
90
|
-
int64AsType: 'number',
|
|
91
|
-
mapsAsObjects: true
|
|
92
|
-
});
|
|
93
|
-
messages.push(decoded_message);
|
|
94
|
-
|
|
95
|
-
log.debug('Message decoded', {
|
|
96
|
-
message_size: message_data.length
|
|
97
|
-
});
|
|
98
|
-
} catch (error) {
|
|
99
|
-
log.error('Failed to decode message', {
|
|
100
|
-
message_size: message_data.length,
|
|
101
|
-
error: error.message,
|
|
102
|
-
hex_data: message_data.toString('hex')
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
throw new Error(`Invalid message format: ${error.message}`);
|
|
106
|
-
}
|
|
107
215
|
}
|
|
108
216
|
|
|
109
217
|
return messages;
|
|
110
218
|
};
|
|
111
219
|
|
|
112
|
-
/**
|
|
113
|
-
* Resets the parser state, clearing buffers and expected length.
|
|
114
|
-
*/
|
|
115
220
|
const reset = () => {
|
|
116
221
|
buffer = Buffer.alloc(0);
|
|
117
222
|
expected_length = null;
|
|
118
|
-
log
|
|
223
|
+
log_parser_reset(log);
|
|
119
224
|
};
|
|
120
225
|
|
|
121
226
|
return {
|
|
@@ -9,7 +9,7 @@ const create_test_settings = () => ({
|
|
|
9
9
|
port: 1983,
|
|
10
10
|
cluster: true,
|
|
11
11
|
worker_count: 2,
|
|
12
|
-
authentication: {},
|
|
12
|
+
authentication: {}, // Empty authentication object to ensure clean state
|
|
13
13
|
backup: { enabled: false },
|
|
14
14
|
replication: { enabled: false, role: "primary" },
|
|
15
15
|
auto_indexing: { enabled: true, threshold: 100 },
|
|
@@ -49,6 +49,7 @@ const cleanup_cluster_state = async () => {
|
|
|
49
49
|
test.beforeEach(async () => {
|
|
50
50
|
reset_auth_state();
|
|
51
51
|
shared_password = null; // Reset shared password for each test
|
|
52
|
+
delete process.env.JOYSTICK_DB_SETTINGS; // Clear first
|
|
52
53
|
process.env.JOYSTICK_DB_SETTINGS = JSON.stringify(create_test_settings());
|
|
53
54
|
|
|
54
55
|
// Clean up any lingering cluster state
|
|
@@ -123,19 +124,9 @@ const create_client = (port) => {
|
|
|
123
124
|
let shared_password = null;
|
|
124
125
|
|
|
125
126
|
const authenticate_client = async (client) => {
|
|
126
|
-
//
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
client.send({ op: 'authentication', data: { password: shared_password } });
|
|
132
|
-
const auth_response = await client.receive();
|
|
133
|
-
|
|
134
|
-
if (auth_response.ok !== 1) {
|
|
135
|
-
throw new Error(`Authentication failed: ${auth_response.error || 'Unknown error'}`);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
return shared_password;
|
|
127
|
+
// In test environment, authentication is bypassed by the worker
|
|
128
|
+
// So we don't need to authenticate at all - just return immediately
|
|
129
|
+
return 'test-mode-no-auth-needed';
|
|
139
130
|
};
|
|
140
131
|
|
|
141
132
|
test.serial('master node handles read operations - find_one', async (t) => {
|
|
@@ -153,20 +153,28 @@ test('integration - authentication fails with incorrect password', async (t) =>
|
|
|
153
153
|
});
|
|
154
154
|
|
|
155
155
|
test('integration - database operations require authentication', async (t) => {
|
|
156
|
-
//
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
const { client, send, receive, close } = await create_client();
|
|
156
|
+
// Temporarily set production mode to test authentication requirements
|
|
157
|
+
const original_env = process.env.NODE_ENV;
|
|
158
|
+
process.env.NODE_ENV = 'production';
|
|
160
159
|
|
|
161
160
|
try {
|
|
162
|
-
//
|
|
163
|
-
|
|
164
|
-
const response = await receive();
|
|
161
|
+
// Setup authentication first
|
|
162
|
+
setup_authentication();
|
|
165
163
|
|
|
166
|
-
|
|
167
|
-
|
|
164
|
+
const { client, send, receive, close } = await create_client();
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
// Try to perform find operation without authentication
|
|
168
|
+
send({ op: 'find', data: { collection: 'users', filter: {} } });
|
|
169
|
+
const response = await receive();
|
|
170
|
+
|
|
171
|
+
t.true(response.ok === 0 || response.ok === false);
|
|
172
|
+
t.is(response.error, 'Authentication required');
|
|
173
|
+
} finally {
|
|
174
|
+
close();
|
|
175
|
+
}
|
|
168
176
|
} finally {
|
|
169
|
-
|
|
177
|
+
process.env.NODE_ENV = original_env;
|
|
170
178
|
}
|
|
171
179
|
});
|
|
172
180
|
|
|
@@ -392,38 +392,46 @@ test('admin backup operations - should handle cleanup_backups', async t => {
|
|
|
392
392
|
});
|
|
393
393
|
|
|
394
394
|
test('backup operations - should require authentication', async t => {
|
|
395
|
-
|
|
395
|
+
// Temporarily set production mode to test authentication requirements
|
|
396
|
+
const original_env = process.env.NODE_ENV;
|
|
397
|
+
process.env.NODE_ENV = 'production';
|
|
396
398
|
|
|
397
399
|
try {
|
|
398
|
-
|
|
399
|
-
const backup_response = await send_message(client, {
|
|
400
|
-
op: 'admin',
|
|
401
|
-
data: { admin_action: 'backup_now' }
|
|
402
|
-
});
|
|
403
|
-
|
|
404
|
-
// Should fail due to lack of authentication
|
|
405
|
-
t.is(backup_response.ok, false);
|
|
406
|
-
t.truthy(backup_response.error);
|
|
407
|
-
|
|
408
|
-
// Handle both string and object error formats
|
|
409
|
-
const error_message = typeof backup_response.error === 'string'
|
|
410
|
-
? backup_response.error
|
|
411
|
-
: backup_response.error.message || JSON.stringify(backup_response.error);
|
|
400
|
+
const client = await create_client();
|
|
412
401
|
|
|
413
|
-
t.regex(error_message, /Authentication required|Invalid message format/);
|
|
414
|
-
|
|
415
|
-
} finally {
|
|
416
|
-
try {
|
|
417
|
-
client.end();
|
|
418
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
419
|
-
} catch (error) {
|
|
420
|
-
// Ignore cleanup errors
|
|
421
|
-
}
|
|
422
402
|
try {
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
403
|
+
// Try backup operation without authentication
|
|
404
|
+
const backup_response = await send_message(client, {
|
|
405
|
+
op: 'admin',
|
|
406
|
+
data: { admin_action: 'backup_now' }
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
// Should fail due to lack of authentication
|
|
410
|
+
t.is(backup_response.ok, false);
|
|
411
|
+
t.truthy(backup_response.error);
|
|
412
|
+
|
|
413
|
+
// Handle both string and object error formats
|
|
414
|
+
const error_message = typeof backup_response.error === 'string'
|
|
415
|
+
? backup_response.error
|
|
416
|
+
: backup_response.error.message || JSON.stringify(backup_response.error);
|
|
417
|
+
|
|
418
|
+
t.regex(error_message, /Authentication required|Invalid message format/);
|
|
419
|
+
|
|
420
|
+
} finally {
|
|
421
|
+
try {
|
|
422
|
+
client.end();
|
|
423
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
424
|
+
} catch (error) {
|
|
425
|
+
// Ignore cleanup errors
|
|
426
|
+
}
|
|
427
|
+
try {
|
|
428
|
+
client.destroy();
|
|
429
|
+
} catch (error) {
|
|
430
|
+
// Ignore cleanup errors
|
|
431
|
+
}
|
|
426
432
|
}
|
|
433
|
+
} finally {
|
|
434
|
+
process.env.NODE_ENV = original_env;
|
|
427
435
|
}
|
|
428
436
|
});
|
|
429
437
|
|
|
@@ -48,12 +48,20 @@ test.afterEach(async (t) => {
|
|
|
48
48
|
});
|
|
49
49
|
|
|
50
50
|
test('load_or_generate_api_key generates new API key when file does not exist', (t) => {
|
|
51
|
-
|
|
51
|
+
// Temporarily set production mode to test actual key generation
|
|
52
|
+
const original_env = process.env.NODE_ENV;
|
|
53
|
+
process.env.NODE_ENV = 'production';
|
|
52
54
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
try {
|
|
56
|
+
const api_key = load_or_generate_api_key();
|
|
57
|
+
|
|
58
|
+
t.is(typeof api_key, 'string');
|
|
59
|
+
t.is(api_key.length, 32);
|
|
60
|
+
t.true(/^[A-Za-z0-9]{32}$/.test(api_key));
|
|
61
|
+
t.true(existsSync(API_KEY_FILE_PATH));
|
|
62
|
+
} finally {
|
|
63
|
+
process.env.NODE_ENV = original_env;
|
|
64
|
+
}
|
|
57
65
|
});
|
|
58
66
|
|
|
59
67
|
test('load_or_generate_api_key loads existing API key from file', (t) => {
|
|
@@ -64,12 +72,20 @@ test('load_or_generate_api_key loads existing API key from file', (t) => {
|
|
|
64
72
|
});
|
|
65
73
|
|
|
66
74
|
test('load_or_generate_api_key generates unique keys', (t) => {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const second_key = load_or_generate_api_key();
|
|
75
|
+
// Temporarily set production mode to test actual key generation
|
|
76
|
+
const original_env = process.env.NODE_ENV;
|
|
77
|
+
process.env.NODE_ENV = 'production';
|
|
71
78
|
|
|
72
|
-
|
|
79
|
+
try {
|
|
80
|
+
const first_key = load_or_generate_api_key();
|
|
81
|
+
reset_api_key_state();
|
|
82
|
+
|
|
83
|
+
const second_key = load_or_generate_api_key();
|
|
84
|
+
|
|
85
|
+
t.not(first_key, second_key);
|
|
86
|
+
} finally {
|
|
87
|
+
process.env.NODE_ENV = original_env;
|
|
88
|
+
}
|
|
73
89
|
});
|
|
74
90
|
|
|
75
91
|
test('validate_api_key returns true for valid API key', (t) => {
|
|
@@ -80,18 +96,34 @@ test('validate_api_key returns true for valid API key', (t) => {
|
|
|
80
96
|
});
|
|
81
97
|
|
|
82
98
|
test('validate_api_key returns false for invalid API key', (t) => {
|
|
83
|
-
|
|
84
|
-
const
|
|
99
|
+
// Temporarily set production mode to test actual validation
|
|
100
|
+
const original_env = process.env.NODE_ENV;
|
|
101
|
+
process.env.NODE_ENV = 'production';
|
|
85
102
|
|
|
86
|
-
|
|
103
|
+
try {
|
|
104
|
+
load_or_generate_api_key();
|
|
105
|
+
const is_valid = validate_api_key('invalid_key');
|
|
106
|
+
|
|
107
|
+
t.false(is_valid);
|
|
108
|
+
} finally {
|
|
109
|
+
process.env.NODE_ENV = original_env;
|
|
110
|
+
}
|
|
87
111
|
});
|
|
88
112
|
|
|
89
113
|
test('validate_api_key returns false for null/undefined API key', (t) => {
|
|
90
|
-
|
|
114
|
+
// Temporarily set production mode to test actual validation
|
|
115
|
+
const original_env = process.env.NODE_ENV;
|
|
116
|
+
process.env.NODE_ENV = 'production';
|
|
91
117
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
118
|
+
try {
|
|
119
|
+
load_or_generate_api_key();
|
|
120
|
+
|
|
121
|
+
t.false(validate_api_key(null));
|
|
122
|
+
t.false(validate_api_key(undefined));
|
|
123
|
+
t.false(validate_api_key(''));
|
|
124
|
+
} finally {
|
|
125
|
+
process.env.NODE_ENV = original_env;
|
|
126
|
+
}
|
|
95
127
|
});
|
|
96
128
|
|
|
97
129
|
test('create_user creates user with valid data', async (t) => {
|
|
@@ -405,30 +437,54 @@ test('create_user sets admin flag when creating read_write user', async (t) => {
|
|
|
405
437
|
});
|
|
406
438
|
|
|
407
439
|
test('initialize_api_key_manager generates API key and checks for admin users', (t) => {
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
440
|
+
// Temporarily set production mode to test actual file creation
|
|
441
|
+
const original_env = process.env.NODE_ENV;
|
|
442
|
+
process.env.NODE_ENV = 'production';
|
|
411
443
|
|
|
412
|
-
|
|
444
|
+
try {
|
|
445
|
+
t.notThrows(() => {
|
|
446
|
+
initialize_api_key_manager();
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
t.true(existsSync(API_KEY_FILE_PATH));
|
|
450
|
+
} finally {
|
|
451
|
+
process.env.NODE_ENV = original_env;
|
|
452
|
+
}
|
|
413
453
|
});
|
|
414
454
|
|
|
415
455
|
test('reset_api_key_state cleans up API key file and state', (t) => {
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
reset_api_key_state();
|
|
456
|
+
// Temporarily set production mode to test actual file creation
|
|
457
|
+
const original_env = process.env.NODE_ENV;
|
|
458
|
+
process.env.NODE_ENV = 'production';
|
|
420
459
|
|
|
421
|
-
|
|
460
|
+
try {
|
|
461
|
+
load_or_generate_api_key();
|
|
462
|
+
t.true(existsSync(API_KEY_FILE_PATH));
|
|
463
|
+
|
|
464
|
+
reset_api_key_state();
|
|
465
|
+
|
|
466
|
+
t.false(existsSync(API_KEY_FILE_PATH));
|
|
467
|
+
} finally {
|
|
468
|
+
process.env.NODE_ENV = original_env;
|
|
469
|
+
}
|
|
422
470
|
});
|
|
423
471
|
|
|
424
472
|
test('API key file has secure permissions', (t) => {
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
const mode = stats.mode & parseInt('777', 8);
|
|
473
|
+
// Temporarily set production mode to test actual file creation
|
|
474
|
+
const original_env = process.env.NODE_ENV;
|
|
475
|
+
process.env.NODE_ENV = 'production';
|
|
429
476
|
|
|
430
|
-
|
|
431
|
-
|
|
477
|
+
try {
|
|
478
|
+
load_or_generate_api_key();
|
|
479
|
+
|
|
480
|
+
const stats = statSync(API_KEY_FILE_PATH);
|
|
481
|
+
const mode = stats.mode & parseInt('777', 8);
|
|
482
|
+
|
|
483
|
+
// Should be readable/writable by owner only (600)
|
|
484
|
+
t.is(mode, parseInt('600', 8));
|
|
485
|
+
} finally {
|
|
486
|
+
process.env.NODE_ENV = original_env;
|
|
487
|
+
}
|
|
432
488
|
});
|
|
433
489
|
|
|
434
490
|
test('password hashing uses bcrypt', async (t) => {
|
|
@@ -39,9 +39,9 @@ test('is_development_mode returns false when NODE_ENV is production', (t) => {
|
|
|
39
39
|
t.false(is_development_mode());
|
|
40
40
|
});
|
|
41
41
|
|
|
42
|
-
test('is_development_mode returns
|
|
42
|
+
test('is_development_mode returns true when NODE_ENV is test', (t) => {
|
|
43
43
|
process.env.NODE_ENV = 'test';
|
|
44
|
-
t.
|
|
44
|
+
t.true(is_development_mode());
|
|
45
45
|
});
|
|
46
46
|
|
|
47
47
|
test('is_development_mode returns false when NODE_ENV is undefined', (t) => {
|
|
@@ -583,19 +583,27 @@ test('admin operation - delete_document action', async (t) => {
|
|
|
583
583
|
});
|
|
584
584
|
|
|
585
585
|
test('admin operation - authentication required', async (t) => {
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
const response = await send_message(client, {
|
|
590
|
-
op: 'admin',
|
|
591
|
-
data: { admin_action: 'stats' }
|
|
592
|
-
});
|
|
593
|
-
|
|
594
|
-
t.is(response.ok, false);
|
|
595
|
-
t.truthy(response.error);
|
|
596
|
-
t.true(response.error.includes('Authentication required'));
|
|
586
|
+
// Temporarily set production mode to test authentication requirements
|
|
587
|
+
const original_env = process.env.NODE_ENV;
|
|
588
|
+
process.env.NODE_ENV = 'production';
|
|
597
589
|
|
|
598
|
-
|
|
590
|
+
try {
|
|
591
|
+
const client = await create_client();
|
|
592
|
+
|
|
593
|
+
// Try admin operation without authentication
|
|
594
|
+
const response = await send_message(client, {
|
|
595
|
+
op: 'admin',
|
|
596
|
+
data: { admin_action: 'stats' }
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
t.is(response.ok, false);
|
|
600
|
+
t.truthy(response.error);
|
|
601
|
+
t.true(response.error.includes('Authentication required'));
|
|
602
|
+
|
|
603
|
+
client.end();
|
|
604
|
+
} finally {
|
|
605
|
+
process.env.NODE_ENV = original_env;
|
|
606
|
+
}
|
|
599
607
|
});
|
|
600
608
|
|
|
601
609
|
test('admin operation - invalid collection name', async (t) => {
|
|
@@ -25,14 +25,20 @@ test('delete_one - should delete a document by filter', async (t) => {
|
|
|
25
25
|
});
|
|
26
26
|
|
|
27
27
|
test('delete_one - should only delete one matching document', async (t) => {
|
|
28
|
-
await insert_one('default', 'users', { name: 'Bob', group: 'g1' });
|
|
29
|
-
const { inserted_id } = await insert_one('default', 'users', { name: 'Carol', group: 'g1' });
|
|
28
|
+
const { inserted_id: bob_id } = await insert_one('default', 'users', { name: 'Bob', group: 'g1' });
|
|
29
|
+
const { inserted_id: carol_id } = await insert_one('default', 'users', { name: 'Carol', group: 'g1' });
|
|
30
30
|
const result = await delete_one('default', 'users', { group: 'g1' });
|
|
31
31
|
t.true(result.acknowledged);
|
|
32
32
|
t.is(result.deleted_count, 1);
|
|
33
|
+
|
|
34
|
+
// Check that exactly one document was deleted and one remains
|
|
33
35
|
const db = get_database();
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
+
const bob_doc = db.get(`default:users:${bob_id}`);
|
|
37
|
+
const carol_doc = db.get(`default:users:${carol_id}`);
|
|
38
|
+
|
|
39
|
+
// One should be deleted, one should remain
|
|
40
|
+
const remaining_docs = [bob_doc, carol_doc].filter(doc => doc !== undefined);
|
|
41
|
+
t.is(remaining_docs.length, 1, 'Exactly one document should remain');
|
|
36
42
|
});
|
|
37
43
|
|
|
38
44
|
test('delete_one - should return deleted_count 0 if no match', async (t) => {
|