test: add unit and integration test suites
- 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
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user