feat(people): align people, users, and categories with active inventory records
This commit is contained in:
@@ -11,17 +11,20 @@ let prisma: PrismaClient
|
||||
let createCategoryUseCase: typeof import("@/use-cases/category.use-cases").createCategoryUseCase
|
||||
let updateCategoryUseCase: typeof import("@/use-cases/category.use-cases").updateCategoryUseCase
|
||||
let deleteCategoryUseCase: typeof import("@/use-cases/category.use-cases").deleteCategoryUseCase
|
||||
let CategoryService: typeof import("@/services/category.service").CategoryService
|
||||
|
||||
beforeAll(async () => {
|
||||
await startIntegrationTestDatabase()
|
||||
|
||||
const prismaModule = await import("@/lib/prisma")
|
||||
const categoryUseCases = await import("@/use-cases/category.use-cases")
|
||||
const categoryService = await import("@/services/category.service")
|
||||
|
||||
prisma = prismaModule.prisma
|
||||
createCategoryUseCase = categoryUseCases.createCategoryUseCase
|
||||
updateCategoryUseCase = categoryUseCases.updateCategoryUseCase
|
||||
deleteCategoryUseCase = categoryUseCases.deleteCategoryUseCase
|
||||
CategoryService = categoryService.CategoryService
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -35,77 +38,83 @@ afterAll(async () => {
|
||||
|
||||
describe("category use-cases", () => {
|
||||
it("creates a category and rejects duplicate names", async () => {
|
||||
await expect(createCategoryUseCase({ name: "Hardware" })).resolves.toEqual({
|
||||
expect(await createCategoryUseCase({ name: "Hardware" })).toEqual({
|
||||
success: true,
|
||||
})
|
||||
|
||||
await expect(
|
||||
prisma.category.findUniqueOrThrow({ where: { name: "Hardware" } }),
|
||||
).resolves.toMatchObject({ name: "Hardware" })
|
||||
expect(
|
||||
await prisma.category.findUniqueOrThrow({ where: { name: "Hardware" } }),
|
||||
).toMatchObject({ name: "Hardware" })
|
||||
|
||||
await expect(createCategoryUseCase({ name: "Hardware" })).resolves.toEqual({
|
||||
expect(await createCategoryUseCase({ name: "Hardware" })).toEqual({
|
||||
success: false,
|
||||
errors: { name: ["Category already exists"] },
|
||||
})
|
||||
|
||||
await expect(prisma.category.count()).resolves.toBe(1)
|
||||
expect(await prisma.category.count()).toBe(1)
|
||||
})
|
||||
|
||||
it("updates a category and rejects unchanged or duplicate names", async () => {
|
||||
const category = await createTestCategory(prisma, { name: "Peripherals" })
|
||||
const other = await createTestCategory(prisma, { name: "Networking" })
|
||||
|
||||
await expect(
|
||||
updateCategoryUseCase({ id: category.id, name: "Accessories" }),
|
||||
).resolves.toEqual({ success: true })
|
||||
expect(
|
||||
await updateCategoryUseCase({ id: category.id, name: "Accessories" }),
|
||||
).toEqual({ success: true })
|
||||
|
||||
await expect(
|
||||
prisma.category.findUniqueOrThrow({ where: { id: category.id } }),
|
||||
).resolves.toMatchObject({ name: "Accessories" })
|
||||
expect(
|
||||
await prisma.category.findUniqueOrThrow({ where: { id: category.id } }),
|
||||
).toMatchObject({ name: "Accessories" })
|
||||
|
||||
await expect(
|
||||
updateCategoryUseCase({ id: category.id, name: "Accessories" }),
|
||||
).resolves.toEqual({
|
||||
expect(
|
||||
await updateCategoryUseCase({ id: category.id, name: "Accessories" }),
|
||||
).toEqual({
|
||||
success: false,
|
||||
errors: { name: ["Category name is the same as the old one"] },
|
||||
})
|
||||
|
||||
await expect(
|
||||
updateCategoryUseCase({ id: category.id, name: other.name }),
|
||||
).resolves.toEqual({
|
||||
expect(
|
||||
await updateCategoryUseCase({ id: category.id, name: other.name }),
|
||||
).toEqual({
|
||||
success: false,
|
||||
errors: { name: ["Category already exists"] },
|
||||
})
|
||||
|
||||
await expect(
|
||||
prisma.category.findUniqueOrThrow({ where: { id: category.id } }),
|
||||
).resolves.toMatchObject({ name: "Accessories" })
|
||||
expect(
|
||||
await prisma.category.findUniqueOrThrow({ where: { id: category.id } }),
|
||||
).toMatchObject({ name: "Accessories" })
|
||||
})
|
||||
|
||||
it("deletes empty categories and blocks deleting categories with items", async () => {
|
||||
it("soft deletes empty categories and keeps deleted records out of active lists", async () => {
|
||||
const categoryWithItems = await createTestCategory(prisma, {
|
||||
name: "Computers",
|
||||
})
|
||||
await createTestItem(prisma, { categoryId: categoryWithItems.id })
|
||||
|
||||
await expect(deleteCategoryUseCase(categoryWithItems.id)).resolves.toEqual({
|
||||
expect(await deleteCategoryUseCase(categoryWithItems.id)).toEqual({
|
||||
success: false,
|
||||
errors: { id: ["Category has items"] },
|
||||
})
|
||||
|
||||
await expect(
|
||||
prisma.category.findUnique({ where: { id: categoryWithItems.id } }),
|
||||
).resolves.not.toBeNull()
|
||||
await expect(prisma.item.count()).resolves.toBe(1)
|
||||
expect(
|
||||
await prisma.category.findUnique({ where: { id: categoryWithItems.id } }),
|
||||
).not.toBeNull()
|
||||
expect(await prisma.item.count()).toBe(1)
|
||||
|
||||
const emptyCategory = await createTestCategory(prisma, { name: "Cables" })
|
||||
|
||||
await expect(deleteCategoryUseCase(emptyCategory.id)).resolves.toEqual({
|
||||
expect(await deleteCategoryUseCase(emptyCategory.id)).toEqual({
|
||||
success: true,
|
||||
})
|
||||
|
||||
await expect(
|
||||
prisma.category.findUnique({ where: { id: emptyCategory.id } }),
|
||||
).resolves.toBeNull()
|
||||
const deletedCategory = await prisma.category.findUnique({
|
||||
where: { id: emptyCategory.id },
|
||||
})
|
||||
expect(deletedCategory).not.toBeNull()
|
||||
expect(deletedCategory?.deletedAt).toBeInstanceOf(Date)
|
||||
|
||||
const activeCategories = await CategoryService.findAll()
|
||||
expect(activeCategories).toHaveLength(1)
|
||||
expect(activeCategories[0].id).toBe(categoryWithItems.id)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -58,11 +58,11 @@ describe("createPersonUserUseCase", () => {
|
||||
})
|
||||
|
||||
// No User record created
|
||||
await expect(
|
||||
prisma.user.findUnique({
|
||||
expect(
|
||||
await prisma.user.findUnique({
|
||||
where: { emailNormalized: normalizeEmail("john@example.test") },
|
||||
}),
|
||||
).resolves.toBeNull()
|
||||
).toBeNull()
|
||||
})
|
||||
|
||||
it("creates a Person with null email when not providing email and role is NO_USER", async () => {
|
||||
@@ -196,9 +196,9 @@ describe("createPersonUserUseCase", () => {
|
||||
if (!user.passwordHash) throw new Error("Expected password hash")
|
||||
|
||||
const { verifyPassword } = await import("@/lib/security")
|
||||
await expect(
|
||||
verifyPassword("plaintext-password", user.passwordHash),
|
||||
).resolves.toBe(true)
|
||||
expect(await verifyPassword("plaintext-password", user.passwordHash)).toBe(
|
||||
true,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -221,7 +221,7 @@ describe("createPersonUserUseCase", () => {
|
||||
errors: { email: ["Email already exists"] },
|
||||
})
|
||||
|
||||
await expect(prisma.person.count()).resolves.toBe(1)
|
||||
expect(await prisma.person.count()).toBe(1)
|
||||
})
|
||||
|
||||
it("rejects submission when email already exists in User table", async () => {
|
||||
@@ -244,8 +244,8 @@ describe("createPersonUserUseCase", () => {
|
||||
})
|
||||
|
||||
// No new Person or User was created
|
||||
await expect(prisma.person.count()).resolves.toBe(0)
|
||||
await expect(prisma.user.count()).resolves.toBe(1)
|
||||
expect(await prisma.person.count()).toBe(0)
|
||||
expect(await prisma.user.count()).toBe(1)
|
||||
})
|
||||
|
||||
it("accepts submission when email is unique across both tables", async () => {
|
||||
|
||||
@@ -10,16 +10,19 @@ import {
|
||||
let prisma: PrismaClient
|
||||
let createPersonUseCase: typeof import("@/use-cases/person.use-cases").createPersonUseCase
|
||||
let updatePersonUseCase: typeof import("@/use-cases/person.use-cases").updatePersonUseCase
|
||||
let PersonService: typeof import("@/services/person.service").PersonService
|
||||
|
||||
beforeAll(async () => {
|
||||
await startIntegrationTestDatabase()
|
||||
|
||||
const prismaModule = await import("@/lib/prisma")
|
||||
const personUseCases = await import("@/use-cases/person.use-cases")
|
||||
const personService = await import("@/services/person.service")
|
||||
|
||||
prisma = prismaModule.prisma
|
||||
createPersonUseCase = personUseCases.createPersonUseCase
|
||||
updatePersonUseCase = personUseCases.updatePersonUseCase
|
||||
PersonService = personService.PersonService
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -43,11 +46,11 @@ describe("person use-cases", () => {
|
||||
}),
|
||||
).resolves.toEqual({ success: true })
|
||||
|
||||
await expect(
|
||||
prisma.person.findFirstOrThrow({
|
||||
expect(
|
||||
await prisma.person.findFirstOrThrow({
|
||||
where: { firstName: "Person", lastName: "One" },
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
).toMatchObject({
|
||||
firstName: "Person",
|
||||
lastName: "One",
|
||||
department: "IT",
|
||||
@@ -71,11 +74,11 @@ describe("person use-cases", () => {
|
||||
}),
|
||||
).resolves.toEqual({ success: true })
|
||||
|
||||
await expect(
|
||||
prisma.person.findFirstOrThrow({
|
||||
expect(
|
||||
await prisma.person.findFirstOrThrow({
|
||||
where: { firstName: "Linked" },
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
).toMatchObject({
|
||||
firstName: "Linked",
|
||||
lastName: "Person",
|
||||
department: "ENGINEERING",
|
||||
@@ -102,7 +105,7 @@ describe("person use-cases", () => {
|
||||
errors: { email: ["Email already exists"] },
|
||||
})
|
||||
|
||||
await expect(prisma.person.count()).resolves.toBe(1)
|
||||
expect(await prisma.person.count()).toBe(1)
|
||||
})
|
||||
|
||||
it("updates a person and rejects duplicate emails", async () => {
|
||||
@@ -125,9 +128,7 @@ describe("person use-cases", () => {
|
||||
}),
|
||||
).resolves.toEqual({ success: true })
|
||||
|
||||
await expect(
|
||||
prisma.person.findUniqueOrThrow({ where: { id: person.id } }),
|
||||
).resolves.toMatchObject({
|
||||
expect(await prisma.person.findUniqueOrThrow({ where: { id: person.id } })).toMatchObject({
|
||||
firstName: "Edited",
|
||||
lastName: "Person",
|
||||
department: "ENGINEERING",
|
||||
@@ -149,12 +150,10 @@ describe("person use-cases", () => {
|
||||
errors: { email: ["Email already exists"] },
|
||||
})
|
||||
|
||||
await expect(
|
||||
prisma.person.findUniqueOrThrow({ where: { id: person.id } }),
|
||||
).resolves.toMatchObject({
|
||||
expect(await prisma.person.findUniqueOrThrow({ where: { id: person.id } })).toMatchObject({
|
||||
email: "edited@example.test",
|
||||
})
|
||||
await expect(prisma.person.count()).resolves.toBe(2)
|
||||
expect(await prisma.person.count()).toBe(2)
|
||||
})
|
||||
|
||||
it("searches by email and name in paginated results", async () => {
|
||||
@@ -163,13 +162,15 @@ describe("person use-cases", () => {
|
||||
lastName: "Smith",
|
||||
email: "alice@company.com",
|
||||
})
|
||||
await createTestPerson(prisma, {
|
||||
firstName: "Bob",
|
||||
lastName: "Jones",
|
||||
email: "bob@other.com",
|
||||
const archivedPerson = await createTestPerson(prisma, {
|
||||
firstName: "Archive",
|
||||
lastName: "Person",
|
||||
email: "archive@company.com",
|
||||
})
|
||||
await prisma.person.update({
|
||||
where: { id: archivedPerson.id },
|
||||
data: { deletedAt: new Date() },
|
||||
})
|
||||
|
||||
const { PersonService } = await import("@/services/person.service")
|
||||
|
||||
const emailResults = await PersonService.findAllPaginated({
|
||||
search: "company",
|
||||
@@ -178,13 +179,16 @@ describe("person use-cases", () => {
|
||||
})
|
||||
expect(emailResults.data).toHaveLength(1)
|
||||
expect(emailResults.data[0].firstName).toBe("Alice")
|
||||
expect(await PersonService.findAllPeopleCount()).toBe(1)
|
||||
|
||||
expect(await PersonService.findById(archivedPerson.id)).toBeNull()
|
||||
|
||||
const nameResults = await PersonService.findAllPaginated({
|
||||
search: "Bob",
|
||||
search: "Alice",
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
})
|
||||
expect(nameResults.data).toHaveLength(1)
|
||||
expect(nameResults.data[0].firstName).toBe("Bob")
|
||||
expect(nameResults.data[0].firstName).toBe("Alice")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -213,7 +213,7 @@ describe("updatePersonUserUseCase", () => {
|
||||
describe("validation errors", () => {
|
||||
it("returns error when person is not found", async () => {
|
||||
const result = await updatePersonUserUseCase({
|
||||
id: "nonexistent-id",
|
||||
id: "00000000-0000-0000-0000-000000000000",
|
||||
firstName: "Ghost",
|
||||
lastName: "Person",
|
||||
department: "OTHER",
|
||||
|
||||
@@ -14,6 +14,9 @@ let updateUserUseCase: typeof import("@/use-cases/user.use-cases").updateUserUse
|
||||
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
|
||||
let getUserById: typeof import("@/services/user.service").getUserById
|
||||
let getUsers: typeof import("@/services/user.service").getUsers
|
||||
let countActiveAdmins: typeof import("@/services/user.service").countActiveAdmins
|
||||
|
||||
beforeAll(async () => {
|
||||
await startIntegrationTestDatabase()
|
||||
@@ -21,6 +24,7 @@ beforeAll(async () => {
|
||||
const prismaModule = await import("@/lib/prisma")
|
||||
const userUseCases = await import("@/use-cases/user.use-cases")
|
||||
const security = await import("@/lib/security")
|
||||
const userService = await import("@/services/user.service")
|
||||
|
||||
prisma = prismaModule.prisma
|
||||
createUserUseCase = userUseCases.createUserUseCase
|
||||
@@ -28,6 +32,9 @@ beforeAll(async () => {
|
||||
setUserActiveUseCase = userUseCases.setUserActiveUseCase
|
||||
resetUserPasswordUseCase = userUseCases.resetUserPasswordUseCase
|
||||
verifyPassword = security.verifyPassword
|
||||
getUserById = userService.getUserById
|
||||
getUsers = userService.getUsers
|
||||
countActiveAdmins = userService.countActiveAdmins
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -88,12 +95,34 @@ describe("user use-cases", () => {
|
||||
errors: { email: ["Email already exists"] },
|
||||
})
|
||||
|
||||
await expect(prisma.user.count()).resolves.toBe(1)
|
||||
await expect(
|
||||
prisma.user.findUniqueOrThrow({
|
||||
expect(await prisma.user.count()).toBe(1)
|
||||
expect(
|
||||
await prisma.user.findUniqueOrThrow({
|
||||
where: { emailNormalized: normalizeEmail("existing@example.test") },
|
||||
}),
|
||||
).resolves.toMatchObject({ email: "existing@example.test" })
|
||||
).toMatchObject({ email: "existing@example.test" })
|
||||
})
|
||||
|
||||
it("excludes soft-deleted users from active queries", async () => {
|
||||
const activeUser = await createTestUser(prisma, {
|
||||
email: "active-user@example.test",
|
||||
role: "ADMIN",
|
||||
})
|
||||
const softDeletedUser = await createTestUser(prisma, {
|
||||
email: "deleted-user@example.test",
|
||||
role: "ADMIN",
|
||||
})
|
||||
|
||||
await prisma.user.update({
|
||||
where: { id: softDeletedUser.id },
|
||||
data: { deletedAt: new Date() },
|
||||
})
|
||||
|
||||
const users = await getUsers({ page: 1, pageSize: 10 })
|
||||
expect(users.data).toHaveLength(1)
|
||||
expect(users.data[0].id).toBe(activeUser.id)
|
||||
expect(await getUserById(softDeletedUser.id)).toBeNull()
|
||||
expect(await countActiveAdmins()).toBe(1)
|
||||
})
|
||||
|
||||
it("updates a user while preserving uniqueness constraints", async () => {
|
||||
@@ -118,9 +147,7 @@ describe("user use-cases", () => {
|
||||
}),
|
||||
).resolves.toEqual({ success: true })
|
||||
|
||||
await expect(
|
||||
prisma.user.findUniqueOrThrow({ where: { id: user.id } }),
|
||||
).resolves.toMatchObject({
|
||||
expect(await prisma.user.findUniqueOrThrow({ where: { id: user.id } })).toMatchObject({
|
||||
name: "Edited User",
|
||||
email: "edited@example.test",
|
||||
role: "MANAGER",
|
||||
@@ -141,9 +168,7 @@ describe("user use-cases", () => {
|
||||
errors: { email: ["Email already exists"] },
|
||||
})
|
||||
|
||||
await expect(
|
||||
prisma.user.findUniqueOrThrow({ where: { id: user.id } }),
|
||||
).resolves.toMatchObject({
|
||||
expect(await prisma.user.findUniqueOrThrow({ where: { id: user.id } })).toMatchObject({
|
||||
email: "edited@example.test",
|
||||
})
|
||||
})
|
||||
@@ -165,9 +190,10 @@ describe("user use-cases", () => {
|
||||
errors: { id: ["You cannot remove your own administrator access"] },
|
||||
})
|
||||
|
||||
await expect(
|
||||
prisma.user.findUniqueOrThrow({ where: { id: admin.id } }),
|
||||
).resolves.toMatchObject({ role: "ADMIN", status: "ACTIVE" })
|
||||
expect(await prisma.user.findUniqueOrThrow({ where: { id: admin.id } })).toMatchObject({
|
||||
role: "ADMIN",
|
||||
status: "ACTIVE",
|
||||
})
|
||||
})
|
||||
|
||||
it("protects the last active administrator but allows deactivation when another active admin exists", async () => {
|
||||
@@ -206,9 +232,9 @@ describe("user use-cases", () => {
|
||||
}),
|
||||
).resolves.toEqual({ success: true })
|
||||
|
||||
await expect(
|
||||
prisma.user.findUniqueOrThrow({ where: { id: firstAdmin.id } }),
|
||||
).resolves.toMatchObject({ status: "DISABLED" })
|
||||
expect(await prisma.user.findUniqueOrThrow({ where: { id: firstAdmin.id } })).toMatchObject({
|
||||
status: "DISABLED",
|
||||
})
|
||||
})
|
||||
|
||||
it("prevents self-deactivation", async () => {
|
||||
@@ -226,9 +252,9 @@ describe("user use-cases", () => {
|
||||
errors: { id: ["You cannot deactivate your own user"] },
|
||||
})
|
||||
|
||||
await expect(
|
||||
prisma.user.findUniqueOrThrow({ where: { id: admin.id } }),
|
||||
).resolves.toMatchObject({ status: "ACTIVE" })
|
||||
expect(await prisma.user.findUniqueOrThrow({ where: { id: admin.id } })).toMatchObject({
|
||||
status: "ACTIVE",
|
||||
})
|
||||
})
|
||||
|
||||
it("resets a user password and rejects missing users", async () => {
|
||||
|
||||
Reference in New Issue
Block a user