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,134 @@
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest"
|
||||
import type { PrismaClient } from "@/generated/prisma/client"
|
||||
import { createTestCategory, createTestUser } from "../helpers/factories"
|
||||
import {
|
||||
resetIntegrationTestDatabase,
|
||||
startIntegrationTestDatabase,
|
||||
stopIntegrationTestDatabase,
|
||||
} from "../helpers/test-db"
|
||||
|
||||
let prisma: PrismaClient
|
||||
let createItemUseCase: typeof import("@/use-cases/item.use-cases").createItemUseCase
|
||||
let deleteItemUseCase: typeof import("@/use-cases/item.use-cases").deleteItemUseCase
|
||||
|
||||
beforeAll(async () => {
|
||||
await startIntegrationTestDatabase()
|
||||
|
||||
const prismaModule = await import("@/lib/prisma")
|
||||
const itemUseCases = await import("@/use-cases/item.use-cases")
|
||||
|
||||
prisma = prismaModule.prisma
|
||||
createItemUseCase = itemUseCases.createItemUseCase
|
||||
deleteItemUseCase = itemUseCases.deleteItemUseCase
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
await resetIntegrationTestDatabase(prisma)
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await prisma?.$disconnect()
|
||||
await stopIntegrationTestDatabase()
|
||||
})
|
||||
|
||||
describe("item use-cases", () => {
|
||||
it("creates an item with initial stock and records an IN movement", async () => {
|
||||
const actor = await createTestUser(prisma)
|
||||
const category = await createTestCategory(prisma)
|
||||
|
||||
const result = await createItemUseCase({
|
||||
actorId: actor.id,
|
||||
name: "Laptop",
|
||||
categoryId: category.id,
|
||||
stock: 3,
|
||||
})
|
||||
|
||||
expect(result).toEqual({ success: true })
|
||||
|
||||
const item = await prisma.item.findUnique({
|
||||
where: { name: "Laptop" },
|
||||
include: { movements: true },
|
||||
})
|
||||
|
||||
expect(item).toMatchObject({
|
||||
name: "Laptop",
|
||||
categoryId: category.id,
|
||||
stock: 3,
|
||||
deletedAt: null,
|
||||
})
|
||||
expect(item?.movements).toHaveLength(1)
|
||||
expect(item?.movements[0]).toMatchObject({
|
||||
type: "IN",
|
||||
quantity: 3,
|
||||
userId: actor.id,
|
||||
})
|
||||
})
|
||||
|
||||
it("rejects duplicate item names", async () => {
|
||||
const actor = await createTestUser(prisma)
|
||||
const category = await createTestCategory(prisma)
|
||||
|
||||
await createItemUseCase({
|
||||
actorId: actor.id,
|
||||
name: "Monitor",
|
||||
categoryId: category.id,
|
||||
stock: 0,
|
||||
})
|
||||
|
||||
const duplicate = await createItemUseCase({
|
||||
actorId: actor.id,
|
||||
name: "Monitor",
|
||||
categoryId: category.id,
|
||||
stock: 0,
|
||||
})
|
||||
|
||||
expect(duplicate).toEqual({
|
||||
success: false,
|
||||
errors: {
|
||||
name: ["An item with this name already exists"],
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it("blocks deleting items with stock and soft deletes empty items", async () => {
|
||||
const actor = await createTestUser(prisma)
|
||||
const category = await createTestCategory(prisma)
|
||||
|
||||
await createItemUseCase({
|
||||
actorId: actor.id,
|
||||
name: "Keyboard",
|
||||
categoryId: category.id,
|
||||
stock: 2,
|
||||
})
|
||||
|
||||
const stockedItem = await prisma.item.findUniqueOrThrow({
|
||||
where: { name: "Keyboard" },
|
||||
})
|
||||
|
||||
await expect(deleteItemUseCase(stockedItem.id)).resolves.toEqual({
|
||||
success: false,
|
||||
errors: { id: ["Item has stock, you cannot delete it"] },
|
||||
})
|
||||
|
||||
await createItemUseCase({
|
||||
actorId: actor.id,
|
||||
name: "Mouse",
|
||||
categoryId: category.id,
|
||||
stock: 0,
|
||||
})
|
||||
|
||||
const emptyItem = await prisma.item.findUniqueOrThrow({
|
||||
where: { name: "Mouse" },
|
||||
})
|
||||
|
||||
await expect(deleteItemUseCase(emptyItem.id)).resolves.toEqual({
|
||||
success: true,
|
||||
})
|
||||
|
||||
const deletedItem = await prisma.item.findUniqueOrThrow({
|
||||
where: { id: emptyItem.id },
|
||||
})
|
||||
|
||||
expect(deletedItem.deletedAt).toBeInstanceOf(Date)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user