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,289 @@
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest"
|
||||
import type { PrismaClient } from "@/generated/prisma/client"
|
||||
import { createTestUser } from "../helpers/factories"
|
||||
import {
|
||||
resetIntegrationTestDatabase,
|
||||
startIntegrationTestDatabase,
|
||||
stopIntegrationTestDatabase,
|
||||
} from "../helpers/test-db"
|
||||
|
||||
let prisma: PrismaClient
|
||||
let createUserUseCase: typeof import("@/use-cases/user.use-cases").createUserUseCase
|
||||
let updateUserUseCase: typeof import("@/use-cases/user.use-cases").updateUserUseCase
|
||||
let setUserActiveUseCase: typeof import("@/use-cases/user.use-cases").setUserActiveUseCase
|
||||
let resetUserPasswordUseCase: typeof import("@/use-cases/user.use-cases").resetUserPasswordUseCase
|
||||
let verifyPassword: typeof import("@/lib/security").verifyPassword
|
||||
|
||||
beforeAll(async () => {
|
||||
await startIntegrationTestDatabase()
|
||||
|
||||
const prismaModule = await import("@/lib/prisma")
|
||||
const userUseCases = await import("@/use-cases/user.use-cases")
|
||||
const security = await import("@/lib/security")
|
||||
|
||||
prisma = prismaModule.prisma
|
||||
createUserUseCase = userUseCases.createUserUseCase
|
||||
updateUserUseCase = userUseCases.updateUserUseCase
|
||||
setUserActiveUseCase = userUseCases.setUserActiveUseCase
|
||||
resetUserPasswordUseCase = userUseCases.resetUserPasswordUseCase
|
||||
verifyPassword = security.verifyPassword
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
await resetIntegrationTestDatabase(prisma)
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await prisma?.$disconnect()
|
||||
await stopIntegrationTestDatabase()
|
||||
})
|
||||
|
||||
describe("user use-cases", () => {
|
||||
it("creates a user with a hashed password", async () => {
|
||||
const result = await createUserUseCase({
|
||||
username: "new-user",
|
||||
name: "New User",
|
||||
email: "new-user@example.test",
|
||||
password: "secure-password",
|
||||
role: "STAFF",
|
||||
isActive: true,
|
||||
})
|
||||
|
||||
expect(result).toEqual({ success: true })
|
||||
|
||||
const user = await prisma.user.findUniqueOrThrow({
|
||||
where: { username: "new-user" },
|
||||
})
|
||||
|
||||
expect(user).toMatchObject({
|
||||
username: "new-user",
|
||||
name: "New User",
|
||||
email: "new-user@example.test",
|
||||
role: "STAFF",
|
||||
isActive: true,
|
||||
})
|
||||
expect(user.password).not.toBe("secure-password")
|
||||
await expect(
|
||||
verifyPassword("secure-password", user.password),
|
||||
).resolves.toBe(true)
|
||||
})
|
||||
|
||||
it("rejects duplicate usernames and duplicate emails", async () => {
|
||||
await createTestUser(prisma, {
|
||||
username: "existing-user",
|
||||
email: "existing@example.test",
|
||||
})
|
||||
|
||||
await expect(
|
||||
createUserUseCase({
|
||||
username: "existing-user",
|
||||
name: "Duplicate Username",
|
||||
email: "unique@example.test",
|
||||
password: "secure-password",
|
||||
role: "STAFF",
|
||||
isActive: true,
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
success: false,
|
||||
errors: { username: ["Username already exists"] },
|
||||
})
|
||||
|
||||
await expect(
|
||||
createUserUseCase({
|
||||
username: "unique-user",
|
||||
name: "Duplicate Email",
|
||||
email: "existing@example.test",
|
||||
password: "secure-password",
|
||||
role: "STAFF",
|
||||
isActive: true,
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
success: false,
|
||||
errors: { email: ["Email already exists"] },
|
||||
})
|
||||
|
||||
await expect(prisma.user.count()).resolves.toBe(1)
|
||||
await expect(
|
||||
prisma.user.findUniqueOrThrow({ where: { username: "existing-user" } }),
|
||||
).resolves.toMatchObject({ email: "existing@example.test" })
|
||||
})
|
||||
|
||||
it("updates a user while preserving uniqueness constraints", async () => {
|
||||
const actor = await createTestUser(prisma, { role: "ADMIN" })
|
||||
const user = await createTestUser(prisma, {
|
||||
username: "editable-user",
|
||||
email: "editable@example.test",
|
||||
role: "STAFF",
|
||||
})
|
||||
const other = await createTestUser(prisma, {
|
||||
username: "other-user",
|
||||
email: "other@example.test",
|
||||
role: "STAFF",
|
||||
})
|
||||
|
||||
await expect(
|
||||
updateUserUseCase({
|
||||
actorId: actor.id,
|
||||
id: user.id,
|
||||
username: "edited-user",
|
||||
name: "Edited User",
|
||||
email: "edited@example.test",
|
||||
role: "MANAGER",
|
||||
isActive: true,
|
||||
}),
|
||||
).resolves.toEqual({ success: true })
|
||||
|
||||
await expect(
|
||||
prisma.user.findUniqueOrThrow({ where: { id: user.id } }),
|
||||
).resolves.toMatchObject({
|
||||
username: "edited-user",
|
||||
name: "Edited User",
|
||||
email: "edited@example.test",
|
||||
role: "MANAGER",
|
||||
isActive: true,
|
||||
})
|
||||
|
||||
await expect(
|
||||
updateUserUseCase({
|
||||
actorId: actor.id,
|
||||
id: user.id,
|
||||
username: other.username,
|
||||
name: "Edited User",
|
||||
email: "another-email@example.test",
|
||||
role: "MANAGER",
|
||||
isActive: true,
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
success: false,
|
||||
errors: { username: ["Username already exists"] },
|
||||
})
|
||||
|
||||
await expect(
|
||||
prisma.user.findUniqueOrThrow({ where: { id: user.id } }),
|
||||
).resolves.toMatchObject({
|
||||
username: "edited-user",
|
||||
email: "edited@example.test",
|
||||
})
|
||||
})
|
||||
|
||||
it("prevents an administrator from removing their own administrator access", async () => {
|
||||
const admin = await createTestUser(prisma, { role: "ADMIN" })
|
||||
|
||||
await expect(
|
||||
updateUserUseCase({
|
||||
actorId: admin.id,
|
||||
id: admin.id,
|
||||
username: admin.username,
|
||||
name: admin.name,
|
||||
email: admin.email,
|
||||
role: "STAFF",
|
||||
isActive: true,
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
success: false,
|
||||
errors: { id: ["You cannot remove your own administrator access"] },
|
||||
})
|
||||
|
||||
await expect(
|
||||
prisma.user.findUniqueOrThrow({ where: { id: admin.id } }),
|
||||
).resolves.toMatchObject({ role: "ADMIN", isActive: true })
|
||||
})
|
||||
|
||||
it("protects the last active administrator but allows deactivation when another active admin exists", async () => {
|
||||
const firstAdmin = await createTestUser(prisma, {
|
||||
username: "first-admin",
|
||||
email: "first-admin@example.test",
|
||||
role: "ADMIN",
|
||||
})
|
||||
const staffActor = await createTestUser(prisma, {
|
||||
username: "staff-actor",
|
||||
email: "staff-actor@example.test",
|
||||
role: "STAFF",
|
||||
})
|
||||
|
||||
await expect(
|
||||
setUserActiveUseCase({
|
||||
actorId: staffActor.id,
|
||||
id: firstAdmin.id,
|
||||
isActive: false,
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
success: false,
|
||||
errors: {
|
||||
id: ["Cannot remove access from the last active administrator"],
|
||||
},
|
||||
})
|
||||
|
||||
const secondAdmin = await createTestUser(prisma, {
|
||||
username: "second-admin",
|
||||
email: "second-admin@example.test",
|
||||
role: "ADMIN",
|
||||
})
|
||||
|
||||
await expect(
|
||||
setUserActiveUseCase({
|
||||
actorId: secondAdmin.id,
|
||||
id: firstAdmin.id,
|
||||
isActive: false,
|
||||
}),
|
||||
).resolves.toEqual({ success: true })
|
||||
|
||||
await expect(
|
||||
prisma.user.findUniqueOrThrow({ where: { id: firstAdmin.id } }),
|
||||
).resolves.toMatchObject({ isActive: false })
|
||||
})
|
||||
|
||||
it("prevents self-deactivation", async () => {
|
||||
const admin = await createTestUser(prisma, { role: "ADMIN" })
|
||||
await createTestUser(prisma, { role: "ADMIN" })
|
||||
|
||||
await expect(
|
||||
setUserActiveUseCase({
|
||||
actorId: admin.id,
|
||||
id: admin.id,
|
||||
isActive: false,
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
success: false,
|
||||
errors: { id: ["You cannot deactivate your own user"] },
|
||||
})
|
||||
|
||||
await expect(
|
||||
prisma.user.findUniqueOrThrow({ where: { id: admin.id } }),
|
||||
).resolves.toMatchObject({ isActive: true })
|
||||
})
|
||||
|
||||
it("resets a user password and rejects missing users", async () => {
|
||||
const user = await createTestUser(prisma, {
|
||||
username: "password-user",
|
||||
email: "password-user@example.test",
|
||||
role: "STAFF",
|
||||
})
|
||||
|
||||
await expect(
|
||||
resetUserPasswordUseCase({
|
||||
id: user.id,
|
||||
password: "new-secure-password",
|
||||
}),
|
||||
).resolves.toEqual({ success: true })
|
||||
|
||||
const updatedUser = await prisma.user.findUniqueOrThrow({
|
||||
where: { id: user.id },
|
||||
})
|
||||
|
||||
expect(updatedUser.password).not.toBe(user.password)
|
||||
await expect(
|
||||
verifyPassword("new-secure-password", updatedUser.password),
|
||||
).resolves.toBe(true)
|
||||
|
||||
await expect(
|
||||
resetUserPasswordUseCase({
|
||||
id: "00000000-0000-0000-0000-000000000000",
|
||||
password: "new-secure-password",
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
success: false,
|
||||
errors: { id: ["User not found"] },
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user