f2b9239d82
Adds the initial testing baseline for the project: Unit coverage: - Zod schemas for items, assignments, movements, categories, auth, recipients, users, and assets - password hashing and verification helpers - auth role helper functions Integration coverage with PostgreSQL Testcontainers: - item use-cases: create, duplicate names, delete constraints - assignment use-cases: create, insufficient stock, return, double return - asset use-cases: available/assigned creation and lifecycle transitions - user use-cases: create/update, uniqueness, admin safeguards, password reset - category use-cases: create/update/delete constraints - recipient use-cases: create/update and uniqueness constraints E2E smoke coverage with Playwright: - unauthenticated redirect to login - seeded admin login - dashboard load - admin users page - inventory items page - assignments page Also configures: - Vitest - Playwright - PostgreSQL Testcontainers helpers - deterministic E2E admin bootstrap - test artifact ignores Validation: - bun run test: 9 files / 37 tests passed - bun run test:e2e: 3 passed - bunx tsc --noEmit: passed - bunx prisma validate: passed
111 lines
2.7 KiB
TypeScript
111 lines
2.7 KiB
TypeScript
import { execFileSync, spawn, type ChildProcess } from "node:child_process"
|
|
import process from "node:process"
|
|
import {
|
|
PostgreSqlContainer,
|
|
type StartedPostgreSqlContainer,
|
|
} from "@testcontainers/postgresql"
|
|
|
|
const port = process.env.E2E_PORT ?? "3100"
|
|
const host = "127.0.0.1"
|
|
const baseUrl = `http://${host}:${port}`
|
|
|
|
let container: StartedPostgreSqlContainer | undefined
|
|
let nextProcess: ChildProcess | undefined
|
|
let shuttingDown = false
|
|
|
|
async function cleanup() {
|
|
if (shuttingDown) return
|
|
shuttingDown = true
|
|
|
|
if (nextProcess && !nextProcess.killed) {
|
|
nextProcess.kill("SIGTERM")
|
|
}
|
|
|
|
await container?.stop()
|
|
container = undefined
|
|
}
|
|
|
|
async function main() {
|
|
container = await new PostgreSqlContainer("postgres:18-alpine")
|
|
.withDatabase("stock_manager_e2e")
|
|
.withUsername("e2e")
|
|
.withPassword("e2e")
|
|
.start()
|
|
|
|
const databaseUrl = container.getConnectionUri()
|
|
const env = {
|
|
...process.env,
|
|
DATABASE_URL: databaseUrl,
|
|
AUTH_SECRET: process.env.AUTH_SECRET ?? "e2e-auth-secret",
|
|
AUTH_URL: baseUrl,
|
|
NEXTAUTH_URL: baseUrl,
|
|
ADMIN_BOOTSTRAP_ENABLED: "true",
|
|
ADMIN_USERNAME: "admin",
|
|
ADMIN_EMAIL: "admin@example.test",
|
|
ADMIN_NAME: "E2E Admin",
|
|
ADMIN_PASSWORD: "admin-password",
|
|
}
|
|
|
|
process.env.DATABASE_URL = databaseUrl
|
|
process.env.AUTH_SECRET = env.AUTH_SECRET
|
|
process.env.AUTH_URL = baseUrl
|
|
process.env.NEXTAUTH_URL = baseUrl
|
|
process.env.ADMIN_BOOTSTRAP_ENABLED = "true"
|
|
process.env.ADMIN_USERNAME = "admin"
|
|
process.env.ADMIN_EMAIL = "admin@example.test"
|
|
process.env.ADMIN_NAME = "E2E Admin"
|
|
process.env.ADMIN_PASSWORD = "admin-password"
|
|
|
|
try {
|
|
execFileSync("bunx", ["prisma", "migrate", "deploy"], {
|
|
env,
|
|
stdio: "inherit",
|
|
})
|
|
|
|
const prismaModule = await import("@/lib/prisma")
|
|
const bootstrapModule = await import("../../../prisma/bootstrap-admin")
|
|
|
|
await bootstrapModule.bootstrapAdmin(prismaModule.prisma)
|
|
await prismaModule.prisma.$disconnect()
|
|
} catch (error) {
|
|
await cleanup()
|
|
throw error
|
|
}
|
|
|
|
const startedNextProcess = spawn(
|
|
"bunx",
|
|
["next", "dev", "--webpack", "--hostname", host, "--port", port],
|
|
{
|
|
env,
|
|
stdio: "inherit",
|
|
},
|
|
)
|
|
|
|
nextProcess = startedNextProcess
|
|
|
|
startedNextProcess.on("exit", async (code, signal) => {
|
|
const wasShuttingDown = shuttingDown
|
|
await cleanup()
|
|
if (!wasShuttingDown && code !== 0) {
|
|
console.error(`Next dev server exited with code ${code} signal ${signal}`)
|
|
process.exit(code ?? 1)
|
|
}
|
|
})
|
|
}
|
|
|
|
process.on("SIGTERM", async () => {
|
|
await cleanup()
|
|
process.exit(0)
|
|
})
|
|
|
|
process.on("SIGINT", async () => {
|
|
await cleanup()
|
|
process.exit(0)
|
|
})
|
|
|
|
main().catch(async (error) => {
|
|
console.error(error)
|
|
await cleanup()
|
|
process.exit(1)
|
|
})
|