Add test infrastructure for hydrators using node:test
- Add docker-compose.test.yml with isolated PostgreSQL on port 5433 - Add environment variable support for database connection config - Add test setup utilities and initial user hydrator tests - Add test and test:watch scripts to package.json Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
13
docker-compose.test.yml
Normal file
13
docker-compose.test.yml
Normal file
@@ -0,0 +1,13 @@
|
||||
name: diachron-test
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:17
|
||||
ports:
|
||||
- "5433:5432"
|
||||
environment:
|
||||
POSTGRES_USER: diachron_test
|
||||
POSTGRES_PASSWORD: diachron_test
|
||||
POSTGRES_DB: diachron_test
|
||||
tmpfs:
|
||||
- /var/lib/postgresql/data
|
||||
@@ -21,13 +21,13 @@ import type { SessionData, TokenId } from "./auth/types";
|
||||
import type { Domain } from "./types";
|
||||
import { AuthenticatedUser, type User, type UserId } from "./user";
|
||||
|
||||
// Connection configuration
|
||||
// Connection configuration (supports environment variable overrides)
|
||||
const connectionConfig = {
|
||||
host: "localhost",
|
||||
port: 5432,
|
||||
user: "diachron",
|
||||
password: "diachron",
|
||||
database: "diachron",
|
||||
host: process.env.DB_HOST ?? "localhost",
|
||||
port: Number(process.env.DB_PORT ?? 5432),
|
||||
user: process.env.DB_USER ?? "diachron",
|
||||
password: process.env.DB_PASSWORD ?? "diachron",
|
||||
database: process.env.DB_NAME ?? "diachron",
|
||||
};
|
||||
|
||||
// Database schema types for Kysely
|
||||
|
||||
29
express/framework/hydrators/hydrator.ts
Normal file
29
express/framework/hydrators/hydrator.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Kysely, PostgresDialect } from "kysely";
|
||||
import { Pool } from "pg";
|
||||
import { connectionConfig } from "../../database";
|
||||
import type { DB } from "../../generated/db";
|
||||
|
||||
const db = new Kysely<DB>({
|
||||
dialect: new PostgresDialect({
|
||||
pool: new Pool(connectionConfig),
|
||||
}),
|
||||
log(event) {
|
||||
if (event.level === "query") {
|
||||
// FIXME: Wire this up to the logging system
|
||||
console.log("SQL:", event.query.sql);
|
||||
console.log("Params:", event.query.parameters);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
abstract class Hydrator<T> {
|
||||
public db: Kysely<DB>;
|
||||
|
||||
protected abstract table: string;
|
||||
|
||||
constructor() {
|
||||
this.db = db;
|
||||
}
|
||||
}
|
||||
|
||||
export { Hydrator, db };
|
||||
44
express/framework/hydrators/tests/setup.ts
Normal file
44
express/framework/hydrators/tests/setup.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
// Test setup for hydrator tests
|
||||
// Run: DB_PORT=5433 DB_USER=diachron_test DB_PASSWORD=diachron_test DB_NAME=diachron_test npx tsx --test tests/*.test.ts
|
||||
|
||||
import { Pool } from "pg";
|
||||
import { connectionConfig, migrate } from "../../../database";
|
||||
|
||||
const pool = new Pool(connectionConfig);
|
||||
|
||||
export async function setupTestDatabase(): Promise<void> {
|
||||
await migrate();
|
||||
}
|
||||
|
||||
export async function cleanupTables(): Promise<void> {
|
||||
// Clean in reverse dependency order
|
||||
await pool.query("DELETE FROM user_emails");
|
||||
await pool.query("DELETE FROM users");
|
||||
}
|
||||
|
||||
export async function teardownTestDatabase(): Promise<void> {
|
||||
await pool.end();
|
||||
}
|
||||
|
||||
export async function insertTestUser(data: {
|
||||
id: string;
|
||||
displayName: string;
|
||||
status: string;
|
||||
email: string;
|
||||
}): Promise<void> {
|
||||
const emailId = crypto.randomUUID();
|
||||
const normalizedEmail = data.email.toLowerCase().trim();
|
||||
|
||||
await pool.query(
|
||||
`INSERT INTO users (id, display_name, status) VALUES ($1, $2, $3)`,
|
||||
[data.id, data.displayName, data.status],
|
||||
);
|
||||
|
||||
await pool.query(
|
||||
`INSERT INTO user_emails (id, user_id, email, normalized_email, is_primary)
|
||||
VALUES ($1, $2, $3, $4, true)`,
|
||||
[emailId, data.id, data.email, normalizedEmail],
|
||||
);
|
||||
}
|
||||
|
||||
export { pool };
|
||||
98
express/framework/hydrators/tests/user.test.ts
Normal file
98
express/framework/hydrators/tests/user.test.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
// Tests for user hydrator
|
||||
// Run with: cd express && DB_PORT=5433 DB_USER=diachron_test DB_PASSWORD=diachron_test DB_NAME=diachron_test ../cmd npx tsx --test framework/hydrators/tests/user.test.ts
|
||||
|
||||
import { describe, it, before, after, beforeEach } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
setupTestDatabase,
|
||||
cleanupTables,
|
||||
teardownTestDatabase,
|
||||
insertTestUser,
|
||||
} from "./setup";
|
||||
import { get } from "../user";
|
||||
|
||||
describe("user hydrator", () => {
|
||||
before(async () => {
|
||||
await setupTestDatabase();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await teardownTestDatabase();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await cleanupTables();
|
||||
});
|
||||
|
||||
describe("get", () => {
|
||||
it("returns null for non-existent user", async () => {
|
||||
const result = await get("00000000-0000-0000-0000-000000000000");
|
||||
assert.equal(result, null);
|
||||
});
|
||||
|
||||
it("returns user when found", async () => {
|
||||
const userId = "cfae0a19-6515-4813-bc2d-1e032b72b203";
|
||||
await insertTestUser({
|
||||
id: userId,
|
||||
displayName: "Test User",
|
||||
status: "active",
|
||||
email: "test@example.com",
|
||||
});
|
||||
|
||||
const result = await get(userId);
|
||||
|
||||
assert.notEqual(result, null);
|
||||
assert.equal(result!.id, userId);
|
||||
assert.equal(result!.display_name, "Test User");
|
||||
assert.equal(result!.status, "active");
|
||||
assert.equal(result!.email, "test@example.com");
|
||||
});
|
||||
|
||||
it("validates user data with zod parser", async () => {
|
||||
const userId = crypto.randomUUID();
|
||||
await insertTestUser({
|
||||
id: userId,
|
||||
displayName: "Valid User",
|
||||
status: "active",
|
||||
email: "valid@example.com",
|
||||
});
|
||||
|
||||
const result = await get(userId);
|
||||
|
||||
// If we get here without throwing, parsing succeeded
|
||||
assert.notEqual(result, null);
|
||||
assert.equal(typeof result!.id, "string");
|
||||
assert.equal(typeof result!.email, "string");
|
||||
});
|
||||
|
||||
it("returns user with pending status", async () => {
|
||||
const userId = crypto.randomUUID();
|
||||
await insertTestUser({
|
||||
id: userId,
|
||||
displayName: "Pending User",
|
||||
status: "pending",
|
||||
email: "pending@example.com",
|
||||
});
|
||||
|
||||
const result = await get(userId);
|
||||
|
||||
assert.notEqual(result, null);
|
||||
assert.equal(result!.status, "pending");
|
||||
});
|
||||
|
||||
it("returns user with suspended status", async () => {
|
||||
const userId = crypto.randomUUID();
|
||||
await insertTestUser({
|
||||
id: userId,
|
||||
displayName: "Suspended User",
|
||||
status: "suspended",
|
||||
email: "suspended@example.com",
|
||||
});
|
||||
|
||||
const result = await get(userId);
|
||||
|
||||
assert.notEqual(result, null);
|
||||
assert.equal(result!.status, "suspended");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -4,7 +4,8 @@
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"test": "DB_PORT=5433 DB_USER=diachron_test DB_PASSWORD=diachron_test DB_NAME=diachron_test tsx --test 'framework/**/*.{test,spec}.ts'",
|
||||
"test:watch": "DB_PORT=5433 DB_USER=diachron_test DB_PASSWORD=diachron_test DB_NAME=diachron_test tsx --test --watch 'framework/**/*.{test,spec}.ts'",
|
||||
"nodemon": "nodemon dist/index.js",
|
||||
"kysely-codegen": "kysely-codegen"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user