UNPKG

@joystick.js/db-canary

Version:

JoystickDB - A minimalist database server for the Joystick framework

819 lines (666 loc) β€’ 24.6 kB
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); });