@joystick.js/db-canary
Version:
JoystickDB - A minimalist database server for the Joystick framework
381 lines (308 loc) • 12.2 kB
JavaScript
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);
});