UNPKG

@joystick.js/db-canary

Version:

JoystickDB - A minimalist database server for the Joystick framework

573 lines (452 loc) 15 kB
import test from 'ava'; import { existsSync, unlinkSync, statSync } from 'fs'; import { load_or_generate_api_key, validate_api_key, create_user, get_all_users, get_user, update_user, delete_user, verify_user_password, check_admin_user_exists, initialize_api_key_manager, reset_api_key_state } from '../../../src/server/lib/api_key_manager.js'; import { initialize_database, cleanup_database } from '../../../src/server/lib/query_engine.js'; const API_KEY_FILE_PATH = './API_KEY'; test.beforeEach(async (t) => { // Clean up any existing database first try { await cleanup_database(); } catch (error) { // Ignore cleanup errors } // Clean up any existing API key file and reset state reset_api_key_state(); // Initialize database for user storage tests with unique path per test const test_db_path = `./test_data_api_key_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; initialize_database(test_db_path); // Store the test db path for cleanup t.context.test_db_path = test_db_path; }); test.afterEach(async (t) => { // Clean up database and API key state try { await cleanup_database(true); // Remove test database directory } catch (error) { // Ignore cleanup errors } reset_api_key_state(); }); test('load_or_generate_api_key generates new API key when file does not exist', (t) => { // Temporarily set production mode to test actual key generation const original_env = process.env.NODE_ENV; process.env.NODE_ENV = 'production'; try { const api_key = load_or_generate_api_key(); t.is(typeof api_key, 'string'); t.is(api_key.length, 32); t.true(/^[A-Za-z0-9]{32}$/.test(api_key)); t.true(existsSync(API_KEY_FILE_PATH)); } finally { process.env.NODE_ENV = original_env; } }); test('load_or_generate_api_key loads existing API key from file', (t) => { const first_key = load_or_generate_api_key(); const second_key = load_or_generate_api_key(); t.is(first_key, second_key); }); test('load_or_generate_api_key generates unique keys', (t) => { // Temporarily set production mode to test actual key generation const original_env = process.env.NODE_ENV; process.env.NODE_ENV = 'production'; try { const first_key = load_or_generate_api_key(); reset_api_key_state(); const second_key = load_or_generate_api_key(); t.not(first_key, second_key); } finally { process.env.NODE_ENV = original_env; } }); test('validate_api_key returns true for valid API key', (t) => { const api_key = load_or_generate_api_key(); const is_valid = validate_api_key(api_key); t.true(is_valid); }); test('validate_api_key returns false for invalid API key', (t) => { // Temporarily set production mode to test actual validation const original_env = process.env.NODE_ENV; process.env.NODE_ENV = 'production'; try { load_or_generate_api_key(); const is_valid = validate_api_key('invalid_key'); t.false(is_valid); } finally { process.env.NODE_ENV = original_env; } }); test('validate_api_key returns false for null/undefined API key', (t) => { // Temporarily set production mode to test actual validation const original_env = process.env.NODE_ENV; process.env.NODE_ENV = 'production'; try { load_or_generate_api_key(); t.false(validate_api_key(null)); t.false(validate_api_key(undefined)); t.false(validate_api_key('')); } finally { process.env.NODE_ENV = original_env; } }); test('create_user creates user with valid data', async (t) => { const user_data = { username: 'testuser', password: 'testpassword123', role: 'read_write' }; const created_user = await create_user(user_data); t.is(created_user.username, 'testuser'); t.is(created_user.role, 'read_write'); t.true(typeof created_user.created_at === 'string'); t.is(created_user.password_hash, undefined); // Should not return password hash }); test('create_user validates username format', async (t) => { const invalid_usernames = ['ab', 'user@name', 'user name', '', null, undefined]; for (const username of invalid_usernames) { const error = await t.throwsAsync(async () => { await create_user({ username, password: 'testpassword123', role: 'read_write' }); }); t.true(error.message.includes('Username must be alphanumeric and 3-50 characters long')); } }); test('create_user validates password requirements', async (t) => { const invalid_passwords = ['short', '', null, undefined]; for (const password of invalid_passwords) { const error = await t.throwsAsync(async () => { await create_user({ username: 'testuser', password, role: 'read_write' }); }); t.true(error.message.includes('Password')); } }); test('create_user validates role values', async (t) => { const invalid_roles = ['admin', 'superuser', '', null, undefined]; for (const role of invalid_roles) { const error = await t.throwsAsync(async () => { await create_user({ username: 'testuser', password: 'testpassword123', role }); }); t.true(error.message.includes('Role must be one of: read, write, read_write')); } }); test('create_user prevents duplicate usernames', async (t) => { const user_data = { username: 'testuser', password: 'testpassword123', role: 'read_write' }; await create_user(user_data); const error = await t.throwsAsync(async () => { await create_user(user_data); }); t.is(error.message, 'Username already exists'); }); test('get_all_users returns empty array when no users exist', (t) => { const users = get_all_users(); t.true(Array.isArray(users)); t.is(users.length, 0); }); test('get_all_users returns all users without password hashes', async (t) => { await create_user({ username: 'user1', password: 'password123', role: 'read' }); await create_user({ username: 'user2', password: 'password456', role: 'write' }); const users = get_all_users(); t.is(users.length, 2); const user1 = users.find(u => u.username === 'user1'); const user2 = users.find(u => u.username === 'user2'); t.truthy(user1); t.truthy(user2); t.is(user1.role, 'read'); t.is(user2.role, 'write'); t.is(user1.password_hash, undefined); t.is(user2.password_hash, undefined); }); test('get_user returns user by username', async (t) => { await create_user({ username: 'testuser', password: 'testpassword123', role: 'read_write' }); const user = get_user('testuser'); t.truthy(user); t.is(user.username, 'testuser'); t.is(user.role, 'read_write'); t.is(user.password_hash, undefined); }); test('get_user returns null for non-existent user', (t) => { const user = get_user('nonexistent'); t.is(user, null); }); test('update_user updates user role', async (t) => { await create_user({ username: 'testuser', password: 'testpassword123', role: 'read' }); const updated_user = await update_user('testuser', { role: 'read_write' }); t.is(updated_user.username, 'testuser'); t.is(updated_user.role, 'read_write'); t.true(typeof updated_user.updated_at === 'string'); }); test('update_user updates user password', async (t) => { await create_user({ username: 'testuser', password: 'oldpassword123', role: 'read' }); const updated_user = await update_user('testuser', { password: 'newpassword456' }); t.is(updated_user.username, 'testuser'); t.is(updated_user.role, 'read'); // Verify new password works const verified_user = await verify_user_password('testuser', 'newpassword456'); t.truthy(verified_user); // Verify old password doesn't work const old_password_result = await verify_user_password('testuser', 'oldpassword123'); t.is(old_password_result, null); }); test('update_user throws error for non-existent user', async (t) => { const error = await t.throwsAsync(async () => { await update_user('nonexistent', { role: 'read_write' }); }); t.is(error.message, 'User not found'); }); test('update_user validates new role', async (t) => { await create_user({ username: 'testuser', password: 'testpassword123', role: 'read' }); const error = await t.throwsAsync(async () => { await update_user('testuser', { role: 'invalid_role' }); }); t.true(error.message.includes('Role must be one of: read, write, read_write')); }); test('update_user validates new password', async (t) => { await create_user({ username: 'testuser', password: 'testpassword123', role: 'read' }); const error = await t.throwsAsync(async () => { await update_user('testuser', { password: 'short' }); }); t.true(error.message.includes('Password must be at least 8 characters long')); }); test('delete_user removes user from database', async (t) => { await create_user({ username: 'testuser', password: 'testpassword123', role: 'read' }); const result = delete_user('testuser'); t.true(result); const user = get_user('testuser'); t.is(user, null); }); test('delete_user throws error for non-existent user', (t) => { const error = t.throws(() => { delete_user('nonexistent'); }); t.is(error.message, 'User not found'); }); test('verify_user_password returns user for correct password', async (t) => { await create_user({ username: 'testuser', password: 'testpassword123', role: 'read_write' }); const verified_user = await verify_user_password('testuser', 'testpassword123'); t.truthy(verified_user); t.is(verified_user.username, 'testuser'); t.is(verified_user.role, 'read_write'); t.is(verified_user.password_hash, undefined); }); test('verify_user_password returns null for incorrect password', async (t) => { await create_user({ username: 'testuser', password: 'testpassword123', role: 'read_write' }); const result = await verify_user_password('testuser', 'wrongpassword'); t.is(result, null); }); test('verify_user_password returns null for non-existent user', async (t) => { const result = await verify_user_password('nonexistent', 'anypassword'); t.is(result, null); }); test('check_admin_user_exists returns false when no users exist', (t) => { const has_admin = check_admin_user_exists(); t.false(has_admin); }); test('check_admin_user_exists returns false when no admin users exist', async (t) => { await create_user({ username: 'readuser', password: 'testpassword123', role: 'read' }); await create_user({ username: 'writeuser', password: 'testpassword123', role: 'write' }); const has_admin = check_admin_user_exists(); t.false(has_admin); }); test('check_admin_user_exists returns true when admin user exists', async (t) => { await create_user({ username: 'admin', password: 'testpassword123', role: 'read_write' }); const has_admin = check_admin_user_exists(); t.true(has_admin); }); test('create_user sets admin flag when creating read_write user', async (t) => { t.false(check_admin_user_exists()); await create_user({ username: 'admin', password: 'testpassword123', role: 'read_write' }); t.true(check_admin_user_exists()); }); test('initialize_api_key_manager generates API key and checks for admin users', (t) => { // Temporarily set production mode to test actual file creation const original_env = process.env.NODE_ENV; process.env.NODE_ENV = 'production'; try { t.notThrows(() => { initialize_api_key_manager(); }); t.true(existsSync(API_KEY_FILE_PATH)); } finally { process.env.NODE_ENV = original_env; } }); test('reset_api_key_state cleans up API key file and state', (t) => { // Temporarily set production mode to test actual file creation const original_env = process.env.NODE_ENV; process.env.NODE_ENV = 'production'; try { load_or_generate_api_key(); t.true(existsSync(API_KEY_FILE_PATH)); reset_api_key_state(); t.false(existsSync(API_KEY_FILE_PATH)); } finally { process.env.NODE_ENV = original_env; } }); test('API key file has secure permissions', (t) => { // Temporarily set production mode to test actual file creation const original_env = process.env.NODE_ENV; process.env.NODE_ENV = 'production'; try { load_or_generate_api_key(); const stats = statSync(API_KEY_FILE_PATH); const mode = stats.mode & parseInt('777', 8); // Should be readable/writable by owner only (600) t.is(mode, parseInt('600', 8)); } finally { process.env.NODE_ENV = original_env; } }); test('password hashing uses bcrypt', async (t) => { const user_data = { username: 'testuser', password: 'testpassword123', role: 'read' }; await create_user(user_data); // Verify password works const verified_user = await verify_user_password('testuser', 'testpassword123'); t.truthy(verified_user); // Verify wrong password fails const wrong_password_result = await verify_user_password('testuser', 'wrongpassword'); t.is(wrong_password_result, null); }); test('username validation accepts valid usernames', async (t) => { const valid_usernames = ['abc', 'user123', 'TestUser', 'a'.repeat(50)]; for (const username of valid_usernames) { await t.notThrowsAsync(async () => { await create_user({ username, password: 'testpassword123', role: 'read' }); }); // Clean up for next iteration delete_user(username); } }); test('password validation accepts valid passwords', async (t) => { const valid_passwords = [ 'password', 'testpassword123', 'a'.repeat(128), // Max length 'complex!@#$%^&*()password' ]; for (let i = 0; i < valid_passwords.length; i++) { const username = `user${i}`; await t.notThrowsAsync(async () => { await create_user({ username, password: valid_passwords[i], role: 'read' }); }); } }); test('password validation rejects passwords that are too long', async (t) => { const too_long_password = 'a'.repeat(129); // Over 128 character limit const error = await t.throwsAsync(async () => { await create_user({ username: 'testuser', password: too_long_password, role: 'read' }); }); t.true(error.message.includes('Password must be no more than 128 characters long')); }); test('role validation accepts all valid roles', async (t) => { const valid_roles = ['read', 'write', 'read_write']; for (let i = 0; i < valid_roles.length; i++) { const username = `user${i}`; await t.notThrowsAsync(async () => { await create_user({ username, password: 'testpassword123', role: valid_roles[i] }); }); } });