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:
2026-06-07 04:14:01 +02:00
parent cb01f4f8ef
commit f2b9239d82
18 changed files with 2372 additions and 9 deletions
+16
View File
@@ -0,0 +1,16 @@
import { describe, expect, it } from "vitest"
import { getPasswordHash, verifyPassword } from "@/lib/security"
describe("security helpers", () => {
it("hashes passwords and verifies the original plain password", async () => {
const hash = await getPasswordHash("secure-password")
expect(hash).not.toBe("secure-password")
await expect(verifyPassword("secure-password", hash)).resolves.toBe(true)
await expect(verifyPassword("wrong-password", hash)).resolves.toBe(false)
})
it("rejects empty passwords when hashing", async () => {
await expect(getPasswordHash("")).rejects.toThrow("Password is required")
})
})
+227
View File
@@ -0,0 +1,227 @@
import { describe, expect, it } from "vitest"
import { createAssetSchema, updateAssetSchema } from "@/schemas/asset.schema"
import { createAssignmentSchema } from "@/schemas/assignment.schema"
import { signInSchema } from "@/schemas/auth.schema"
import { createCategorySchema } from "@/schemas/category.schema"
import { createItemSchema, updateItemSchema } from "@/schemas/item.schema"
import { createMovementSchema } from "@/schemas/movement.schema"
import { createRecipientSchema } from "@/schemas/recipient.schema"
import { createUserSchema, updateUserSchema } from "@/schemas/user.schema"
describe("core schemas", () => {
it("coerces valid numeric fields", () => {
expect(
createItemSchema.safeParse({
name: "Laptop",
categoryId: "category-id",
stock: "3",
}),
).toMatchObject({ success: true, data: { stock: 3 } })
expect(
createAssignmentSchema.safeParse({
itemId: "item-id",
recipientId: "recipient-id",
quantity: "2",
}),
).toMatchObject({ success: true, data: { quantity: 2 } })
expect(
createMovementSchema.safeParse({
type: "IN",
quantity: "1",
itemId: "item-id",
}),
).toMatchObject({ success: true, data: { quantity: 1 } })
})
it("rejects invalid numeric fields", () => {
expect(
createItemSchema.safeParse({
name: "Laptop",
categoryId: "category-id",
stock: -1,
}).success,
).toBe(false)
expect(
createAssignmentSchema.safeParse({
itemId: "item-id",
recipientId: "recipient-id",
quantity: 0,
}).success,
).toBe(false)
expect(
createMovementSchema.safeParse({
type: "IN",
quantity: 0,
itemId: "item-id",
}).success,
).toBe(false)
})
it("validates required identifiers for update schemas", () => {
expect(
updateItemSchema.safeParse({
id: "item-id",
name: "Laptop",
categoryId: "category-id",
stock: 1,
}).success,
).toBe(true)
expect(
updateItemSchema.safeParse({
id: "",
name: "Laptop",
categoryId: "category-id",
stock: 1,
}).success,
).toBe(false)
expect(
updateUserSchema.safeParse({
id: "user-id",
username: "user",
name: "User",
email: "user@example.test",
role: "ADMIN",
isActive: true,
}).success,
).toBe(true)
expect(
updateUserSchema.safeParse({
id: "",
username: "user",
name: "User",
email: "user@example.test",
role: "ADMIN",
isActive: true,
}).success,
).toBe(false)
})
it("validates category and auth requirements", () => {
expect(createCategorySchema.safeParse({ name: "IT" }).success).toBe(false)
expect(createCategorySchema.safeParse({ name: "Hardware" }).success).toBe(
true,
)
expect(
signInSchema.safeParse({ username: "admin", password: "abc" }).success,
).toBe(true)
expect(
signInSchema.safeParse({ username: "", password: "abc" }).success,
).toBe(false)
expect(
signInSchema.safeParse({ username: "admin", password: "ab" }).success,
).toBe(false)
})
it("validates recipient email only when present", () => {
expect(
createRecipientSchema.safeParse({
username: "recipient",
firstName: "Rec",
lastName: "Ipient",
department: "IT",
email: "recipient@example.test",
}).success,
).toBe(true)
expect(
createRecipientSchema.safeParse({
username: "recipient",
firstName: "Rec",
lastName: "Ipient",
department: "IT",
email: "not-an-email",
}).success,
).toBe(false)
expect(
createRecipientSchema.safeParse({
username: "recipient",
firstName: "Rec",
lastName: "Ipient",
department: "IT",
email: "",
}).success,
).toBe(true)
})
it("validates user password, email, and role", () => {
expect(
createUserSchema.safeParse({
username: "user",
name: "User",
email: "user@example.test",
password: "password1",
role: "STAFF",
isActive: true,
}).success,
).toBe(true)
expect(
createUserSchema.safeParse({
username: "user",
name: "User",
email: "bad-email",
password: "password1",
role: "STAFF",
isActive: true,
}).success,
).toBe(false)
expect(
createUserSchema.safeParse({
username: "user",
name: "User",
email: "user@example.test",
password: "short",
role: "STAFF",
isActive: true,
}).success,
).toBe(false)
expect(
createUserSchema.safeParse({
username: "user",
name: "User",
email: "user@example.test",
password: "password1",
role: "OWNER",
isActive: true,
}).success,
).toBe(false)
})
it("validates asset status options", () => {
expect(
createAssetSchema.safeParse({
itemId: "item-id",
serialNumber: "SERIAL-001",
status: "AVAILABLE",
}).success,
).toBe(true)
expect(
createAssetSchema.safeParse({
itemId: "item-id",
serialNumber: "SERIAL-001",
status: "BROKEN",
}).success,
).toBe(false)
expect(
updateAssetSchema.safeParse({
id: "asset-id",
itemId: "item-id",
serialNumber: "SERIAL-001",
status: "BROKEN",
}).success,
).toBe(true)
})
})
+64
View File
@@ -0,0 +1,64 @@
import { describe, expect, it, vi } from "vitest"
vi.mock("next/navigation", () => ({
redirect: vi.fn((path: string) => {
throw new Error(`redirect:${path}`)
}),
}))
vi.mock("@/lib/auth", () => ({
auth: vi.fn(),
}))
import type { Session } from "next-auth"
import {
hasAnyRole,
hasMinimumRole,
hasRole,
isAdmin,
} from "@/services/auth.service"
function sessionWithRole(role: Session["user"]["role"]): Session {
return {
expires: new Date(Date.now() + 60_000).toISOString(),
user: {
id: "user-id",
name: "Test User",
email: "test@example.test",
role,
},
}
}
describe("auth service role helpers", () => {
it("checks exact roles", () => {
const admin = sessionWithRole("ADMIN")
const staff = sessionWithRole("STAFF")
expect(hasRole(admin, "ADMIN")).toBe(true)
expect(hasRole(staff, "ADMIN")).toBe(false)
expect(hasRole(null, "ADMIN")).toBe(false)
})
it("checks any allowed role", () => {
const manager = sessionWithRole("MANAGER")
expect(hasAnyRole(manager, ["ADMIN", "MANAGER"])).toBe(true)
expect(hasAnyRole(manager, ["ADMIN", "STAFF"])).toBe(false)
expect(hasAnyRole(null, ["ADMIN", "MANAGER"])).toBe(false)
})
it("checks minimum role hierarchy", () => {
expect(hasMinimumRole(sessionWithRole("ADMIN"), "MANAGER")).toBe(true)
expect(hasMinimumRole(sessionWithRole("MANAGER"), "STAFF")).toBe(true)
expect(hasMinimumRole(sessionWithRole("STAFF"), "MANAGER")).toBe(false)
expect(hasMinimumRole(sessionWithRole("VIEWER"), "STAFF")).toBe(false)
expect(hasMinimumRole(null, "VIEWER")).toBe(false)
})
it("identifies admins", () => {
expect(isAdmin(sessionWithRole("ADMIN"))).toBe(true)
expect(isAdmin(sessionWithRole("MANAGER"))).toBe(false)
expect(isAdmin(null)).toBe(false)
})
})