- Extract evaluateCondition + rule types into src/lib/rules.ts for testability - 48 unit tests for evaluateCondition (all fields/operators) and formatCategory - 21 integration tests for getTransactions filters and getParticipantBalances - Vitest configs for unit (vitest.config.ts) and integration (vitest.integration.config.ts) - setup-test-db.sh creates personal_test DB from production schema via pg_dump - Use vi.doMock + dynamic import pattern to isolate test DB from Prisma singleton
84 lines
2.5 KiB
TypeScript
84 lines
2.5 KiB
TypeScript
import { Pool } from "pg";
|
|
import { vi } from "vitest";
|
|
|
|
export function createPool() {
|
|
return new Pool({ connectionString: process.env.DATABASE_URL });
|
|
}
|
|
|
|
// Replace the app's Prisma-based queryRaw with a direct pg call so that
|
|
// tests don't depend on Prisma's singleton picking up the right DATABASE_URL.
|
|
// Must be called BEFORE dynamically importing any module that uses @/lib/db.
|
|
// Uses vi.doMock (not vi.mock) so it is NOT hoisted and CAN close over `p`.
|
|
export function mockDbWithPool(p: Pool) {
|
|
vi.resetModules(); // clear module cache so fresh imports pick up the mock
|
|
vi.doMock("@/lib/db", () => ({
|
|
queryRaw: async (sql: string, params: unknown[] = []) => {
|
|
const result = await p.query(sql, params);
|
|
return result.rows;
|
|
},
|
|
prisma: p,
|
|
}));
|
|
}
|
|
|
|
/** Wipe all data tables and restart sequences between tests. */
|
|
export async function resetDB(pool: Pool) {
|
|
await pool.query(`
|
|
TRUNCATE
|
|
split_payments,
|
|
transaction_splits,
|
|
transaction_tags,
|
|
transaction_overrides,
|
|
rule_apply_runs,
|
|
rules,
|
|
budgets,
|
|
account_owner_mappings,
|
|
transactions,
|
|
statements,
|
|
tags,
|
|
participants
|
|
RESTART IDENTITY CASCADE
|
|
`);
|
|
}
|
|
|
|
/** Seed two participants and return their IDs. */
|
|
export async function seedParticipants(pool: Pool, names: [string, string] = ["Alice", "Bob"]) {
|
|
const r1 = await pool.query(
|
|
`INSERT INTO participants (name) VALUES ($1) RETURNING id`,
|
|
[names[0]]
|
|
);
|
|
const r2 = await pool.query(
|
|
`INSERT INTO participants (name) VALUES ($1) RETURNING id`,
|
|
[names[1]]
|
|
);
|
|
return { ownerId: r1.rows[0].id as number, otherId: r2.rows[0].id as number };
|
|
}
|
|
|
|
/** Insert a manual transaction (no statement) and return its id. */
|
|
export async function insertTransaction(
|
|
pool: Pool,
|
|
ownerId: number,
|
|
overrides: {
|
|
description?: string;
|
|
amount?: number;
|
|
category?: string;
|
|
transaction_type?: string;
|
|
transaction_date?: string;
|
|
merchant_normalized?: string;
|
|
} = {}
|
|
): Promise<number> {
|
|
const r = await pool.query(
|
|
`INSERT INTO transactions
|
|
(owner_id, statement_id, transaction_date, description, amount, transaction_type, category, row_index)
|
|
VALUES ($1, NULL, $2, $3, $4, $5, $6, 0) RETURNING id`,
|
|
[
|
|
ownerId,
|
|
overrides.transaction_date ?? "2024-06-15",
|
|
overrides.description ?? "Test transaction",
|
|
overrides.amount ?? 100,
|
|
overrides.transaction_type ?? "debit",
|
|
overrides.category ?? "other",
|
|
]
|
|
);
|
|
return r.rows[0].id as number;
|
|
}
|