feat: add comprehensive test suite and CI pipeline

- Add 402 unit tests with 61.7% code coverage
- Add Vitest configuration with coverage reporting
- Extract testable utility functions from services
- Create test.yml workflow (runs on PR and push to main)
- Update docker-build.yml to require tests before building
- Add scheduler-utils.ts and server-config.ts for testable code

Test files added:
- auth.test.ts, medications.test.ts, planner.test.ts
- settings.test.ts, doses.test.ts, share.test.ts
- database.test.ts, server.test.ts, services.test.ts
- env.test.ts, translations.test.ts, integration.test.ts
- e2e-routes.test.ts, stock-calculation.test.ts
This commit is contained in:
Daniel Volz
2025-12-30 11:14:52 +01:00
parent fe9310d3d4
commit ba3ebd27f4
27 changed files with 12666 additions and 401 deletions
+116 -64
View File
@@ -6,61 +6,46 @@ import dotenv from "dotenv";
dotenv.config({ path: process.env.DOTENV_PATH || ".env" });
// Use absolute path to ensure it works in Docker
const dataDir = resolve(process.cwd(), "data");
const dbPath = resolve(dataDir, "medassist-ng.db");
const url = `file:${dbPath}`;
// =============================================================================
// Exported utility functions for testing
// =============================================================================
console.log(`[DB] Data directory: ${dataDir}`);
console.log(`[DB] Database path: ${dbPath}`);
console.log(`[DB] Database URL: ${url}`);
/** Build the database URL from a path */
export function buildDbUrl(dbPath: string): string {
return `file:${dbPath}`;
}
// Ensure data directory exists and is writable
try {
if (!existsSync(dataDir)) {
mkdirSync(dataDir, { recursive: true });
console.log(`[DB] Created data directory: ${dataDir}`);
} else {
console.log(`[DB] Data directory exists: ${dataDir}`);
/** Get data directory and database path */
export function getDbPaths(cwd: string = process.cwd()): { dataDir: string; dbPath: string; url: string } {
const dataDir = resolve(cwd, "data");
const dbPath = resolve(dataDir, "medassist-ng.db");
const url = buildDbUrl(dbPath);
return { dataDir, dbPath, url };
}
/** Ensure data directory exists and is writable */
export function ensureDataDirectory(dataDir: string): { success: boolean; error?: string } {
try {
if (!existsSync(dataDir)) {
mkdirSync(dataDir, { recursive: true });
}
// Check if directory is writable
accessSync(dataDir, constants.W_OK);
// Try to create a test file to verify write access
const testFile = resolve(dataDir, ".write-test");
writeFileSync(testFile, "test");
return { success: true };
} catch (err: any) {
return { success: false, error: err.message };
}
// Check if directory is writable
accessSync(dataDir, constants.W_OK);
console.log(`[DB] Data directory is writable`);
// Log directory stats
const stats = statSync(dataDir);
console.log(`[DB] Directory permissions: ${stats.mode.toString(8)}`);
console.log(`[DB] Directory UID: ${stats.uid}, GID: ${stats.gid}`);
// Try to create a test file to verify write access
const testFile = resolve(dataDir, ".write-test");
writeFileSync(testFile, "test");
console.log(`[DB] Write test successful`);
} catch (err: any) {
console.error(`[DB] ERROR: Cannot access data directory: ${err.message}`);
console.error(`[DB] Please ensure the volume mount has correct permissions.`);
console.error(`[DB] Try running on host: sudo chown -R 1000:1000 ${dataDir}`);
process.exit(1);
}
let client: Client;
try {
client = createClient({ url });
console.log(`[DB] Database client created successfully`);
} catch (err: any) {
console.error(`[DB] ERROR: Failed to create database client: ${err.message}`);
console.error(`[DB] Database path: ${dbPath}`);
process.exit(1);
}
export const db = drizzle(client);
// Auto-run migrations (self-healing database)
async function runMigrations() {
// First, ensure all tables exist (for fresh databases)
const tableCreations = [
/** Get the SQL statements for creating all tables */
export function getTableCreationSQL(): string[] {
return [
`CREATE TABLE IF NOT EXISTS users (
id integer PRIMARY KEY AUTOINCREMENT,
username text NOT NULL UNIQUE,
@@ -148,31 +133,98 @@ async function runMigrations() {
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)`,
];
}
/** Run table creation migrations on a client */
export async function runTableMigrations(client: Client): Promise<{ success: boolean; errors: string[] }> {
const tableCreations = getTableCreationSQL();
const errors: string[] = [];
for (const sql of tableCreations) {
try {
await client.execute(sql);
} catch (e: any) {
console.error(`[DB] Table creation error:`, e.message);
errors.push(e.message);
}
}
return { success: errors.length === 0, errors };
}
/** Ensure default user exists for auth-disabled mode */
export async function ensureDefaultUser(client: Client, authEnabled: boolean): Promise<boolean> {
if (authEnabled) {
return false; // No default user needed
}
try {
const result = await client.execute("SELECT id FROM users WHERE id = 1");
if (result.rows.length === 0) {
await client.execute(
"INSERT INTO users (id, username, auth_provider) VALUES (1, 'default', 'local')"
);
return true; // Created
}
return false; // Already exists
} catch (e: any) {
console.error(`[DB] Error creating default user:`, e.message);
return false;
}
}
// =============================================================================
// Database initialization (runs on import)
// =============================================================================
// Use absolute path to ensure it works in Docker
const { dataDir, dbPath, url } = getDbPaths();
console.log(`[DB] Data directory: ${dataDir}`);
console.log(`[DB] Database path: ${dbPath}`);
console.log(`[DB] Database URL: ${url}`);
// Ensure data directory exists and is writable
const dirResult = ensureDataDirectory(dataDir);
if (!dirResult.success) {
console.error(`[DB] ERROR: Cannot access data directory: ${dirResult.error}`);
console.error(`[DB] Please ensure the volume mount has correct permissions.`);
console.error(`[DB] Try running on host: sudo chown -R 1000:1000 ${dataDir}`);
process.exit(1);
} else {
console.log(`[DB] Data directory is writable`);
// Log directory stats
const stats = statSync(dataDir);
console.log(`[DB] Directory permissions: ${stats.mode.toString(8)}`);
console.log(`[DB] Directory UID: ${stats.uid}, GID: ${stats.gid}`);
console.log(`[DB] Write test successful`);
}
let client: Client;
try {
client = createClient({ url });
console.log(`[DB] Database client created successfully`);
} catch (err: any) {
console.error(`[DB] ERROR: Failed to create database client: ${err.message}`);
console.error(`[DB] Database path: ${dbPath}`);
process.exit(1);
}
export const db = drizzle(client);
// Auto-run migrations (self-healing database)
async function runMigrations() {
const result = await runTableMigrations(client);
if (result.errors.length > 0) {
result.errors.forEach(err => console.error(`[DB] Table creation error:`, err));
}
console.log(`[DB] Tables verified/created`);
// If auth is disabled, ensure a default user exists (ID=1)
const authEnabled = process.env.AUTH_ENABLED === "true";
if (!authEnabled) {
try {
// Check if default user exists
const result = await client.execute("SELECT id FROM users WHERE id = 1");
if (result.rows.length === 0) {
await client.execute(
"INSERT INTO users (id, username, auth_provider) VALUES (1, 'default', 'local')"
);
console.log(`[DB] Created default user for auth-disabled mode`);
}
} catch (e: any) {
console.error(`[DB] Error creating default user:`, e.message);
}
const created = await ensureDefaultUser(client, authEnabled);
if (created) {
console.log(`[DB] Created default user for auth-disabled mode`);
}
}