refactor: remove username from User model, login by email only
This commit is contained in:
@@ -18,7 +18,6 @@ STOCK_MANAGER_DEFAULT_LOCALE=en
|
||||
|
||||
# ADMIN BOOTSTRAP
|
||||
ADMIN_BOOTSTRAP_ENABLED=true
|
||||
ADMIN_USERNAME=admin
|
||||
ADMIN_EMAIL=admin@localhost
|
||||
ADMIN_NAME=Administrator
|
||||
ADMIN_PASSWORD=change-me
|
||||
@@ -80,7 +80,7 @@ Variables principales:
|
||||
|-------|-----------|
|
||||
| Base de datos | `DATABASE_URL`, `POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_DB`, `POSTGRES_HOST`, `POSTGRES_PORT` |
|
||||
| Auth | `AUTH_SECRET`, `AUTH_TRUST_HOST`, `DOMAIN`, `NODE_ENV`, `DEMO_MODE` |
|
||||
| Bootstrap admin | `ADMIN_BOOTSTRAP_ENABLED`, `ADMIN_USERNAME`, `ADMIN_EMAIL`, `ADMIN_NAME`, `ADMIN_PASSWORD` |
|
||||
| Bootstrap admin | `ADMIN_BOOTSTRAP_ENABLED`, `ADMIN_EMAIL`, `ADMIN_NAME`, `ADMIN_PASSWORD` |
|
||||
|
||||
### Bootstrap admin
|
||||
|
||||
|
||||
@@ -38,7 +38,6 @@ services:
|
||||
AUTH_TRUST_HOST: ${AUTH_TRUST_HOST}
|
||||
AUTH_SECRET: ${AUTH_SECRET}
|
||||
ADMIN_BOOTSTRAP_ENABLED: ${ADMIN_BOOTSTRAP_ENABLED:-"true"}
|
||||
ADMIN_USERNAME: ${ADMIN_USERNAME:-"admin"}
|
||||
ADMIN_EMAIL: ${ADMIN_EMAIL:-"admin@localhost"}
|
||||
ADMIN_NAME: ${ADMIN_NAME:-"Administrator"}
|
||||
ADMIN_PASSWORD: ${ADMIN_PASSWORD}
|
||||
|
||||
@@ -3,7 +3,6 @@ import { getPasswordHash } from "@/lib/security"
|
||||
import prisma from "../src/lib/prisma"
|
||||
|
||||
type BootstrapAdminInput = {
|
||||
username: string
|
||||
email: string
|
||||
name: string
|
||||
password: string
|
||||
@@ -12,7 +11,6 @@ type BootstrapAdminInput = {
|
||||
function getBootstrapAdminInput(): BootstrapAdminInput {
|
||||
const isProduction = process.env.NODE_ENV === "production"
|
||||
|
||||
const username = process.env.ADMIN_USERNAME ?? "admin"
|
||||
const email = process.env.ADMIN_EMAIL ?? "admin@localhost"
|
||||
const name = process.env.ADMIN_NAME ?? "Administrator"
|
||||
const password = process.env.ADMIN_PASSWORD
|
||||
@@ -22,7 +20,6 @@ function getBootstrapAdminInput(): BootstrapAdminInput {
|
||||
}
|
||||
|
||||
return {
|
||||
username,
|
||||
email,
|
||||
name,
|
||||
password: password ?? "admin",
|
||||
@@ -55,7 +52,6 @@ export async function bootstrapAdmin(client: typeof prisma) {
|
||||
},
|
||||
create: {
|
||||
name: admin.name,
|
||||
username: admin.username,
|
||||
email: admin.email,
|
||||
role: "ADMIN",
|
||||
password: await getPasswordHash(admin.password),
|
||||
|
||||
@@ -13,7 +13,6 @@ CREATE TYPE "MovementType" AS ENUM ('IN', 'OUT', 'ASSIGNMENT', 'RETURN', 'ADJUST
|
||||
-- CreateTable
|
||||
CREATE TABLE "User" (
|
||||
"id" TEXT NOT NULL,
|
||||
"username" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"email" TEXT NOT NULL,
|
||||
"password" TEXT NOT NULL,
|
||||
@@ -119,9 +118,6 @@ CREATE TABLE "Movement" (
|
||||
CONSTRAINT "Movement_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "User_username_key" ON "User"("username");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
|
||||
|
||||
|
||||
@@ -23,7 +23,6 @@ enum UserRole {
|
||||
|
||||
model User {
|
||||
id String @id @default(uuid())
|
||||
username String @unique
|
||||
name String
|
||||
email String @unique
|
||||
password String
|
||||
@@ -133,8 +132,8 @@ model Assignment {
|
||||
item Item? @relation(fields: [itemId], references: [id], onDelete: SetNull, onUpdate: Cascade)
|
||||
assetId String? @unique
|
||||
asset Asset? @relation(fields: [assetId], references: [id], onDelete: SetNull, onUpdate: Cascade)
|
||||
personId String?
|
||||
person Person? @relation(fields: [personId], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
||||
personId String?
|
||||
person Person? @relation(fields: [personId], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
||||
assignmentDate DateTime @default(now())
|
||||
returnDate DateTime?
|
||||
createdBy String
|
||||
@@ -170,8 +169,8 @@ model Movement {
|
||||
asset Asset? @relation(fields: [assetId], references: [id], onDelete: SetNull, onUpdate: Cascade)
|
||||
previousStock Int?
|
||||
newStock Int?
|
||||
personId String?
|
||||
person Person? @relation(fields: [personId], references: [id], onDelete: SetNull, onUpdate: Cascade)
|
||||
personId String?
|
||||
person Person? @relation(fields: [personId], references: [id], onDelete: SetNull, onUpdate: Cascade)
|
||||
assignmentId String?
|
||||
assignment Assignment? @relation(fields: [assignmentId], references: [id], onDelete: SetNull, onUpdate: Cascade)
|
||||
userId String
|
||||
@@ -183,4 +182,4 @@ model Movement {
|
||||
@@index([personId])
|
||||
@@index([type])
|
||||
@@index([userId])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,11 +6,11 @@ import { signIn } from "@/lib/auth"
|
||||
import type { SignInFormType } from "@/schemas/auth.schema"
|
||||
|
||||
export async function signInAction(values: SignInFormType) {
|
||||
const { username, password } = values
|
||||
const { email, password } = values
|
||||
|
||||
try {
|
||||
await signIn("credentials", {
|
||||
username,
|
||||
email,
|
||||
password,
|
||||
redirect: false,
|
||||
})
|
||||
|
||||
@@ -5,7 +5,6 @@ type UserActionCopy = Dictionary["admin"]["users"]["actions"]
|
||||
type FieldErrors = Record<string, string[]>
|
||||
|
||||
const userErrorMessageKeys = {
|
||||
"Username already exists": "duplicateUsername",
|
||||
"Email already exists": "duplicateEmail",
|
||||
"User not found": "notFound",
|
||||
"Cannot remove access from the last active administrator": "lastActiveAdmin",
|
||||
|
||||
@@ -22,7 +22,7 @@ export default function SignInForm({ copy }: SignInFormProps) {
|
||||
const { register, handleSubmit, formState } = useForm<SignInFormType>({
|
||||
resolver: zodResolver(signInSchema),
|
||||
defaultValues: {
|
||||
username: "",
|
||||
email: "",
|
||||
password: "",
|
||||
},
|
||||
})
|
||||
@@ -42,15 +42,15 @@ export default function SignInForm({ copy }: SignInFormProps) {
|
||||
return (
|
||||
<form className="flex flex-col gap-4" onSubmit={handleSubmit(onSubmit)}>
|
||||
<label className="flex flex-col gap-1">
|
||||
{copy.usernameLabel}
|
||||
{copy.emailLabel}
|
||||
<input
|
||||
{...register("username")}
|
||||
name="username"
|
||||
{...register("email")}
|
||||
name="email"
|
||||
type="text"
|
||||
className="border-input w-full rounded-md border-2 p-2"
|
||||
/>
|
||||
{formState.errors.username && (
|
||||
<p className="text-error">{formState.errors.username.message}</p>
|
||||
{formState.errors.email && (
|
||||
<p className="text-error">{formState.errors.email.message}</p>
|
||||
)}
|
||||
</label>
|
||||
<label className="flex flex-col gap-1">
|
||||
|
||||
@@ -45,7 +45,6 @@ export default function EditUserForm({
|
||||
defaultValues: {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
isActive: user.isActive,
|
||||
@@ -84,13 +83,6 @@ export default function EditUserForm({
|
||||
placeholder={formCopy.namePlaceholder}
|
||||
register={register("name")}
|
||||
/>
|
||||
<UserTextInput
|
||||
error={errors.username?.message}
|
||||
id="username"
|
||||
label={formCopy.usernameLabel}
|
||||
placeholder={formCopy.usernamePlaceholder}
|
||||
register={register("username")}
|
||||
/>
|
||||
<UserTextInput
|
||||
error={errors.email?.message}
|
||||
id="email"
|
||||
|
||||
@@ -76,13 +76,6 @@ export default function NewUserForm({
|
||||
placeholder={formCopy.namePlaceholder}
|
||||
register={register("name")}
|
||||
/>
|
||||
<UserTextInput
|
||||
error={errors.username?.message}
|
||||
id="username"
|
||||
label={formCopy.usernameLabel}
|
||||
placeholder={formCopy.usernamePlaceholder}
|
||||
register={register("username")}
|
||||
/>
|
||||
<UserTextInput
|
||||
error={errors.email?.message}
|
||||
id="email"
|
||||
|
||||
@@ -55,9 +55,6 @@ export default async function UsersPage(props: {
|
||||
<th scope="col" className="p-4">
|
||||
{copy.list.columns.name}
|
||||
</th>
|
||||
<th scope="col" className="p-4">
|
||||
{copy.list.columns.username}
|
||||
</th>
|
||||
<th scope="col" className="p-4">
|
||||
{copy.list.columns.email}
|
||||
</th>
|
||||
@@ -76,7 +73,6 @@ export default async function UsersPage(props: {
|
||||
{users.map((user) => (
|
||||
<tr key={user.id} className="border-b">
|
||||
<td className="p-4">{user.name}</td>
|
||||
<td className="p-4">{user.username}</td>
|
||||
<td className="p-4">{user.email}</td>
|
||||
<td className="p-4">
|
||||
{formatUserRole(
|
||||
|
||||
@@ -435,7 +435,6 @@ export const en = {
|
||||
empty: "No users found.",
|
||||
columns: {
|
||||
name: "Name",
|
||||
username: "Username",
|
||||
email: "Email",
|
||||
role: "Role",
|
||||
status: "Status",
|
||||
@@ -454,8 +453,6 @@ export const en = {
|
||||
form: {
|
||||
nameLabel: "Name",
|
||||
namePlaceholder: "Full name",
|
||||
usernameLabel: "Username",
|
||||
usernamePlaceholder: "Username",
|
||||
emailLabel: "Email",
|
||||
emailPlaceholder: "user@example.com",
|
||||
passwordLabel: "Password",
|
||||
@@ -490,7 +487,6 @@ export const en = {
|
||||
toggleStatusFailure: "Failed to update user status",
|
||||
resetPasswordSuccess: "Password reset successfully",
|
||||
resetPasswordFailure: "Failed to reset password",
|
||||
duplicateUsername: "Username already exists",
|
||||
duplicateEmail: "Email already exists",
|
||||
notFound: "User not found",
|
||||
lastActiveAdmin:
|
||||
@@ -499,7 +495,6 @@ export const en = {
|
||||
selfDeactivate: "You cannot deactivate your own user",
|
||||
},
|
||||
schema: {
|
||||
usernameRequired: "Username is required",
|
||||
nameRequired: "Name is required",
|
||||
emailInvalid: "Invalid email",
|
||||
passwordMinLength: "Password must be at least 8 characters",
|
||||
@@ -512,7 +507,7 @@ export const en = {
|
||||
},
|
||||
login: {
|
||||
title: "Sign In",
|
||||
usernameLabel: "Username",
|
||||
emailLabel: "Email",
|
||||
passwordLabel: "Password",
|
||||
submitLabel: "Sign In",
|
||||
},
|
||||
|
||||
@@ -440,7 +440,6 @@ export const es = {
|
||||
empty: "No se encontraron usuarios.",
|
||||
columns: {
|
||||
name: "Nombre",
|
||||
username: "Usuario",
|
||||
email: "Correo electrónico",
|
||||
role: "Rol",
|
||||
status: "Estado",
|
||||
@@ -459,8 +458,6 @@ export const es = {
|
||||
form: {
|
||||
nameLabel: "Nombre",
|
||||
namePlaceholder: "Nombre completo",
|
||||
usernameLabel: "Usuario",
|
||||
usernamePlaceholder: "Usuario",
|
||||
emailLabel: "Correo electrónico",
|
||||
emailPlaceholder: "usuario@ejemplo.com",
|
||||
passwordLabel: "Contraseña",
|
||||
@@ -495,7 +492,6 @@ export const es = {
|
||||
toggleStatusFailure: "Error al actualizar el estado del usuario",
|
||||
resetPasswordSuccess: "Contraseña restablecida correctamente",
|
||||
resetPasswordFailure: "Error al restablecer la contraseña",
|
||||
duplicateUsername: "El nombre de usuario ya existe",
|
||||
duplicateEmail: "El correo electrónico ya existe",
|
||||
notFound: "Usuario no encontrado",
|
||||
lastActiveAdmin:
|
||||
@@ -504,7 +500,6 @@ export const es = {
|
||||
selfDeactivate: "No puedes desactivar tu propio usuario",
|
||||
},
|
||||
schema: {
|
||||
usernameRequired: "El usuario es obligatorio",
|
||||
nameRequired: "El nombre es obligatorio",
|
||||
emailInvalid: "Correo electrónico no válido",
|
||||
passwordMinLength: "La contraseña debe tener al menos 8 caracteres",
|
||||
@@ -517,7 +512,7 @@ export const es = {
|
||||
},
|
||||
login: {
|
||||
title: "Iniciar sesión",
|
||||
usernameLabel: "Usuario",
|
||||
emailLabel: "Correo electrónico",
|
||||
passwordLabel: "Contraseña",
|
||||
submitLabel: "Iniciar sesión",
|
||||
},
|
||||
|
||||
+8
-8
@@ -6,7 +6,7 @@ import type { UserRole } from "@/generated/prisma/client"
|
||||
import { SIGN_IN_URL, TOKEN_EXPIRATION_SECONDS } from "@/lib/constants"
|
||||
import { verifyPassword } from "@/lib/security"
|
||||
import { signInSchema } from "@/schemas/auth.schema"
|
||||
import { getUserByUsername } from "@/services/user.service"
|
||||
import { getUserByEmail } from "@/services/user.service"
|
||||
|
||||
declare module "next-auth" {
|
||||
interface Session {
|
||||
@@ -29,27 +29,27 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
|
||||
providers: [
|
||||
Credentials({
|
||||
credentials: {
|
||||
username: {},
|
||||
email: {},
|
||||
password: {},
|
||||
},
|
||||
authorize: async (credentials) => {
|
||||
try {
|
||||
const { data, success } = signInSchema.safeParse(credentials)
|
||||
|
||||
if (!success) throw new Error("Invalid username or password")
|
||||
if (!success) throw new Error("Invalid email or password")
|
||||
|
||||
const user = await getUserByUsername(data.username)
|
||||
const user = await getUserByEmail(data.email)
|
||||
|
||||
if (!user) {
|
||||
throw new Error("Invalid username or password")
|
||||
throw new Error("Invalid email or password")
|
||||
}
|
||||
|
||||
if (!user.isActive) {
|
||||
throw new Error("Invalid username or password")
|
||||
throw new Error("Invalid email or password")
|
||||
}
|
||||
|
||||
if (!(await verifyPassword(data.password, user.password)))
|
||||
throw new Error("Invalid username or password")
|
||||
throw new Error("Invalid email or password")
|
||||
|
||||
return {
|
||||
...user,
|
||||
@@ -59,7 +59,7 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
|
||||
if (error instanceof ZodError) {
|
||||
return null
|
||||
}
|
||||
throw new Error("Invalid username or password")
|
||||
throw new Error("Invalid email or password")
|
||||
}
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
import { z } from "zod"
|
||||
|
||||
export const signInSchema = z.object({
|
||||
username: z
|
||||
.string()
|
||||
.min(1, {
|
||||
error: "Invalid username",
|
||||
})
|
||||
.nonempty("Username is required"),
|
||||
email: z.email().nonempty("Email is required"),
|
||||
password: z
|
||||
.string()
|
||||
.min(3, {
|
||||
|
||||
@@ -5,7 +5,6 @@ import type { Dictionary } from "@/i18n/dictionaries"
|
||||
export type UserSchemaCopy = Dictionary["admin"]["users"]["schema"]
|
||||
|
||||
export const defaultUserSchemaCopy: UserSchemaCopy = {
|
||||
usernameRequired: "Username is required",
|
||||
nameRequired: "Name is required",
|
||||
emailInvalid: "Invalid email",
|
||||
passwordMinLength: "Password must be at least 8 characters",
|
||||
@@ -20,7 +19,6 @@ function buildUserPasswordSchema(copy: UserSchemaCopy) {
|
||||
|
||||
export function buildCreateUserSchema(copy: UserSchemaCopy) {
|
||||
return z.object({
|
||||
username: z.string().trim().min(1, { error: copy.usernameRequired }),
|
||||
name: z.string().trim().min(1, { error: copy.nameRequired }),
|
||||
email: z.email({ error: copy.emailInvalid }),
|
||||
password: buildUserPasswordSchema(copy),
|
||||
@@ -34,7 +32,6 @@ export const createUserSchema = buildCreateUserSchema(defaultUserSchemaCopy)
|
||||
export function buildUpdateUserSchema(copy: UserSchemaCopy) {
|
||||
return z.object({
|
||||
id: z.string().min(1, { error: copy.userIdRequired }),
|
||||
username: z.string().trim().min(1, { error: copy.usernameRequired }),
|
||||
name: z.string().trim().min(1, { error: copy.nameRequired }),
|
||||
email: z.email({ error: copy.emailInvalid }),
|
||||
role: userRoleSchema,
|
||||
|
||||
@@ -7,7 +7,6 @@ import type { User } from "@/types/user"
|
||||
|
||||
const userWithoutPasswordSelect = {
|
||||
id: true,
|
||||
username: true,
|
||||
name: true,
|
||||
email: true,
|
||||
role: true,
|
||||
@@ -20,12 +19,12 @@ export type UserWithoutPassword = Prisma.UserGetPayload<{
|
||||
select: typeof userWithoutPasswordSelect
|
||||
}>
|
||||
|
||||
type CreateUserData = Pick<User, "username" | "name" | "email" | "password"> & {
|
||||
type CreateUserData = Pick<User, "name" | "email" | "password"> & {
|
||||
role?: UserRole
|
||||
isActive?: boolean
|
||||
}
|
||||
|
||||
type UpdateUserData = Partial<Pick<User, "username" | "name" | "email">> & {
|
||||
type UpdateUserData = Partial<Pick<User, "name" | "email">> & {
|
||||
role?: UserRole
|
||||
isActive?: boolean
|
||||
}
|
||||
@@ -44,7 +43,6 @@ export async function createUser(
|
||||
) {
|
||||
const newUser = await db.user.create({
|
||||
data: {
|
||||
username: data.username,
|
||||
name: data.name,
|
||||
email: data.email,
|
||||
password: await getPasswordHash(data.password),
|
||||
@@ -72,13 +70,6 @@ export async function getUserProfileById(
|
||||
})
|
||||
}
|
||||
|
||||
export async function getUserByUsername(
|
||||
username: string,
|
||||
db: Prisma.TransactionClient | typeof prisma = prisma,
|
||||
) {
|
||||
return await db.user.findUnique({ where: { username } })
|
||||
}
|
||||
|
||||
export async function getUserByEmail(
|
||||
email: string,
|
||||
db: Prisma.TransactionClient | typeof prisma = prisma,
|
||||
@@ -103,7 +94,6 @@ export async function getUsers({
|
||||
...(search
|
||||
? {
|
||||
OR: [
|
||||
{ username: { contains: search, mode: "insensitive" } },
|
||||
{ name: { contains: search, mode: "insensitive" } },
|
||||
{ email: { contains: search, mode: "insensitive" } },
|
||||
],
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
createUser,
|
||||
getUserByEmail,
|
||||
getUserById,
|
||||
getUserByUsername,
|
||||
resetUserPassword,
|
||||
setUserActive,
|
||||
updateUser,
|
||||
@@ -49,15 +48,11 @@ function uniqueErrorFor(error: unknown): FieldErrors | null {
|
||||
|
||||
const target = Array.isArray(error.meta?.target) ? error.meta.target : []
|
||||
|
||||
if (target.includes("username")) {
|
||||
return { username: ["Username already exists"] }
|
||||
}
|
||||
|
||||
if (target.includes("email")) {
|
||||
return { email: ["Email already exists"] }
|
||||
}
|
||||
|
||||
return { username: ["Username already exists"] }
|
||||
return { email: ["Email already exists"] }
|
||||
}
|
||||
|
||||
function isTransactionConflictError(error: unknown) {
|
||||
@@ -113,14 +108,10 @@ async function getAdminAccessLossError(
|
||||
export async function createUserUseCase(
|
||||
input: CreateUserFormType,
|
||||
): Promise<UserUseCaseResult> {
|
||||
const { username, email } = input
|
||||
const { email } = input
|
||||
|
||||
try {
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
if (await getUserByUsername(username, tx)) {
|
||||
return userError({ username: ["Username already exists"] })
|
||||
}
|
||||
|
||||
if (await getUserByEmail(email, tx)) {
|
||||
return userError({ email: ["Email already exists"] })
|
||||
}
|
||||
@@ -145,7 +136,7 @@ export async function createUserUseCase(
|
||||
export async function updateUserUseCase(
|
||||
input: UpdateUserFormType & ActorInput,
|
||||
): Promise<UserUseCaseResult> {
|
||||
const { actorId, id, username, email, role, isActive, ...data } = input
|
||||
const { actorId, id, email, role, isActive, ...data } = input
|
||||
|
||||
try {
|
||||
return await runSerializableUserTransaction(async (tx) => {
|
||||
@@ -166,17 +157,12 @@ export async function updateUserUseCase(
|
||||
if (error) return userError({ id: [error] })
|
||||
}
|
||||
|
||||
const existingUsername = await getUserByUsername(username, tx)
|
||||
if (existingUsername && existingUsername.id !== id) {
|
||||
return userError({ username: ["Username already exists"] })
|
||||
}
|
||||
|
||||
const existingEmail = await getUserByEmail(email, tx)
|
||||
if (existingEmail && existingEmail.id !== id) {
|
||||
return userError({ email: ["Email already exists"] })
|
||||
}
|
||||
|
||||
await updateUser(id, { ...data, username, email, role, isActive }, tx)
|
||||
await updateUser(id, { ...data, email, role, isActive }, tx)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
|
||||
@@ -40,7 +40,6 @@ async function main() {
|
||||
AUTH_URL: baseUrl,
|
||||
NEXTAUTH_URL: baseUrl,
|
||||
ADMIN_BOOTSTRAP_ENABLED: "true",
|
||||
ADMIN_USERNAME: "admin",
|
||||
ADMIN_EMAIL: "admin@example.test",
|
||||
ADMIN_NAME: "E2E Admin",
|
||||
ADMIN_PASSWORD: "admin-password",
|
||||
@@ -51,7 +50,6 @@ async function main() {
|
||||
process.env.AUTH_URL = baseUrl
|
||||
process.env.NEXTAUTH_URL = baseUrl
|
||||
process.env.ADMIN_BOOTSTRAP_ENABLED = "true"
|
||||
process.env.ADMIN_USERNAME = "admin"
|
||||
process.env.ADMIN_EMAIL = "admin@example.test"
|
||||
process.env.ADMIN_NAME = "E2E Admin"
|
||||
process.env.ADMIN_PASSWORD = "admin-password"
|
||||
|
||||
@@ -14,7 +14,6 @@ function nextSuffix() {
|
||||
export async function createTestUser(
|
||||
prisma: PrismaClient,
|
||||
overrides: Partial<{
|
||||
username: string
|
||||
email: string
|
||||
name: string
|
||||
role: UserRole
|
||||
@@ -25,7 +24,6 @@ export async function createTestUser(
|
||||
|
||||
return prisma.user.create({
|
||||
data: {
|
||||
username: overrides.username ?? `test-user-${suffix}`,
|
||||
email: overrides.email ?? `test-user-${suffix}@example.test`,
|
||||
name: overrides.name ?? "Test User",
|
||||
password: "hashed-password",
|
||||
|
||||
@@ -41,7 +41,6 @@ afterAll(async () => {
|
||||
describe("user use-cases", () => {
|
||||
it("creates a user with a hashed password", async () => {
|
||||
const result = await createUserUseCase({
|
||||
username: "new-user",
|
||||
name: "New User",
|
||||
email: "new-user@example.test",
|
||||
password: "secure-password",
|
||||
@@ -52,11 +51,10 @@ describe("user use-cases", () => {
|
||||
expect(result).toEqual({ success: true })
|
||||
|
||||
const user = await prisma.user.findUniqueOrThrow({
|
||||
where: { username: "new-user" },
|
||||
where: { email: "new-user@example.test" },
|
||||
})
|
||||
|
||||
expect(user).toMatchObject({
|
||||
username: "new-user",
|
||||
name: "New User",
|
||||
email: "new-user@example.test",
|
||||
role: "STAFF",
|
||||
@@ -68,29 +66,13 @@ describe("user use-cases", () => {
|
||||
).resolves.toBe(true)
|
||||
})
|
||||
|
||||
it("rejects duplicate usernames and duplicate emails", async () => {
|
||||
it("rejects duplicate emails", async () => {
|
||||
await createTestUser(prisma, {
|
||||
username: "existing-user",
|
||||
email: "existing@example.test",
|
||||
})
|
||||
|
||||
await expect(
|
||||
createUserUseCase({
|
||||
username: "existing-user",
|
||||
name: "Duplicate Username",
|
||||
email: "unique@example.test",
|
||||
password: "secure-password",
|
||||
role: "STAFF",
|
||||
isActive: true,
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
success: false,
|
||||
errors: { username: ["Username already exists"] },
|
||||
})
|
||||
|
||||
await expect(
|
||||
createUserUseCase({
|
||||
username: "unique-user",
|
||||
name: "Duplicate Email",
|
||||
email: "existing@example.test",
|
||||
password: "secure-password",
|
||||
@@ -104,19 +86,17 @@ describe("user use-cases", () => {
|
||||
|
||||
await expect(prisma.user.count()).resolves.toBe(1)
|
||||
await expect(
|
||||
prisma.user.findUniqueOrThrow({ where: { username: "existing-user" } }),
|
||||
prisma.user.findUniqueOrThrow({ where: { email: "existing@example.test" } }),
|
||||
).resolves.toMatchObject({ email: "existing@example.test" })
|
||||
})
|
||||
|
||||
it("updates a user while preserving uniqueness constraints", async () => {
|
||||
const actor = await createTestUser(prisma, { role: "ADMIN" })
|
||||
const user = await createTestUser(prisma, {
|
||||
username: "editable-user",
|
||||
email: "editable@example.test",
|
||||
role: "STAFF",
|
||||
})
|
||||
const other = await createTestUser(prisma, {
|
||||
username: "other-user",
|
||||
email: "other@example.test",
|
||||
role: "STAFF",
|
||||
})
|
||||
@@ -125,7 +105,6 @@ describe("user use-cases", () => {
|
||||
updateUserUseCase({
|
||||
actorId: actor.id,
|
||||
id: user.id,
|
||||
username: "edited-user",
|
||||
name: "Edited User",
|
||||
email: "edited@example.test",
|
||||
role: "MANAGER",
|
||||
@@ -136,7 +115,6 @@ describe("user use-cases", () => {
|
||||
await expect(
|
||||
prisma.user.findUniqueOrThrow({ where: { id: user.id } }),
|
||||
).resolves.toMatchObject({
|
||||
username: "edited-user",
|
||||
name: "Edited User",
|
||||
email: "edited@example.test",
|
||||
role: "MANAGER",
|
||||
@@ -147,21 +125,19 @@ describe("user use-cases", () => {
|
||||
updateUserUseCase({
|
||||
actorId: actor.id,
|
||||
id: user.id,
|
||||
username: other.username,
|
||||
name: "Edited User",
|
||||
email: "another-email@example.test",
|
||||
email: other.email,
|
||||
role: "MANAGER",
|
||||
isActive: true,
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
success: false,
|
||||
errors: { username: ["Username already exists"] },
|
||||
errors: { email: ["Email already exists"] },
|
||||
})
|
||||
|
||||
await expect(
|
||||
prisma.user.findUniqueOrThrow({ where: { id: user.id } }),
|
||||
).resolves.toMatchObject({
|
||||
username: "edited-user",
|
||||
email: "edited@example.test",
|
||||
})
|
||||
})
|
||||
@@ -173,7 +149,6 @@ describe("user use-cases", () => {
|
||||
updateUserUseCase({
|
||||
actorId: admin.id,
|
||||
id: admin.id,
|
||||
username: admin.username,
|
||||
name: admin.name,
|
||||
email: admin.email,
|
||||
role: "STAFF",
|
||||
@@ -191,12 +166,10 @@ describe("user use-cases", () => {
|
||||
|
||||
it("protects the last active administrator but allows deactivation when another active admin exists", async () => {
|
||||
const firstAdmin = await createTestUser(prisma, {
|
||||
username: "first-admin",
|
||||
email: "first-admin@example.test",
|
||||
role: "ADMIN",
|
||||
})
|
||||
const staffActor = await createTestUser(prisma, {
|
||||
username: "staff-actor",
|
||||
email: "staff-actor@example.test",
|
||||
role: "STAFF",
|
||||
})
|
||||
@@ -215,7 +188,6 @@ describe("user use-cases", () => {
|
||||
})
|
||||
|
||||
const secondAdmin = await createTestUser(prisma, {
|
||||
username: "second-admin",
|
||||
email: "second-admin@example.test",
|
||||
role: "ADMIN",
|
||||
})
|
||||
@@ -255,7 +227,6 @@ describe("user use-cases", () => {
|
||||
|
||||
it("resets a user password and rejects missing users", async () => {
|
||||
const user = await createTestUser(prisma, {
|
||||
username: "password-user",
|
||||
email: "password-user@example.test",
|
||||
role: "STAFF",
|
||||
})
|
||||
|
||||
@@ -50,7 +50,6 @@ describe("user actions localization", () => {
|
||||
describe("createUserAction", () => {
|
||||
it("returns localized schema validation errors for invalid create input", async () => {
|
||||
const result = await createUserAction({
|
||||
username: "",
|
||||
name: "",
|
||||
email: "bad",
|
||||
password: "short",
|
||||
@@ -63,7 +62,6 @@ describe("user actions localization", () => {
|
||||
expect(result).toEqual({
|
||||
success: false,
|
||||
errors: {
|
||||
username: [es.admin.users.schema.usernameRequired],
|
||||
name: [es.admin.users.schema.nameRequired],
|
||||
email: [es.admin.users.schema.emailInvalid],
|
||||
password: [es.admin.users.schema.passwordMinLength],
|
||||
@@ -75,13 +73,11 @@ describe("user actions localization", () => {
|
||||
mocks.createUserUseCase.mockResolvedValue({
|
||||
success: false,
|
||||
errors: {
|
||||
username: ["Username already exists"],
|
||||
email: ["Email already exists"],
|
||||
},
|
||||
})
|
||||
|
||||
const result = await createUserAction({
|
||||
username: "ada",
|
||||
name: "Ada",
|
||||
email: "ada@example.test",
|
||||
password: "password1",
|
||||
@@ -92,7 +88,6 @@ describe("user actions localization", () => {
|
||||
expect(result).toEqual({
|
||||
success: false,
|
||||
errors: {
|
||||
username: [es.admin.users.actions.duplicateUsername],
|
||||
email: [es.admin.users.actions.duplicateEmail],
|
||||
},
|
||||
message: es.admin.users.actions.createFailure,
|
||||
@@ -103,7 +98,6 @@ describe("user actions localization", () => {
|
||||
mocks.createUserUseCase.mockResolvedValue({ success: true })
|
||||
|
||||
const result = await createUserAction({
|
||||
username: "ada",
|
||||
name: "Ada",
|
||||
email: "ada@example.test",
|
||||
password: "password1",
|
||||
@@ -123,7 +117,6 @@ describe("user actions localization", () => {
|
||||
it("returns localized schema validation errors for invalid update input", async () => {
|
||||
const result = await updateUserAction({
|
||||
id: "",
|
||||
username: "",
|
||||
name: "",
|
||||
email: "bad",
|
||||
role: "ADMIN",
|
||||
@@ -136,7 +129,6 @@ describe("user actions localization", () => {
|
||||
success: false,
|
||||
errors: {
|
||||
id: [es.admin.users.schema.userIdRequired],
|
||||
username: [es.admin.users.schema.usernameRequired],
|
||||
name: [es.admin.users.schema.nameRequired],
|
||||
email: [es.admin.users.schema.emailInvalid],
|
||||
},
|
||||
@@ -156,7 +148,6 @@ describe("user actions localization", () => {
|
||||
|
||||
const result = await updateUserAction({
|
||||
id: "user-1",
|
||||
username: "admin",
|
||||
name: "Admin",
|
||||
email: "admin@example.test",
|
||||
role: "MANAGER",
|
||||
@@ -180,7 +171,6 @@ describe("user actions localization", () => {
|
||||
|
||||
const result = await updateUserAction({
|
||||
id: "user-1",
|
||||
username: "admin",
|
||||
name: "Admin",
|
||||
email: "admin@example.test",
|
||||
role: "ADMIN",
|
||||
|
||||
@@ -6,11 +6,10 @@ import { es } from "@/i18n/dictionaries/es"
|
||||
const actionCopy = es.admin.users.actions
|
||||
|
||||
describe("user action message localization", () => {
|
||||
it("localizes all 6 known use-case error strings to dictionary keys", () => {
|
||||
it("localizes all 5 known use-case error strings to dictionary keys", () => {
|
||||
expect(
|
||||
localizeUserFieldErrors(
|
||||
{
|
||||
username: ["Username already exists"],
|
||||
email: ["Email already exists"],
|
||||
id: [
|
||||
"User not found",
|
||||
@@ -22,7 +21,6 @@ describe("user action message localization", () => {
|
||||
actionCopy,
|
||||
),
|
||||
).toEqual({
|
||||
username: [actionCopy.duplicateUsername],
|
||||
email: [actionCopy.duplicateEmail],
|
||||
id: [
|
||||
actionCopy.notFound,
|
||||
|
||||
@@ -74,7 +74,6 @@ describe("new user form localization", () => {
|
||||
|
||||
// Form labels from dictionary
|
||||
expect(html).toContain("Nombre")
|
||||
expect(html).toContain("Usuario")
|
||||
expect(html).toContain("Correo electrónico")
|
||||
expect(html).toContain("Contraseña")
|
||||
expect(html).toContain("Rol")
|
||||
@@ -104,7 +103,6 @@ describe("new user form localization", () => {
|
||||
|
||||
expect(html).toContain("New User")
|
||||
expect(html).toContain("Full name")
|
||||
expect(html).toContain("Username")
|
||||
expect(html).toContain("Password")
|
||||
expect(html).toContain("Minimum 8 characters")
|
||||
expect(html).toContain("Create User")
|
||||
@@ -136,7 +134,6 @@ describe("edit user form localization", () => {
|
||||
mocks.getUserProfileById.mockResolvedValue({
|
||||
id: "user-1",
|
||||
name: "Ada Lovelace",
|
||||
username: "ada",
|
||||
email: "ada@example.test",
|
||||
role: "ADMIN",
|
||||
isActive: true,
|
||||
|
||||
@@ -51,7 +51,6 @@ describe("user pages localization", () => {
|
||||
{
|
||||
id: "user-1",
|
||||
name: "Ada Lovelace",
|
||||
username: "ada",
|
||||
email: "ada@example.test",
|
||||
role: "ADMIN",
|
||||
isActive: true,
|
||||
@@ -59,7 +58,6 @@ describe("user pages localization", () => {
|
||||
{
|
||||
id: "user-2",
|
||||
name: "Grace Hopper",
|
||||
username: "grace",
|
||||
email: "grace@example.test",
|
||||
role: "STAFF",
|
||||
isActive: false,
|
||||
@@ -78,7 +76,6 @@ describe("user pages localization", () => {
|
||||
|
||||
// Table headers from dictionary
|
||||
expect(html).toContain("Nombre")
|
||||
expect(html).toContain("Usuario")
|
||||
expect(html).toContain("Correo electrónico")
|
||||
expect(html).toContain("Rol")
|
||||
expect(html).toContain("Estado")
|
||||
@@ -94,10 +91,8 @@ describe("user pages localization", () => {
|
||||
|
||||
// User data is never translated
|
||||
expect(html).toContain("Ada Lovelace")
|
||||
expect(html).toContain("ada")
|
||||
expect(html).toContain("ada@example.test")
|
||||
expect(html).toContain("Grace Hopper")
|
||||
expect(html).toContain("grace")
|
||||
expect(html).toContain("grace@example.test")
|
||||
|
||||
// Canonical role values must NOT appear as display text
|
||||
@@ -133,7 +128,6 @@ describe("user pages localization", () => {
|
||||
{
|
||||
id: "user-1",
|
||||
name: "Ada Lovelace",
|
||||
username: "ada",
|
||||
email: "ada@example.test",
|
||||
role: "MANAGER",
|
||||
isActive: true,
|
||||
@@ -150,7 +144,6 @@ describe("user pages localization", () => {
|
||||
expect(html).toContain("Users")
|
||||
expect(html).toContain("Add User")
|
||||
expect(html).toContain("Name")
|
||||
expect(html).toContain("Username")
|
||||
expect(html).toContain("Email")
|
||||
expect(html).toContain("Role")
|
||||
expect(html).toContain("Status")
|
||||
@@ -172,7 +165,6 @@ describe("user pages localization", () => {
|
||||
{
|
||||
id: "user-1",
|
||||
name: "Test User",
|
||||
username: "testuser",
|
||||
email: "test@example.test",
|
||||
role: "UNKNOWN_ROLE",
|
||||
isActive: true,
|
||||
|
||||
@@ -12,7 +12,6 @@ describe("admin users dictionary", () => {
|
||||
empty: "No users found.",
|
||||
columns: {
|
||||
name: "Name",
|
||||
username: "Username",
|
||||
email: "Email",
|
||||
role: "Role",
|
||||
status: "Status",
|
||||
@@ -29,8 +28,6 @@ describe("admin users dictionary", () => {
|
||||
expect(users.form).toEqual({
|
||||
nameLabel: "Name",
|
||||
namePlaceholder: "Full name",
|
||||
usernameLabel: "Username",
|
||||
usernamePlaceholder: "Username",
|
||||
emailLabel: "Email",
|
||||
emailPlaceholder: "user@example.com",
|
||||
passwordLabel: "Password",
|
||||
@@ -69,7 +66,6 @@ describe("admin users dictionary", () => {
|
||||
toggleStatusFailure: "Failed to update user status",
|
||||
resetPasswordSuccess: "Password reset successfully",
|
||||
resetPasswordFailure: "Failed to reset password",
|
||||
duplicateUsername: "Username already exists",
|
||||
duplicateEmail: "Email already exists",
|
||||
notFound: "User not found",
|
||||
lastActiveAdmin:
|
||||
@@ -79,7 +75,6 @@ describe("admin users dictionary", () => {
|
||||
})
|
||||
|
||||
expect(users.schema).toEqual({
|
||||
usernameRequired: "Username is required",
|
||||
nameRequired: "Name is required",
|
||||
emailInvalid: "Invalid email",
|
||||
passwordMinLength: "Password must be at least 8 characters",
|
||||
@@ -100,7 +95,6 @@ describe("admin users dictionary", () => {
|
||||
empty: "No se encontraron usuarios.",
|
||||
columns: {
|
||||
name: "Nombre",
|
||||
username: "Usuario",
|
||||
email: "Correo electrónico",
|
||||
role: "Rol",
|
||||
status: "Estado",
|
||||
@@ -117,8 +111,6 @@ describe("admin users dictionary", () => {
|
||||
expect(users.form).toEqual({
|
||||
nameLabel: "Nombre",
|
||||
namePlaceholder: "Nombre completo",
|
||||
usernameLabel: "Usuario",
|
||||
usernamePlaceholder: "Usuario",
|
||||
emailLabel: "Correo electrónico",
|
||||
emailPlaceholder: "usuario@ejemplo.com",
|
||||
passwordLabel: "Contraseña",
|
||||
@@ -157,7 +149,6 @@ describe("admin users dictionary", () => {
|
||||
toggleStatusFailure: "Error al actualizar el estado del usuario",
|
||||
resetPasswordSuccess: "Contraseña restablecida correctamente",
|
||||
resetPasswordFailure: "Error al restablecer la contraseña",
|
||||
duplicateUsername: "El nombre de usuario ya existe",
|
||||
duplicateEmail: "El correo electrónico ya existe",
|
||||
notFound: "Usuario no encontrado",
|
||||
lastActiveAdmin:
|
||||
@@ -167,7 +158,6 @@ describe("admin users dictionary", () => {
|
||||
})
|
||||
|
||||
expect(users.schema).toEqual({
|
||||
usernameRequired: "El usuario es obligatorio",
|
||||
nameRequired: "El nombre es obligatorio",
|
||||
emailInvalid: "Correo electrónico no válido",
|
||||
passwordMinLength: "La contraseña debe tener al menos 8 caracteres",
|
||||
|
||||
@@ -13,14 +13,14 @@ describe("i18n dictionaries", () => {
|
||||
it("returns localized login copy for English and Spanish", () => {
|
||||
expect(getDictionary("en").login).toEqual({
|
||||
title: "Sign In",
|
||||
usernameLabel: "Username",
|
||||
emailLabel: "Email",
|
||||
passwordLabel: "Password",
|
||||
submitLabel: "Sign In",
|
||||
})
|
||||
|
||||
expect(getDictionary("es").login).toEqual({
|
||||
title: "Iniciar sesión",
|
||||
usernameLabel: "Usuario",
|
||||
emailLabel: "Correo electrónico",
|
||||
passwordLabel: "Contraseña",
|
||||
submitLabel: "Iniciar sesión",
|
||||
})
|
||||
|
||||
@@ -83,7 +83,6 @@ describe("core schemas", () => {
|
||||
expect(
|
||||
updateUserSchema.safeParse({
|
||||
id: "user-id",
|
||||
username: "user",
|
||||
name: "User",
|
||||
email: "user@example.test",
|
||||
role: "ADMIN",
|
||||
@@ -94,7 +93,6 @@ describe("core schemas", () => {
|
||||
expect(
|
||||
updateUserSchema.safeParse({
|
||||
id: "",
|
||||
username: "user",
|
||||
name: "User",
|
||||
email: "user@example.test",
|
||||
role: "ADMIN",
|
||||
@@ -110,13 +108,13 @@ describe("core schemas", () => {
|
||||
)
|
||||
|
||||
expect(
|
||||
signInSchema.safeParse({ username: "admin", password: "abc" }).success,
|
||||
signInSchema.safeParse({ email: "admin@test.com", password: "abc" }).success,
|
||||
).toBe(true)
|
||||
expect(
|
||||
signInSchema.safeParse({ username: "", password: "abc" }).success,
|
||||
signInSchema.safeParse({ email: "", password: "abc" }).success,
|
||||
).toBe(false)
|
||||
expect(
|
||||
signInSchema.safeParse({ username: "admin", password: "ab" }).success,
|
||||
signInSchema.safeParse({ email: "admin@test.com", password: "ab" }).success,
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
@@ -152,7 +150,6 @@ describe("core schemas", () => {
|
||||
it("validates user password, email, and role", () => {
|
||||
expect(
|
||||
createUserSchema.safeParse({
|
||||
username: "user",
|
||||
name: "User",
|
||||
email: "user@example.test",
|
||||
password: "password1",
|
||||
@@ -163,7 +160,6 @@ describe("core schemas", () => {
|
||||
|
||||
expect(
|
||||
createUserSchema.safeParse({
|
||||
username: "user",
|
||||
name: "User",
|
||||
email: "bad-email",
|
||||
password: "password1",
|
||||
@@ -174,7 +170,6 @@ describe("core schemas", () => {
|
||||
|
||||
expect(
|
||||
createUserSchema.safeParse({
|
||||
username: "user",
|
||||
name: "User",
|
||||
email: "user@example.test",
|
||||
password: "short",
|
||||
@@ -185,7 +180,6 @@ describe("core schemas", () => {
|
||||
|
||||
expect(
|
||||
createUserSchema.safeParse({
|
||||
username: "user",
|
||||
name: "User",
|
||||
email: "user@example.test",
|
||||
password: "password1",
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
} from "@/schemas/user.schema"
|
||||
|
||||
const esCopy: UserSchemaCopy = {
|
||||
usernameRequired: "El usuario es obligatorio",
|
||||
nameRequired: "El nombre es obligatorio",
|
||||
emailInvalid: "Correo electrónico no válido",
|
||||
passwordMinLength: "La contraseña debe tener al menos 8 caracteres",
|
||||
@@ -20,7 +19,6 @@ const esCopy: UserSchemaCopy = {
|
||||
}
|
||||
|
||||
const validCreateData = {
|
||||
username: "admin",
|
||||
name: "Admin User",
|
||||
email: "admin@example.test",
|
||||
password: "password1",
|
||||
@@ -30,7 +28,6 @@ const validCreateData = {
|
||||
|
||||
const validUpdateData = {
|
||||
id: "user-id",
|
||||
username: "admin",
|
||||
name: "Admin User",
|
||||
email: "admin@example.test",
|
||||
role: "ADMIN" as const,
|
||||
@@ -40,7 +37,6 @@ const validUpdateData = {
|
||||
describe("user schema localization", () => {
|
||||
it("buildCreateUserSchema with default copy produces same validation as createUserSchema", () => {
|
||||
const result = buildCreateUserSchema(defaultUserSchemaCopy).safeParse({
|
||||
username: "",
|
||||
name: "",
|
||||
email: "bad",
|
||||
password: "short",
|
||||
@@ -51,7 +47,6 @@ describe("user schema localization", () => {
|
||||
expect(result.success).toBe(false)
|
||||
if (!result.success) {
|
||||
const errors = result.error.flatten().fieldErrors
|
||||
expect(errors.username).toContain("Username is required")
|
||||
expect(errors.name).toContain("Name is required")
|
||||
expect(errors.email).toContain("Invalid email")
|
||||
expect(errors.password).toContain(
|
||||
@@ -64,7 +59,6 @@ describe("user schema localization", () => {
|
||||
|
||||
it("buildCreateUserSchema with Spanish copy produces Spanish error messages", () => {
|
||||
const result = buildCreateUserSchema(esCopy).safeParse({
|
||||
username: "",
|
||||
name: "",
|
||||
email: "bad",
|
||||
password: "short",
|
||||
@@ -75,7 +69,6 @@ describe("user schema localization", () => {
|
||||
expect(result.success).toBe(false)
|
||||
if (!result.success) {
|
||||
const errors = result.error.flatten().fieldErrors
|
||||
expect(errors.username).toContain(esCopy.usernameRequired)
|
||||
expect(errors.name).toContain(esCopy.nameRequired)
|
||||
expect(errors.email).toContain(esCopy.emailInvalid)
|
||||
expect(errors.password).toContain(esCopy.passwordMinLength)
|
||||
@@ -85,7 +78,6 @@ describe("user schema localization", () => {
|
||||
it("buildUpdateUserSchema with default copy produces same validation as updateUserSchema", () => {
|
||||
const result = buildUpdateUserSchema(defaultUserSchemaCopy).safeParse({
|
||||
id: "",
|
||||
username: "",
|
||||
name: "",
|
||||
email: "bad",
|
||||
role: "INVALID",
|
||||
@@ -96,7 +88,6 @@ describe("user schema localization", () => {
|
||||
if (!result.success) {
|
||||
const errors = result.error.flatten().fieldErrors
|
||||
expect(errors.id).toContain("User id is required")
|
||||
expect(errors.username).toContain("Username is required")
|
||||
expect(errors.name).toContain("Name is required")
|
||||
expect(errors.email).toContain("Invalid email")
|
||||
}
|
||||
@@ -105,7 +96,6 @@ describe("user schema localization", () => {
|
||||
it("buildUpdateUserSchema with Spanish copy produces Spanish error messages", () => {
|
||||
const result = buildUpdateUserSchema(esCopy).safeParse({
|
||||
id: "",
|
||||
username: "",
|
||||
name: "",
|
||||
email: "bad",
|
||||
role: "INVALID",
|
||||
@@ -116,7 +106,6 @@ describe("user schema localization", () => {
|
||||
if (!result.success) {
|
||||
const errors = result.error.flatten().fieldErrors
|
||||
expect(errors.id).toContain(esCopy.userIdRequired)
|
||||
expect(errors.username).toContain(esCopy.usernameRequired)
|
||||
expect(errors.name).toContain(esCopy.nameRequired)
|
||||
expect(errors.email).toContain(esCopy.emailInvalid)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user