refactor: consolidate admin/users management under /people

This commit is contained in:
2026-06-17 09:32:26 +02:00
parent 4f370eee70
commit d6b42d78e7
31 changed files with 1928 additions and 855 deletions
@@ -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"] },
})
})
})
})