@joystick.js/db-canary
Version:
JoystickDB - A minimalist database server for the Joystick framework
337 lines (279 loc) • 10.4 kB
JavaScript
import test from 'ava';
import sinon from 'sinon';
import net from 'net';
import { create_server } from '../../../src/server/index.js';
import { encode_message, create_message_parser } from '../../../src/server/lib/tcp_protocol.js';
import { initialize_database, cleanup_database } from '../../../src/server/lib/query_engine.js';
import { setup_initial_admin, reset_auth_state } from '../../../src/server/lib/user_auth_manager.js';
// Dynamic port allocation
let current_port = 3000;
const get_next_port = () => {
return ++current_port;
};
let server;
let port;
test.beforeEach(async () => {
// Reset auth state and clean up environment variables
await reset_auth_state();
delete process.env.JOYSTICK_DB_SETTINGS;
// Initialize database for testing
const db = initialize_database();
// Clear any existing user data from the database
try {
const user_prefix = '_admin:_users:';
for (const { key } of db.getRange({ start: user_prefix, end: user_prefix + '\xFF' })) {
db.remove(key);
}
} catch (error) {
// Ignore errors during cleanup
}
// Create server with dynamic port
const test_port = get_next_port();
server = await create_server();
// Start server on specific port
await new Promise((resolve) => {
server.listen(test_port, () => {
port = test_port;
setTimeout(resolve, 100);
});
});
});
test.afterEach(async () => {
if (server) {
await server.cleanup();
await new Promise((resolve) => {
server.close(resolve);
});
server = null;
}
// Clean up database
try {
await cleanup_database(true); // Remove test database directory
} catch (error) {
// Ignore cleanup errors
}
// Clean up environment variables
await reset_auth_state();
delete process.env.JOYSTICK_DB_SETTINGS;
});
const create_client = () => {
return new Promise((resolve, reject) => {
const client = net.createConnection(port, 'localhost');
const parser = create_message_parser();
client.on('connect', () => {
resolve({
client,
send: (data) => {
const encoded = encode_message(data);
client.write(encoded);
},
receive: () => {
return new Promise((resolve) => {
const handler = (data) => {
try {
const messages = parser.parse_messages(data);
for (const message of messages) {
client.off('data', handler);
resolve(message);
return;
}
} catch (error) {
// Continue listening
}
};
client.on('data', handler);
});
},
close: () => {
client.end();
}
});
});
client.on('error', reject);
});
};
test('integration - setup command creates authentication and returns password', async (t) => {
const { client, send, receive, close } = await create_client();
try {
send({
op: 'admin',
data: {
admin_action: 'setup_initial_admin',
username: 'admin',
password: 'admin123',
email: 'admin@test.com'
}
});
const response = await receive();
t.is(response.ok, 1);
// Check for message existence and content
t.truthy(response.message);
t.true(response.message.includes('Initial admin user created successfully'));
// Wait a bit for async operations to complete
await new Promise(resolve => setTimeout(resolve, 100));
// Verify environment variable was created
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.created_at === 'string');
} finally {
close();
}
});
test('integration - authentication succeeds with correct username and password', async (t) => {
// Setup authentication first
await setup_initial_admin('admin', 'admin123', 'admin@test.com');
const { client, send, receive, close } = await create_client();
try {
send({ op: 'authentication', data: { username: 'admin', password: 'admin123' } });
const response = await receive();
t.is(response.ok, 1);
t.is(response.version, '1.0.0');
t.is(response.message, 'Authentication successful');
} finally {
close();
}
});
test('integration - authentication fails with incorrect password', async (t) => {
// Setup authentication first
await setup_initial_admin('admin', 'admin123', 'admin@test.com');
const { client, send, receive, close } = await create_client();
try {
send({ op: 'authentication', data: { username: 'admin', password: 'wrong_password' } });
const response = await receive();
t.true(response.ok === 0 || response.ok === false);
t.true(response.error === 'Authentication failed' || response.error === 'Invalid credentials');
} finally {
close();
}
});
test('integration - database operations require authentication', async (t) => {
// Temporarily set production mode to test authentication requirements
const original_env = process.env.NODE_ENV;
process.env.NODE_ENV = 'production';
try {
// Setup authentication first
await setup_initial_admin('admin', 'admin123', 'admin@test.com');
const { client, send, receive, close } = await create_client();
try {
// Try to perform find operation without authentication
send({ op: 'find', data: { collection: 'users', filter: {} } });
const response = await receive();
t.true(response.ok === 0 || response.ok === false);
t.is(response.error, 'Authentication required');
} finally {
close();
}
} finally {
process.env.NODE_ENV = original_env;
}
});
test('integration - database operations work after authentication', async (t) => {
// Setup authentication first
await setup_initial_admin('admin', 'admin123', 'admin@test.com');
const { client, send, receive, close } = await create_client();
try {
// First authenticate
send({ op: 'authentication', data: { username: 'admin', password: 'admin123' } });
const auth_response = await receive();
t.is(auth_response.ok, 1);
t.is(auth_response.version, '1.0.0');
// Now try a database operation
send({ op: 'ping' });
const ping_response = await receive();
t.is(ping_response.ok, 1);
} finally {
close();
}
});
test('integration - admin operation includes authentication stats', async (t) => {
// Setup authentication first
await setup_initial_admin('admin', 'admin123', 'admin@test.com');
const { client, send, receive, close } = await create_client();
try {
// First authenticate
send({ op: 'authentication', data: { username: 'admin', password: 'admin123' } });
const auth_response = await receive();
t.is(auth_response.ok, 1);
t.is(auth_response.version, '1.0.0');
// Now try admin operation
send({ op: 'admin' });
const admin_response = await receive();
// This should be the admin response
t.true(typeof admin_response.authentication === 'object');
t.is(admin_response.authentication.authenticated_clients, 1);
// Check if authentication is configured by verifying we have user_count or configured flag
t.true(typeof admin_response.authentication.user_count === 'number' || admin_response.authentication.configured === true);
t.true(typeof admin_response.server === 'object');
t.true(typeof admin_response.database === 'object');
} finally {
close();
}
});
test('integration - rate limiting blocks multiple failed attempts', async (t) => {
// Setup authentication first
await setup_initial_admin('admin', 'admin123', 'admin@test.com');
// Make 5 failed attempts
for (let i = 0; i < 5; i++) {
const { client, send, receive, close } = await create_client();
try {
send({ op: 'authentication', data: { username: 'admin', password: 'wrong_password' } });
const response = await receive();
t.true(response.ok === 0 || response.ok === false);
t.true(response.error === 'Authentication failed' || response.error === 'Invalid credentials');
} finally {
close();
}
// Small delay between attempts
await new Promise(resolve => setTimeout(resolve, 100));
}
// 6th attempt should be rate limited
const { client, send, receive, close } = await create_client();
try {
send({ op: 'authentication', data: { username: 'admin', password: 'wrong_password' } });
const response = await receive();
t.true(response.ok === 0 || response.ok === false);
t.true(response.error.includes('Too many failed attempts'));
} finally {
close();
}
});
test('integration - setup fails when authentication already configured', async (t) => {
// Setup authentication first
await setup_initial_admin('admin', 'admin123', 'admin@test.com');
const { client, send, receive, close } = await create_client();
try {
// Try to setup again
send({
op: 'admin',
data: {
admin_action: 'setup_initial_admin',
username: 'admin2',
password: 'admin456',
email: 'admin2@test.com'
}
});
const response = await receive();
t.true(response.success === false);
// Check for error message with null safety
t.truthy(response.error || response.message);
const error_message = response.error || response.message;
t.true(error_message.includes('Initial admin already exists') || error_message.includes('already configured') || error_message.includes('already exists'));
} finally {
close();
}
});
test('integration - protocol versioning returns correct version', async (t) => {
// Setup authentication first
await setup_initial_admin('admin', 'admin123', 'admin@test.com');
const { client, send, receive, close } = await create_client();
try {
send({ op: 'authentication', data: { username: 'admin', password: 'admin123' } });
const response = await receive();
t.is(response.ok, 1);
t.is(response.version, '1.0.0');
t.is(response.message, 'Authentication successful');
} finally {
close();
}
});