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 prisma from "@/lib/prisma"
|
||||||
import type { Category, CategorySummary, CategoryWithItemsCount } from "@/types"
|
import type { Category, CategorySummary, CategoryWithItemsCount } from "@/types"
|
||||||
|
|
||||||
|
const activeRecordWhere = {
|
||||||
|
deletedAt: null,
|
||||||
|
} as const
|
||||||
|
|
||||||
export const CategoryService = {
|
export const CategoryService = {
|
||||||
findAll: async (): Promise<CategorySummary[]> => {
|
findAll: async (): Promise<CategorySummary[]> => {
|
||||||
return prisma.category.findMany({
|
return prisma.category.findMany({
|
||||||
|
where: activeRecordWhere,
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
name: true,
|
name: true,
|
||||||
@@ -15,6 +20,7 @@ export const CategoryService = {
|
|||||||
|
|
||||||
findAllWithItemsCount: async (): Promise<CategoryWithItemsCount[]> => {
|
findAllWithItemsCount: async (): Promise<CategoryWithItemsCount[]> => {
|
||||||
return prisma.category.findMany({
|
return prisma.category.findMany({
|
||||||
|
where: activeRecordWhere,
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
name: true,
|
name: true,
|
||||||
@@ -38,6 +44,7 @@ export const CategoryService = {
|
|||||||
page,
|
page,
|
||||||
pageSize,
|
pageSize,
|
||||||
where: {
|
where: {
|
||||||
|
...activeRecordWhere,
|
||||||
...(search
|
...(search
|
||||||
? {
|
? {
|
||||||
name: { contains: search, mode: "insensitive" },
|
name: { contains: search, mode: "insensitive" },
|
||||||
@@ -58,7 +65,10 @@ export const CategoryService = {
|
|||||||
db: Prisma.TransactionClient | typeof prisma = prisma,
|
db: Prisma.TransactionClient | typeof prisma = prisma,
|
||||||
): Promise<Category | null> => {
|
): Promise<Category | null> => {
|
||||||
return db.category.findFirst({
|
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,
|
id: string,
|
||||||
db: Prisma.TransactionClient | typeof prisma = prisma,
|
db: Prisma.TransactionClient | typeof prisma = prisma,
|
||||||
): Promise<Category | null> => {
|
): Promise<Category | null> => {
|
||||||
return db.category.findUnique({ where: { id } })
|
return db.category.findFirst({ where: { id, ...activeRecordWhere } })
|
||||||
},
|
},
|
||||||
|
|
||||||
findByIdWithItemsCount: async (
|
findByIdWithItemsCount: async (
|
||||||
id: string,
|
id: string,
|
||||||
db: Prisma.TransactionClient | typeof prisma = prisma,
|
db: Prisma.TransactionClient | typeof prisma = prisma,
|
||||||
): Promise<CategoryWithItemsCount | null> => {
|
): Promise<CategoryWithItemsCount | null> => {
|
||||||
return db.category.findUnique({
|
return db.category.findFirst({
|
||||||
where: { id },
|
where: { id, ...activeRecordWhere },
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
name: true,
|
name: true,
|
||||||
@@ -102,6 +112,9 @@ export const CategoryService = {
|
|||||||
id: string,
|
id: string,
|
||||||
db: Prisma.TransactionClient | typeof prisma = prisma,
|
db: Prisma.TransactionClient | typeof prisma = prisma,
|
||||||
): Promise<Category> => {
|
): 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
|
} as const
|
||||||
|
|
||||||
|
const activeRecordWhere = {
|
||||||
|
deletedAt: null,
|
||||||
|
} as const
|
||||||
|
|
||||||
export type PersonWithUser = Prisma.PersonGetPayload<
|
export type PersonWithUser = Prisma.PersonGetPayload<
|
||||||
typeof personWithUserSelect
|
typeof personWithUserSelect
|
||||||
>
|
>
|
||||||
@@ -25,6 +29,7 @@ export type PersonWithUser = Prisma.PersonGetPayload<
|
|||||||
export const PersonService = {
|
export const PersonService = {
|
||||||
findAll: async (): Promise<Person[]> => {
|
findAll: async (): Promise<Person[]> => {
|
||||||
return prisma.person.findMany({
|
return prisma.person.findMany({
|
||||||
|
where: activeRecordWhere,
|
||||||
orderBy: {
|
orderBy: {
|
||||||
firstName: "asc",
|
firstName: "asc",
|
||||||
},
|
},
|
||||||
@@ -45,6 +50,7 @@ export const PersonService = {
|
|||||||
pageSize,
|
pageSize,
|
||||||
include: personWithUserSelect.include,
|
include: personWithUserSelect.include,
|
||||||
where: {
|
where: {
|
||||||
|
...activeRecordWhere,
|
||||||
...(search
|
...(search
|
||||||
? {
|
? {
|
||||||
OR: [
|
OR: [
|
||||||
@@ -58,22 +64,22 @@ export const PersonService = {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
findAllPeopleCount: async (): Promise<number> => {
|
findAllPeopleCount: async (): Promise<number> => {
|
||||||
return prisma.person.count()
|
return prisma.person.count({ where: activeRecordWhere })
|
||||||
},
|
},
|
||||||
|
|
||||||
findById: async (
|
findById: async (
|
||||||
id: string,
|
id: string,
|
||||||
db: Prisma.TransactionClient | typeof prisma = prisma,
|
db: Prisma.TransactionClient | typeof prisma = prisma,
|
||||||
): Promise<Person | null> => {
|
): Promise<Person | null> => {
|
||||||
return db.person.findUnique({ where: { id } })
|
return db.person.findFirst({ where: { id, ...activeRecordWhere } })
|
||||||
},
|
},
|
||||||
|
|
||||||
findByIdWithUser: async (
|
findByIdWithUser: async (
|
||||||
id: string,
|
id: string,
|
||||||
db: Prisma.TransactionClient | typeof prisma = prisma,
|
db: Prisma.TransactionClient | typeof prisma = prisma,
|
||||||
): Promise<PersonWithUser | null> => {
|
): Promise<PersonWithUser | null> => {
|
||||||
return db.person.findUnique({
|
return db.person.findFirst({
|
||||||
where: { id },
|
where: { id, ...activeRecordWhere },
|
||||||
include: personWithUserSelect.include,
|
include: personWithUserSelect.include,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -82,7 +88,7 @@ export const PersonService = {
|
|||||||
email: string,
|
email: string,
|
||||||
db: Prisma.TransactionClient | typeof prisma = prisma,
|
db: Prisma.TransactionClient | typeof prisma = prisma,
|
||||||
): Promise<Person | null> => {
|
): Promise<Person | null> => {
|
||||||
return db.person.findFirst({ where: { email } })
|
return db.person.findFirst({ where: { email, ...activeRecordWhere } })
|
||||||
},
|
},
|
||||||
|
|
||||||
create: async (
|
create: async (
|
||||||
|
|||||||
@@ -43,6 +43,10 @@ type GetUsersParams = {
|
|||||||
isActive?: boolean
|
isActive?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const activeRecordWhere = {
|
||||||
|
deletedAt: null,
|
||||||
|
} as const
|
||||||
|
|
||||||
function userStatusFromActive(isActive: boolean | undefined) {
|
function userStatusFromActive(isActive: boolean | undefined) {
|
||||||
if (typeof isActive !== "boolean") return undefined
|
if (typeof isActive !== "boolean") return undefined
|
||||||
return isActive ? UserStatus.ACTIVE : UserStatus.DISABLED
|
return isActive ? UserStatus.ACTIVE : UserStatus.DISABLED
|
||||||
@@ -85,8 +89,8 @@ export async function getUserById(
|
|||||||
id: string,
|
id: string,
|
||||||
db: Prisma.TransactionClient | typeof prisma = prisma,
|
db: Prisma.TransactionClient | typeof prisma = prisma,
|
||||||
) {
|
) {
|
||||||
const user = await db.user.findUnique({
|
const user = await db.user.findFirst({
|
||||||
where: { id },
|
where: { id, ...activeRecordWhere },
|
||||||
select: userWithoutPasswordSelect,
|
select: userWithoutPasswordSelect,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -96,8 +100,8 @@ export async function getUserById(
|
|||||||
export async function getUserProfileById(
|
export async function getUserProfileById(
|
||||||
id: string,
|
id: string,
|
||||||
): Promise<UserWithoutPassword | null> {
|
): Promise<UserWithoutPassword | null> {
|
||||||
const user = await prisma.user.findUnique({
|
const user = await prisma.user.findFirst({
|
||||||
where: { id },
|
where: { id, ...activeRecordWhere },
|
||||||
select: userWithoutPasswordSelect,
|
select: userWithoutPasswordSelect,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -108,8 +112,8 @@ export async function getUserByEmail(
|
|||||||
email: string,
|
email: string,
|
||||||
db: Prisma.TransactionClient | typeof prisma = prisma,
|
db: Prisma.TransactionClient | typeof prisma = prisma,
|
||||||
) {
|
) {
|
||||||
const user = await db.user.findUnique({
|
const user = await db.user.findFirst({
|
||||||
where: { emailNormalized: normalizeEmail(email) },
|
where: { emailNormalized: normalizeEmail(email), ...activeRecordWhere },
|
||||||
select: userWithoutPasswordSelect,
|
select: userWithoutPasswordSelect,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -120,8 +124,8 @@ export async function getUserCredentialsByEmail(
|
|||||||
email: string,
|
email: string,
|
||||||
db: Prisma.TransactionClient | typeof prisma = prisma,
|
db: Prisma.TransactionClient | typeof prisma = prisma,
|
||||||
) {
|
) {
|
||||||
return await db.user.findUnique({
|
return await db.user.findFirst({
|
||||||
where: { emailNormalized: normalizeEmail(email) },
|
where: { emailNormalized: normalizeEmail(email), ...activeRecordWhere },
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
name: true,
|
name: true,
|
||||||
@@ -145,6 +149,7 @@ export async function getUsers({
|
|||||||
page,
|
page,
|
||||||
pageSize,
|
pageSize,
|
||||||
where: {
|
where: {
|
||||||
|
...activeRecordWhere,
|
||||||
...(typeof isActive === "boolean"
|
...(typeof isActive === "boolean"
|
||||||
? { status: userStatusFromActive(isActive) }
|
? { status: userStatusFromActive(isActive) }
|
||||||
: {}),
|
: {}),
|
||||||
@@ -231,6 +236,7 @@ export async function countActiveAdmins(
|
|||||||
): Promise<number> {
|
): Promise<number> {
|
||||||
return db.user.count({
|
return db.user.count({
|
||||||
where: {
|
where: {
|
||||||
|
...activeRecordWhere,
|
||||||
role: UserRole.ADMIN,
|
role: UserRole.ADMIN,
|
||||||
status: UserStatus.ACTIVE,
|
status: UserStatus.ACTIVE,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -248,10 +248,7 @@ export async function updatePersonUserUseCase(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
return await prisma.$transaction(async (tx) => {
|
return await prisma.$transaction(async (tx) => {
|
||||||
const existing = await tx.person.findUnique({
|
const existing = await PersonService.findById(id, tx)
|
||||||
where: { id },
|
|
||||||
select: { id: true, userId: true },
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
return personError({ id: ["Person not found"] })
|
return personError({ id: ["Person not found"] })
|
||||||
|
|||||||
@@ -11,17 +11,20 @@ let prisma: PrismaClient
|
|||||||
let createCategoryUseCase: typeof import("@/use-cases/category.use-cases").createCategoryUseCase
|
let createCategoryUseCase: typeof import("@/use-cases/category.use-cases").createCategoryUseCase
|
||||||
let updateCategoryUseCase: typeof import("@/use-cases/category.use-cases").updateCategoryUseCase
|
let updateCategoryUseCase: typeof import("@/use-cases/category.use-cases").updateCategoryUseCase
|
||||||
let deleteCategoryUseCase: typeof import("@/use-cases/category.use-cases").deleteCategoryUseCase
|
let deleteCategoryUseCase: typeof import("@/use-cases/category.use-cases").deleteCategoryUseCase
|
||||||
|
let CategoryService: typeof import("@/services/category.service").CategoryService
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await startIntegrationTestDatabase()
|
await startIntegrationTestDatabase()
|
||||||
|
|
||||||
const prismaModule = await import("@/lib/prisma")
|
const prismaModule = await import("@/lib/prisma")
|
||||||
const categoryUseCases = await import("@/use-cases/category.use-cases")
|
const categoryUseCases = await import("@/use-cases/category.use-cases")
|
||||||
|
const categoryService = await import("@/services/category.service")
|
||||||
|
|
||||||
prisma = prismaModule.prisma
|
prisma = prismaModule.prisma
|
||||||
createCategoryUseCase = categoryUseCases.createCategoryUseCase
|
createCategoryUseCase = categoryUseCases.createCategoryUseCase
|
||||||
updateCategoryUseCase = categoryUseCases.updateCategoryUseCase
|
updateCategoryUseCase = categoryUseCases.updateCategoryUseCase
|
||||||
deleteCategoryUseCase = categoryUseCases.deleteCategoryUseCase
|
deleteCategoryUseCase = categoryUseCases.deleteCategoryUseCase
|
||||||
|
CategoryService = categoryService.CategoryService
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
@@ -35,77 +38,83 @@ afterAll(async () => {
|
|||||||
|
|
||||||
describe("category use-cases", () => {
|
describe("category use-cases", () => {
|
||||||
it("creates a category and rejects duplicate names", async () => {
|
it("creates a category and rejects duplicate names", async () => {
|
||||||
await expect(createCategoryUseCase({ name: "Hardware" })).resolves.toEqual({
|
expect(await createCategoryUseCase({ name: "Hardware" })).toEqual({
|
||||||
success: true,
|
success: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
await expect(
|
expect(
|
||||||
prisma.category.findUniqueOrThrow({ where: { name: "Hardware" } }),
|
await prisma.category.findUniqueOrThrow({ where: { name: "Hardware" } }),
|
||||||
).resolves.toMatchObject({ name: "Hardware" })
|
).toMatchObject({ name: "Hardware" })
|
||||||
|
|
||||||
await expect(createCategoryUseCase({ name: "Hardware" })).resolves.toEqual({
|
expect(await createCategoryUseCase({ name: "Hardware" })).toEqual({
|
||||||
success: false,
|
success: false,
|
||||||
errors: { name: ["Category already exists"] },
|
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 () => {
|
it("updates a category and rejects unchanged or duplicate names", async () => {
|
||||||
const category = await createTestCategory(prisma, { name: "Peripherals" })
|
const category = await createTestCategory(prisma, { name: "Peripherals" })
|
||||||
const other = await createTestCategory(prisma, { name: "Networking" })
|
const other = await createTestCategory(prisma, { name: "Networking" })
|
||||||
|
|
||||||
await expect(
|
expect(
|
||||||
updateCategoryUseCase({ id: category.id, name: "Accessories" }),
|
await updateCategoryUseCase({ id: category.id, name: "Accessories" }),
|
||||||
).resolves.toEqual({ success: true })
|
).toEqual({ success: true })
|
||||||
|
|
||||||
await expect(
|
expect(
|
||||||
prisma.category.findUniqueOrThrow({ where: { id: category.id } }),
|
await prisma.category.findUniqueOrThrow({ where: { id: category.id } }),
|
||||||
).resolves.toMatchObject({ name: "Accessories" })
|
).toMatchObject({ name: "Accessories" })
|
||||||
|
|
||||||
await expect(
|
expect(
|
||||||
updateCategoryUseCase({ id: category.id, name: "Accessories" }),
|
await updateCategoryUseCase({ id: category.id, name: "Accessories" }),
|
||||||
).resolves.toEqual({
|
).toEqual({
|
||||||
success: false,
|
success: false,
|
||||||
errors: { name: ["Category name is the same as the old one"] },
|
errors: { name: ["Category name is the same as the old one"] },
|
||||||
})
|
})
|
||||||
|
|
||||||
await expect(
|
expect(
|
||||||
updateCategoryUseCase({ id: category.id, name: other.name }),
|
await updateCategoryUseCase({ id: category.id, name: other.name }),
|
||||||
).resolves.toEqual({
|
).toEqual({
|
||||||
success: false,
|
success: false,
|
||||||
errors: { name: ["Category already exists"] },
|
errors: { name: ["Category already exists"] },
|
||||||
})
|
})
|
||||||
|
|
||||||
await expect(
|
expect(
|
||||||
prisma.category.findUniqueOrThrow({ where: { id: category.id } }),
|
await prisma.category.findUniqueOrThrow({ where: { id: category.id } }),
|
||||||
).resolves.toMatchObject({ name: "Accessories" })
|
).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, {
|
const categoryWithItems = await createTestCategory(prisma, {
|
||||||
name: "Computers",
|
name: "Computers",
|
||||||
})
|
})
|
||||||
await createTestItem(prisma, { categoryId: categoryWithItems.id })
|
await createTestItem(prisma, { categoryId: categoryWithItems.id })
|
||||||
|
|
||||||
await expect(deleteCategoryUseCase(categoryWithItems.id)).resolves.toEqual({
|
expect(await deleteCategoryUseCase(categoryWithItems.id)).toEqual({
|
||||||
success: false,
|
success: false,
|
||||||
errors: { id: ["Category has items"] },
|
errors: { id: ["Category has items"] },
|
||||||
})
|
})
|
||||||
|
|
||||||
await expect(
|
expect(
|
||||||
prisma.category.findUnique({ where: { id: categoryWithItems.id } }),
|
await prisma.category.findUnique({ where: { id: categoryWithItems.id } }),
|
||||||
).resolves.not.toBeNull()
|
).not.toBeNull()
|
||||||
await expect(prisma.item.count()).resolves.toBe(1)
|
expect(await prisma.item.count()).toBe(1)
|
||||||
|
|
||||||
const emptyCategory = await createTestCategory(prisma, { name: "Cables" })
|
const emptyCategory = await createTestCategory(prisma, { name: "Cables" })
|
||||||
|
|
||||||
await expect(deleteCategoryUseCase(emptyCategory.id)).resolves.toEqual({
|
expect(await deleteCategoryUseCase(emptyCategory.id)).toEqual({
|
||||||
success: true,
|
success: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
await expect(
|
const deletedCategory = await prisma.category.findUnique({
|
||||||
prisma.category.findUnique({ where: { id: emptyCategory.id } }),
|
where: { id: emptyCategory.id },
|
||||||
).resolves.toBeNull()
|
})
|
||||||
|
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
|
// No User record created
|
||||||
await expect(
|
expect(
|
||||||
prisma.user.findUnique({
|
await prisma.user.findUnique({
|
||||||
where: { emailNormalized: normalizeEmail("john@example.test") },
|
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 () => {
|
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")
|
if (!user.passwordHash) throw new Error("Expected password hash")
|
||||||
|
|
||||||
const { verifyPassword } = await import("@/lib/security")
|
const { verifyPassword } = await import("@/lib/security")
|
||||||
await expect(
|
expect(await verifyPassword("plaintext-password", user.passwordHash)).toBe(
|
||||||
verifyPassword("plaintext-password", user.passwordHash),
|
true,
|
||||||
).resolves.toBe(true)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -221,7 +221,7 @@ describe("createPersonUserUseCase", () => {
|
|||||||
errors: { email: ["Email already exists"] },
|
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 () => {
|
it("rejects submission when email already exists in User table", async () => {
|
||||||
@@ -244,8 +244,8 @@ describe("createPersonUserUseCase", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// No new Person or User was created
|
// No new Person or User was created
|
||||||
await expect(prisma.person.count()).resolves.toBe(0)
|
expect(await prisma.person.count()).toBe(0)
|
||||||
await expect(prisma.user.count()).resolves.toBe(1)
|
expect(await prisma.user.count()).toBe(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("accepts submission when email is unique across both tables", async () => {
|
it("accepts submission when email is unique across both tables", async () => {
|
||||||
|
|||||||
@@ -10,16 +10,19 @@ import {
|
|||||||
let prisma: PrismaClient
|
let prisma: PrismaClient
|
||||||
let createPersonUseCase: typeof import("@/use-cases/person.use-cases").createPersonUseCase
|
let createPersonUseCase: typeof import("@/use-cases/person.use-cases").createPersonUseCase
|
||||||
let updatePersonUseCase: typeof import("@/use-cases/person.use-cases").updatePersonUseCase
|
let updatePersonUseCase: typeof import("@/use-cases/person.use-cases").updatePersonUseCase
|
||||||
|
let PersonService: typeof import("@/services/person.service").PersonService
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await startIntegrationTestDatabase()
|
await startIntegrationTestDatabase()
|
||||||
|
|
||||||
const prismaModule = await import("@/lib/prisma")
|
const prismaModule = await import("@/lib/prisma")
|
||||||
const personUseCases = await import("@/use-cases/person.use-cases")
|
const personUseCases = await import("@/use-cases/person.use-cases")
|
||||||
|
const personService = await import("@/services/person.service")
|
||||||
|
|
||||||
prisma = prismaModule.prisma
|
prisma = prismaModule.prisma
|
||||||
createPersonUseCase = personUseCases.createPersonUseCase
|
createPersonUseCase = personUseCases.createPersonUseCase
|
||||||
updatePersonUseCase = personUseCases.updatePersonUseCase
|
updatePersonUseCase = personUseCases.updatePersonUseCase
|
||||||
|
PersonService = personService.PersonService
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
@@ -43,11 +46,11 @@ describe("person use-cases", () => {
|
|||||||
}),
|
}),
|
||||||
).resolves.toEqual({ success: true })
|
).resolves.toEqual({ success: true })
|
||||||
|
|
||||||
await expect(
|
expect(
|
||||||
prisma.person.findFirstOrThrow({
|
await prisma.person.findFirstOrThrow({
|
||||||
where: { firstName: "Person", lastName: "One" },
|
where: { firstName: "Person", lastName: "One" },
|
||||||
}),
|
}),
|
||||||
).resolves.toMatchObject({
|
).toMatchObject({
|
||||||
firstName: "Person",
|
firstName: "Person",
|
||||||
lastName: "One",
|
lastName: "One",
|
||||||
department: "IT",
|
department: "IT",
|
||||||
@@ -71,11 +74,11 @@ describe("person use-cases", () => {
|
|||||||
}),
|
}),
|
||||||
).resolves.toEqual({ success: true })
|
).resolves.toEqual({ success: true })
|
||||||
|
|
||||||
await expect(
|
expect(
|
||||||
prisma.person.findFirstOrThrow({
|
await prisma.person.findFirstOrThrow({
|
||||||
where: { firstName: "Linked" },
|
where: { firstName: "Linked" },
|
||||||
}),
|
}),
|
||||||
).resolves.toMatchObject({
|
).toMatchObject({
|
||||||
firstName: "Linked",
|
firstName: "Linked",
|
||||||
lastName: "Person",
|
lastName: "Person",
|
||||||
department: "ENGINEERING",
|
department: "ENGINEERING",
|
||||||
@@ -102,7 +105,7 @@ describe("person use-cases", () => {
|
|||||||
errors: { email: ["Email already exists"] },
|
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 () => {
|
it("updates a person and rejects duplicate emails", async () => {
|
||||||
@@ -125,9 +128,7 @@ describe("person use-cases", () => {
|
|||||||
}),
|
}),
|
||||||
).resolves.toEqual({ success: true })
|
).resolves.toEqual({ success: true })
|
||||||
|
|
||||||
await expect(
|
expect(await prisma.person.findUniqueOrThrow({ where: { id: person.id } })).toMatchObject({
|
||||||
prisma.person.findUniqueOrThrow({ where: { id: person.id } }),
|
|
||||||
).resolves.toMatchObject({
|
|
||||||
firstName: "Edited",
|
firstName: "Edited",
|
||||||
lastName: "Person",
|
lastName: "Person",
|
||||||
department: "ENGINEERING",
|
department: "ENGINEERING",
|
||||||
@@ -149,12 +150,10 @@ describe("person use-cases", () => {
|
|||||||
errors: { email: ["Email already exists"] },
|
errors: { email: ["Email already exists"] },
|
||||||
})
|
})
|
||||||
|
|
||||||
await expect(
|
expect(await prisma.person.findUniqueOrThrow({ where: { id: person.id } })).toMatchObject({
|
||||||
prisma.person.findUniqueOrThrow({ where: { id: person.id } }),
|
|
||||||
).resolves.toMatchObject({
|
|
||||||
email: "edited@example.test",
|
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 () => {
|
it("searches by email and name in paginated results", async () => {
|
||||||
@@ -163,13 +162,15 @@ describe("person use-cases", () => {
|
|||||||
lastName: "Smith",
|
lastName: "Smith",
|
||||||
email: "alice@company.com",
|
email: "alice@company.com",
|
||||||
})
|
})
|
||||||
await createTestPerson(prisma, {
|
const archivedPerson = await createTestPerson(prisma, {
|
||||||
firstName: "Bob",
|
firstName: "Archive",
|
||||||
lastName: "Jones",
|
lastName: "Person",
|
||||||
email: "bob@other.com",
|
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({
|
const emailResults = await PersonService.findAllPaginated({
|
||||||
search: "company",
|
search: "company",
|
||||||
@@ -178,13 +179,16 @@ describe("person use-cases", () => {
|
|||||||
})
|
})
|
||||||
expect(emailResults.data).toHaveLength(1)
|
expect(emailResults.data).toHaveLength(1)
|
||||||
expect(emailResults.data[0].firstName).toBe("Alice")
|
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({
|
const nameResults = await PersonService.findAllPaginated({
|
||||||
search: "Bob",
|
search: "Alice",
|
||||||
page: 1,
|
page: 1,
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
})
|
})
|
||||||
expect(nameResults.data).toHaveLength(1)
|
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", () => {
|
describe("validation errors", () => {
|
||||||
it("returns error when person is not found", async () => {
|
it("returns error when person is not found", async () => {
|
||||||
const result = await updatePersonUserUseCase({
|
const result = await updatePersonUserUseCase({
|
||||||
id: "nonexistent-id",
|
id: "00000000-0000-0000-0000-000000000000",
|
||||||
firstName: "Ghost",
|
firstName: "Ghost",
|
||||||
lastName: "Person",
|
lastName: "Person",
|
||||||
department: "OTHER",
|
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 setUserActiveUseCase: typeof import("@/use-cases/user.use-cases").setUserActiveUseCase
|
||||||
let resetUserPasswordUseCase: typeof import("@/use-cases/user.use-cases").resetUserPasswordUseCase
|
let resetUserPasswordUseCase: typeof import("@/use-cases/user.use-cases").resetUserPasswordUseCase
|
||||||
let verifyPassword: typeof import("@/lib/security").verifyPassword
|
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 () => {
|
beforeAll(async () => {
|
||||||
await startIntegrationTestDatabase()
|
await startIntegrationTestDatabase()
|
||||||
@@ -21,6 +24,7 @@ beforeAll(async () => {
|
|||||||
const prismaModule = await import("@/lib/prisma")
|
const prismaModule = await import("@/lib/prisma")
|
||||||
const userUseCases = await import("@/use-cases/user.use-cases")
|
const userUseCases = await import("@/use-cases/user.use-cases")
|
||||||
const security = await import("@/lib/security")
|
const security = await import("@/lib/security")
|
||||||
|
const userService = await import("@/services/user.service")
|
||||||
|
|
||||||
prisma = prismaModule.prisma
|
prisma = prismaModule.prisma
|
||||||
createUserUseCase = userUseCases.createUserUseCase
|
createUserUseCase = userUseCases.createUserUseCase
|
||||||
@@ -28,6 +32,9 @@ beforeAll(async () => {
|
|||||||
setUserActiveUseCase = userUseCases.setUserActiveUseCase
|
setUserActiveUseCase = userUseCases.setUserActiveUseCase
|
||||||
resetUserPasswordUseCase = userUseCases.resetUserPasswordUseCase
|
resetUserPasswordUseCase = userUseCases.resetUserPasswordUseCase
|
||||||
verifyPassword = security.verifyPassword
|
verifyPassword = security.verifyPassword
|
||||||
|
getUserById = userService.getUserById
|
||||||
|
getUsers = userService.getUsers
|
||||||
|
countActiveAdmins = userService.countActiveAdmins
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
@@ -88,12 +95,34 @@ describe("user use-cases", () => {
|
|||||||
errors: { email: ["Email already exists"] },
|
errors: { email: ["Email already exists"] },
|
||||||
})
|
})
|
||||||
|
|
||||||
await expect(prisma.user.count()).resolves.toBe(1)
|
expect(await prisma.user.count()).toBe(1)
|
||||||
await expect(
|
expect(
|
||||||
prisma.user.findUniqueOrThrow({
|
await prisma.user.findUniqueOrThrow({
|
||||||
where: { emailNormalized: normalizeEmail("existing@example.test") },
|
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 () => {
|
it("updates a user while preserving uniqueness constraints", async () => {
|
||||||
@@ -118,9 +147,7 @@ describe("user use-cases", () => {
|
|||||||
}),
|
}),
|
||||||
).resolves.toEqual({ success: true })
|
).resolves.toEqual({ success: true })
|
||||||
|
|
||||||
await expect(
|
expect(await prisma.user.findUniqueOrThrow({ where: { id: user.id } })).toMatchObject({
|
||||||
prisma.user.findUniqueOrThrow({ where: { id: user.id } }),
|
|
||||||
).resolves.toMatchObject({
|
|
||||||
name: "Edited User",
|
name: "Edited User",
|
||||||
email: "edited@example.test",
|
email: "edited@example.test",
|
||||||
role: "MANAGER",
|
role: "MANAGER",
|
||||||
@@ -141,9 +168,7 @@ describe("user use-cases", () => {
|
|||||||
errors: { email: ["Email already exists"] },
|
errors: { email: ["Email already exists"] },
|
||||||
})
|
})
|
||||||
|
|
||||||
await expect(
|
expect(await prisma.user.findUniqueOrThrow({ where: { id: user.id } })).toMatchObject({
|
||||||
prisma.user.findUniqueOrThrow({ where: { id: user.id } }),
|
|
||||||
).resolves.toMatchObject({
|
|
||||||
email: "edited@example.test",
|
email: "edited@example.test",
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -165,9 +190,10 @@ describe("user use-cases", () => {
|
|||||||
errors: { id: ["You cannot remove your own administrator access"] },
|
errors: { id: ["You cannot remove your own administrator access"] },
|
||||||
})
|
})
|
||||||
|
|
||||||
await expect(
|
expect(await prisma.user.findUniqueOrThrow({ where: { id: admin.id } })).toMatchObject({
|
||||||
prisma.user.findUniqueOrThrow({ where: { id: admin.id } }),
|
role: "ADMIN",
|
||||||
).resolves.toMatchObject({ role: "ADMIN", status: "ACTIVE" })
|
status: "ACTIVE",
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it("protects the last active administrator but allows deactivation when another active admin exists", async () => {
|
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 })
|
).resolves.toEqual({ success: true })
|
||||||
|
|
||||||
await expect(
|
expect(await prisma.user.findUniqueOrThrow({ where: { id: firstAdmin.id } })).toMatchObject({
|
||||||
prisma.user.findUniqueOrThrow({ where: { id: firstAdmin.id } }),
|
status: "DISABLED",
|
||||||
).resolves.toMatchObject({ status: "DISABLED" })
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it("prevents self-deactivation", async () => {
|
it("prevents self-deactivation", async () => {
|
||||||
@@ -226,9 +252,9 @@ describe("user use-cases", () => {
|
|||||||
errors: { id: ["You cannot deactivate your own user"] },
|
errors: { id: ["You cannot deactivate your own user"] },
|
||||||
})
|
})
|
||||||
|
|
||||||
await expect(
|
expect(await prisma.user.findUniqueOrThrow({ where: { id: admin.id } })).toMatchObject({
|
||||||
prisma.user.findUniqueOrThrow({ where: { id: admin.id } }),
|
status: "ACTIVE",
|
||||||
).resolves.toMatchObject({ status: "ACTIVE" })
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it("resets a user password and rejects missing users", async () => {
|
it("resets a user password and rejects missing users", async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user