UNPKG

@joystick.js/db-canary

Version:

JoystickDB - A minimalist database server for the Joystick framework

318 lines (241 loc) 10.7 kB
import test from 'ava'; import bcrypt from 'bcrypt'; import { setup_authentication, verify_password, get_client_ip, get_auth_stats, initialize_auth_manager, reset_auth_state } from '../../../src/server/lib/auth_manager.js'; test.beforeEach(() => { // Reset module state and clean up environment variables reset_auth_state(); delete process.env.JOYSTICK_DB_SETTINGS; }); test.afterEach(() => { // Reset module state and clean up environment variables after each test reset_auth_state(); delete process.env.JOYSTICK_DB_SETTINGS; }); test('setup_authentication creates authentication in environment variable with valid structure', (t) => { const password = setup_authentication(); t.true(process.env.JOYSTICK_DB_SETTINGS !== undefined); t.is(typeof password, 'string'); t.is(password.length, 32); const settings_data = JSON.parse(process.env.JOYSTICK_DB_SETTINGS); const auth_data = settings_data.authentication; t.true(typeof auth_data.password_hash === 'string'); t.true(typeof auth_data.created_at === 'string'); t.true(typeof auth_data.last_updated === 'string'); t.true(typeof auth_data.failed_attempts === 'object'); t.true(typeof auth_data.rate_limits === 'object'); // Verify password hash is valid bcrypt hash t.true(bcrypt.compareSync(password, auth_data.password_hash)); }); test('setup_authentication throws error when authentication already exists', (t) => { setup_authentication(); const error = t.throws(() => { setup_authentication(); }); t.is(error.message, 'Authentication already configured. Update JOYSTICK_DB_SETTINGS environment variable to reconfigure.'); }); test('setup_authentication generates unique passwords', (t) => { const password1 = setup_authentication(); delete process.env.JOYSTICK_DB_SETTINGS; reset_auth_state(); // Reset in-memory state after clearing environment variable const password2 = setup_authentication(); t.not(password1, password2); }); test('verify_password returns true for correct password', async (t) => { const password = setup_authentication(); const client_ip = '192.168.1.100'; const result = await verify_password(password, client_ip); t.true(result); }); test('verify_password returns false for incorrect password', async (t) => { setup_authentication(); const client_ip = '192.168.1.100'; const result = await verify_password('wrong_password', client_ip); t.false(result); }); test('verify_password throws error when authentication not configured', async (t) => { // Ensure no environment variable exists and use a fresh IP delete process.env.JOYSTICK_DB_SETTINGS; const client_ip = '192.168.1.200'; // Use completely fresh IP const error = await t.throwsAsync(async () => { await verify_password('any_password', client_ip); }); t.is(error.message, 'Authentication not configured. Run setup first.'); }); test('verify_password implements timing attack protection', async (t) => { const password = setup_authentication(); const client_ip = '192.168.1.100'; const start_time = Date.now(); await verify_password('wrong_password', client_ip); const wrong_duration = Date.now() - start_time; const start_time2 = Date.now(); await verify_password(password, client_ip); const correct_duration = Date.now() - start_time2; // Both should take at least 100ms due to timing protection t.true(wrong_duration >= 100); t.true(correct_duration >= 100); }); test('verify_password records failed attempts for non-whitelisted IPs', async (t) => { setup_authentication(); const client_ip = '192.168.1.100'; await verify_password('wrong_password', client_ip); const settings_data = JSON.parse(process.env.JOYSTICK_DB_SETTINGS); const auth_data = settings_data.authentication; t.true(Array.isArray(auth_data.failed_attempts[client_ip])); t.is(auth_data.failed_attempts[client_ip].length, 1); }); test('verify_password records failed attempts for all IPs in test environment', async (t) => { setup_authentication(); const test_ip = '127.0.0.1'; await verify_password('wrong_password', test_ip); const settings_data = JSON.parse(process.env.JOYSTICK_DB_SETTINGS); const auth_data = settings_data.authentication; // In test environment, IP whitelisting is disabled, so failed attempts are recorded for all IPs t.true(Array.isArray(auth_data.failed_attempts[test_ip])); t.is(auth_data.failed_attempts[test_ip].length, 1); }); test('verify_password clears failed attempts on successful authentication', async (t) => { const password = setup_authentication(); const client_ip = '192.168.1.101'; // Use different IP to avoid cross-test contamination // Record a failed attempt await verify_password('wrong_password', client_ip); let settings_data = JSON.parse(process.env.JOYSTICK_DB_SETTINGS); let auth_data = settings_data.authentication; t.is(auth_data.failed_attempts[client_ip].length, 1); // Successful authentication should clear failed attempts await verify_password(password, client_ip); settings_data = JSON.parse(process.env.JOYSTICK_DB_SETTINGS); auth_data = settings_data.authentication; t.is(auth_data.failed_attempts[client_ip], undefined); }); test('rate limiting blocks after maximum failed attempts', async (t) => { setup_authentication(); const client_ip = '192.168.1.102'; // Use different IP to avoid cross-test contamination // Make 5 failed attempts for (let i = 0; i < 5; i++) { await verify_password('wrong_password', client_ip); } // 6th attempt should be rate limited const error = await t.throwsAsync(async () => { await verify_password('wrong_password', client_ip); }); t.is(error.message, 'Too many failed attempts. Please try again later.'); }); test('rate limiting affects all IPs in test environment', async (t) => { setup_authentication(); const test_ip = '192.168.1.200'; // Use different IP to avoid cross-test contamination // Make 5 failed attempts to trigger rate limiting for (let i = 0; i < 5; i++) { const result = await verify_password('wrong_password', test_ip); t.false(result); } // 6th attempt should be rate limited (even for localhost in test environment) const error = await t.throwsAsync(async () => { await verify_password('wrong_password', test_ip); }); t.is(error.message, 'Too many failed attempts. Please try again later.'); }); test('get_client_ip extracts IP from socket', (t) => { const mock_socket = { remoteAddress: '192.168.1.100' }; const ip = get_client_ip(mock_socket); t.is(ip, '192.168.1.100'); }); test('get_client_ip returns localhost for socket without remoteAddress', (t) => { const mock_socket = {}; const ip = get_client_ip(mock_socket); t.is(ip, '127.0.0.1'); }); test('get_auth_stats returns correct structure when configured', (t) => { setup_authentication(); const stats = get_auth_stats(); t.true(stats.configured); t.is(typeof stats.failed_attempts_count, 'number'); t.is(typeof stats.rate_limited_ips, 'number'); t.is(typeof stats.created_at, 'string'); t.is(typeof stats.last_updated, 'string'); }); test('initialize_auth_manager loads existing authentication data', (t) => { // Create auth file first setup_authentication(); // Initialize should load the data without error t.notThrows(() => { initialize_auth_manager(); }); }); test('initialize_auth_manager handles missing authentication file gracefully', (t) => { // Should not throw when no auth file exists t.notThrows(() => { initialize_auth_manager(); }); }); test('authentication data is stored in environment variable', (t) => { setup_authentication(); // Verify environment variable exists and contains valid JSON 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.password_hash === 'string'); }); test('password generation creates 32-character hex string', (t) => { const password = setup_authentication(); // Should be 32 characters long t.is(password.length, 32); // Should be valid hex string t.true(/^[0-9a-f]{32}$/.test(password)); }); test('bcrypt hashing works correctly', (t) => { const password = setup_authentication(); const settings_data = JSON.parse(process.env.JOYSTICK_DB_SETTINGS); const auth_data = settings_data.authentication; // Should be able to verify the password with the stored hash t.true(bcrypt.compareSync(password, auth_data.password_hash)); // Should not verify with wrong password t.false(bcrypt.compareSync('wrong_password', auth_data.password_hash)); }); test('authentication environment variable corruption is handled gracefully', (t) => { // Create corrupted environment variable process.env.JOYSTICK_DB_SETTINGS = 'invalid json content'; // initialize_auth_manager doesn't throw, it logs warnings and continues // So we test that it doesn't throw and the environment variable remains corrupted t.notThrows(() => { initialize_auth_manager(); }); // The corrupted environment variable should still exist t.true(process.env.JOYSTICK_DB_SETTINGS !== undefined); // And should still be corrupted t.is(process.env.JOYSTICK_DB_SETTINGS, 'invalid json content'); }); test('rate limiting implements exponential backoff', async (t) => { setup_authentication(); const client_ip = '192.168.1.103'; // Use different IP to avoid cross-test contamination // Make 5 failed attempts to trigger rate limiting for (let i = 0; i < 5; i++) { await verify_password('wrong_password', client_ip); } // Make the 6th attempt that should trigger rate limiting try { await verify_password('wrong_password', client_ip); t.fail('Should have thrown rate limiting error'); } catch (error) { t.is(error.message, 'Too many failed attempts. Please try again later.'); } // Now check that rate limit info is stored const settings_data = JSON.parse(process.env.JOYSTICK_DB_SETTINGS); const auth_data = settings_data.authentication; t.true(typeof auth_data.rate_limits[client_ip] === 'object'); t.true(typeof auth_data.rate_limits[client_ip].expires_at === 'number'); t.true(typeof auth_data.rate_limits[client_ip].attempts === 'number'); }); test('whitelisted IPs behavior is tested through other tests', (t) => { // This test verifies that whitelisted IP behavior is covered // by the other tests (like rate limiting not affecting whitelisted IPs) t.pass(); });