test: add initial unit integration and e2e coverage
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
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
import { expect, type Page, test } from "@playwright/test"
|
||||
|
||||
async function signInAsAdmin(page: Page) {
|
||||
await page.goto("/login")
|
||||
await page.getByLabel("Username").fill("admin")
|
||||
await page.getByLabel("Password").fill("admin-password")
|
||||
await page.getByRole("button", { name: "Sign In" }).click()
|
||||
await expect(page).toHaveURL("/")
|
||||
}
|
||||
|
||||
test.describe("main app smoke", () => {
|
||||
test("redirects unauthenticated users to login", async ({ page }) => {
|
||||
await page.goto("/admin/users")
|
||||
|
||||
await expect(page).toHaveURL(/\/login/)
|
||||
await expect(page.getByLabel("Username")).toBeVisible()
|
||||
await expect(page.getByRole("button", { name: "Sign In" })).toBeVisible()
|
||||
})
|
||||
|
||||
test("signs in as seeded admin and opens the dashboard", async ({ page }) => {
|
||||
await signInAsAdmin(page)
|
||||
|
||||
await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible()
|
||||
await expect(page.getByText("E2E Admin")).toBeVisible()
|
||||
await expect(
|
||||
page.getByRole("link", { name: /Stock Manager/i }),
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test("admin can open users and inventory pages", async ({ page }) => {
|
||||
await signInAsAdmin(page)
|
||||
|
||||
await page.getByRole("link", { name: "Users" }).click()
|
||||
await expect(page).toHaveURL(/\/admin\/users/)
|
||||
await expect(page.getByRole("heading", { name: "Users" })).toBeVisible()
|
||||
await expect(
|
||||
page.getByRole("cell", { name: "admin@example.test" }),
|
||||
).toBeVisible()
|
||||
|
||||
await page.goto("/inventory/items")
|
||||
await expect(page.getByRole("heading", { name: "Items" })).toBeVisible()
|
||||
await expect(page.getByText("No items found.")).toBeVisible()
|
||||
|
||||
await page.goto("/assignments")
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Assignments" }),
|
||||
).toBeVisible()
|
||||
await expect(page.getByText("No assignments found")).toBeVisible()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,110 @@
|
||||
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)
|
||||
})
|
||||
Reference in New Issue
Block a user