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
+897
View File
@@ -0,0 +1,897 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { createClient } from "@libsql/client";
import { drizzle } from "drizzle-orm/libsql";
import { mkdirSync, rmSync, existsSync } from "fs";
import { resolve } from "path";
import { tmpdir } from "os";
// Import the exported utility functions from client.ts
import {
buildDbUrl,
getDbPaths,
ensureDataDirectory,
getTableCreationSQL,
runTableMigrations,
ensureDefaultUser,
} from "../db/client.js";
// Import the exported utility functions from migrate.ts
import {
getMigrationSQL,
splitSQLStatements,
executeMigration,
getStatementPreview,
} from "../db/migrate.js";
describe("Migration Script Utilities", () => {
describe("getMigrationSQL", () => {
it("should return a non-empty SQL string", () => {
const sql = getMigrationSQL();
expect(typeof sql).toBe("string");
expect(sql.length).toBeGreaterThan(100);
});
it("should contain all table definitions", () => {
const sql = getMigrationSQL();
expect(sql).toContain("CREATE TABLE IF NOT EXISTS users");
expect(sql).toContain("CREATE TABLE IF NOT EXISTS medications");
expect(sql).toContain("CREATE TABLE IF NOT EXISTS user_settings");
expect(sql).toContain("CREATE TABLE IF NOT EXISTS refresh_tokens");
expect(sql).toContain("CREATE TABLE IF NOT EXISTS share_tokens");
expect(sql).toContain("CREATE TABLE IF NOT EXISTS dose_tracking");
});
});
describe("splitSQLStatements", () => {
it("should split SQL by semicolons", () => {
const sql = "SELECT 1; SELECT 2; SELECT 3;";
const statements = splitSQLStatements(sql);
expect(statements).toHaveLength(3);
});
it("should filter out empty statements", () => {
const sql = "SELECT 1;; ; SELECT 2;";
const statements = splitSQLStatements(sql);
expect(statements).toHaveLength(2);
});
it("should handle statements without trailing semicolon", () => {
const sql = "SELECT 1; SELECT 2";
const statements = splitSQLStatements(sql);
expect(statements).toHaveLength(2);
});
it("should split migration SQL into 6 statements", () => {
const sql = getMigrationSQL();
const statements = splitSQLStatements(sql);
expect(statements).toHaveLength(6);
});
it("should preserve whitespace within statements", () => {
const sql = "CREATE TABLE test (\n id INTEGER\n);";
const statements = splitSQLStatements(sql);
expect(statements[0]).toContain("\n");
});
});
describe("getStatementPreview", () => {
it("should return full string if shorter than maxLength", () => {
const preview = getStatementPreview("SELECT 1", 50);
expect(preview).toBe("SELECT 1");
});
it("should truncate and add ellipsis if longer than maxLength", () => {
const preview = getStatementPreview("SELECT * FROM very_long_table_name WHERE condition = true", 20);
expect(preview).toBe("SELECT * FROM very_l...");
expect(preview.length).toBe(23); // 20 + "..."
});
it("should use default maxLength of 50", () => {
const longStmt = "A".repeat(100);
const preview = getStatementPreview(longStmt);
expect(preview).toBe("A".repeat(50) + "...");
});
it("should trim whitespace", () => {
const preview = getStatementPreview(" SELECT 1 ", 50);
expect(preview).toBe("SELECT 1");
});
it("should handle CREATE TABLE statements", () => {
const stmt = "CREATE TABLE IF NOT EXISTS users (id integer PRIMARY KEY)";
const preview = getStatementPreview(stmt, 30);
expect(preview).toBe("CREATE TABLE IF NOT EXISTS use...");
});
});
describe("executeMigration", () => {
let client: ReturnType<typeof createClient>;
beforeEach(() => {
client = createClient({ url: ":memory:" });
});
it("should execute all migrations successfully", async () => {
const result = await executeMigration(client);
expect(result.success).toBe(true);
expect(result.executed).toBe(6);
expect(result.errors).toHaveLength(0);
});
it("should create all tables", async () => {
await executeMigration(client);
const tables = await client.execute(
"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"
);
const tableNames = tables.rows.map(r => r.name);
expect(tableNames).toContain("users");
expect(tableNames).toContain("medications");
expect(tableNames).toContain("user_settings");
expect(tableNames).toContain("refresh_tokens");
expect(tableNames).toContain("share_tokens");
expect(tableNames).toContain("dose_tracking");
});
it("should be idempotent", async () => {
await executeMigration(client);
const result = await executeMigration(client);
expect(result.success).toBe(true);
expect(result.executed).toBe(6);
});
it("should allow inserting data after migration", async () => {
await executeMigration(client);
await client.execute("INSERT INTO users (username) VALUES ('testuser')");
const result = await client.execute("SELECT * FROM users");
expect(result.rows).toHaveLength(1);
});
});
});
describe("Database Client Utilities", () => {
describe("buildDbUrl", () => {
it("should build a file:// URL from path", () => {
const url = buildDbUrl("/path/to/db.sqlite");
expect(url).toBe("file:/path/to/db.sqlite");
});
it("should handle relative paths", () => {
const url = buildDbUrl("./data/test.db");
expect(url).toBe("file:./data/test.db");
});
});
describe("getDbPaths", () => {
it("should return correct paths based on cwd", () => {
const paths = getDbPaths("/app");
expect(paths.dataDir).toBe("/app/data");
expect(paths.dbPath).toBe("/app/data/medassist-ng.db");
expect(paths.url).toBe("file:/app/data/medassist-ng.db");
});
it("should use process.cwd() by default", () => {
const paths = getDbPaths();
expect(paths.dataDir).toContain("data");
expect(paths.dbPath).toContain("medassist-ng.db");
});
});
describe("ensureDataDirectory", () => {
const testDir = resolve(tmpdir(), `test-data-dir-${Date.now()}`);
afterEach(() => {
try {
if (existsSync(testDir)) {
rmSync(testDir, { recursive: true, force: true });
}
} catch {
// ignore cleanup errors
}
});
it("should create directory if it does not exist", () => {
const result = ensureDataDirectory(testDir);
expect(result.success).toBe(true);
expect(existsSync(testDir)).toBe(true);
});
it("should succeed if directory already exists", () => {
mkdirSync(testDir, { recursive: true });
const result = ensureDataDirectory(testDir);
expect(result.success).toBe(true);
});
it("should create .write-test file", () => {
const result = ensureDataDirectory(testDir);
expect(result.success).toBe(true);
expect(existsSync(resolve(testDir, ".write-test"))).toBe(true);
});
it("should return error for invalid path", () => {
// Try to create in a path that can't exist
const result = ensureDataDirectory("/nonexistent/root/path/that/cannot/exist");
expect(result.success).toBe(false);
expect(result.error).toBeDefined();
});
});
describe("getTableCreationSQL", () => {
it("should return array of SQL statements", () => {
const statements = getTableCreationSQL();
expect(Array.isArray(statements)).toBe(true);
expect(statements.length).toBe(6);
});
it("should include users table", () => {
const statements = getTableCreationSQL();
const usersSQL = statements.find(s => s.includes("CREATE TABLE IF NOT EXISTS users"));
expect(usersSQL).toBeDefined();
expect(usersSQL).toContain("username text NOT NULL UNIQUE");
expect(usersSQL).toContain("password_hash text");
expect(usersSQL).toContain("auth_provider text NOT NULL DEFAULT 'local'");
});
it("should include medications table", () => {
const statements = getTableCreationSQL();
const medsSQL = statements.find(s => s.includes("CREATE TABLE IF NOT EXISTS medications"));
expect(medsSQL).toBeDefined();
expect(medsSQL).toContain("user_id integer NOT NULL");
expect(medsSQL).toContain("taken_by_json text NOT NULL DEFAULT '[]'");
expect(medsSQL).toContain("FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE");
});
it("should include user_settings table", () => {
const statements = getTableCreationSQL();
const settingsSQL = statements.find(s => s.includes("CREATE TABLE IF NOT EXISTS user_settings"));
expect(settingsSQL).toBeDefined();
expect(settingsSQL).toContain("email_enabled integer NOT NULL DEFAULT 0");
expect(settingsSQL).toContain("language text NOT NULL DEFAULT 'en'");
});
it("should include refresh_tokens table", () => {
const statements = getTableCreationSQL();
const tokensSQL = statements.find(s => s.includes("CREATE TABLE IF NOT EXISTS refresh_tokens"));
expect(tokensSQL).toBeDefined();
expect(tokensSQL).toContain("token_id text NOT NULL UNIQUE");
});
it("should include share_tokens table", () => {
const statements = getTableCreationSQL();
const shareSQL = statements.find(s => s.includes("CREATE TABLE IF NOT EXISTS share_tokens"));
expect(shareSQL).toBeDefined();
expect(shareSQL).toContain("taken_by text NOT NULL");
});
it("should include dose_tracking table", () => {
const statements = getTableCreationSQL();
const doseSQL = statements.find(s => s.includes("CREATE TABLE IF NOT EXISTS dose_tracking"));
expect(doseSQL).toBeDefined();
expect(doseSQL).toContain("dose_id text NOT NULL");
expect(doseSQL).toContain("marked_by text");
});
});
describe("runTableMigrations", () => {
let client: ReturnType<typeof createClient>;
beforeEach(() => {
client = createClient({ url: ":memory:" });
});
it("should create all tables successfully", async () => {
const result = await runTableMigrations(client);
expect(result.success).toBe(true);
expect(result.errors).toHaveLength(0);
});
it("should be idempotent (run twice without errors)", async () => {
await runTableMigrations(client);
const result = await runTableMigrations(client);
expect(result.success).toBe(true);
expect(result.errors).toHaveLength(0);
});
it("should create all 6 tables", async () => {
await runTableMigrations(client);
const tables = await client.execute(
"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"
);
const tableNames = tables.rows.map(r => r.name);
expect(tableNames).toContain("users");
expect(tableNames).toContain("medications");
expect(tableNames).toContain("user_settings");
expect(tableNames).toContain("refresh_tokens");
expect(tableNames).toContain("share_tokens");
expect(tableNames).toContain("dose_tracking");
});
});
describe("ensureDefaultUser", () => {
let client: ReturnType<typeof createClient>;
beforeEach(async () => {
client = createClient({ url: ":memory:" });
await runTableMigrations(client);
});
it("should create default user when auth is disabled", async () => {
const created = await ensureDefaultUser(client, false);
expect(created).toBe(true);
const result = await client.execute("SELECT * FROM users WHERE id = 1");
expect(result.rows).toHaveLength(1);
expect(result.rows[0].username).toBe("default");
expect(result.rows[0].auth_provider).toBe("local");
});
it("should not create user when auth is enabled", async () => {
const created = await ensureDefaultUser(client, true);
expect(created).toBe(false);
const result = await client.execute("SELECT * FROM users WHERE id = 1");
expect(result.rows).toHaveLength(0);
});
it("should not duplicate user if already exists", async () => {
// First call creates the user
await ensureDefaultUser(client, false);
// Second call should not create again
const created = await ensureDefaultUser(client, false);
expect(created).toBe(false);
// Should still have only one user
const result = await client.execute("SELECT * FROM users");
expect(result.rows).toHaveLength(1);
});
});
});
describe("Database Client", () => {
describe("In-Memory Database Creation", () => {
it("should create an in-memory SQLite client", () => {
const client = createClient({ url: ":memory:" });
expect(client).toBeDefined();
});
it("should create a drizzle instance from client", () => {
const client = createClient({ url: ":memory:" });
const db = drizzle(client);
expect(db).toBeDefined();
});
it("should execute SQL statements", async () => {
const client = createClient({ url: ":memory:" });
// Create a simple test table
await client.execute(`
CREATE TABLE IF NOT EXISTS test_table (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL
)
`);
// Insert a row
await client.execute("INSERT INTO test_table (name) VALUES ('test')");
// Query the row
const result = await client.execute("SELECT * FROM test_table");
expect(result.rows).toHaveLength(1);
expect(result.rows[0].name).toBe("test");
});
});
describe("Table Schema Creation", () => {
let client: ReturnType<typeof createClient>;
beforeEach(async () => {
client = createClient({ url: ":memory:" });
});
it("should create users table", async () => {
await client.execute(`
CREATE TABLE IF NOT EXISTS users (
id integer PRIMARY KEY AUTOINCREMENT,
username text NOT NULL UNIQUE,
password_hash text,
avatar_url text,
auth_provider text NOT NULL DEFAULT 'local',
oidc_subject text,
is_active integer NOT NULL DEFAULT 1,
last_login_at integer,
created_at integer NOT NULL DEFAULT (strftime('%s','now')),
updated_at integer NOT NULL DEFAULT (strftime('%s','now'))
)
`);
// Verify table exists
const tables = await client.execute(
"SELECT name FROM sqlite_master WHERE type='table' AND name='users'"
);
expect(tables.rows).toHaveLength(1);
});
it("should create medications table with foreign key", async () => {
// First create users table
await client.execute(`
CREATE TABLE IF NOT EXISTS users (
id integer PRIMARY KEY AUTOINCREMENT,
username text NOT NULL UNIQUE,
auth_provider text NOT NULL DEFAULT 'local'
)
`);
await client.execute(`
CREATE TABLE IF NOT EXISTS medications (
id integer PRIMARY KEY AUTOINCREMENT,
user_id integer NOT NULL,
name text NOT NULL,
generic_name text,
taken_by_json text NOT NULL DEFAULT '[]',
pack_count integer NOT NULL DEFAULT 1,
blisters_per_pack integer NOT NULL DEFAULT 1,
pills_per_blister integer NOT NULL DEFAULT 1,
loose_tablets integer NOT NULL DEFAULT 0,
pill_weight_mg integer,
usage_json text NOT NULL DEFAULT '[]',
every_json text NOT NULL DEFAULT '[]',
start_json text NOT NULL DEFAULT '[]',
image_url text,
expiry_date text,
notes text,
intake_reminders_enabled integer NOT NULL DEFAULT 0,
updated_at integer NOT NULL DEFAULT (strftime('%s','now')),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)
`);
const tables = await client.execute(
"SELECT name FROM sqlite_master WHERE type='table' AND name='medications'"
);
expect(tables.rows).toHaveLength(1);
});
it("should create user_settings table", async () => {
await client.execute(`
CREATE TABLE IF NOT EXISTS users (
id integer PRIMARY KEY AUTOINCREMENT,
username text NOT NULL UNIQUE,
auth_provider text NOT NULL DEFAULT 'local'
)
`);
await client.execute(`
CREATE TABLE IF NOT EXISTS user_settings (
id integer PRIMARY KEY AUTOINCREMENT,
user_id integer NOT NULL UNIQUE,
email_enabled integer NOT NULL DEFAULT 0,
notification_email text,
email_stock_reminders integer NOT NULL DEFAULT 1,
email_intake_reminders integer NOT NULL DEFAULT 1,
shoutrrr_enabled integer NOT NULL DEFAULT 0,
shoutrrr_url text,
shoutrrr_stock_reminders integer NOT NULL DEFAULT 1,
shoutrrr_intake_reminders integer NOT NULL DEFAULT 1,
reminder_days_before integer NOT NULL DEFAULT 7,
repeat_daily_reminders integer NOT NULL DEFAULT 0,
low_stock_days integer NOT NULL DEFAULT 30,
normal_stock_days integer NOT NULL DEFAULT 90,
high_stock_days integer NOT NULL DEFAULT 180,
expiry_warning_days integer NOT NULL DEFAULT 90,
language text NOT NULL DEFAULT 'en',
stock_calculation_mode text NOT NULL DEFAULT 'automatic',
last_auto_email_sent text,
last_notification_type text,
last_notification_channel text,
updated_at integer NOT NULL DEFAULT (strftime('%s','now')),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)
`);
const tables = await client.execute(
"SELECT name FROM sqlite_master WHERE type='table' AND name='user_settings'"
);
expect(tables.rows).toHaveLength(1);
});
it("should create refresh_tokens table", async () => {
await client.execute(`
CREATE TABLE IF NOT EXISTS users (
id integer PRIMARY KEY AUTOINCREMENT,
username text NOT NULL UNIQUE,
auth_provider text NOT NULL DEFAULT 'local'
)
`);
await client.execute(`
CREATE TABLE IF NOT EXISTS refresh_tokens (
id integer PRIMARY KEY AUTOINCREMENT,
user_id integer NOT NULL,
token_id text NOT NULL UNIQUE,
expires_at integer NOT NULL,
rotated_at integer,
revoked integer NOT NULL DEFAULT 0,
created_at integer NOT NULL DEFAULT (strftime('%s','now')),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)
`);
const tables = await client.execute(
"SELECT name FROM sqlite_master WHERE type='table' AND name='refresh_tokens'"
);
expect(tables.rows).toHaveLength(1);
});
it("should create share_tokens table", async () => {
await client.execute(`
CREATE TABLE IF NOT EXISTS users (
id integer PRIMARY KEY AUTOINCREMENT,
username text NOT NULL UNIQUE,
auth_provider text NOT NULL DEFAULT 'local'
)
`);
await client.execute(`
CREATE TABLE IF NOT EXISTS share_tokens (
id integer PRIMARY KEY AUTOINCREMENT,
user_id integer NOT NULL,
token text NOT NULL UNIQUE,
taken_by text NOT NULL,
schedule_days integer NOT NULL DEFAULT 30,
created_at integer NOT NULL DEFAULT (strftime('%s','now')),
expires_at integer,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)
`);
const tables = await client.execute(
"SELECT name FROM sqlite_master WHERE type='table' AND name='share_tokens'"
);
expect(tables.rows).toHaveLength(1);
});
it("should create dose_tracking table", async () => {
await client.execute(`
CREATE TABLE IF NOT EXISTS users (
id integer PRIMARY KEY AUTOINCREMENT,
username text NOT NULL UNIQUE,
auth_provider text NOT NULL DEFAULT 'local'
)
`);
await client.execute(`
CREATE TABLE IF NOT EXISTS dose_tracking (
id integer PRIMARY KEY AUTOINCREMENT,
user_id integer NOT NULL,
dose_id text NOT NULL,
taken_at integer NOT NULL DEFAULT (strftime('%s','now')),
marked_by text,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)
`);
const tables = await client.execute(
"SELECT name FROM sqlite_master WHERE type='table' AND name='dose_tracking'"
);
expect(tables.rows).toHaveLength(1);
});
it("should enforce unique constraint on username", async () => {
await client.execute(`
CREATE TABLE IF NOT EXISTS users (
id integer PRIMARY KEY AUTOINCREMENT,
username text NOT NULL UNIQUE,
auth_provider text NOT NULL DEFAULT 'local'
)
`);
await client.execute("INSERT INTO users (username) VALUES ('testuser')");
await expect(
client.execute("INSERT INTO users (username) VALUES ('testuser')")
).rejects.toThrow();
});
it("should enforce unique constraint on refresh token_id", async () => {
await client.execute(`
CREATE TABLE IF NOT EXISTS users (
id integer PRIMARY KEY AUTOINCREMENT,
username text NOT NULL UNIQUE,
auth_provider text NOT NULL DEFAULT 'local'
)
`);
await client.execute("INSERT INTO users (username) VALUES ('testuser')");
await client.execute(`
CREATE TABLE IF NOT EXISTS refresh_tokens (
id integer PRIMARY KEY AUTOINCREMENT,
user_id integer NOT NULL,
token_id text NOT NULL UNIQUE,
expires_at integer NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)
`);
await client.execute(
"INSERT INTO refresh_tokens (user_id, token_id, expires_at) VALUES (1, 'token123', 1735689600)"
);
await expect(
client.execute(
"INSERT INTO refresh_tokens (user_id, token_id, expires_at) VALUES (1, 'token123', 1735689600)"
)
).rejects.toThrow();
});
});
describe("Default Values", () => {
let client: ReturnType<typeof createClient>;
beforeEach(async () => {
client = createClient({ url: ":memory:" });
await client.execute(`
CREATE TABLE IF NOT EXISTS users (
id integer PRIMARY KEY AUTOINCREMENT,
username text NOT NULL UNIQUE,
auth_provider text NOT NULL DEFAULT 'local',
is_active integer NOT NULL DEFAULT 1,
created_at integer NOT NULL DEFAULT (strftime('%s','now'))
)
`);
});
it("should use default values for auth_provider", async () => {
await client.execute("INSERT INTO users (username) VALUES ('testuser')");
const result = await client.execute("SELECT auth_provider FROM users WHERE username = 'testuser'");
expect(result.rows[0].auth_provider).toBe("local");
});
it("should use default values for is_active", async () => {
await client.execute("INSERT INTO users (username) VALUES ('testuser')");
const result = await client.execute("SELECT is_active FROM users WHERE username = 'testuser'");
expect(result.rows[0].is_active).toBe(1);
});
it("should generate created_at timestamp", async () => {
await client.execute("INSERT INTO users (username) VALUES ('testuser')");
const result = await client.execute("SELECT created_at FROM users WHERE username = 'testuser'");
expect(typeof result.rows[0].created_at).toBe("number");
// Should be a reasonable Unix timestamp (after year 2020)
expect(Number(result.rows[0].created_at)).toBeGreaterThan(1577836800);
});
});
describe("User Settings Defaults", () => {
let client: ReturnType<typeof createClient>;
beforeEach(async () => {
client = createClient({ url: ":memory:" });
await client.execute(`
CREATE TABLE IF NOT EXISTS users (
id integer PRIMARY KEY AUTOINCREMENT,
username text NOT NULL UNIQUE,
auth_provider text NOT NULL DEFAULT 'local'
)
`);
await client.execute("INSERT INTO users (username) VALUES ('testuser')");
await client.execute(`
CREATE TABLE IF NOT EXISTS user_settings (
id integer PRIMARY KEY AUTOINCREMENT,
user_id integer NOT NULL UNIQUE,
email_enabled integer NOT NULL DEFAULT 0,
shoutrrr_enabled integer NOT NULL DEFAULT 0,
reminder_days_before integer NOT NULL DEFAULT 7,
repeat_daily_reminders integer NOT NULL DEFAULT 0,
low_stock_days integer NOT NULL DEFAULT 30,
normal_stock_days integer NOT NULL DEFAULT 90,
high_stock_days integer NOT NULL DEFAULT 180,
expiry_warning_days integer NOT NULL DEFAULT 90,
language text NOT NULL DEFAULT 'en',
stock_calculation_mode text NOT NULL DEFAULT 'automatic',
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)
`);
});
it("should use default notification settings", async () => {
await client.execute("INSERT INTO user_settings (user_id) VALUES (1)");
const result = await client.execute("SELECT * FROM user_settings WHERE user_id = 1");
expect(result.rows[0].email_enabled).toBe(0);
expect(result.rows[0].shoutrrr_enabled).toBe(0);
});
it("should use default stock threshold settings", async () => {
await client.execute("INSERT INTO user_settings (user_id) VALUES (1)");
const result = await client.execute("SELECT * FROM user_settings WHERE user_id = 1");
expect(result.rows[0].low_stock_days).toBe(30);
expect(result.rows[0].normal_stock_days).toBe(90);
expect(result.rows[0].high_stock_days).toBe(180);
expect(result.rows[0].expiry_warning_days).toBe(90);
});
it("should use default language (en)", async () => {
await client.execute("INSERT INTO user_settings (user_id) VALUES (1)");
const result = await client.execute("SELECT language FROM user_settings WHERE user_id = 1");
expect(result.rows[0].language).toBe("en");
});
it("should use default stock_calculation_mode (automatic)", async () => {
await client.execute("INSERT INTO user_settings (user_id) VALUES (1)");
const result = await client.execute("SELECT stock_calculation_mode FROM user_settings WHERE user_id = 1");
expect(result.rows[0].stock_calculation_mode).toBe("automatic");
});
it("should use default reminder_days_before (7)", async () => {
await client.execute("INSERT INTO user_settings (user_id) VALUES (1)");
const result = await client.execute("SELECT reminder_days_before FROM user_settings WHERE user_id = 1");
expect(result.rows[0].reminder_days_before).toBe(7);
});
});
describe("Medication Defaults", () => {
let client: ReturnType<typeof createClient>;
beforeEach(async () => {
client = createClient({ url: ":memory:" });
await client.execute(`
CREATE TABLE IF NOT EXISTS users (
id integer PRIMARY KEY AUTOINCREMENT,
username text NOT NULL UNIQUE,
auth_provider text NOT NULL DEFAULT 'local'
)
`);
await client.execute("INSERT INTO users (username) VALUES ('testuser')");
await client.execute(`
CREATE TABLE IF NOT EXISTS medications (
id integer PRIMARY KEY AUTOINCREMENT,
user_id integer NOT NULL,
name text NOT NULL,
taken_by_json text NOT NULL DEFAULT '[]',
pack_count integer NOT NULL DEFAULT 1,
blisters_per_pack integer NOT NULL DEFAULT 1,
pills_per_blister integer NOT NULL DEFAULT 1,
loose_tablets integer NOT NULL DEFAULT 0,
usage_json text NOT NULL DEFAULT '[]',
every_json text NOT NULL DEFAULT '[]',
start_json text NOT NULL DEFAULT '[]',
intake_reminders_enabled integer NOT NULL DEFAULT 0,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)
`);
});
it("should use default inventory values", async () => {
await client.execute("INSERT INTO medications (user_id, name) VALUES (1, 'Test Med')");
const result = await client.execute("SELECT * FROM medications WHERE name = 'Test Med'");
expect(result.rows[0].pack_count).toBe(1);
expect(result.rows[0].blisters_per_pack).toBe(1);
expect(result.rows[0].pills_per_blister).toBe(1);
expect(result.rows[0].loose_tablets).toBe(0);
});
it("should use default JSON arrays for schedules", async () => {
await client.execute("INSERT INTO medications (user_id, name) VALUES (1, 'Test Med')");
const result = await client.execute("SELECT * FROM medications WHERE name = 'Test Med'");
expect(result.rows[0].taken_by_json).toBe("[]");
expect(result.rows[0].usage_json).toBe("[]");
expect(result.rows[0].every_json).toBe("[]");
expect(result.rows[0].start_json).toBe("[]");
});
it("should default intake_reminders_enabled to false (0)", async () => {
await client.execute("INSERT INTO medications (user_id, name) VALUES (1, 'Test Med')");
const result = await client.execute("SELECT intake_reminders_enabled FROM medications WHERE name = 'Test Med'");
expect(result.rows[0].intake_reminders_enabled).toBe(0);
});
});
describe("Foreign Key Constraints", () => {
let client: ReturnType<typeof createClient>;
beforeEach(async () => {
client = createClient({ url: ":memory:" });
// Enable foreign keys
await client.execute("PRAGMA foreign_keys = ON");
await client.execute(`
CREATE TABLE IF NOT EXISTS users (
id integer PRIMARY KEY AUTOINCREMENT,
username text NOT NULL UNIQUE
)
`);
await client.execute(`
CREATE TABLE IF NOT EXISTS medications (
id integer PRIMARY KEY AUTOINCREMENT,
user_id integer NOT NULL,
name text NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)
`);
});
it("should cascade delete medications when user is deleted", async () => {
await client.execute("INSERT INTO users (username) VALUES ('testuser')");
await client.execute("INSERT INTO medications (user_id, name) VALUES (1, 'Med1')");
await client.execute("INSERT INTO medications (user_id, name) VALUES (1, 'Med2')");
// Verify medications exist
let meds = await client.execute("SELECT * FROM medications");
expect(meds.rows).toHaveLength(2);
// Delete user
await client.execute("DELETE FROM users WHERE id = 1");
// Medications should be deleted too
meds = await client.execute("SELECT * FROM medications");
expect(meds.rows).toHaveLength(0);
});
});
describe("Default User Creation (Auth Disabled)", () => {
let client: ReturnType<typeof createClient>;
beforeEach(async () => {
client = createClient({ url: ":memory:" });
await client.execute(`
CREATE TABLE IF NOT EXISTS users (
id integer PRIMARY KEY AUTOINCREMENT,
username text NOT NULL UNIQUE,
auth_provider text NOT NULL DEFAULT 'local'
)
`);
});
it("should be able to create a default user with ID 1", async () => {
// This mimics the auth-disabled mode behavior
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')"
);
}
const user = await client.execute("SELECT * FROM users WHERE id = 1");
expect(user.rows).toHaveLength(1);
expect(user.rows[0].username).toBe("default");
expect(user.rows[0].auth_provider).toBe("local");
});
it("should not duplicate default user if already exists", async () => {
await client.execute(
"INSERT INTO users (id, username, auth_provider) VALUES (1, 'default', 'local')"
);
// Check if exists before insert (mimics runtime behavior)
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')"
);
}
// Should still have only one user
const users = await client.execute("SELECT * FROM users");
expect(users.rows).toHaveLength(1);
});
});
});