UNPKG

@joystick.js/db-canary

Version:

JoystickDB - A minimalist database server for the Joystick framework

381 lines (308 loc) 12.2 kB
import test from 'ava'; import http from 'http'; import { URL } from 'url'; import fs from 'fs'; import { start_http_server, stop_http_server, get_setup_info, is_setup_required } from '../../../src/server/lib/http_server.js'; import { reset_auth_state, setup_authentication } from '../../../src/server/lib/auth_manager.js'; // NOTE: Simple port allocation using a fixed range to avoid conflicts. let current_port = 9000; const get_next_port = () => { return ++current_port; }; const make_http_request = (url, method = 'GET', data = null) => { return new Promise((resolve, reject) => { const parsed_url = new URL(url); const options = { hostname: parsed_url.hostname, port: parsed_url.port, path: parsed_url.pathname + parsed_url.search, method: method, headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }; const req = http.request(options, (res) => { let body = ''; res.on('data', (chunk) => { body += chunk; }); res.on('end', () => { resolve({ status_code: res.statusCode, headers: res.headers, body: body }); }); }); req.on('error', (error) => { reject(error); }); if (data && method === 'POST') { req.write(data); } req.end(); }); }; test.beforeEach(() => { reset_auth_state(); }); test.afterEach(async () => { // NOTE: Clean up HTTP server. await stop_http_server(); reset_auth_state(); }); test('is_setup_required returns true when authentication not configured', (t) => { const setup_required = is_setup_required(); t.true(setup_required); }); test('is_setup_required returns false when authentication is configured', (t) => { setup_authentication(); const setup_required = is_setup_required(); t.false(setup_required); }); test('start_http_server starts server even when setup not required (for recovery)', async (t) => { setup_authentication(); const port = get_next_port(); const server = await start_http_server(port); t.truthy(server); const setup_info = get_setup_info(); t.false(setup_info.setup_required); t.is(setup_info.setup_token, null); t.false(setup_info.setup_completed); t.true(setup_info.http_server_running); }); test('start_http_server starts server when setup is required', async (t) => { const port = get_next_port(); const server = await start_http_server(port); t.truthy(server); const setup_info = get_setup_info(); t.true(setup_info.setup_required); t.truthy(setup_info.setup_token); t.false(setup_info.setup_completed); t.true(setup_info.http_server_running); }); test('HTTP server serves 404 for unknown paths', async (t) => { const port = get_next_port(); await start_http_server(port); const response = await make_http_request(`http://localhost:${port}/unknown`); t.is(response.status_code, 404); t.true(response.body.includes('404 Not Found')); }); test('setup endpoint requires valid token', async (t) => { const port = get_next_port(); await start_http_server(port); const response = await make_http_request(`http://localhost:${port}/setup?token=invalid`); t.is(response.status_code, 403); t.true(response.body.includes('Invalid or missing setup token')); }); test('setup endpoint serves form with valid token', async (t) => { const port = get_next_port(); await start_http_server(port); const setup_info = get_setup_info(); const response = await make_http_request(`http://localhost:${port}/setup?token=${setup_info.setup_token}`); t.is(response.status_code, 200); t.true(response.body.includes('JoystickDB Setup')); t.true(response.body.includes('Setup JoystickDB')); t.true(response.body.includes('form method="POST"')); }); test('setup endpoint handles POST request with valid token', async (t) => { const port = get_next_port(); await start_http_server(port); const setup_info = get_setup_info(); const response = await make_http_request( `http://localhost:${port}/setup?token=${setup_info.setup_token}`, 'POST', '' ); t.is(response.status_code, 200); t.true(response.body.includes('Setup Completed Successfully')); t.true(response.body.includes('Generated Password')); t.true(response.body.includes('JOYSTICKDB_PASSWORD=')); // NOTE: Verify authentication was configured in environment variable. t.truthy(process.env.JOYSTICK_DB_SETTINGS); }); test('setup endpoint rejects POST with invalid token', async (t) => { const port = get_next_port(); await start_http_server(port); const response = await make_http_request( `http://localhost:${port}/setup?token=invalid`, 'POST', '' ); t.is(response.status_code, 403); t.true(response.body.includes('Invalid or missing setup token')); }); test('setup endpoint rejects setup when already configured', async (t) => { setup_authentication(); const port = get_next_port(); const server = await start_http_server(port); // NOTE: Server should start even when authentication is configured (for recovery operations). t.truthy(server); // NOTE: Verify that is_setup_required returns false. t.false(is_setup_required()); // NOTE: But setup endpoint should reject setup attempts. const response = await make_http_request(`http://localhost:${port}/setup?token=any`); t.is(response.status_code, 400); t.true(response.body.includes('Setup has already been completed')); }); test('setup endpoint handles missing token parameter', async (t) => { const port = get_next_port(); await start_http_server(port); const response = await make_http_request(`http://localhost:${port}/setup`); t.is(response.status_code, 403); t.true(response.body.includes('Invalid or missing setup token')); }); test('setup endpoint handles unsupported HTTP methods', async (t) => { const port = get_next_port(); await start_http_server(port); const setup_info = get_setup_info(); const response = await make_http_request( `http://localhost:${port}/setup?token=${setup_info.setup_token}`, 'PUT', '' ); t.is(response.status_code, 405); t.true(response.body.includes('Method not allowed')); }); test('rate limiting prevents excessive setup attempts', async (t) => { const port = get_next_port(); await start_http_server(port); // NOTE: Make 11 failed attempts (exceeds limit of 10). for (let i = 0; i < 11; i++) { await make_http_request(`http://localhost:${port}/setup?token=invalid`); } const response = await make_http_request(`http://localhost:${port}/setup?token=invalid`); t.is(response.status_code, 429); t.true(response.body.includes('Too many setup attempts')); }); test('stop_http_server stops running server', async (t) => { const port = get_next_port(); const server = await start_http_server(port); t.truthy(server); await stop_http_server(); const setup_info = get_setup_info(); t.false(setup_info.http_server_running); // NOTE: Verify server is no longer accessible. try { await make_http_request(`http://localhost:${port}/setup?token=any`); t.fail('Server should not be accessible after stop'); } catch (error) { t.true(error.code === 'ECONNREFUSED' || error.code === 'ENOTFOUND'); } }); test('stop_http_server handles no running server gracefully', async (t) => { await t.notThrowsAsync(async () => { await stop_http_server(); }); }); test('get_setup_info returns correct information', async (t) => { let setup_info = get_setup_info(); t.true(setup_info.setup_required); t.is(setup_info.setup_token, null); t.false(setup_info.setup_completed); t.false(setup_info.http_server_running); const port = get_next_port(); await start_http_server(port); setup_info = get_setup_info(); t.true(setup_info.setup_required); t.truthy(setup_info.setup_token); t.false(setup_info.setup_completed); t.true(setup_info.http_server_running); }); test('setup completion updates setup info', async (t) => { const port = get_next_port(); await start_http_server(port); const setup_info = get_setup_info(); await make_http_request( `http://localhost:${port}/setup?token=${setup_info.setup_token}`, 'POST', '' ); const updated_info = get_setup_info(); t.false(updated_info.setup_required); t.is(updated_info.setup_token, null); t.true(updated_info.setup_completed); }); test('HTTP server handles port conflicts gracefully', async (t) => { const test_port = get_next_port(); // NOTE: Start first server. const server1 = await start_http_server(test_port); t.truthy(server1); // NOTE: Wait a moment to ensure the first server is fully established. await new Promise(resolve => setTimeout(resolve, 100)); // NOTE: Try to start second server on same port - in test environment this should return null. const server2 = await start_http_server(test_port); t.is(server2, null, 'Second server should return null due to port conflict in test environment'); // NOTE: Verify first server is still running and accessible. const setup_info = get_setup_info(); t.true(setup_info.http_server_running); // NOTE: Clean up the first server. await stop_http_server(); }); test('setup form includes security information', async (t) => { const port = get_next_port(); await start_http_server(port); const setup_info = get_setup_info(); const response = await make_http_request(`http://localhost:${port}/setup?token=${setup_info.setup_token}`); t.true(response.body.includes('What happens during setup?')); t.true(response.body.includes('secure random password')); t.true(response.body.includes('settings.db.json')); t.true(response.body.includes('After setup:')); t.true(response.body.includes('JOYSTICKDB_PASSWORD=')); t.true(response.body.includes('TCP port 1983')); }); test('success page includes complete instructions', async (t) => { const port = get_next_port(); await start_http_server(port); const setup_info = get_setup_info(); const response = await make_http_request( `http://localhost:${port}/setup?token=${setup_info.setup_token}`, 'POST', '' ); t.true(response.body.includes('Setup Completed Successfully')); t.true(response.body.includes('Save this password immediately')); t.true(response.body.includes('Generated Password:')); t.true(response.body.includes('Next Steps:')); t.true(response.body.includes('Set environment variable')); t.true(response.body.includes('Restart your application')); t.true(response.body.includes('Connect using the TCP client')); t.true(response.body.includes('Client Connection Example:')); t.true(response.body.includes('import joystickdb')); t.true(response.body.includes('Configuration Details:')); }); test('error pages include troubleshooting information', async (t) => { const port = get_next_port(); await start_http_server(port); const response = await make_http_request(`http://localhost:${port}/setup?token=invalid`); t.true(response.body.includes('Setup Error')); t.true(response.body.includes('Troubleshooting:')); t.true(response.body.includes('authentication')); t.true(response.body.includes('settings.db.json')); t.true(response.body.includes('write permissions')); t.true(response.body.includes('server logs')); t.true(response.body.includes('Go Back')); }); test('HTTP server uses configured port from settings', async (t) => { const custom_port = get_next_port(); const server = await start_http_server(custom_port); t.truthy(server); const setup_info = get_setup_info(); const response = await make_http_request(`http://localhost:${custom_port}/setup?token=${setup_info.setup_token}`); t.is(response.status_code, 200); }); test('setup token is unique across server restarts', async (t) => { const port1 = get_next_port(); await start_http_server(port1); const first_token = get_setup_info().setup_token; await stop_http_server(); const port2 = get_next_port(); await start_http_server(port2); const second_token = get_setup_info().setup_token; t.not(first_token, second_token); });