fix: migrate E2E tests from SQLite to Postgres
- 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:
@@ -30,11 +30,24 @@ jobs:
|
|||||||
run: bun run build
|
run: bun run build
|
||||||
|
|
||||||
e2e:
|
e2e:
|
||||||
if: false # Disabled: E2E requires Postgres (Phase 14) — SQLite schema diverged at v2.0
|
|
||||||
needs: ci
|
needs: ci
|
||||||
runs-on: docker
|
runs-on: docker
|
||||||
container:
|
container:
|
||||||
image: mcr.microsoft.com/playwright:v1.59.1-noble
|
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:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -228,6 +228,7 @@ uploads/*
|
|||||||
|
|
||||||
# Playwright
|
# Playwright
|
||||||
e2e/test.db
|
e2e/test.db
|
||||||
|
e2e/pgdata
|
||||||
test-results/
|
test-results/
|
||||||
playwright-report/
|
playwright-report/
|
||||||
|
|
||||||
|
|||||||
280
e2e/seed.ts
280
e2e/seed.ts
@@ -1,224 +1,228 @@
|
|||||||
import { Database } from "bun:sqlite";
|
import { sql } from "drizzle-orm";
|
||||||
import { unlink } from "node:fs/promises";
|
import { drizzle } from "drizzle-orm/postgres-js";
|
||||||
import { drizzle } from "drizzle-orm/bun-sqlite";
|
import { migrate } from "drizzle-orm/postgres-js/migrator";
|
||||||
import { migrate } from "drizzle-orm/bun-sqlite/migrator";
|
import postgres from "postgres";
|
||||||
import * as schema from "../src/db/schema";
|
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() {
|
export async function seedTestDatabase() {
|
||||||
// Remove old test DB if it exists
|
const client = postgres(DATABASE_URL, { max: 1 });
|
||||||
try {
|
const db = drizzle(client, { schema });
|
||||||
await unlink(DB_PATH);
|
|
||||||
} catch {
|
// Run migrations
|
||||||
// File doesn't exist, that's fine
|
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);
|
// ── User ──
|
||||||
sqlite.run("PRAGMA journal_mode = WAL");
|
const [user] = await db
|
||||||
sqlite.run("PRAGMA foreign_keys = ON");
|
.insert(schema.users)
|
||||||
|
.values({ logtoSub: "e2e-test-user" })
|
||||||
const db = drizzle(sqlite, { schema });
|
.returning();
|
||||||
migrate(db, { migrationsFolder: "./drizzle" });
|
const userId = user.id;
|
||||||
|
|
||||||
// ── Categories ──
|
// ── Categories ──
|
||||||
const [uncategorized] = db
|
const [uncategorized] = await db
|
||||||
.insert(schema.categories)
|
.insert(schema.categories)
|
||||||
.values({ name: "Uncategorized", icon: "package" })
|
.values({ name: "Uncategorized", icon: "package", userId })
|
||||||
.returning()
|
.returning();
|
||||||
.all();
|
|
||||||
|
|
||||||
const [shelter] = db
|
const [shelter] = await db
|
||||||
.insert(schema.categories)
|
.insert(schema.categories)
|
||||||
.values({ name: "Shelter", icon: "tent" })
|
.values({ name: "Shelter", icon: "tent", userId })
|
||||||
.returning()
|
.returning();
|
||||||
.all();
|
|
||||||
|
|
||||||
const [sleep] = db
|
const [sleep] = await db
|
||||||
.insert(schema.categories)
|
.insert(schema.categories)
|
||||||
.values({ name: "Sleep System", icon: "moon" })
|
.values({ name: "Sleep System", icon: "moon", userId })
|
||||||
.returning()
|
.returning();
|
||||||
.all();
|
|
||||||
|
|
||||||
const [cook] = db
|
const [cook] = await db
|
||||||
.insert(schema.categories)
|
.insert(schema.categories)
|
||||||
.values({ name: "Cook Kit", icon: "flame" })
|
.values({ name: "Cook Kit", icon: "flame", userId })
|
||||||
.returning()
|
.returning();
|
||||||
.all();
|
|
||||||
|
|
||||||
// ── Items ──
|
// ── Items ──
|
||||||
const tent = db
|
const [tent] = await db
|
||||||
.insert(schema.items)
|
.insert(schema.items)
|
||||||
.values({
|
.values({
|
||||||
name: "Zpacks Duplex",
|
name: "Zpacks Duplex",
|
||||||
weightGrams: 539,
|
weightGrams: 539,
|
||||||
priceCents: 67900,
|
priceCents: 67900,
|
||||||
categoryId: shelter.id,
|
categoryId: shelter.id,
|
||||||
|
userId,
|
||||||
notes: "DCF shelter, 2-person",
|
notes: "DCF shelter, 2-person",
|
||||||
})
|
})
|
||||||
.returning()
|
.returning();
|
||||||
.get();
|
|
||||||
|
|
||||||
db.insert(schema.items)
|
await db.insert(schema.items).values({
|
||||||
.values({
|
name: "Borah Gear Tarp",
|
||||||
name: "Borah Gear Tarp",
|
weightGrams: 156,
|
||||||
weightGrams: 156,
|
priceCents: 11000,
|
||||||
priceCents: 11000,
|
categoryId: shelter.id,
|
||||||
categoryId: shelter.id,
|
userId,
|
||||||
})
|
});
|
||||||
.run();
|
|
||||||
|
|
||||||
const quilt = db
|
const [quilt] = await db
|
||||||
.insert(schema.items)
|
.insert(schema.items)
|
||||||
.values({
|
.values({
|
||||||
name: "Enlightened Equipment Enigma 20",
|
name: "Enlightened Equipment Enigma 20",
|
||||||
weightGrams: 595,
|
weightGrams: 595,
|
||||||
priceCents: 34000,
|
priceCents: 34000,
|
||||||
categoryId: sleep.id,
|
categoryId: sleep.id,
|
||||||
|
userId,
|
||||||
notes: "20F quilt",
|
notes: "20F quilt",
|
||||||
})
|
})
|
||||||
.returning()
|
.returning();
|
||||||
.get();
|
|
||||||
|
|
||||||
const pad = db
|
const [pad] = await db
|
||||||
.insert(schema.items)
|
.insert(schema.items)
|
||||||
.values({
|
.values({
|
||||||
name: "Therm-a-Rest NeoAir XLite",
|
name: "Therm-a-Rest NeoAir XLite",
|
||||||
weightGrams: 354,
|
weightGrams: 354,
|
||||||
priceCents: 20999,
|
priceCents: 20999,
|
||||||
categoryId: sleep.id,
|
categoryId: sleep.id,
|
||||||
|
userId,
|
||||||
})
|
})
|
||||||
.returning()
|
.returning();
|
||||||
.get();
|
|
||||||
|
|
||||||
const stove = db
|
const [stove] = await db
|
||||||
.insert(schema.items)
|
.insert(schema.items)
|
||||||
.values({
|
.values({
|
||||||
name: "BRS-3000T Stove",
|
name: "BRS-3000T Stove",
|
||||||
weightGrams: 25,
|
weightGrams: 25,
|
||||||
priceCents: 2000,
|
priceCents: 2000,
|
||||||
categoryId: cook.id,
|
categoryId: cook.id,
|
||||||
|
userId,
|
||||||
})
|
})
|
||||||
.returning()
|
.returning();
|
||||||
.get();
|
|
||||||
|
|
||||||
db.insert(schema.items)
|
await db.insert(schema.items).values({
|
||||||
.values({
|
name: "Toaks 750ml Pot",
|
||||||
name: "Toaks 750ml Pot",
|
weightGrams: 103,
|
||||||
weightGrams: 103,
|
priceCents: 3000,
|
||||||
priceCents: 3000,
|
categoryId: cook.id,
|
||||||
categoryId: cook.id,
|
userId,
|
||||||
})
|
});
|
||||||
.run();
|
|
||||||
|
|
||||||
// ── Active Thread with 3 Candidates ──
|
// ── Active Thread with 3 Candidates ──
|
||||||
const activeThread = db
|
const [activeThread] = await db
|
||||||
.insert(schema.threads)
|
.insert(schema.threads)
|
||||||
.values({
|
.values({
|
||||||
name: "New Backpack",
|
name: "New Backpack",
|
||||||
status: "active",
|
status: "active",
|
||||||
categoryId: uncategorized.id,
|
categoryId: uncategorized.id,
|
||||||
|
userId,
|
||||||
})
|
})
|
||||||
.returning()
|
.returning();
|
||||||
.get();
|
|
||||||
|
|
||||||
db.insert(schema.threadCandidates)
|
await db.insert(schema.threadCandidates).values({
|
||||||
.values({
|
threadId: activeThread.id,
|
||||||
threadId: activeThread.id,
|
name: "ULA Circuit",
|
||||||
name: "ULA Circuit",
|
weightGrams: 1077,
|
||||||
weightGrams: 1077,
|
priceCents: 27500,
|
||||||
priceCents: 27500,
|
categoryId: uncategorized.id,
|
||||||
categoryId: uncategorized.id,
|
pros: "Great hip belt\nLarge capacity",
|
||||||
pros: "Great hip belt\nLarge capacity",
|
cons: "Heavier than competitors",
|
||||||
cons: "Heavier than competitors",
|
sortOrder: 1000,
|
||||||
sortOrder: 1000,
|
status: "researching",
|
||||||
status: "researching",
|
});
|
||||||
})
|
|
||||||
.run();
|
|
||||||
|
|
||||||
db.insert(schema.threadCandidates)
|
await db.insert(schema.threadCandidates).values({
|
||||||
.values({
|
threadId: activeThread.id,
|
||||||
threadId: activeThread.id,
|
name: "Gossamer Gear Mariposa",
|
||||||
name: "Gossamer Gear Mariposa",
|
weightGrams: 737,
|
||||||
weightGrams: 737,
|
priceCents: 28500,
|
||||||
priceCents: 28500,
|
categoryId: uncategorized.id,
|
||||||
categoryId: uncategorized.id,
|
pros: "Very lightweight\nGood ventilation",
|
||||||
pros: "Very lightweight\nGood ventilation",
|
cons: "Smaller hip belt pockets",
|
||||||
cons: "Smaller hip belt pockets",
|
sortOrder: 2000,
|
||||||
sortOrder: 2000,
|
status: "researching",
|
||||||
status: "researching",
|
});
|
||||||
})
|
|
||||||
.run();
|
|
||||||
|
|
||||||
db.insert(schema.threadCandidates)
|
await db.insert(schema.threadCandidates).values({
|
||||||
.values({
|
threadId: activeThread.id,
|
||||||
threadId: activeThread.id,
|
name: "Granite Gear Crown2 38",
|
||||||
name: "Granite Gear Crown2 38",
|
weightGrams: 850,
|
||||||
weightGrams: 850,
|
priceCents: 18000,
|
||||||
priceCents: 18000,
|
categoryId: uncategorized.id,
|
||||||
categoryId: uncategorized.id,
|
sortOrder: 3000,
|
||||||
sortOrder: 3000,
|
status: "ordered",
|
||||||
status: "ordered",
|
});
|
||||||
})
|
|
||||||
.run();
|
|
||||||
|
|
||||||
// ── Resolved Thread ──
|
// ── Resolved Thread ──
|
||||||
const resolvedThread = db
|
const [resolvedThread] = await db
|
||||||
.insert(schema.threads)
|
.insert(schema.threads)
|
||||||
.values({
|
.values({
|
||||||
name: "Camp Stove",
|
name: "Camp Stove",
|
||||||
status: "resolved",
|
status: "resolved",
|
||||||
categoryId: cook.id,
|
categoryId: cook.id,
|
||||||
|
userId,
|
||||||
resolvedCandidateId: 1,
|
resolvedCandidateId: 1,
|
||||||
})
|
})
|
||||||
.returning()
|
.returning();
|
||||||
.get();
|
|
||||||
|
|
||||||
db.insert(schema.threadCandidates)
|
await db.insert(schema.threadCandidates).values({
|
||||||
.values({
|
threadId: resolvedThread.id,
|
||||||
threadId: resolvedThread.id,
|
name: "BRS-3000T",
|
||||||
name: "BRS-3000T",
|
weightGrams: 25,
|
||||||
weightGrams: 25,
|
priceCents: 2000,
|
||||||
priceCents: 2000,
|
categoryId: cook.id,
|
||||||
categoryId: cook.id,
|
sortOrder: 1000,
|
||||||
sortOrder: 1000,
|
status: "arrived",
|
||||||
status: "arrived",
|
});
|
||||||
})
|
|
||||||
.run();
|
|
||||||
|
|
||||||
// ── Setup with Items ──
|
// ── Setup with Items ──
|
||||||
const setup = db
|
const [setup] = await db
|
||||||
.insert(schema.setups)
|
.insert(schema.setups)
|
||||||
.values({ name: "Weekend Overnighter" })
|
.values({ name: "Weekend Overnighter", userId })
|
||||||
.returning()
|
.returning();
|
||||||
.get();
|
|
||||||
|
|
||||||
db.insert(schema.setupItems)
|
await db.insert(schema.setupItems).values([
|
||||||
.values([
|
{ setupId: setup.id, itemId: tent.id, classification: "base" },
|
||||||
{ setupId: setup.id, itemId: tent.id, classification: "base" },
|
{ setupId: setup.id, itemId: quilt.id, classification: "base" },
|
||||||
{ setupId: setup.id, itemId: quilt.id, classification: "base" },
|
{ setupId: setup.id, itemId: pad.id, classification: "base" },
|
||||||
{ setupId: setup.id, itemId: pad.id, classification: "base" },
|
{ setupId: setup.id, itemId: stove.id, classification: "consumable" },
|
||||||
{ setupId: setup.id, itemId: stove.id, classification: "consumable" },
|
]);
|
||||||
])
|
|
||||||
.run();
|
|
||||||
|
|
||||||
// ── API Key for E2E Authentication ──
|
// ── API Key for E2E Authentication ──
|
||||||
const rawKey = "e2e-test-api-key-for-gearbox-testing";
|
const rawKey = "e2e-test-api-key-for-gearbox-testing";
|
||||||
const keyHash = await Bun.password.hash(rawKey);
|
const keyHash = await Bun.password.hash(rawKey);
|
||||||
const keyPrefix = rawKey.slice(0, 8);
|
const keyPrefix = rawKey.slice(0, 8);
|
||||||
db.insert(schema.apiKeys)
|
await db
|
||||||
.values({ name: "E2E Test Key", keyHash, keyPrefix })
|
.insert(schema.apiKeys)
|
||||||
.run();
|
.values({ name: "E2E Test Key", keyHash, keyPrefix, userId });
|
||||||
|
|
||||||
// ── Settings ──
|
// ── Settings ──
|
||||||
db.insert(schema.settings)
|
await db.insert(schema.settings).values([
|
||||||
.values([
|
{ key: "weightUnit", value: "g", userId },
|
||||||
{ key: "weightUnit", value: "g" },
|
{ key: "currency", value: "USD", userId },
|
||||||
{ key: "currency", value: "USD" },
|
{ key: "onboardingComplete", value: "true", userId },
|
||||||
{ key: "onboardingComplete", value: "true" },
|
]);
|
||||||
])
|
|
||||||
.run();
|
|
||||||
|
|
||||||
sqlite.close();
|
await client.end();
|
||||||
console.log("E2E test database seeded at", DB_PATH);
|
console.log("E2E test database seeded via", DATABASE_URL);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# Seed the test database, then start the production server
|
# Seed the test database, then start the production server
|
||||||
bun run e2e/global-setup.ts
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user