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,95 @@
|
||||
import type {
|
||||
PrismaClient,
|
||||
RecipientDepartment,
|
||||
UserRole,
|
||||
} from "@/generated/prisma/client"
|
||||
|
||||
let sequence = 0
|
||||
|
||||
function nextSuffix() {
|
||||
sequence += 1
|
||||
return `${Date.now()}-${sequence}`
|
||||
}
|
||||
|
||||
export async function createTestUser(
|
||||
prisma: PrismaClient,
|
||||
overrides: Partial<{
|
||||
username: string
|
||||
email: string
|
||||
name: string
|
||||
role: UserRole
|
||||
isActive: boolean
|
||||
}> = {},
|
||||
) {
|
||||
const suffix = nextSuffix()
|
||||
|
||||
return prisma.user.create({
|
||||
data: {
|
||||
username: overrides.username ?? `test-user-${suffix}`,
|
||||
email: overrides.email ?? `test-user-${suffix}@example.test`,
|
||||
name: overrides.name ?? "Test User",
|
||||
password: "hashed-password",
|
||||
role: overrides.role ?? "ADMIN",
|
||||
isActive: overrides.isActive ?? true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export async function createTestCategory(
|
||||
prisma: PrismaClient,
|
||||
overrides: Partial<{ name: string }> = {},
|
||||
) {
|
||||
const suffix = nextSuffix()
|
||||
|
||||
return prisma.category.create({
|
||||
data: {
|
||||
name: overrides.name ?? `Test Category ${suffix}`,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export async function createTestRecipient(
|
||||
prisma: PrismaClient,
|
||||
overrides: Partial<{
|
||||
username: string
|
||||
firstName: string
|
||||
lastName: string
|
||||
department: RecipientDepartment
|
||||
email: string | null
|
||||
phone: string | null
|
||||
}> = {},
|
||||
) {
|
||||
const suffix = nextSuffix()
|
||||
|
||||
return prisma.recipient.create({
|
||||
data: {
|
||||
username: overrides.username ?? `test-recipient-${suffix}`,
|
||||
firstName: overrides.firstName ?? "Test",
|
||||
lastName: overrides.lastName ?? "Recipient",
|
||||
department: overrides.department ?? "OTHER",
|
||||
email: overrides.email ?? null,
|
||||
phone: overrides.phone ?? null,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export async function createTestItem(
|
||||
prisma: PrismaClient,
|
||||
overrides: Partial<{
|
||||
name: string
|
||||
stock: number
|
||||
categoryId: string
|
||||
}> = {},
|
||||
) {
|
||||
const categoryId =
|
||||
overrides.categoryId ?? (await createTestCategory(prisma)).id
|
||||
const suffix = nextSuffix()
|
||||
|
||||
return prisma.item.create({
|
||||
data: {
|
||||
name: overrides.name ?? `Test Item ${suffix}`,
|
||||
stock: overrides.stock ?? 0,
|
||||
category: { connect: { id: categoryId } },
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { execFileSync } from "node:child_process"
|
||||
import {
|
||||
PostgreSqlContainer,
|
||||
type StartedPostgreSqlContainer,
|
||||
} from "@testcontainers/postgresql"
|
||||
import type { PrismaClient } from "@/generated/prisma/client"
|
||||
|
||||
type TestDatabaseState = {
|
||||
container?: StartedPostgreSqlContainer
|
||||
databaseUrl?: string
|
||||
}
|
||||
|
||||
const state: TestDatabaseState = {}
|
||||
|
||||
const TABLES_TO_TRUNCATE = [
|
||||
"Movement",
|
||||
"Assignment",
|
||||
"Asset",
|
||||
"Item",
|
||||
"Category",
|
||||
"Recipient",
|
||||
"User",
|
||||
]
|
||||
|
||||
export async function startIntegrationTestDatabase() {
|
||||
if (state.container && state.databaseUrl) {
|
||||
return state.databaseUrl
|
||||
}
|
||||
|
||||
const container = await new PostgreSqlContainer("postgres:18-alpine")
|
||||
.withDatabase("stock_manager_test")
|
||||
.withUsername("test")
|
||||
.withPassword("test")
|
||||
.start()
|
||||
|
||||
const databaseUrl = container.getConnectionUri()
|
||||
|
||||
state.container = container
|
||||
state.databaseUrl = databaseUrl
|
||||
|
||||
process.env.DATABASE_URL = databaseUrl
|
||||
process.env.AUTH_SECRET = process.env.AUTH_SECRET || "test-auth-secret"
|
||||
|
||||
try {
|
||||
execFileSync("bunx", ["prisma", "migrate", "deploy"], {
|
||||
env: {
|
||||
...process.env,
|
||||
DATABASE_URL: databaseUrl,
|
||||
},
|
||||
stdio: "inherit",
|
||||
})
|
||||
} catch (error) {
|
||||
await stopIntegrationTestDatabase()
|
||||
throw error
|
||||
}
|
||||
|
||||
return databaseUrl
|
||||
}
|
||||
|
||||
export async function resetIntegrationTestDatabase(prisma: PrismaClient) {
|
||||
const quotedTables = TABLES_TO_TRUNCATE.map((table) => `"${table}"`).join(
|
||||
", ",
|
||||
)
|
||||
|
||||
await prisma.$executeRawUnsafe(`TRUNCATE TABLE ${quotedTables} CASCADE`)
|
||||
}
|
||||
|
||||
export async function stopIntegrationTestDatabase() {
|
||||
await state.container?.stop()
|
||||
state.container = undefined
|
||||
state.databaseUrl = undefined
|
||||
}
|
||||
Reference in New Issue
Block a user