UNPKG

@joystick.js/db-canary

Version:

JoystickDB - A minimalist database server for the Joystick framework

526 lines (404 loc) 18 kB
import test from 'ava'; import { setup_initial_admin, create_user, get_user, update_user, delete_user, list_users, verify_credentials, reset_user_password, get_auth_stats, reset_auth_state, set_storage_engine } from '../../../src/server/lib/user_auth_manager.js'; import { initialize_database } from '../../../src/server/lib/query_engine.js'; import { existsSync, unlinkSync } from 'fs'; import { mkdirSync } from 'fs'; let storage_engine; test.beforeEach(async () => { await reset_auth_state(); // Clean up any existing database files try { if (existsSync('./.joystick/data/joystickdb_test/data.mdb')) { unlinkSync('./.joystick/data/joystickdb_test/data.mdb'); } if (existsSync('./.joystick/data/joystickdb_test/lock.mdb')) { unlinkSync('./.joystick/data/joystickdb_test/lock.mdb'); } } catch (error) { // Ignore cleanup errors } // Create test directory if it doesn't exist try { mkdirSync('./.joystick/data/joystickdb_test', { recursive: true }); } catch (error) { // Directory might already exist } // Create real storage engine for testing storage_engine = initialize_database('./.joystick/data/joystickdb_test'); set_storage_engine(storage_engine); }); test.afterEach(async () => { // Clean up users from admin database try { const users_result = await list_users(); if (users_result.success && users_result.users && users_result.users.length > 0) { for (const user of users_result.users) { await delete_user(user.username); } } } catch (error) { // Ignore cleanup errors } await reset_auth_state(); // NOTE: Don't close database between tests to avoid transaction errors // The database will be closed when the test process exits }); // Initial Admin Setup Tests test('setup_initial_admin creates first admin user successfully', async (t) => { const result = await setup_initial_admin('admin', 'admin123', 'admin@test.com'); t.is(result.success, true); t.truthy(result.admin_user); t.is(result.admin_user.user.username, 'admin'); t.is(result.admin_user.user.role, 'admin'); t.is(result.admin_user.user.active, true); t.truthy(result.admin_user.user.created_at); t.falsy(result.admin_user.user.password_hash); // Should not return password hash // Verify user can be retrieved const user_result = await get_user('admin'); t.is(user_result.success, true); t.is(user_result.user.username, 'admin'); t.is(user_result.user.role, 'admin'); }); test('setup_initial_admin prevents duplicate admin creation', async (t) => { // Create initial admin await setup_initial_admin('admin', 'admin123', 'admin@test.com'); // Try to create another admin const result = await setup_initial_admin('admin2', 'admin456', 'admin2@test.com'); t.is(result.success, false); t.truthy(result.error.includes('Initial admin user already exists')); }); test('setup_initial_admin validates required fields', async (t) => { // Missing username let result = await setup_initial_admin('', 'admin123', 'admin@test.com'); t.is(result.success, false); t.truthy(result.error.includes('Username, password, and email are required')); // Missing password result = await setup_initial_admin('admin', '', 'admin@test.com'); t.is(result.success, false); t.truthy(result.error.includes('Username, password, and email are required')); // Missing email result = await setup_initial_admin('admin', 'admin123', ''); t.is(result.success, false); t.truthy(result.error.includes('Username, password, and email are required')); }); test('setup_initial_admin validates password strength', async (t) => { const result = await setup_initial_admin('admin', '123', 'admin@test.com'); t.is(result.success, false); t.truthy(result.error.includes('Password must be at least 6 characters long')); }); test('setup_initial_admin validates email format', async (t) => { const result = await setup_initial_admin('admin', 'admin123', 'invalid-email'); t.is(result.success, false); t.truthy(result.error.includes('Invalid email format')); }); // User Creation Tests test('create_user creates regular user successfully', async (t) => { // Setup admin first await setup_initial_admin('admin', 'admin123', 'admin@test.com'); const result = await create_user('user1', 'user12345678', 'user1@test.com', 'user'); t.is(result.success, true); t.is(result.user.username, 'user1'); t.is(result.user.role, 'user'); t.is(result.user.active, true); t.falsy(result.user.password_hash); // Should not return password hash // Verify user can be retrieved const user_result = await get_user('user1'); t.is(user_result.success, true); t.is(user_result.user.username, 'user1'); t.is(user_result.user.role, 'user'); }); test('create_user prevents duplicate usernames', async (t) => { await setup_initial_admin('admin', 'admin123', 'admin@test.com'); await create_user('user1', 'user12345678', 'user1@test.com'); const result = await create_user('user1', 'user456789', 'user1b@test.com'); t.is(result.success, false); t.truthy(result.error.includes('User already exists')); }); test('create_user validates input fields', async (t) => { await setup_initial_admin('admin', 'admin123', 'admin@test.com'); // Missing username let result = await create_user('', 'user12345678', 'user@test.com'); t.is(result.success, false); t.truthy(result.error && (result.error.includes('Username is required') || result.error.includes('Username, password, and email are required') || result.error.includes('required'))); // Invalid role result = await create_user('user1', 'user12345678', 'user@test.com', 'invalid'); t.is(result.success, false); t.truthy(result.error && (result.error.includes('Role must be either "admin" or "user"') || result.error.includes('invalid role') || result.error.includes('Role'))); }); // User Retrieval Tests test('get_user retrieves existing user', async (t) => { await setup_initial_admin('admin', 'admin123', 'admin@test.com'); await create_user('user1', 'user12345678', 'user1@test.com'); const result = await get_user('user1'); t.is(result.success, true); t.is(result.user.username, 'user1'); t.is(result.user.role, 'user'); t.is(result.user.active, true); t.falsy(result.user.password_hash); // Should not return password hash }); test('get_user returns error for non-existent user', async (t) => { const result = await get_user('nonexistent'); t.is(result.success, false); t.truthy(result.error.includes('User not found')); }); // User Update Tests test('update_user updates user fields successfully', async (t) => { await setup_initial_admin('admin', 'admin123', 'admin@test.com'); await create_user('user1', 'user123', 'user1@test.com'); const updates = { email: 'user1_new@test.com', role: 'admin', active: false }; const result = await update_user('user1', updates); t.is(result.success, true); t.truthy(result.message.includes('User updated successfully')); // Verify updates const user_result = await get_user('user1'); t.is(user_result.user.email, 'user1_new@test.com'); t.is(user_result.user.role, 'admin'); t.is(user_result.user.active, false); }); test('update_user validates email format', async (t) => { await setup_initial_admin('admin', 'admin123', 'admin@test.com'); await create_user('user1', 'user123', 'user1@test.com'); const result = await update_user('user1', { email: 'invalid-email' }); t.is(result.success, false); t.truthy(result.error.includes('Invalid email format')); }); test('update_user validates role values', async (t) => { await setup_initial_admin('admin', 'admin123', 'admin@test.com'); await create_user('user1', 'user123', 'user1@test.com'); const result = await update_user('user1', { role: 'invalid' }); t.is(result.success, false); t.truthy(result.error.includes('Role must be either "admin" or "user"')); }); // Password Reset Tests test('reset_user_password updates password successfully', async (t) => { await setup_initial_admin('admin', 'admin123', 'admin@test.com'); await create_user('user1', 'user123', 'user1@test.com'); const result = await reset_user_password('user1', 'new_password123'); t.is(result.success, true); t.truthy(result.message.includes('Password reset successfully')); // Verify new password works const auth_result = await verify_credentials('user1', 'new_password123', '127.0.0.1'); t.is(auth_result.success, true); // Verify old password no longer works const old_auth_result = await verify_credentials('user1', 'user123', '127.0.0.1'); t.is(old_auth_result.success, false); }); test('reset_user_password validates password strength', async (t) => { await setup_initial_admin('admin', 'admin123', 'admin@test.com'); await create_user('user1', 'user123', 'user1@test.com'); const result = await reset_user_password('user1', '123'); t.is(result.success, false); t.truthy(result.error.includes('Password must be at least 6 characters long')); }); // User Deletion Tests test('delete_user removes user successfully', async (t) => { await setup_initial_admin('admin', 'admin123', 'admin@test.com'); await create_user('user1', 'user123', 'user1@test.com'); const result = await delete_user('user1'); t.is(result.success, true); t.truthy(result.message.includes('User deleted successfully')); // Verify user was deleted const get_result = await get_user('user1'); t.is(get_result.success, false); t.truthy(get_result.error.includes('User not found')); }); test('delete_user returns error for non-existent user', async (t) => { const result = await delete_user('nonexistent'); t.is(result.success, false); t.truthy(result.error.includes('User not found')); }); // User Listing Tests test('list_users returns all users', async (t) => { await setup_initial_admin('admin', 'admin123', 'admin@test.com'); await create_user('user1', 'user123', 'user1@test.com'); await create_user('user2', 'user123', 'user2@test.com'); const result = await list_users(); t.is(result.success, true); t.is(result.users.length, 3); const usernames = result.users.map(u => u.username); t.true(usernames.includes('admin')); t.true(usernames.includes('user1')); t.true(usernames.includes('user2')); // Verify no password hashes are returned result.users.forEach(user => { t.falsy(user.password_hash); }); }); test('list_users handles empty user list', async (t) => { const result = await list_users(); t.is(result.success, true); t.is(result.users.length, 0); }); // Authentication Tests test('verify_credentials authenticates valid user successfully', async (t) => { await setup_initial_admin('admin', 'admin123', 'admin@test.com'); const result = await verify_credentials('admin', 'admin123', '127.0.0.1'); t.is(result.success, true); t.is(result.user.username, 'admin'); t.is(result.user.role, 'admin'); t.truthy(result.user.last_login); }); test('verify_credentials rejects invalid password', async (t) => { await setup_initial_admin('admin', 'admin123', 'admin@test.com'); const result = await verify_credentials('admin', 'wrong_password', '127.0.0.1'); t.is(result.success, false); t.truthy(result.error.includes('Invalid credentials')); }); test('verify_credentials rejects non-existent user', async (t) => { const result = await verify_credentials('nonexistent', 'password', '127.0.0.1'); t.is(result.success, false); t.truthy(result.error.includes('Invalid credentials')); }); test('verify_credentials rejects inactive user', async (t) => { await setup_initial_admin('admin', 'admin123', 'admin@test.com'); await create_user('user1', 'user123', 'user1@test.com'); // Deactivate user await update_user('user1', { active: false }); const result = await verify_credentials('user1', 'user123', '127.0.0.1'); t.is(result.success, false); t.truthy(result.error.includes('Account is disabled')); }); test('verify_credentials implements rate limiting', async (t) => { await setup_initial_admin('admin', 'admin123', 'admin@test.com'); const ip = '192.168.1.100'; // Make 5 failed attempts for (let i = 0; i < 5; i++) { await verify_credentials('admin', 'wrong_password', ip); } // 6th attempt should be rate limited const result = await verify_credentials('admin', 'wrong_password', ip); t.is(result.success, false); t.truthy(result.error.includes('Too many failed attempts')); }); test('verify_credentials updates last_login on successful authentication', async (t) => { await setup_initial_admin('admin', 'admin123', 'admin@test.com'); const before_login = Date.now(); const result = await verify_credentials('admin', 'admin123', '127.0.0.1'); t.is(result.success, true); // Get user to check last_login was updated const user_result = await get_user('admin'); const last_login = new Date(user_result.user.last_login).getTime(); t.true(last_login >= before_login); }); // Authentication Stats Tests test('get_auth_stats returns authentication statistics', async (t) => { await setup_initial_admin('admin', 'admin123', 'admin@test.com'); await create_user('user1', 'user123', 'user1@test.com'); // Make some authentication attempts await verify_credentials('admin', 'admin123', '127.0.0.1'); await verify_credentials('user1', 'wrong_password', '192.168.1.1'); const result = await get_auth_stats(); t.is(result.configured, true); t.is(result.user_based, true); t.is(typeof result.user_count, 'number'); t.is(typeof result.failed_attempts_count, 'number'); t.is(typeof result.rate_limited_ips, 'number'); }); // Edge Cases and Error Handling test('functions handle storage engine errors gracefully', async (t) => { // Mock storage engine that throws errors const error_storage = { get: () => { throw new Error('Storage error'); }, put: () => { throw new Error('Storage error'); }, del: () => { throw new Error('Storage error'); }, getRange: () => { throw new Error('Storage error'); } }; set_storage_engine(error_storage); const result = await setup_initial_admin('admin', 'admin123', 'admin@test.com'); t.is(result.success, false); t.truthy(result.error.includes('Storage error')); }); test('functions validate input parameters', async (t) => { // Test with null/undefined parameters let result = await create_user(null, 'password', 'email@test.com'); t.is(result.success, false); result = await get_user(undefined); t.is(result.success, false); result = await update_user('', {}); t.is(result.success, false); }); test('password hashing is secure and consistent', async (t) => { await setup_initial_admin('admin', 'admin123', 'admin@test.com'); // Verify password authentication works (proves hashing works) const auth_result1 = await verify_credentials('admin', 'admin123', '127.0.0.1'); t.is(auth_result1.success, true); t.is(auth_result1.user.username, 'admin'); // Verify wrong password fails (proves password was hashed, not stored as plaintext) const auth_result2 = await verify_credentials('admin', 'wrong_password', '127.0.0.1'); t.is(auth_result2.success, false); // Create another user with same password to test salt uniqueness await create_user('user1', 'admin12345678', 'user1@test.com'); const auth_result3 = await verify_credentials('user1', 'admin12345678', '127.0.0.1'); t.is(auth_result3.success, true); t.is(auth_result3.user.username, 'user1'); }); test('authentication timing is consistent to prevent timing attacks', async (t) => { await setup_initial_admin('admin', 'admin123', 'admin@test.com'); // Test authentication with existing vs non-existing user const start1 = Date.now(); await verify_credentials('admin', 'wrong_password', '127.0.0.1'); const time1 = Date.now() - start1; const start2 = Date.now(); await verify_credentials('nonexistent', 'wrong_password', '127.0.0.1'); const time2 = Date.now() - start2; // Times should be roughly similar (within reasonable margin) // This is a basic check - in practice, timing attack prevention is more complex const time_diff = Math.abs(time1 - time2); t.true(time_diff < 1000); // Increased threshold for bcrypt operations }); // Integration Tests test('complete user lifecycle workflow', async (t) => { // 1. Setup initial admin let result = await setup_initial_admin('admin', 'admin123', 'admin@test.com'); t.is(result.success, true); // 2. Admin authenticates result = await verify_credentials('admin', 'admin123', '127.0.0.1'); t.is(result.success, true); t.is(result.user.role, 'admin'); // 3. Admin creates regular user result = await create_user('user1', 'user123', 'user1@test.com'); t.is(result.success, true); // 4. User authenticates result = await verify_credentials('user1', 'user123', '127.0.0.1'); t.is(result.success, true); t.is(result.user.role, 'user'); // 5. Admin updates user result = await update_user('user1', { email: 'user1_updated@test.com' }); t.is(result.success, true); // 6. Admin resets user password result = await reset_user_password('user1', 'new_password123'); t.is(result.success, true); // 7. User authenticates with new password result = await verify_credentials('user1', 'new_password123', '127.0.0.1'); t.is(result.success, true); // 8. List all users result = await list_users(); t.is(result.success, true); t.is(result.users.length, 2); // 9. Get user stats result = await get_auth_stats(); t.is(result.configured, true); t.is(result.user_count, 2); // 10. Admin deletes user result = await delete_user('user1'); t.is(result.success, true); // 11. Verify user is deleted result = await get_user('user1'); t.is(result.success, false); });