// Tests for database.ts // Requires test PostgreSQL: docker compose -f docker-compose.test.yml up -d import assert from "node:assert/strict"; import { after, before, beforeEach, describe, it } from "node:test"; import { connectionConfig, db, migrate, migrationStatus, PostgresAuthStore, pool, raw, rawPool, } from "./database"; import type { UserId } from "./user"; describe("database", () => { before(async () => { // Run migrations to set up schema await migrate(); }); after(async () => { await pool.end(); }); describe("connectionConfig", () => { it("has required fields", () => { assert.ok("host" in connectionConfig); assert.ok("port" in connectionConfig); assert.ok("user" in connectionConfig); assert.ok("password" in connectionConfig); assert.ok("database" in connectionConfig); }); it("port is a number", () => { assert.equal(typeof connectionConfig.port, "number"); }); }); describe("raw", () => { it("executes raw SQL queries", async () => { const result = await raw<{ one: number }>("SELECT 1 as one"); assert.equal(result.length, 1); assert.equal(result[0].one, 1); }); it("supports parameterized queries", async () => { const result = await raw<{ sum: number }>( "SELECT $1::int + $2::int as sum", [2, 3], ); assert.equal(result[0].sum, 5); }); }); describe("db (Kysely instance)", () => { it("can execute SELECT queries", async () => { const result = await db .selectFrom("users") .select("id") .limit(1) .execute(); // May be empty, just verify it runs assert.ok(Array.isArray(result)); }); }); describe("rawPool", () => { it("is a pg Pool instance", () => { assert.ok(rawPool.query !== undefined); }); it("can execute queries", async () => { const result = await rawPool.query("SELECT 1 as one"); assert.equal(result.rows[0].one, 1); }); }); describe("migrate", () => { it("runs without error when migrations are up to date", async () => { // Should not throw await migrate(); }); }); describe("migrationStatus", () => { it("returns applied and pending arrays", async () => { const status = await migrationStatus(); assert.ok(Array.isArray(status.applied)); assert.ok(Array.isArray(status.pending)); }); it("shows framework migrations as applied", async () => { const status = await migrationStatus(); // At least the users migration should be applied const hasUsersMigration = status.applied.some((m) => m.includes("users"), ); assert.ok(hasUsersMigration); }); }); describe("PostgresAuthStore", () => { let store: PostgresAuthStore; before(() => { store = new PostgresAuthStore(); }); beforeEach(async () => { // Clean up test data before each test await rawPool.query("DELETE FROM sessions"); await rawPool.query("DELETE FROM user_credentials"); await rawPool.query("DELETE FROM user_emails"); await rawPool.query("DELETE FROM users"); }); describe("createUser", () => { it("creates a user with pending status", async () => { const user = await store.createUser({ email: "test@example.com", passwordHash: "hash123", displayName: "Test User", }); assert.equal(user.email, "test@example.com"); assert.equal(user.displayName, "Test User"); assert.equal(user.status, "pending"); }); it("stores the password hash", async () => { const user = await store.createUser({ email: "test@example.com", passwordHash: "secrethash", }); const hash = await store.getUserPasswordHash(user.id); assert.equal(hash, "secrethash"); }); }); describe("getUserByEmail", () => { it("returns user when found", async () => { await store.createUser({ email: "find@example.com", passwordHash: "hash", }); const user = await store.getUserByEmail("find@example.com"); assert.notEqual(user, null); assert.equal(user!.email, "find@example.com"); }); it("is case-insensitive", async () => { await store.createUser({ email: "UPPER@EXAMPLE.COM", passwordHash: "hash", }); const user = await store.getUserByEmail("upper@example.com"); assert.notEqual(user, null); }); it("returns null when not found", async () => { const user = await store.getUserByEmail("notfound@example.com"); assert.equal(user, null); }); }); describe("getUserById", () => { it("returns user when found", async () => { const created = await store.createUser({ email: "test@example.com", passwordHash: "hash", }); const user = await store.getUserById(created.id); assert.notEqual(user, null); assert.equal(user!.id, created.id); }); it("returns null when not found", async () => { const user = await store.getUserById( "00000000-0000-0000-0000-000000000000" as UserId, ); assert.equal(user, null); }); }); describe("setUserPassword", () => { it("updates the password hash", async () => { const user = await store.createUser({ email: "test@example.com", passwordHash: "oldhash", }); await store.setUserPassword(user.id, "newhash"); const hash = await store.getUserPasswordHash(user.id); assert.equal(hash, "newhash"); }); }); describe("updateUserEmailVerified", () => { it("sets user status to active", async () => { const created = await store.createUser({ email: "test@example.com", passwordHash: "hash", }); assert.equal(created.status, "pending"); await store.updateUserEmailVerified(created.id); const user = await store.getUserById(created.id); assert.equal(user!.status, "active"); }); }); describe("session operations", () => { let userId: UserId; beforeEach(async () => { const user = await store.createUser({ email: "session@example.com", passwordHash: "hash", }); userId = user.id; }); it("creates and retrieves sessions", async () => { const { token, session } = await store.createSession({ userId, tokenType: "session", authMethod: "cookie", expiresAt: new Date(Date.now() + 3600000), }); assert.ok(token.length > 0); assert.equal(session.userId, userId); assert.equal(session.tokenType, "session"); }); it("deletes sessions", async () => { const { session } = await store.createSession({ userId, tokenType: "session", authMethod: "cookie", expiresAt: new Date(Date.now() + 3600000), }); await store.deleteSession(session.tokenId as any); // Session should be soft-deleted (revoked) const retrieved = await store.getSession( session.tokenId as any, ); assert.equal(retrieved, null); }); it("deletes all user sessions", async () => { await store.createSession({ userId, tokenType: "session", authMethod: "cookie", expiresAt: new Date(Date.now() + 3600000), }); await store.createSession({ userId, tokenType: "session", authMethod: "bearer", expiresAt: new Date(Date.now() + 3600000), }); const count = await store.deleteUserSessions(userId); assert.equal(count, 2); }); }); }); });