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,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")
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user