feat(auth): align login and bootstrap with new user schema

This commit is contained in:
2026-06-19 01:05:33 +02:00
parent 2ed9445f7f
commit 01d89cd21b
10 changed files with 503 additions and 60 deletions
+164
View File
@@ -0,0 +1,164 @@
import { beforeEach, describe, expect, it, vi } from "vitest"
const mocks = vi.hoisted(() => ({
getPasswordHash: vi.fn(),
}))
vi.mock("@/lib/security", () => ({
getPasswordHash: mocks.getPasswordHash,
}))
vi.mock("../../../src/lib/prisma", () => ({
default: {},
}))
import { bootstrapAdmin } from "../../../prisma/bootstrap-admin"
describe("bootstrapAdmin", () => {
beforeEach(() => {
vi.clearAllMocks()
process.env.ADMIN_BOOTSTRAP_ENABLED = "true"
process.env.ADMIN_EMAIL = "Admin@Example.Test"
process.env.ADMIN_NAME = "E2E Admin"
process.env.ADMIN_PASSWORD = "admin-password"
vi.stubEnv("NODE_ENV", "development")
mocks.getPasswordHash.mockResolvedValue("hashed-password")
})
it("creates an active user and links a person on first run", async () => {
const userFindUnique = vi.fn().mockResolvedValue(null)
const userCreate = vi.fn().mockResolvedValue({
id: "user-1",
person: null,
})
const userUpdate = vi.fn()
const personUpsert = vi.fn().mockResolvedValue({ id: "person-1" })
const client = {
user: {
findUnique: userFindUnique,
create: userCreate,
update: userUpdate,
},
person: {
upsert: personUpsert,
},
}
await bootstrapAdmin(client as never)
expect(userFindUnique).toHaveBeenCalledWith(
expect.objectContaining({
where: {
emailNormalized: "admin@example.test",
},
}),
)
expect(userCreate).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
emailNormalized: "admin@example.test",
passwordHash: "hashed-password",
status: "ACTIVE",
}),
}),
)
expect(userUpdate).not.toHaveBeenCalled()
expect(personUpsert).toHaveBeenCalledWith(
expect.objectContaining({
where: {
userId: "user-1",
},
create: expect.objectContaining({
firstName: "E2E",
lastName: "Admin",
email: "Admin@Example.Test",
}),
update: expect.objectContaining({
firstName: "E2E",
lastName: "Admin",
email: "Admin@Example.Test",
}),
}),
)
})
it("is idempotent when the admin already has a linked person", async () => {
const userFindUnique = vi.fn().mockResolvedValue({
id: "user-1",
passwordHash: "existing-hash",
activatedAt: new Date("2024-01-01T00:00:00.000Z"),
person: { id: "person-1" },
})
const userCreate = vi.fn()
const userUpdate = vi.fn().mockResolvedValue({
id: "user-1",
person: { id: "person-1" },
})
const personUpsert = vi.fn()
const client = {
user: {
findUnique: userFindUnique,
create: userCreate,
update: userUpdate,
},
person: {
upsert: personUpsert,
},
}
await bootstrapAdmin(client as never)
expect(mocks.getPasswordHash).not.toHaveBeenCalled()
expect(userCreate).not.toHaveBeenCalled()
expect(userUpdate).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.not.objectContaining({
passwordHash: expect.any(String),
activatedAt: expect.any(Date),
passwordChangedAt: expect.any(Date),
}),
}),
)
expect(personUpsert).not.toHaveBeenCalled()
})
it("links a missing person without rehashing an existing admin password", async () => {
const userFindUnique = vi.fn().mockResolvedValue({
id: "user-1",
passwordHash: "existing-hash",
activatedAt: new Date("2024-01-01T00:00:00.000Z"),
person: null,
})
const userCreate = vi.fn()
const userUpdate = vi.fn().mockResolvedValue({
id: "user-1",
person: null,
})
const personUpsert = vi.fn().mockResolvedValue({ id: "person-1" })
const client = {
user: {
findUnique: userFindUnique,
create: userCreate,
update: userUpdate,
},
person: {
upsert: personUpsert,
},
}
await bootstrapAdmin(client as never)
expect(mocks.getPasswordHash).not.toHaveBeenCalled()
expect(userCreate).not.toHaveBeenCalled()
expect(personUpsert).toHaveBeenCalledWith(
expect.objectContaining({
where: {
userId: "user-1",
},
}),
)
})
})