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