Tests for: - user.ts: User class, roles, permissions, status checks - util.ts: loadFile utility - handlers.ts: multiHandler - types.ts: methodParser, requireAuth, requirePermission - logging.ts: module structure - database.ts: connectionConfig, raw queries, PostgresAuthStore - auth/token.ts: generateToken, hashToken, parseAuthorizationHeader - auth/password.ts: hashPassword, verifyPassword (scrypt) - auth/types.ts: Zod parsers, Session class, tokenLifetimes - auth/store.ts: InMemoryAuthStore - auth/service.ts: AuthService (login, register, verify, reset) - basic/*.ts: route structure tests Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
286 lines
9.2 KiB
TypeScript
286 lines
9.2 KiB
TypeScript
// 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);
|
|
});
|
|
});
|
|
});
|
|
});
|