fix: migrate E2E tests from SQLite to Postgres
Some checks failed
CI / ci (push) Successful in 1m0s
CI / e2e (push) Failing after 8m39s

- Rewrite e2e/seed.ts to use postgres driver instead of bun:sqlite
- Add userId to all seeded entities (multi-user schema)
- Add Postgres service container to CI E2E job
- Remove DATABASE_PATH from test server start script
- Re-enable E2E job in CI

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-06 20:56:11 +02:00
parent b769034b45
commit 54614869cf
4 changed files with 158 additions and 140 deletions

View File

@@ -30,11 +30,24 @@ jobs:
run: bun run build
e2e:
if: false # Disabled: E2E requires Postgres (Phase 14) — SQLite schema diverged at v2.0
needs: ci
runs-on: docker
container:
image: mcr.microsoft.com/playwright:v1.59.1-noble
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: gearbox
POSTGRES_PASSWORD: gearbox
POSTGRES_DB: gearbox
options: >-
--health-cmd "pg_isready -U gearbox"
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
DATABASE_URL: postgresql://gearbox:gearbox@postgres:5432/gearbox
steps:
- name: Checkout
uses: actions/checkout@v4

1
.gitignore vendored
View File

@@ -228,6 +228,7 @@ uploads/*
# Playwright
e2e/test.db
e2e/pgdata
test-results/
playwright-report/

View File

@@ -1,129 +1,145 @@
import { Database } from "bun:sqlite";
import { unlink } from "node:fs/promises";
import { drizzle } from "drizzle-orm/bun-sqlite";
import { migrate } from "drizzle-orm/bun-sqlite/migrator";
import { sql } from "drizzle-orm";
import { drizzle } from "drizzle-orm/postgres-js";
import { migrate } from "drizzle-orm/postgres-js/migrator";
import postgres from "postgres";
import * as schema from "../src/db/schema";
const DB_PATH = "./e2e/test.db";
const DATABASE_URL =
process.env.DATABASE_URL ||
"postgresql://gearbox:gearbox@localhost:5432/gearbox";
export async function seedTestDatabase() {
// Remove old test DB if it exists
try {
await unlink(DB_PATH);
} catch {
// File doesn't exist, that's fine
const client = postgres(DATABASE_URL, { max: 1 });
const db = drizzle(client, { schema });
// Run migrations
await migrate(db, { migrationsFolder: "./drizzle-pg" });
// Clean all tables for a fresh seed
const tables = [
"setup_items",
"setups",
"thread_candidates",
"threads",
"items",
"global_item_tags",
"global_items",
"tags",
"oauth_tokens",
"oauth_codes",
"oauth_clients",
"api_keys",
"settings",
"categories",
"users",
];
for (const t of tables) {
await db.execute(sql.raw(`TRUNCATE TABLE "${t}" RESTART IDENTITY CASCADE`));
}
const sqlite = new Database(DB_PATH);
sqlite.run("PRAGMA journal_mode = WAL");
sqlite.run("PRAGMA foreign_keys = ON");
const db = drizzle(sqlite, { schema });
migrate(db, { migrationsFolder: "./drizzle" });
// ── User ──
const [user] = await db
.insert(schema.users)
.values({ logtoSub: "e2e-test-user" })
.returning();
const userId = user.id;
// ── Categories ──
const [uncategorized] = db
const [uncategorized] = await db
.insert(schema.categories)
.values({ name: "Uncategorized", icon: "package" })
.returning()
.all();
.values({ name: "Uncategorized", icon: "package", userId })
.returning();
const [shelter] = db
const [shelter] = await db
.insert(schema.categories)
.values({ name: "Shelter", icon: "tent" })
.returning()
.all();
.values({ name: "Shelter", icon: "tent", userId })
.returning();
const [sleep] = db
const [sleep] = await db
.insert(schema.categories)
.values({ name: "Sleep System", icon: "moon" })
.returning()
.all();
.values({ name: "Sleep System", icon: "moon", userId })
.returning();
const [cook] = db
const [cook] = await db
.insert(schema.categories)
.values({ name: "Cook Kit", icon: "flame" })
.returning()
.all();
.values({ name: "Cook Kit", icon: "flame", userId })
.returning();
// ── Items ──
const tent = db
const [tent] = await db
.insert(schema.items)
.values({
name: "Zpacks Duplex",
weightGrams: 539,
priceCents: 67900,
categoryId: shelter.id,
userId,
notes: "DCF shelter, 2-person",
})
.returning()
.get();
.returning();
db.insert(schema.items)
.values({
await db.insert(schema.items).values({
name: "Borah Gear Tarp",
weightGrams: 156,
priceCents: 11000,
categoryId: shelter.id,
})
.run();
userId,
});
const quilt = db
const [quilt] = await db
.insert(schema.items)
.values({
name: "Enlightened Equipment Enigma 20",
weightGrams: 595,
priceCents: 34000,
categoryId: sleep.id,
userId,
notes: "20F quilt",
})
.returning()
.get();
.returning();
const pad = db
const [pad] = await db
.insert(schema.items)
.values({
name: "Therm-a-Rest NeoAir XLite",
weightGrams: 354,
priceCents: 20999,
categoryId: sleep.id,
userId,
})
.returning()
.get();
.returning();
const stove = db
const [stove] = await db
.insert(schema.items)
.values({
name: "BRS-3000T Stove",
weightGrams: 25,
priceCents: 2000,
categoryId: cook.id,
userId,
})
.returning()
.get();
.returning();
db.insert(schema.items)
.values({
await db.insert(schema.items).values({
name: "Toaks 750ml Pot",
weightGrams: 103,
priceCents: 3000,
categoryId: cook.id,
})
.run();
userId,
});
// ── Active Thread with 3 Candidates ──
const activeThread = db
const [activeThread] = await db
.insert(schema.threads)
.values({
name: "New Backpack",
status: "active",
categoryId: uncategorized.id,
userId,
})
.returning()
.get();
.returning();
db.insert(schema.threadCandidates)
.values({
await db.insert(schema.threadCandidates).values({
threadId: activeThread.id,
name: "ULA Circuit",
weightGrams: 1077,
@@ -133,11 +149,9 @@ export async function seedTestDatabase() {
cons: "Heavier than competitors",
sortOrder: 1000,
status: "researching",
})
.run();
});
db.insert(schema.threadCandidates)
.values({
await db.insert(schema.threadCandidates).values({
threadId: activeThread.id,
name: "Gossamer Gear Mariposa",
weightGrams: 737,
@@ -147,11 +161,9 @@ export async function seedTestDatabase() {
cons: "Smaller hip belt pockets",
sortOrder: 2000,
status: "researching",
})
.run();
});
db.insert(schema.threadCandidates)
.values({
await db.insert(schema.threadCandidates).values({
threadId: activeThread.id,
name: "Granite Gear Crown2 38",
weightGrams: 850,
@@ -159,23 +171,21 @@ export async function seedTestDatabase() {
categoryId: uncategorized.id,
sortOrder: 3000,
status: "ordered",
})
.run();
});
// ── Resolved Thread ──
const resolvedThread = db
const [resolvedThread] = await db
.insert(schema.threads)
.values({
name: "Camp Stove",
status: "resolved",
categoryId: cook.id,
userId,
resolvedCandidateId: 1,
})
.returning()
.get();
.returning();
db.insert(schema.threadCandidates)
.values({
await db.insert(schema.threadCandidates).values({
threadId: resolvedThread.id,
name: "BRS-3000T",
weightGrams: 25,
@@ -183,42 +193,36 @@ export async function seedTestDatabase() {
categoryId: cook.id,
sortOrder: 1000,
status: "arrived",
})
.run();
});
// ── Setup with Items ──
const setup = db
const [setup] = await db
.insert(schema.setups)
.values({ name: "Weekend Overnighter" })
.returning()
.get();
.values({ name: "Weekend Overnighter", userId })
.returning();
db.insert(schema.setupItems)
.values([
await db.insert(schema.setupItems).values([
{ setupId: setup.id, itemId: tent.id, classification: "base" },
{ setupId: setup.id, itemId: quilt.id, classification: "base" },
{ setupId: setup.id, itemId: pad.id, classification: "base" },
{ setupId: setup.id, itemId: stove.id, classification: "consumable" },
])
.run();
]);
// ── API Key for E2E Authentication ──
const rawKey = "e2e-test-api-key-for-gearbox-testing";
const keyHash = await Bun.password.hash(rawKey);
const keyPrefix = rawKey.slice(0, 8);
db.insert(schema.apiKeys)
.values({ name: "E2E Test Key", keyHash, keyPrefix })
.run();
await db
.insert(schema.apiKeys)
.values({ name: "E2E Test Key", keyHash, keyPrefix, userId });
// ── Settings ──
db.insert(schema.settings)
.values([
{ key: "weightUnit", value: "g" },
{ key: "currency", value: "USD" },
{ key: "onboardingComplete", value: "true" },
])
.run();
await db.insert(schema.settings).values([
{ key: "weightUnit", value: "g", userId },
{ key: "currency", value: "USD", userId },
{ key: "onboardingComplete", value: "true", userId },
]);
sqlite.close();
console.log("E2E test database seeded at", DB_PATH);
await client.end();
console.log("E2E test database seeded via", DATABASE_URL);
}

View File

@@ -1,4 +1,4 @@
#!/bin/sh
# Seed the test database, then start the production server
bun run e2e/global-setup.ts
NODE_ENV=production DATABASE_PATH=./e2e/test.db bun run src/server/index.ts
NODE_ENV=production bun run src/server/index.ts