@joystick.js/db-canary
Version:
JoystickDB - A minimalist database server for the Joystick framework
819 lines (666 loc) β’ 24.6 kB
JavaScript
import test from 'ava';
import sinon from 'sinon';
import net from 'net';
import { encode as encode_messagepack, decode as decode_messagepack } from 'msgpackr';
import { parse_data, check_op_type, create_server } from '../../src/server/index.js';
import op_types from '../../src/server/lib/op_types.js';
import { load_settings, get_settings } from '../../src/server/lib/load_settings.js';
import { reset_auth_state, set_storage_engine, initialize_user_auth_manager } from '../../src/server/lib/user_auth_manager.js';
import { initialize_database, cleanup_database } from '../../src/server/lib/query_engine.js';
let test_db = null;
let test_counter = 0;
// Helper function to decode server response data with length prefix
const decode_response = (response_data) => {
try {
// Server uses length-prefixed MessagePack format
// First 4 bytes contain the length, followed by MessagePack data
if (response_data.length < 4) {
return response_data.toString();
}
const length = response_data.readUInt32BE(0);
const messagepack_data = response_data.slice(4, 4 + length);
const decoded = decode_messagepack(messagepack_data);
if (typeof decoded === 'string') {
// If it's a JSON string, parse it
return JSON.parse(decoded);
}
return decoded;
} catch (error) {
// Fallback to treating as raw string
return response_data.toString();
}
};
test.beforeEach(async () => {
// Reset user auth state and clean up environment variables
await reset_auth_state();
delete process.env.JOYSTICK_DB_SETTINGS;
// Clean up any existing test database
if (test_db) {
try {
await cleanup_database();
} catch (error) {
// Ignore cleanup errors
}
}
// Initialize storage engine with unique path for each test
test_counter++;
const unique_db_path = `./test/test_auth_data_${test_counter}_${Date.now()}.mdb`;
test_db = initialize_database(unique_db_path);
initialize_user_auth_manager();
set_storage_engine(test_db);
});
test.afterEach(async () => {
// Clean up environment variables and database after each test
await reset_auth_state();
delete process.env.JOYSTICK_DB_SETTINGS;
// Clean up test database
if (test_db) {
try {
await cleanup_database();
} catch (error) {
// Ignore cleanup errors
}
test_db = null;
}
});
test('setup operation creates authentication and returns password', async (t) => {
const { setup } = await import('../../src/server/index.js');
const { get_auth_stats } = await import('../../src/server/lib/user_auth_manager.js');
// Debug: Check initial auth state
const initial_auth_stats = await get_auth_stats();
t.false(initial_auth_stats.configured, 'Authentication should not be configured initially');
let response_data = null;
const mock_socket = {
write: (data) => {
response_data = data;
},
end: sinon.stub()
};
await setup(mock_socket, {});
// Verify response was sent
t.truthy(response_data);
// Decode the response to check structure
const response = decode_response(response_data);
// Debug: Log actual response if it fails
if (response.ok !== 1) {
console.log('Setup response:', response);
}
t.is(response.ok, 1);
t.truthy(response.password);
t.true(response.message.includes('Authentication setup completed successfully'));
// Verify authentication environment variable was created (set by user auth manager)
// Allow some time for async operations to complete
await new Promise(resolve => setTimeout(resolve, 100));
t.true(process.env.JOYSTICK_DB_SETTINGS !== undefined);
});
test('setup operation handles already configured authentication', async (t) => {
const { setup } = await import('../../src/server/index.js');
const { setup_initial_admin } = await import('../../src/server/lib/user_auth_manager.js');
// Setup authentication first
await setup_initial_admin('admin', 'admin123', 'admin@test.com');
let response_data = null;
const mock_socket = {
write: (data) => {
response_data = data;
},
end: sinon.stub()
};
await setup(mock_socket, {});
// Verify error response was sent
t.truthy(response_data);
// Decode the response to check structure
const response = decode_response(response_data);
t.is(response.ok, 0);
t.truthy(response.error);
t.true(response.error.includes('Authentication already configured'));
});
test('authentication operation requires username and password in data', async (t) => {
const { authentication } = await import('../../src/server/index.js');
let response_data = null;
const mock_socket = {
id: 'test-socket',
write: (data) => {
response_data = data;
},
end: sinon.stub()
};
await authentication(mock_socket, {});
// Verify error response was sent and socket was closed
t.truthy(response_data);
t.true(mock_socket.end.calledOnce);
// Decode the response to check structure
const response = decode_response(response_data);
t.is(response.ok, 0);
t.is(response.error, 'Authentication requires username and password');
});
test('authentication operation succeeds with correct username and password', async (t) => {
const { authentication } = await import('../../src/server/index.js');
const { setup_initial_admin } = await import('../../src/server/lib/user_auth_manager.js');
// Setup initial admin user
await setup_initial_admin('admin', 'admin123', 'admin@test.com');
let response_data = null;
const mock_socket = {
id: 'test-socket',
remoteAddress: '127.0.0.1',
write: (data) => {
response_data = data;
},
end: sinon.stub()
};
await authentication(mock_socket, { username: 'admin', password: 'admin123' });
// Verify success response was sent and socket was NOT closed
t.truthy(response_data);
t.false(mock_socket.end.called);
// Decode the response to check structure
const response = decode_response(response_data);
t.is(response.ok, 1);
t.is(response.version, '1.0.0');
t.is(response.message, 'Authentication successful');
t.is(response.role, 'admin');
});
test('authentication operation fails with incorrect username/password', async (t) => {
const { authentication } = await import('../../src/server/index.js');
const { setup_initial_admin } = await import('../../src/server/lib/user_auth_manager.js');
// Setup initial admin user
await setup_initial_admin('admin', 'admin123', 'admin@test.com');
let response_data = null;
const mock_socket = {
id: 'test-socket',
remoteAddress: '192.168.1.100',
write: (data) => {
response_data = data;
},
end: sinon.stub()
};
await authentication(mock_socket, { username: 'admin', password: 'wrong_password' });
// Verify error response was sent and socket was closed
t.truthy(response_data);
t.true(mock_socket.end.calledOnce);
// Decode the response to check structure
const response = decode_response(response_data);
t.is(response.ok, 0);
t.true(response.error === 'Invalid credentials' || response.error === 'Authentication failed');
});
test('authentication operation handles rate limiting', async (t) => {
const { authentication } = await import('../../src/server/index.js');
const { setup_initial_admin } = await import('../../src/server/lib/user_auth_manager.js');
// Setup initial admin user
await setup_initial_admin('admin', 'admin123', 'admin@test.com');
let response_data = null;
const mock_socket = {
id: 'test-socket',
remoteAddress: '192.168.1.100',
write: (data) => {
response_data = data;
},
end: sinon.stub()
};
// Make 5 failed attempts to trigger rate limiting
for (let i = 0; i < 5; i++) {
await authentication(mock_socket, { username: 'admin', password: 'wrong_password' });
}
// Reset for the rate limited attempt
response_data = null;
mock_socket.end.resetHistory();
// 6th attempt should be rate limited
await authentication(mock_socket, { username: 'admin', password: 'wrong_password' });
// Verify rate limiting error response was sent and socket was closed
t.truthy(response_data);
t.true(mock_socket.end.calledOnce);
// Decode the response to check structure
const response = decode_response(response_data);
t.is(response.ok, 0);
t.true(response.error.includes('Too many failed attempts'));
});
test('authentication operation fails for inactive user', async (t) => {
const { authentication } = await import('../../src/server/index.js');
const { setup_initial_admin, create_user, update_user } = await import('../../src/server/lib/user_auth_manager.js');
// Setup admin and create inactive user
await setup_initial_admin('admin', 'admin123', 'admin@test.com');
await create_user('user1', 'user123', 'user1@test.com');
await update_user('user1', { active: false });
let response_data = null;
const mock_socket = {
id: 'test-socket',
remoteAddress: '192.168.1.100',
write: (data) => {
response_data = data;
},
end: sinon.stub()
};
await authentication(mock_socket, { username: 'user1', password: 'user123' });
// Verify error response was sent and socket was closed
t.truthy(response_data);
t.true(mock_socket.end.calledOnce);
// Decode the response to check structure
const response = decode_response(response_data);
t.is(response.ok, 0);
t.true(response.error === 'Account is disabled' || response.error === 'Authentication failed');
});
test('authentication operation works for different user roles', async (t) => {
const { authentication } = await import('../../src/server/index.js');
const { setup_initial_admin, create_user } = await import('../../src/server/lib/user_auth_manager.js');
// Setup admin and regular user
await setup_initial_admin('admin', 'admin123', 'admin@test.com');
await create_user('user1', 'user123', 'user1@test.com');
// Test admin authentication
let response_data = null;
let mock_socket = {
id: 'admin-socket',
remoteAddress: '127.0.0.1',
write: (data) => {
response_data = data;
},
end: sinon.stub()
};
await authentication(mock_socket, { username: 'admin', password: 'admin123' });
t.truthy(response_data);
t.false(mock_socket.end.called);
// Decode the response to check structure
let response = decode_response(response_data);
t.is(response.ok, 1);
t.is(response.version, '1.0.0');
t.is(response.message, 'Authentication successful');
t.is(response.role, 'admin');
// Test regular user authentication
response_data = null;
mock_socket = {
id: 'user-socket',
remoteAddress: '127.0.0.1',
write: (data) => {
response_data = data;
},
end: sinon.stub()
};
await authentication(mock_socket, { username: 'user1', password: 'user123' });
t.truthy(response_data);
t.false(mock_socket.end.called);
// Decode the response to check structure
response = decode_response(response_data);
t.is(response.ok, 1);
t.is(response.version, '1.0.0');
t.is(response.message, 'Authentication successful');
t.is(response.role, 'user');
});
test('parse_data processes valid messagepack with JSON data', (t = {}) => {
const json_data = { name: "test", value: 123 };
const json_string = JSON.stringify(json_data);
const messagepack_data = encode_messagepack(json_string);
const raw_data = Buffer.from(messagepack_data);
const result = parse_data(raw_data);
t.deepEqual(result, json_data);
});
test('check_op_type returns true for valid operation types', (t = {}) => {
op_types.forEach((op_type) => {
const result = check_op_type(op_type);
t.true(result, `Expected ${op_type} to be valid`);
});
});
test('check_op_type validates setup operation type', (t = {}) => {
const result = check_op_type('setup');
t.true(result);
});
test('check_op_type validates authentication operation type', (t = {}) => {
const result = check_op_type('authentication');
t.true(result);
});
test('check_op_type returns false for invalid operation types', (t = {}) => {
const invalid_op_types = [
"invalid_op",
"unknown",
"select",
"create",
"drop",
"alter",
"FIND_ONE",
"Authentication",
"ping_test"
];
invalid_op_types.forEach((op_type) => {
const result = check_op_type(op_type);
t.false(result, `Expected ${op_type} to be invalid`);
});
});
test('check_op_type throws error when no op_type is provided', (t = {}) => {
const error = t.throws(() => {
check_op_type();
});
t.is(error.message, 'Must pass an op type for operation.');
});
test('check_op_type throws error when empty string is provided', (t = {}) => {
const error = t.throws(() => {
check_op_type('');
});
t.is(error.message, 'Must pass an op type for operation.');
});
test('check_op_type throws error when null is provided', (t = {}) => {
const error = t.throws(() => {
check_op_type(null);
});
t.is(error.message, 'Must pass an op type for operation.');
});
test('check_op_type throws error when undefined is provided', (t = {}) => {
const error = t.throws(() => {
check_op_type(undefined);
});
t.is(error.message, 'Must pass an op type for operation.');
});
test('check_op_type handles non-string input types', (t = {}) => {
const non_string_inputs = [
123,
true,
{},
[],
Symbol('test')
];
non_string_inputs.forEach((input) => {
const result = check_op_type(input);
t.false(result, `Expected ${typeof input} input to be invalid`);
});
});
test('parse_data processes messagepack with array JSON', (t = {}) => {
const json_data = [1, 2, 3, "test"];
const json_string = JSON.stringify(json_data);
const messagepack_data = encode_messagepack(json_string);
const raw_data = Buffer.from(messagepack_data);
const result = parse_data(raw_data);
t.deepEqual(result, json_data);
});
test('parse_data processes messagepack with primitive JSON values', (t = {}) => {
const test_cases = [
{ input: true, expected: true },
{ input: false, expected: false },
{ input: null, expected: null },
{ input: 42, expected: 42 },
{ input: "hello", expected: "hello" }
];
test_cases.forEach(({ input, expected }) => {
const json_string = JSON.stringify(input);
const messagepack_data = encode_messagepack(json_string);
const raw_data = Buffer.from(messagepack_data);
const result = parse_data(raw_data);
t.is(result, expected);
});
});
test('parse_data returns null for messagepack with invalid JSON', (t = {}) => {
const invalid_json = '{"name": "test", "value":}';
const messagepack_data = encode_messagepack(invalid_json);
const raw_data = Buffer.from(messagepack_data);
const result = parse_data(raw_data);
t.is(result, null);
});
test('parse_data returns null for messagepack with empty string', (t = {}) => {
const empty_string = '';
const messagepack_data = encode_messagepack(empty_string);
const raw_data = Buffer.from(messagepack_data);
const result = parse_data(raw_data);
t.is(result, null);
});
test('parse_data processes complex nested JSON objects', (t = {}) => {
const complex_data = {
user: {
id: 1,
name: "John Doe",
preferences: {
theme: "dark",
notifications: true
}
},
items: [
{ id: 1, name: "Item 1" },
{ id: 2, name: "Item 2" }
]
};
const json_string = JSON.stringify(complex_data);
const messagepack_data = encode_messagepack(json_string);
const raw_data = Buffer.from(messagepack_data);
const result = parse_data(raw_data);
t.deepEqual(result, complex_data);
});
test('parse_data processes buffer input as raw_data', (t = {}) => {
const json_data = { message: "test" };
const json_string = JSON.stringify(json_data);
const messagepack_data = encode_messagepack(json_string);
const raw_data_buffer = Buffer.from(messagepack_data);
const result = parse_data(raw_data_buffer);
t.deepEqual(result, json_data);
});
test('parse_data handles messagepack with JSON containing special characters', (t = {}) => {
const json_data = {
message: "Hello \"world\" with 'quotes' and \n newlines",
unicode: "π Unicode test δΈζ",
escaped: "Line 1\nLine 2\tTabbed"
};
const json_string = JSON.stringify(json_data);
const messagepack_data = encode_messagepack(json_string);
const raw_data = Buffer.from(messagepack_data);
const result = parse_data(raw_data);
t.deepEqual(result, json_data);
});
test('admin operation requires authentication', (t) => {
const mock_socket = {
id: 'unauthenticated-socket',
write: sinon.stub(),
end: sinon.stub()
};
const send_error_stub = sinon.stub();
const authenticated_clients = new Set();
const check_authentication = (socket) => {
return authenticated_clients.has(socket.id);
};
const is_authenticated = check_authentication(mock_socket);
t.false(is_authenticated);
if (!is_authenticated) {
send_error_stub(mock_socket, { message: 'Authentication required' });
}
t.true(send_error_stub.calledOnce);
t.true(send_error_stub.calledWith(mock_socket, { message: 'Authentication required' }));
});
test('admin operation returns server information structure', (t) => {
const mock_db = {
getStats: () => ({ pages: 100, entries: 50 }),
getRange: () => [
{ key: 'users:1' },
{ key: 'users:2' },
{ key: 'posts:1' },
{ key: 'posts:2' },
{ key: 'posts:3' }
]
};
const mock_settings = {
port: 1983,
cluster: true
};
const collections = new Map();
let total_documents = 0;
for (const { key } of mock_db.getRange()) {
if (typeof key === 'string' && key.includes(':')) {
const collection_name = key.split(':')[0];
collections.set(collection_name, (collections.get(collection_name) || 0) + 1);
total_documents++;
}
}
const admin_info = {
server: {
uptime: process.uptime(),
memory_usage: process.memoryUsage(),
node_version: process.version,
platform: process.platform,
pid: process.pid
},
database: {
total_documents,
collections: Object.fromEntries(collections),
stats: mock_db.getStats()
},
authentication: {
authenticated_clients: 0
},
settings: {
port: mock_settings.port || 1983,
cluster_enabled: !!mock_settings.cluster
}
};
t.is(typeof admin_info.server.uptime, 'number');
t.is(typeof admin_info.server.memory_usage, 'object');
t.is(typeof admin_info.server.node_version, 'string');
t.is(typeof admin_info.server.platform, 'string');
t.is(typeof admin_info.server.pid, 'number');
t.is(admin_info.database.total_documents, 5);
t.deepEqual(admin_info.database.collections, { users: 2, posts: 3 });
t.deepEqual(admin_info.database.stats, { pages: 100, entries: 50 });
t.is(admin_info.authentication.authenticated_clients, 0);
t.is(admin_info.settings.port, 1983);
t.true(admin_info.settings.cluster_enabled);
});
test('admin operation handles database without getStats method', (t) => {
const mock_db = {
getRange: () => [
{ key: 'users:1' },
{ key: 'posts:1' }
]
};
const stats = mock_db.getStats ? mock_db.getStats() : {};
const collections = new Map();
let total_documents = 0;
for (const { key } of mock_db.getRange()) {
if (typeof key === 'string' && key.includes(':')) {
const collection_name = key.split(':')[0];
collections.set(collection_name, (collections.get(collection_name) || 0) + 1);
total_documents++;
}
}
t.deepEqual(stats, {});
t.is(total_documents, 2);
t.deepEqual(Object.fromEntries(collections), { users: 1, posts: 1 });
});
test('admin operation counts authenticated clients correctly', (t) => {
const authenticated_clients = new Set(['socket1', 'socket2', 'socket3']);
const admin_info = {
authentication: {
authenticated_clients: authenticated_clients.size
}
};
t.is(admin_info.authentication.authenticated_clients, 3);
});
test('reload operation requires authentication', (t) => {
const mock_socket = {
id: 'unauthenticated-socket',
write: sinon.stub(),
end: sinon.stub()
};
const send_error_stub = sinon.stub();
const authenticated_clients = new Set();
const check_authentication = (socket) => {
return authenticated_clients.has(socket.id);
};
const is_authenticated = check_authentication(mock_socket);
t.false(is_authenticated);
if (!is_authenticated) {
send_error_stub(mock_socket, { message: 'Authentication required' });
}
t.true(send_error_stub.calledOnce);
t.true(send_error_stub.calledWith(mock_socket, { message: 'Authentication required' }));
});
test('reload operation detects configuration changes', (t) => {
const old_settings = {
port: 1983,
authentication: { password_hash: 'old-hash' },
cluster: false
};
const new_settings = {
port: 2000,
authentication: { password_hash: 'new-hash' },
cluster: true
};
const reload_info = {
status: 'success',
message: 'Configuration reloaded successfully',
changes: {
port_changed: old_settings.port !== new_settings.port,
authentication_changed: old_settings.authentication?.password_hash !== new_settings.authentication?.password_hash,
cluster_changed: old_settings.cluster !== new_settings.cluster
},
timestamp: new Date().toISOString()
};
t.is(reload_info.status, 'success');
t.is(reload_info.message, 'Configuration reloaded successfully');
t.true(reload_info.changes.port_changed);
t.true(reload_info.changes.authentication_changed);
t.true(reload_info.changes.cluster_changed);
t.is(typeof reload_info.timestamp, 'string');
});
test('reload operation detects no changes when settings are identical', (t) => {
const old_settings = {
port: 1983,
authentication: { password_hash: 'same-hash' },
cluster: true
};
const new_settings = {
port: 1983,
authentication: { password_hash: 'same-hash' },
cluster: true
};
const reload_info = {
status: 'success',
message: 'Configuration reloaded successfully',
changes: {
port_changed: old_settings.port !== new_settings.port,
authentication_changed: old_settings.authentication?.password_hash !== new_settings.authentication?.password_hash,
cluster_changed: old_settings.cluster !== new_settings.cluster
},
timestamp: new Date().toISOString()
};
t.is(reload_info.status, 'success');
t.false(reload_info.changes.port_changed);
t.false(reload_info.changes.authentication_changed);
t.false(reload_info.changes.cluster_changed);
});
test('reload operation handles missing authentication in old settings', (t) => {
const old_settings = {
port: 1983,
cluster: false
};
const new_settings = {
port: 1983,
authentication: { password_hash: 'new-hash' },
cluster: false
};
const reload_info = {
status: 'success',
message: 'Configuration reloaded successfully',
changes: {
port_changed: old_settings.port !== new_settings.port,
authentication_changed: old_settings.authentication?.password_hash !== new_settings.authentication?.password_hash,
cluster_changed: old_settings.cluster !== new_settings.cluster
},
timestamp: new Date().toISOString()
};
t.false(reload_info.changes.port_changed);
t.true(reload_info.changes.authentication_changed);
t.false(reload_info.changes.cluster_changed);
});
test('reload operation handles missing authentication in new settings', (t) => {
const old_settings = {
port: 1983,
authentication: { password_hash: 'old-hash' },
cluster: false
};
const new_settings = {
port: 1983,
cluster: false
};
const reload_info = {
status: 'success',
message: 'Configuration reloaded successfully',
changes: {
port_changed: old_settings.port !== new_settings.port,
authentication_changed: old_settings.authentication?.password_hash !== new_settings.authentication?.password_hash,
cluster_changed: old_settings.cluster !== new_settings.cluster
},
timestamp: new Date().toISOString()
};
t.false(reload_info.changes.port_changed);
t.true(reload_info.changes.authentication_changed);
t.false(reload_info.changes.cluster_changed);
});