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