UNPKG

@joystick.js/db-canary

Version:

JoystickDB - A minimalist database server for the Joystick framework

337 lines (279 loc) 10.4 kB
import test from 'ava'; import sinon from 'sinon'; import net from 'net'; import { create_server } from '../../../src/server/index.js'; import { encode_message, create_message_parser } from '../../../src/server/lib/tcp_protocol.js'; import { initialize_database, cleanup_database } from '../../../src/server/lib/query_engine.js'; import { setup_initial_admin, reset_auth_state } from '../../../src/server/lib/user_auth_manager.js'; // Dynamic port allocation let current_port = 3000; const get_next_port = () => { return ++current_port; }; let server; let port; test.beforeEach(async () => { // Reset auth state and clean up environment variables await reset_auth_state(); delete process.env.JOYSTICK_DB_SETTINGS; // Initialize database for testing const db = initialize_database(); // Clear any existing user data from the database try { const user_prefix = '_admin:_users:'; for (const { key } of db.getRange({ start: user_prefix, end: user_prefix + '\xFF' })) { db.remove(key); } } catch (error) { // Ignore errors during cleanup } // Create server with dynamic port const test_port = get_next_port(); server = await create_server(); // Start server on specific port await new Promise((resolve) => { server.listen(test_port, () => { port = test_port; setTimeout(resolve, 100); }); }); }); test.afterEach(async () => { if (server) { await server.cleanup(); await new Promise((resolve) => { server.close(resolve); }); server = null; } // Clean up database try { await cleanup_database(true); // Remove test database directory } catch (error) { // Ignore cleanup errors } // Clean up environment variables await reset_auth_state(); delete process.env.JOYSTICK_DB_SETTINGS; }); const create_client = () => { return new Promise((resolve, reject) => { const client = net.createConnection(port, 'localhost'); const parser = create_message_parser(); client.on('connect', () => { resolve({ client, send: (data) => { const encoded = encode_message(data); client.write(encoded); }, receive: () => { return new Promise((resolve) => { const handler = (data) => { try { const messages = parser.parse_messages(data); for (const message of messages) { client.off('data', handler); resolve(message); return; } } catch (error) { // Continue listening } }; client.on('data', handler); }); }, close: () => { client.end(); } }); }); client.on('error', reject); }); }; test('integration - setup command creates authentication and returns password', async (t) => { const { client, send, receive, close } = await create_client(); try { send({ op: 'admin', data: { admin_action: 'setup_initial_admin', username: 'admin', password: 'admin123', email: 'admin@test.com' } }); const response = await receive(); t.is(response.ok, 1); // Check for message existence and content t.truthy(response.message); t.true(response.message.includes('Initial admin user created successfully')); // Wait a bit for async operations to complete await new Promise(resolve => setTimeout(resolve, 100)); // Verify environment variable was created t.true(process.env.JOYSTICK_DB_SETTINGS !== undefined); const settings_data = JSON.parse(process.env.JOYSTICK_DB_SETTINGS); t.true(typeof settings_data.authentication === 'object'); t.true(typeof settings_data.authentication.created_at === 'string'); } finally { close(); } }); test('integration - authentication succeeds with correct username and password', async (t) => { // Setup authentication first await setup_initial_admin('admin', 'admin123', 'admin@test.com'); const { client, send, receive, close } = await create_client(); try { send({ op: 'authentication', data: { username: 'admin', password: 'admin123' } }); const response = await receive(); t.is(response.ok, 1); t.is(response.version, '1.0.0'); t.is(response.message, 'Authentication successful'); } finally { close(); } }); test('integration - authentication fails with incorrect password', async (t) => { // Setup authentication first await setup_initial_admin('admin', 'admin123', 'admin@test.com'); const { client, send, receive, close } = await create_client(); try { send({ op: 'authentication', data: { username: 'admin', password: 'wrong_password' } }); const response = await receive(); t.true(response.ok === 0 || response.ok === false); t.true(response.error === 'Authentication failed' || response.error === 'Invalid credentials'); } finally { close(); } }); test('integration - database operations require authentication', async (t) => { // Temporarily set production mode to test authentication requirements const original_env = process.env.NODE_ENV; process.env.NODE_ENV = 'production'; try { // Setup authentication first await setup_initial_admin('admin', 'admin123', 'admin@test.com'); const { client, send, receive, close } = await create_client(); try { // Try to perform find operation without authentication send({ op: 'find', data: { collection: 'users', filter: {} } }); const response = await receive(); t.true(response.ok === 0 || response.ok === false); t.is(response.error, 'Authentication required'); } finally { close(); } } finally { process.env.NODE_ENV = original_env; } }); test('integration - database operations work after authentication', async (t) => { // Setup authentication first await setup_initial_admin('admin', 'admin123', 'admin@test.com'); const { client, send, receive, close } = await create_client(); try { // First authenticate send({ op: 'authentication', data: { username: 'admin', password: 'admin123' } }); const auth_response = await receive(); t.is(auth_response.ok, 1); t.is(auth_response.version, '1.0.0'); // Now try a database operation send({ op: 'ping' }); const ping_response = await receive(); t.is(ping_response.ok, 1); } finally { close(); } }); test('integration - admin operation includes authentication stats', async (t) => { // Setup authentication first await setup_initial_admin('admin', 'admin123', 'admin@test.com'); const { client, send, receive, close } = await create_client(); try { // First authenticate send({ op: 'authentication', data: { username: 'admin', password: 'admin123' } }); const auth_response = await receive(); t.is(auth_response.ok, 1); t.is(auth_response.version, '1.0.0'); // Now try admin operation send({ op: 'admin' }); const admin_response = await receive(); // This should be the admin response t.true(typeof admin_response.authentication === 'object'); t.is(admin_response.authentication.authenticated_clients, 1); // Check if authentication is configured by verifying we have user_count or configured flag t.true(typeof admin_response.authentication.user_count === 'number' || admin_response.authentication.configured === true); t.true(typeof admin_response.server === 'object'); t.true(typeof admin_response.database === 'object'); } finally { close(); } }); test('integration - rate limiting blocks multiple failed attempts', async (t) => { // Setup authentication first await setup_initial_admin('admin', 'admin123', 'admin@test.com'); // Make 5 failed attempts for (let i = 0; i < 5; i++) { const { client, send, receive, close } = await create_client(); try { send({ op: 'authentication', data: { username: 'admin', password: 'wrong_password' } }); const response = await receive(); t.true(response.ok === 0 || response.ok === false); t.true(response.error === 'Authentication failed' || response.error === 'Invalid credentials'); } finally { close(); } // Small delay between attempts await new Promise(resolve => setTimeout(resolve, 100)); } // 6th attempt should be rate limited const { client, send, receive, close } = await create_client(); try { send({ op: 'authentication', data: { username: 'admin', password: 'wrong_password' } }); const response = await receive(); t.true(response.ok === 0 || response.ok === false); t.true(response.error.includes('Too many failed attempts')); } finally { close(); } }); test('integration - setup fails when authentication already configured', async (t) => { // Setup authentication first await setup_initial_admin('admin', 'admin123', 'admin@test.com'); const { client, send, receive, close } = await create_client(); try { // Try to setup again send({ op: 'admin', data: { admin_action: 'setup_initial_admin', username: 'admin2', password: 'admin456', email: 'admin2@test.com' } }); const response = await receive(); t.true(response.success === false); // Check for error message with null safety t.truthy(response.error || response.message); const error_message = response.error || response.message; t.true(error_message.includes('Initial admin already exists') || error_message.includes('already configured') || error_message.includes('already exists')); } finally { close(); } }); test('integration - protocol versioning returns correct version', async (t) => { // Setup authentication first await setup_initial_admin('admin', 'admin123', 'admin@test.com'); const { client, send, receive, close } = await create_client(); try { send({ op: 'authentication', data: { username: 'admin', password: 'admin123' } }); const response = await receive(); t.is(response.ok, 1); t.is(response.version, '1.0.0'); t.is(response.message, 'Authentication successful'); } finally { close(); } });