feat(people): align people, users, and categories with active inventory records
This commit is contained in:
@@ -3,9 +3,14 @@ import { paginate } from "@/lib/paginate"
|
||||
import prisma from "@/lib/prisma"
|
||||
import type { Category, CategorySummary, CategoryWithItemsCount } from "@/types"
|
||||
|
||||
const activeRecordWhere = {
|
||||
deletedAt: null,
|
||||
} as const
|
||||
|
||||
export const CategoryService = {
|
||||
findAll: async (): Promise<CategorySummary[]> => {
|
||||
return prisma.category.findMany({
|
||||
where: activeRecordWhere,
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
@@ -15,6 +20,7 @@ export const CategoryService = {
|
||||
|
||||
findAllWithItemsCount: async (): Promise<CategoryWithItemsCount[]> => {
|
||||
return prisma.category.findMany({
|
||||
where: activeRecordWhere,
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
@@ -38,6 +44,7 @@ export const CategoryService = {
|
||||
page,
|
||||
pageSize,
|
||||
where: {
|
||||
...activeRecordWhere,
|
||||
...(search
|
||||
? {
|
||||
name: { contains: search, mode: "insensitive" },
|
||||
@@ -58,7 +65,10 @@ export const CategoryService = {
|
||||
db: Prisma.TransactionClient | typeof prisma = prisma,
|
||||
): Promise<Category | null> => {
|
||||
return db.category.findFirst({
|
||||
where: { name: { equals: name, mode: "insensitive" } },
|
||||
where: {
|
||||
name: { equals: name, mode: "insensitive" },
|
||||
...activeRecordWhere,
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
@@ -66,15 +76,15 @@ export const CategoryService = {
|
||||
id: string,
|
||||
db: Prisma.TransactionClient | typeof prisma = prisma,
|
||||
): Promise<Category | null> => {
|
||||
return db.category.findUnique({ where: { id } })
|
||||
return db.category.findFirst({ where: { id, ...activeRecordWhere } })
|
||||
},
|
||||
|
||||
findByIdWithItemsCount: async (
|
||||
id: string,
|
||||
db: Prisma.TransactionClient | typeof prisma = prisma,
|
||||
): Promise<CategoryWithItemsCount | null> => {
|
||||
return db.category.findUnique({
|
||||
where: { id },
|
||||
return db.category.findFirst({
|
||||
where: { id, ...activeRecordWhere },
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
@@ -102,6 +112,9 @@ export const CategoryService = {
|
||||
id: string,
|
||||
db: Prisma.TransactionClient | typeof prisma = prisma,
|
||||
): Promise<Category> => {
|
||||
return db.category.delete({ where: { id } })
|
||||
return db.category.update({
|
||||
where: { id },
|
||||
data: { deletedAt: new Date() },
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
@@ -18,6 +18,10 @@ const personWithUserSelect = {
|
||||
},
|
||||
} as const
|
||||
|
||||
const activeRecordWhere = {
|
||||
deletedAt: null,
|
||||
} as const
|
||||
|
||||
export type PersonWithUser = Prisma.PersonGetPayload<
|
||||
typeof personWithUserSelect
|
||||
>
|
||||
@@ -25,6 +29,7 @@ export type PersonWithUser = Prisma.PersonGetPayload<
|
||||
export const PersonService = {
|
||||
findAll: async (): Promise<Person[]> => {
|
||||
return prisma.person.findMany({
|
||||
where: activeRecordWhere,
|
||||
orderBy: {
|
||||
firstName: "asc",
|
||||
},
|
||||
@@ -45,6 +50,7 @@ export const PersonService = {
|
||||
pageSize,
|
||||
include: personWithUserSelect.include,
|
||||
where: {
|
||||
...activeRecordWhere,
|
||||
...(search
|
||||
? {
|
||||
OR: [
|
||||
@@ -58,22 +64,22 @@ export const PersonService = {
|
||||
})
|
||||
},
|
||||
findAllPeopleCount: async (): Promise<number> => {
|
||||
return prisma.person.count()
|
||||
return prisma.person.count({ where: activeRecordWhere })
|
||||
},
|
||||
|
||||
findById: async (
|
||||
id: string,
|
||||
db: Prisma.TransactionClient | typeof prisma = prisma,
|
||||
): Promise<Person | null> => {
|
||||
return db.person.findUnique({ where: { id } })
|
||||
return db.person.findFirst({ where: { id, ...activeRecordWhere } })
|
||||
},
|
||||
|
||||
findByIdWithUser: async (
|
||||
id: string,
|
||||
db: Prisma.TransactionClient | typeof prisma = prisma,
|
||||
): Promise<PersonWithUser | null> => {
|
||||
return db.person.findUnique({
|
||||
where: { id },
|
||||
return db.person.findFirst({
|
||||
where: { id, ...activeRecordWhere },
|
||||
include: personWithUserSelect.include,
|
||||
})
|
||||
},
|
||||
@@ -82,7 +88,7 @@ export const PersonService = {
|
||||
email: string,
|
||||
db: Prisma.TransactionClient | typeof prisma = prisma,
|
||||
): Promise<Person | null> => {
|
||||
return db.person.findFirst({ where: { email } })
|
||||
return db.person.findFirst({ where: { email, ...activeRecordWhere } })
|
||||
},
|
||||
|
||||
create: async (
|
||||
|
||||
@@ -43,6 +43,10 @@ type GetUsersParams = {
|
||||
isActive?: boolean
|
||||
}
|
||||
|
||||
const activeRecordWhere = {
|
||||
deletedAt: null,
|
||||
} as const
|
||||
|
||||
function userStatusFromActive(isActive: boolean | undefined) {
|
||||
if (typeof isActive !== "boolean") return undefined
|
||||
return isActive ? UserStatus.ACTIVE : UserStatus.DISABLED
|
||||
@@ -85,8 +89,8 @@ export async function getUserById(
|
||||
id: string,
|
||||
db: Prisma.TransactionClient | typeof prisma = prisma,
|
||||
) {
|
||||
const user = await db.user.findUnique({
|
||||
where: { id },
|
||||
const user = await db.user.findFirst({
|
||||
where: { id, ...activeRecordWhere },
|
||||
select: userWithoutPasswordSelect,
|
||||
})
|
||||
|
||||
@@ -96,8 +100,8 @@ export async function getUserById(
|
||||
export async function getUserProfileById(
|
||||
id: string,
|
||||
): Promise<UserWithoutPassword | null> {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id },
|
||||
const user = await prisma.user.findFirst({
|
||||
where: { id, ...activeRecordWhere },
|
||||
select: userWithoutPasswordSelect,
|
||||
})
|
||||
|
||||
@@ -108,8 +112,8 @@ export async function getUserByEmail(
|
||||
email: string,
|
||||
db: Prisma.TransactionClient | typeof prisma = prisma,
|
||||
) {
|
||||
const user = await db.user.findUnique({
|
||||
where: { emailNormalized: normalizeEmail(email) },
|
||||
const user = await db.user.findFirst({
|
||||
where: { emailNormalized: normalizeEmail(email), ...activeRecordWhere },
|
||||
select: userWithoutPasswordSelect,
|
||||
})
|
||||
|
||||
@@ -120,8 +124,8 @@ export async function getUserCredentialsByEmail(
|
||||
email: string,
|
||||
db: Prisma.TransactionClient | typeof prisma = prisma,
|
||||
) {
|
||||
return await db.user.findUnique({
|
||||
where: { emailNormalized: normalizeEmail(email) },
|
||||
return await db.user.findFirst({
|
||||
where: { emailNormalized: normalizeEmail(email), ...activeRecordWhere },
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
@@ -145,6 +149,7 @@ export async function getUsers({
|
||||
page,
|
||||
pageSize,
|
||||
where: {
|
||||
...activeRecordWhere,
|
||||
...(typeof isActive === "boolean"
|
||||
? { status: userStatusFromActive(isActive) }
|
||||
: {}),
|
||||
@@ -231,6 +236,7 @@ export async function countActiveAdmins(
|
||||
): Promise<number> {
|
||||
return db.user.count({
|
||||
where: {
|
||||
...activeRecordWhere,
|
||||
role: UserRole.ADMIN,
|
||||
status: UserStatus.ACTIVE,
|
||||
},
|
||||
|
||||
@@ -248,10 +248,7 @@ export async function updatePersonUserUseCase(
|
||||
|
||||
try {
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
const existing = await tx.person.findUnique({
|
||||
where: { id },
|
||||
select: { id: true, userId: true },
|
||||
})
|
||||
const existing = await PersonService.findById(id, tx)
|
||||
|
||||
if (!existing) {
|
||||
return personError({ id: ["Person not found"] })
|
||||
|
||||
@@ -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