refactor: consolidate admin/users management under /people
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest"
|
||||
import type { PrismaClient } from "@/generated/prisma/client"
|
||||
import { createTestPerson, createTestUser } from "../helpers/factories"
|
||||
import {
|
||||
resetIntegrationTestDatabase,
|
||||
startIntegrationTestDatabase,
|
||||
stopIntegrationTestDatabase,
|
||||
} from "../helpers/test-db"
|
||||
|
||||
let prisma: PrismaClient
|
||||
|
||||
beforeAll(async () => {
|
||||
await startIntegrationTestDatabase()
|
||||
const prismaModule = await import("@/lib/prisma")
|
||||
prisma = prismaModule.prisma
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
await resetIntegrationTestDatabase(prisma)
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await prisma?.$disconnect()
|
||||
await stopIntegrationTestDatabase()
|
||||
})
|
||||
|
||||
describe("/admin/users -> /people redirect routes", () => {
|
||||
it("does not have a /admin/users list page (route is consolidated into /people)", async () => {
|
||||
const fs = await import("node:fs/promises")
|
||||
const path = await import("node:path")
|
||||
|
||||
// /admin/users/page.tsx must still exist (as a redirect stub) — verify it's just a redirect.
|
||||
const adminUsersPage = path.join(
|
||||
process.cwd(),
|
||||
"src/app/(dashboard)/admin/users/page.tsx",
|
||||
)
|
||||
const contents = await fs.readFile(adminUsersPage, "utf-8")
|
||||
expect(contents).toMatch(/redirect\s*\(\s*["']\/people["']\s*\)/)
|
||||
})
|
||||
|
||||
it("resolves a userId back to its linked personId", async () => {
|
||||
// Build a Person<->User link to verify the redirect can find the person by userId.
|
||||
const user = await createTestUser(prisma, {
|
||||
email: "linked@example.test",
|
||||
})
|
||||
const person = await createTestPerson(prisma, {
|
||||
email: "linked@example.test",
|
||||
})
|
||||
await prisma.person.update({
|
||||
where: { id: person.id },
|
||||
data: { userId: user.id },
|
||||
})
|
||||
|
||||
const found = await prisma.person.findFirst({
|
||||
where: { userId: user.id },
|
||||
select: { id: true },
|
||||
})
|
||||
|
||||
expect(found?.id).toBe(person.id)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,247 @@
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest"
|
||||
import type { PrismaClient } from "@/generated/prisma/client"
|
||||
import { getPasswordHash } from "@/lib/security"
|
||||
import { createTestPerson, createTestUser } from "../helpers/factories"
|
||||
import {
|
||||
resetIntegrationTestDatabase,
|
||||
startIntegrationTestDatabase,
|
||||
stopIntegrationTestDatabase,
|
||||
} from "../helpers/test-db"
|
||||
|
||||
let prisma: PrismaClient
|
||||
let updatePersonUserUseCase: typeof import("@/use-cases/person.use-cases").updatePersonUserUseCase
|
||||
|
||||
beforeAll(async () => {
|
||||
await startIntegrationTestDatabase()
|
||||
|
||||
const prismaModule = await import("@/lib/prisma")
|
||||
const personUseCases = await import("@/use-cases/person.use-cases")
|
||||
|
||||
prisma = prismaModule.prisma
|
||||
updatePersonUserUseCase = personUseCases.updatePersonUserUseCase
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
await resetIntegrationTestDatabase(prisma)
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await prisma?.$disconnect()
|
||||
await stopIntegrationTestDatabase()
|
||||
})
|
||||
|
||||
describe("updatePersonUserUseCase", () => {
|
||||
describe("person-only update", () => {
|
||||
it("updates only the Person when person has no linked User", async () => {
|
||||
const person = await createTestPerson(prisma, {
|
||||
firstName: "Old",
|
||||
lastName: "Name",
|
||||
email: "old@example.test",
|
||||
})
|
||||
|
||||
const result = await updatePersonUserUseCase({
|
||||
id: person.id,
|
||||
firstName: "New",
|
||||
lastName: "Name",
|
||||
department: "IT",
|
||||
email: "new@example.test",
|
||||
phone: "1234",
|
||||
})
|
||||
|
||||
expect(result).toEqual({ success: true })
|
||||
|
||||
const updated = await prisma.person.findUniqueOrThrow({
|
||||
where: { id: person.id },
|
||||
})
|
||||
expect(updated).toMatchObject({
|
||||
firstName: "New",
|
||||
lastName: "Name",
|
||||
department: "IT",
|
||||
email: "new@example.test",
|
||||
phone: "1234",
|
||||
userId: null,
|
||||
})
|
||||
})
|
||||
|
||||
it("normalizes empty email to null when person has no User", async () => {
|
||||
const person = await createTestPerson(prisma, { email: null })
|
||||
|
||||
const result = await updatePersonUserUseCase({
|
||||
id: person.id,
|
||||
firstName: "Empty",
|
||||
lastName: "Email",
|
||||
department: "OTHER",
|
||||
email: "",
|
||||
phone: null,
|
||||
})
|
||||
|
||||
expect(result).toEqual({ success: true })
|
||||
|
||||
const updated = await prisma.person.findUniqueOrThrow({
|
||||
where: { id: person.id },
|
||||
})
|
||||
expect(updated.email).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe("person+user update", () => {
|
||||
it("updates Person fields and User role/isActive when person has a User linked", async () => {
|
||||
const user = await createTestUser(prisma, {
|
||||
email: "user-update@example.test",
|
||||
name: "Old Name",
|
||||
role: "STAFF",
|
||||
isActive: true,
|
||||
})
|
||||
const person = await createTestPerson(prisma, {
|
||||
firstName: "Linked",
|
||||
lastName: "Person",
|
||||
email: "user-update@example.test",
|
||||
})
|
||||
await prisma.person.update({
|
||||
where: { id: person.id },
|
||||
data: { userId: user.id },
|
||||
})
|
||||
|
||||
const result = await updatePersonUserUseCase({
|
||||
id: person.id,
|
||||
firstName: "Linked",
|
||||
lastName: "Person",
|
||||
department: "ENGINEERING",
|
||||
email: "user-update@example.test",
|
||||
phone: null,
|
||||
role: "ADMIN",
|
||||
isActive: false,
|
||||
})
|
||||
|
||||
expect(result).toEqual({ success: true })
|
||||
|
||||
const updatedPerson = await prisma.person.findUniqueOrThrow({
|
||||
where: { id: person.id },
|
||||
include: { user: true },
|
||||
})
|
||||
expect(updatedPerson.department).toBe("ENGINEERING")
|
||||
expect(updatedPerson.user).toMatchObject({
|
||||
id: user.id,
|
||||
role: "ADMIN",
|
||||
isActive: false,
|
||||
})
|
||||
})
|
||||
|
||||
it("resets the User password when password is provided", async () => {
|
||||
const user = await createTestUser(prisma, {
|
||||
email: "pw-reset@example.test",
|
||||
})
|
||||
const person = await createTestPerson(prisma, {
|
||||
email: "pw-reset@example.test",
|
||||
})
|
||||
await prisma.person.update({
|
||||
where: { id: person.id },
|
||||
data: { userId: user.id },
|
||||
})
|
||||
|
||||
const result = await updatePersonUserUseCase({
|
||||
id: person.id,
|
||||
firstName: person.firstName,
|
||||
lastName: person.lastName,
|
||||
department: "OTHER",
|
||||
email: "pw-reset@example.test",
|
||||
phone: null,
|
||||
role: "STAFF",
|
||||
isActive: true,
|
||||
password: "new-password-1",
|
||||
})
|
||||
|
||||
expect(result).toEqual({ success: true })
|
||||
|
||||
const updated = await prisma.user.findUniqueOrThrow({
|
||||
where: { id: user.id },
|
||||
})
|
||||
const { verifyPassword } = await import("@/lib/security")
|
||||
await expect(
|
||||
verifyPassword("new-password-1", updated.password),
|
||||
).resolves.toBe(true)
|
||||
})
|
||||
|
||||
it("does not change the password when password is not provided", async () => {
|
||||
const originalHash = await getPasswordHash("original-password-1")
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
email: "no-pw@example.test",
|
||||
name: "No PW",
|
||||
password: originalHash,
|
||||
role: "STAFF",
|
||||
isActive: true,
|
||||
},
|
||||
})
|
||||
const person = await createTestPerson(prisma, {
|
||||
email: "no-pw@example.test",
|
||||
})
|
||||
await prisma.person.update({
|
||||
where: { id: person.id },
|
||||
data: { userId: user.id },
|
||||
})
|
||||
|
||||
const result = await updatePersonUserUseCase({
|
||||
id: person.id,
|
||||
firstName: person.firstName,
|
||||
lastName: person.lastName,
|
||||
department: "OTHER",
|
||||
email: "no-pw@example.test",
|
||||
phone: null,
|
||||
role: "STAFF",
|
||||
isActive: true,
|
||||
})
|
||||
|
||||
expect(result).toEqual({ success: true })
|
||||
|
||||
const updated = await prisma.user.findUniqueOrThrow({
|
||||
where: { id: user.id },
|
||||
})
|
||||
const { verifyPassword: verify } = await import("@/lib/security")
|
||||
await expect(
|
||||
verify("original-password-1", updated.password),
|
||||
).resolves.toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("validation errors", () => {
|
||||
it("returns error when person is not found", async () => {
|
||||
const result = await updatePersonUserUseCase({
|
||||
id: "nonexistent-id",
|
||||
firstName: "Ghost",
|
||||
lastName: "Person",
|
||||
department: "OTHER",
|
||||
email: "ghost@example.test",
|
||||
phone: null,
|
||||
})
|
||||
|
||||
expect(result.success).toBe(false)
|
||||
if (!result.success) {
|
||||
expect(result.errors.id).toBeDefined()
|
||||
}
|
||||
})
|
||||
|
||||
it("rejects duplicate email in Person table", async () => {
|
||||
const person = await createTestPerson(prisma, {
|
||||
email: "mine@example.test",
|
||||
})
|
||||
await createTestPerson(prisma, {
|
||||
email: "theirs@example.test",
|
||||
})
|
||||
|
||||
const result = await updatePersonUserUseCase({
|
||||
id: person.id,
|
||||
firstName: "Mine",
|
||||
lastName: "Person",
|
||||
department: "OTHER",
|
||||
email: "theirs@example.test",
|
||||
phone: null,
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
success: false,
|
||||
errors: { email: ["Email already exists"] },
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user