feat(i18n): localize admin users backbone
This commit is contained in:
+53
-29
@@ -1,17 +1,17 @@
|
|||||||
"use server"
|
"use server"
|
||||||
|
|
||||||
import { revalidatePath } from "next/cache"
|
import { revalidatePath } from "next/cache"
|
||||||
import { flattenError } from "zod"
|
|
||||||
|
|
||||||
|
import { getI18n } from "@/i18n/server"
|
||||||
import {
|
import {
|
||||||
|
buildCreateUserSchema,
|
||||||
|
buildResetUserPasswordSchema,
|
||||||
|
buildSetUserActiveSchema,
|
||||||
|
buildUpdateUserSchema,
|
||||||
type CreateUserFormType,
|
type CreateUserFormType,
|
||||||
createUserSchema,
|
|
||||||
type ResetUserPasswordFormType,
|
type ResetUserPasswordFormType,
|
||||||
resetUserPasswordSchema,
|
|
||||||
type SetUserActiveFormType,
|
type SetUserActiveFormType,
|
||||||
setUserActiveSchema,
|
|
||||||
type UpdateUserFormType,
|
type UpdateUserFormType,
|
||||||
updateUserSchema,
|
|
||||||
} from "@/schemas/user.schema"
|
} from "@/schemas/user.schema"
|
||||||
import { requireRole } from "@/services/auth.service"
|
import { requireRole } from "@/services/auth.service"
|
||||||
import {
|
import {
|
||||||
@@ -21,12 +21,14 @@ import {
|
|||||||
updateUserUseCase,
|
updateUserUseCase,
|
||||||
} from "@/use-cases/user.use-cases"
|
} from "@/use-cases/user.use-cases"
|
||||||
|
|
||||||
|
import { localizeUserFieldErrors } from "./user.messages"
|
||||||
|
|
||||||
const USERS_PATH = "/admin/users"
|
const USERS_PATH = "/admin/users"
|
||||||
|
|
||||||
export async function createUserAction(formData: CreateUserFormType) {
|
export async function createUserAction(formData: CreateUserFormType) {
|
||||||
await requireRole("ADMIN")
|
const { dictionary } = await getI18n()
|
||||||
|
const copy = dictionary.admin.users
|
||||||
const validatedFields = createUserSchema.safeParse(formData)
|
const validatedFields = buildCreateUserSchema(copy.schema).safeParse(formData)
|
||||||
|
|
||||||
if (!validatedFields.success) {
|
if (!validatedFields.success) {
|
||||||
return {
|
return {
|
||||||
@@ -39,27 +41,32 @@ export async function createUserAction(formData: CreateUserFormType) {
|
|||||||
const result = await createUserUseCase(validatedFields.data)
|
const result = await createUserUseCase(validatedFields.data)
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
return result
|
return {
|
||||||
|
...result,
|
||||||
|
errors: localizeUserFieldErrors(result.errors, copy.actions),
|
||||||
|
message: copy.actions.createFailure,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
revalidatePath(USERS_PATH)
|
revalidatePath(USERS_PATH)
|
||||||
|
|
||||||
return { success: true, message: "User created successfully" }
|
return { success: true, message: copy.actions.createSuccess }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Database error:", error)
|
console.error("Database error:", error)
|
||||||
return { success: false, message: "Failed to create user" }
|
return { success: false, message: copy.actions.createFailure }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateUserAction(formData: UpdateUserFormType) {
|
export async function updateUserAction(formData: UpdateUserFormType) {
|
||||||
const session = await requireRole("ADMIN")
|
const session = await requireRole("ADMIN")
|
||||||
|
const { dictionary } = await getI18n()
|
||||||
const validatedFields = updateUserSchema.safeParse(formData)
|
const copy = dictionary.admin.users
|
||||||
|
const validatedFields = buildUpdateUserSchema(copy.schema).safeParse(formData)
|
||||||
|
|
||||||
if (!validatedFields.success) {
|
if (!validatedFields.success) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
errors: flattenError(validatedFields.error).fieldErrors,
|
errors: validatedFields.error.flatten().fieldErrors,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,27 +77,34 @@ export async function updateUserAction(formData: UpdateUserFormType) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
return result
|
return {
|
||||||
|
...result,
|
||||||
|
errors: localizeUserFieldErrors(result.errors, copy.actions),
|
||||||
|
message: copy.actions.updateFailure,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
revalidatePath(USERS_PATH)
|
revalidatePath(USERS_PATH)
|
||||||
|
|
||||||
return { success: true, message: "User updated successfully" }
|
return { success: true, message: copy.actions.updateSuccess }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Database error:", error)
|
console.error("Database error:", error)
|
||||||
return { success: false, message: "Failed to update user" }
|
return { success: false, message: copy.actions.updateFailure }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setUserActiveAction(formData: SetUserActiveFormType) {
|
export async function setUserActiveAction(formData: SetUserActiveFormType) {
|
||||||
const session = await requireRole("ADMIN")
|
const session = await requireRole("ADMIN")
|
||||||
|
const { dictionary } = await getI18n()
|
||||||
const validatedFields = setUserActiveSchema.safeParse(formData)
|
const copy = dictionary.admin.users
|
||||||
|
const validatedFields = buildSetUserActiveSchema(copy.schema).safeParse(
|
||||||
|
formData,
|
||||||
|
)
|
||||||
|
|
||||||
if (!validatedFields.success) {
|
if (!validatedFields.success) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
errors: flattenError(validatedFields.error).fieldErrors,
|
errors: validatedFields.error.flatten().fieldErrors,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,24 +115,30 @@ export async function setUserActiveAction(formData: SetUserActiveFormType) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
return result
|
return {
|
||||||
|
...result,
|
||||||
|
errors: localizeUserFieldErrors(result.errors, copy.actions),
|
||||||
|
message: copy.actions.toggleStatusFailure,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
revalidatePath(USERS_PATH)
|
revalidatePath(USERS_PATH)
|
||||||
|
|
||||||
return { success: true, message: "User status updated successfully" }
|
return { success: true, message: copy.actions.toggleStatusSuccess }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Database error:", error)
|
console.error("Database error:", error)
|
||||||
return { success: false, message: "Failed to update user status" }
|
return { success: false, message: copy.actions.toggleStatusFailure }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function resetUserPasswordAction(
|
export async function resetUserPasswordAction(
|
||||||
formData: ResetUserPasswordFormType,
|
formData: ResetUserPasswordFormType,
|
||||||
) {
|
) {
|
||||||
await requireRole("ADMIN")
|
const { dictionary } = await getI18n()
|
||||||
|
const copy = dictionary.admin.users
|
||||||
const validatedFields = resetUserPasswordSchema.safeParse(formData)
|
const validatedFields = buildResetUserPasswordSchema(copy.schema).safeParse(
|
||||||
|
formData,
|
||||||
|
)
|
||||||
|
|
||||||
if (!validatedFields.success) {
|
if (!validatedFields.success) {
|
||||||
return {
|
return {
|
||||||
@@ -131,14 +151,18 @@ export async function resetUserPasswordAction(
|
|||||||
const result = await resetUserPasswordUseCase(validatedFields.data)
|
const result = await resetUserPasswordUseCase(validatedFields.data)
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
return result
|
return {
|
||||||
|
...result,
|
||||||
|
errors: localizeUserFieldErrors(result.errors, copy.actions),
|
||||||
|
message: copy.actions.resetPasswordFailure,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
revalidatePath(USERS_PATH)
|
revalidatePath(USERS_PATH)
|
||||||
|
|
||||||
return { success: true, message: "Password reset successfully" }
|
return { success: true, message: copy.actions.resetPasswordSuccess }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Database error:", error)
|
console.error("Database error:", error)
|
||||||
return { success: false, message: "Failed to reset password" }
|
return { success: false, message: copy.actions.resetPasswordFailure }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import type { Dictionary } from "@/i18n/dictionaries"
|
||||||
|
|
||||||
|
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",
|
||||||
|
"You cannot remove your own administrator access": "selfAdminAccess",
|
||||||
|
"You cannot deactivate your own user": "selfDeactivate",
|
||||||
|
} as const satisfies Record<string, keyof UserActionCopy>
|
||||||
|
|
||||||
|
function isUserErrorMessage(
|
||||||
|
message: string,
|
||||||
|
): message is keyof typeof userErrorMessageKeys {
|
||||||
|
return message in userErrorMessageKeys
|
||||||
|
}
|
||||||
|
|
||||||
|
function localizeUserMessage(message: string, copy: UserActionCopy): string {
|
||||||
|
if (!isUserErrorMessage(message)) return message
|
||||||
|
|
||||||
|
return copy[userErrorMessageKeys[message]]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function localizeUserFieldErrors(
|
||||||
|
errors: FieldErrors | undefined,
|
||||||
|
copy: UserActionCopy,
|
||||||
|
): FieldErrors | undefined {
|
||||||
|
if (!errors) return undefined
|
||||||
|
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(errors).map(([field, messages]) => [
|
||||||
|
field,
|
||||||
|
messages.map((message) => localizeUserMessage(message, copy)),
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import type { Dictionary } from "@/i18n/dictionaries"
|
||||||
|
|
||||||
|
export type UserListCopy = Dictionary["admin"]["users"]["list"]
|
||||||
|
export type UserFormCopy = Dictionary["admin"]["users"]["form"]
|
||||||
|
export type UserResetPasswordCopy =
|
||||||
|
Dictionary["admin"]["users"]["resetPassword"]
|
||||||
|
export type UserRolesCopy = Dictionary["admin"]["users"]["roles"]
|
||||||
|
export type UserStatusCopy = Dictionary["admin"]["users"]["status"]
|
||||||
|
export type UserActionCopy = Dictionary["admin"]["users"]["actions"]
|
||||||
|
export type UserSchemaCopy = Dictionary["admin"]["users"]["schema"]
|
||||||
|
export type UserFallbackCopy = Dictionary["admin"]["users"]["fallback"]
|
||||||
@@ -432,6 +432,88 @@ export const en = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
admin: {
|
||||||
|
users: {
|
||||||
|
list: {
|
||||||
|
title: "Users",
|
||||||
|
empty: "No users found.",
|
||||||
|
columns: {
|
||||||
|
name: "Name",
|
||||||
|
username: "Username",
|
||||||
|
email: "Email",
|
||||||
|
role: "Role",
|
||||||
|
status: "Status",
|
||||||
|
actions: "Actions",
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
edit: "Edit user",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
new: {
|
||||||
|
title: "New User",
|
||||||
|
},
|
||||||
|
edit: {
|
||||||
|
title: "Edit User",
|
||||||
|
},
|
||||||
|
form: {
|
||||||
|
nameLabel: "Name",
|
||||||
|
namePlaceholder: "Full name",
|
||||||
|
usernameLabel: "Username",
|
||||||
|
usernamePlaceholder: "Username",
|
||||||
|
emailLabel: "Email",
|
||||||
|
emailPlaceholder: "user@example.com",
|
||||||
|
passwordLabel: "Password",
|
||||||
|
passwordPlaceholder: "Minimum 8 characters",
|
||||||
|
roleLabel: "Role",
|
||||||
|
activeLabel: "Active user",
|
||||||
|
createSubmit: "Create User",
|
||||||
|
updateSubmit: "Update User",
|
||||||
|
},
|
||||||
|
resetPassword: {
|
||||||
|
title: "Reset password",
|
||||||
|
passwordLabel: "New password",
|
||||||
|
passwordPlaceholder: "Minimum 8 characters",
|
||||||
|
submit: "Reset Password",
|
||||||
|
},
|
||||||
|
roles: {
|
||||||
|
ADMIN: "Admin",
|
||||||
|
MANAGER: "Manager",
|
||||||
|
STAFF: "Staff",
|
||||||
|
VIEWER: "Viewer",
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
active: "Active",
|
||||||
|
inactive: "Inactive",
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
createSuccess: "User created successfully",
|
||||||
|
createFailure: "Failed to create user",
|
||||||
|
updateSuccess: "User updated successfully",
|
||||||
|
updateFailure: "Failed to update user",
|
||||||
|
toggleStatusSuccess: "User status updated successfully",
|
||||||
|
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:
|
||||||
|
"Cannot remove access from the last active administrator",
|
||||||
|
selfAdminAccess: "You cannot remove your own administrator access",
|
||||||
|
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",
|
||||||
|
userIdRequired: "User id is required",
|
||||||
|
},
|
||||||
|
fallback: {
|
||||||
|
unknownRole: "Unknown role",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
login: {
|
login: {
|
||||||
title: "Sign In",
|
title: "Sign In",
|
||||||
usernameLabel: "Username",
|
usernameLabel: "Username",
|
||||||
|
|||||||
@@ -437,6 +437,88 @@ export const es = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
admin: {
|
||||||
|
users: {
|
||||||
|
list: {
|
||||||
|
title: "Usuarios",
|
||||||
|
empty: "No se encontraron usuarios.",
|
||||||
|
columns: {
|
||||||
|
name: "Nombre",
|
||||||
|
username: "Usuario",
|
||||||
|
email: "Correo electrónico",
|
||||||
|
role: "Rol",
|
||||||
|
status: "Estado",
|
||||||
|
actions: "Acciones",
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
edit: "Editar usuario",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
new: {
|
||||||
|
title: "Nuevo usuario",
|
||||||
|
},
|
||||||
|
edit: {
|
||||||
|
title: "Editar usuario",
|
||||||
|
},
|
||||||
|
form: {
|
||||||
|
nameLabel: "Nombre",
|
||||||
|
namePlaceholder: "Nombre completo",
|
||||||
|
usernameLabel: "Usuario",
|
||||||
|
usernamePlaceholder: "Usuario",
|
||||||
|
emailLabel: "Correo electrónico",
|
||||||
|
emailPlaceholder: "usuario@ejemplo.com",
|
||||||
|
passwordLabel: "Contraseña",
|
||||||
|
passwordPlaceholder: "Mínimo 8 caracteres",
|
||||||
|
roleLabel: "Rol",
|
||||||
|
activeLabel: "Usuario activo",
|
||||||
|
createSubmit: "Crear usuario",
|
||||||
|
updateSubmit: "Actualizar usuario",
|
||||||
|
},
|
||||||
|
resetPassword: {
|
||||||
|
title: "Restablecer contraseña",
|
||||||
|
passwordLabel: "Nueva contraseña",
|
||||||
|
passwordPlaceholder: "Mínimo 8 caracteres",
|
||||||
|
submit: "Restablecer contraseña",
|
||||||
|
},
|
||||||
|
roles: {
|
||||||
|
ADMIN: "Administrador",
|
||||||
|
MANAGER: "Gerente",
|
||||||
|
STAFF: "Personal",
|
||||||
|
VIEWER: "Visor",
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
active: "Activo",
|
||||||
|
inactive: "Inactivo",
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
createSuccess: "Usuario creado correctamente",
|
||||||
|
createFailure: "Error al crear el usuario",
|
||||||
|
updateSuccess: "Usuario actualizado correctamente",
|
||||||
|
updateFailure: "Error al actualizar el usuario",
|
||||||
|
toggleStatusSuccess: "Estado del usuario actualizado correctamente",
|
||||||
|
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:
|
||||||
|
"No se puede eliminar el acceso del último administrador activo",
|
||||||
|
selfAdminAccess: "No puedes eliminar tu propio acceso de administrador",
|
||||||
|
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",
|
||||||
|
userIdRequired: "El ID de usuario es obligatorio",
|
||||||
|
},
|
||||||
|
fallback: {
|
||||||
|
unknownRole: "Rol desconocido",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
login: {
|
login: {
|
||||||
title: "Iniciar sesión",
|
title: "Iniciar sesión",
|
||||||
usernameLabel: "Usuario",
|
usernameLabel: "Usuario",
|
||||||
|
|||||||
+54
-22
@@ -1,38 +1,70 @@
|
|||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
|
|
||||||
|
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",
|
||||||
|
userIdRequired: "User id is required",
|
||||||
|
}
|
||||||
|
|
||||||
export const userRoleSchema = z.enum(["ADMIN", "MANAGER", "STAFF", "VIEWER"])
|
export const userRoleSchema = z.enum(["ADMIN", "MANAGER", "STAFF", "VIEWER"])
|
||||||
|
|
||||||
const passwordSchema = z
|
function buildUserPasswordSchema(copy: UserSchemaCopy) {
|
||||||
.string()
|
return z.string().min(8, { error: copy.passwordMinLength })
|
||||||
.min(8, { error: "Password must be at least 8 characters" })
|
}
|
||||||
|
|
||||||
export const createUserSchema = z.object({
|
export function buildCreateUserSchema(copy: UserSchemaCopy) {
|
||||||
username: z.string().trim().min(1, { error: "Username is required" }),
|
return z.object({
|
||||||
name: z.string().trim().min(1, { error: "Name is required" }),
|
username: z.string().trim().min(1, { error: copy.usernameRequired }),
|
||||||
email: z.email({ error: "Invalid email" }),
|
name: z.string().trim().min(1, { error: copy.nameRequired }),
|
||||||
password: passwordSchema,
|
email: z.email({ error: copy.emailInvalid }),
|
||||||
|
password: buildUserPasswordSchema(copy),
|
||||||
role: userRoleSchema,
|
role: userRoleSchema,
|
||||||
isActive: z.boolean(),
|
isActive: z.boolean(),
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export const updateUserSchema = z.object({
|
export const createUserSchema = buildCreateUserSchema(defaultUserSchemaCopy)
|
||||||
id: z.string().min(1, { error: "User id is required" }),
|
|
||||||
username: z.string().trim().min(1, { error: "Username is required" }),
|
export function buildUpdateUserSchema(copy: UserSchemaCopy) {
|
||||||
name: z.string().trim().min(1, { error: "Name is required" }),
|
return z.object({
|
||||||
email: z.email({ error: "Invalid email" }),
|
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,
|
role: userRoleSchema,
|
||||||
isActive: z.boolean(),
|
isActive: z.boolean(),
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export const setUserActiveSchema = z.object({
|
export const updateUserSchema = buildUpdateUserSchema(defaultUserSchemaCopy)
|
||||||
id: z.string().min(1, { error: "User id is required" }),
|
|
||||||
|
export function buildSetUserActiveSchema(copy: UserSchemaCopy) {
|
||||||
|
return z.object({
|
||||||
|
id: z.string().min(1, { error: copy.userIdRequired }),
|
||||||
isActive: z.boolean(),
|
isActive: z.boolean(),
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export const resetUserPasswordSchema = z.object({
|
export const setUserActiveSchema = buildSetUserActiveSchema(
|
||||||
id: z.string().min(1, { error: "User id is required" }),
|
defaultUserSchemaCopy,
|
||||||
password: passwordSchema,
|
)
|
||||||
})
|
|
||||||
|
export function buildResetUserPasswordSchema(copy: UserSchemaCopy) {
|
||||||
|
return z.object({
|
||||||
|
id: z.string().min(1, { error: copy.userIdRequired }),
|
||||||
|
password: buildUserPasswordSchema(copy),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const resetUserPasswordSchema = buildResetUserPasswordSchema(
|
||||||
|
defaultUserSchemaCopy,
|
||||||
|
)
|
||||||
|
|
||||||
export type CreateUserFormType = z.infer<typeof createUserSchema>
|
export type CreateUserFormType = z.infer<typeof createUserSchema>
|
||||||
export type UpdateUserFormType = z.infer<typeof updateUserSchema>
|
export type UpdateUserFormType = z.infer<typeof updateUserSchema>
|
||||||
|
|||||||
@@ -0,0 +1,306 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest"
|
||||||
|
|
||||||
|
import { es } from "@/i18n/dictionaries/es"
|
||||||
|
|
||||||
|
const mocks = vi.hoisted(() => ({
|
||||||
|
revalidatePath: vi.fn(),
|
||||||
|
getI18n: vi.fn(),
|
||||||
|
requireRole: vi.fn(),
|
||||||
|
createUserUseCase: vi.fn(),
|
||||||
|
updateUserUseCase: vi.fn(),
|
||||||
|
setUserActiveUseCase: vi.fn(),
|
||||||
|
resetUserPasswordUseCase: vi.fn(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock("next/cache", () => ({
|
||||||
|
revalidatePath: mocks.revalidatePath,
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock("@/i18n/server", () => ({
|
||||||
|
getI18n: mocks.getI18n,
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock("@/services/auth.service", () => ({
|
||||||
|
requireRole: mocks.requireRole,
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock("@/use-cases/user.use-cases", () => ({
|
||||||
|
createUserUseCase: mocks.createUserUseCase,
|
||||||
|
updateUserUseCase: mocks.updateUserUseCase,
|
||||||
|
setUserActiveUseCase: mocks.setUserActiveUseCase,
|
||||||
|
resetUserPasswordUseCase: mocks.resetUserPasswordUseCase,
|
||||||
|
}))
|
||||||
|
|
||||||
|
import {
|
||||||
|
createUserAction,
|
||||||
|
resetUserPasswordAction,
|
||||||
|
setUserActiveAction,
|
||||||
|
updateUserAction,
|
||||||
|
} from "@/actions/user.actions"
|
||||||
|
|
||||||
|
describe("user actions localization", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
mocks.getI18n.mockResolvedValue({ dictionary: es, locale: "es" })
|
||||||
|
mocks.requireRole.mockResolvedValue({
|
||||||
|
user: { id: "admin-1" },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("createUserAction", () => {
|
||||||
|
it("returns localized schema validation errors for invalid create input", async () => {
|
||||||
|
const result = await createUserAction({
|
||||||
|
username: "",
|
||||||
|
name: "",
|
||||||
|
email: "bad",
|
||||||
|
password: "short",
|
||||||
|
role: "ADMIN",
|
||||||
|
isActive: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(mocks.getI18n).toHaveBeenCalledOnce()
|
||||||
|
expect(mocks.createUserUseCase).not.toHaveBeenCalled()
|
||||||
|
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],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("localizes mapped duplicate field errors for create failures", async () => {
|
||||||
|
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",
|
||||||
|
role: "STAFF",
|
||||||
|
isActive: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
success: false,
|
||||||
|
errors: {
|
||||||
|
username: [es.admin.users.actions.duplicateUsername],
|
||||||
|
email: [es.admin.users.actions.duplicateEmail],
|
||||||
|
},
|
||||||
|
message: es.admin.users.actions.createFailure,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns a localized create success message and revalidates", async () => {
|
||||||
|
mocks.createUserUseCase.mockResolvedValue({ success: true })
|
||||||
|
|
||||||
|
const result = await createUserAction({
|
||||||
|
username: "ada",
|
||||||
|
name: "Ada",
|
||||||
|
email: "ada@example.test",
|
||||||
|
password: "password1",
|
||||||
|
role: "STAFF",
|
||||||
|
isActive: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
success: true,
|
||||||
|
message: es.admin.users.actions.createSuccess,
|
||||||
|
})
|
||||||
|
expect(mocks.revalidatePath).toHaveBeenCalledWith("/admin/users")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("updateUserAction", () => {
|
||||||
|
it("returns localized schema validation errors for invalid update input", async () => {
|
||||||
|
const result = await updateUserAction({
|
||||||
|
id: "",
|
||||||
|
username: "",
|
||||||
|
name: "",
|
||||||
|
email: "bad",
|
||||||
|
role: "ADMIN",
|
||||||
|
isActive: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(mocks.getI18n).toHaveBeenCalledOnce()
|
||||||
|
expect(mocks.updateUserUseCase).not.toHaveBeenCalled()
|
||||||
|
expect(result).toEqual({
|
||||||
|
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],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("localizes mapped use-case errors for update failures", async () => {
|
||||||
|
mocks.updateUserUseCase.mockResolvedValue({
|
||||||
|
success: false,
|
||||||
|
errors: {
|
||||||
|
id: [
|
||||||
|
"Cannot remove access from the last active administrator",
|
||||||
|
"You cannot remove your own administrator access",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await updateUserAction({
|
||||||
|
id: "user-1",
|
||||||
|
username: "admin",
|
||||||
|
name: "Admin",
|
||||||
|
email: "admin@example.test",
|
||||||
|
role: "MANAGER",
|
||||||
|
isActive: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
success: false,
|
||||||
|
errors: {
|
||||||
|
id: [
|
||||||
|
es.admin.users.actions.lastActiveAdmin,
|
||||||
|
es.admin.users.actions.selfAdminAccess,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
message: es.admin.users.actions.updateFailure,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns a localized update success message and revalidates", async () => {
|
||||||
|
mocks.updateUserUseCase.mockResolvedValue({ success: true })
|
||||||
|
|
||||||
|
const result = await updateUserAction({
|
||||||
|
id: "user-1",
|
||||||
|
username: "admin",
|
||||||
|
name: "Admin",
|
||||||
|
email: "admin@example.test",
|
||||||
|
role: "ADMIN",
|
||||||
|
isActive: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
success: true,
|
||||||
|
message: es.admin.users.actions.updateSuccess,
|
||||||
|
})
|
||||||
|
expect(mocks.revalidatePath).toHaveBeenCalledWith("/admin/users")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("setUserActiveAction", () => {
|
||||||
|
it("returns localized schema validation errors for invalid input", async () => {
|
||||||
|
const result = await setUserActiveAction({
|
||||||
|
id: "",
|
||||||
|
isActive: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(mocks.getI18n).toHaveBeenCalledOnce()
|
||||||
|
expect(mocks.setUserActiveUseCase).not.toHaveBeenCalled()
|
||||||
|
expect(result).toEqual({
|
||||||
|
success: false,
|
||||||
|
errors: {
|
||||||
|
id: [es.admin.users.schema.userIdRequired],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("localizes mapped use-case errors for toggle failures", async () => {
|
||||||
|
mocks.setUserActiveUseCase.mockResolvedValue({
|
||||||
|
success: false,
|
||||||
|
errors: {
|
||||||
|
id: ["You cannot deactivate your own user"],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await setUserActiveAction({
|
||||||
|
id: "user-1",
|
||||||
|
isActive: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
success: false,
|
||||||
|
errors: {
|
||||||
|
id: [es.admin.users.actions.selfDeactivate],
|
||||||
|
},
|
||||||
|
message: es.admin.users.actions.toggleStatusFailure,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns a localized toggle status success message and revalidates", async () => {
|
||||||
|
mocks.setUserActiveUseCase.mockResolvedValue({ success: true })
|
||||||
|
|
||||||
|
const result = await setUserActiveAction({
|
||||||
|
id: "user-1",
|
||||||
|
isActive: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
success: true,
|
||||||
|
message: es.admin.users.actions.toggleStatusSuccess,
|
||||||
|
})
|
||||||
|
expect(mocks.revalidatePath).toHaveBeenCalledWith("/admin/users")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("resetUserPasswordAction", () => {
|
||||||
|
it("returns localized schema validation errors for invalid input", async () => {
|
||||||
|
const result = await resetUserPasswordAction({
|
||||||
|
id: "",
|
||||||
|
password: "short",
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(mocks.getI18n).toHaveBeenCalledOnce()
|
||||||
|
expect(mocks.resetUserPasswordUseCase).not.toHaveBeenCalled()
|
||||||
|
expect(result).toEqual({
|
||||||
|
success: false,
|
||||||
|
errors: {
|
||||||
|
id: [es.admin.users.schema.userIdRequired],
|
||||||
|
password: [es.admin.users.schema.passwordMinLength],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("localizes mapped use-case errors for reset failures", async () => {
|
||||||
|
mocks.resetUserPasswordUseCase.mockResolvedValue({
|
||||||
|
success: false,
|
||||||
|
errors: { id: ["User not found"] },
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await resetUserPasswordAction({
|
||||||
|
id: "nonexistent",
|
||||||
|
password: "newpassword1",
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
success: false,
|
||||||
|
errors: {
|
||||||
|
id: [es.admin.users.actions.notFound],
|
||||||
|
},
|
||||||
|
message: es.admin.users.actions.resetPasswordFailure,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns a localized reset success message and revalidates", async () => {
|
||||||
|
mocks.resetUserPasswordUseCase.mockResolvedValue({ success: true })
|
||||||
|
|
||||||
|
const result = await resetUserPasswordAction({
|
||||||
|
id: "user-1",
|
||||||
|
password: "newpassword1",
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
success: true,
|
||||||
|
message: es.admin.users.actions.resetPasswordSuccess,
|
||||||
|
})
|
||||||
|
expect(mocks.revalidatePath).toHaveBeenCalledWith("/admin/users")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { describe, expect, it } from "vitest"
|
||||||
|
|
||||||
|
import { localizeUserFieldErrors } from "@/actions/user.messages"
|
||||||
|
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", () => {
|
||||||
|
expect(
|
||||||
|
localizeUserFieldErrors(
|
||||||
|
{
|
||||||
|
username: ["Username already exists"],
|
||||||
|
email: ["Email already exists"],
|
||||||
|
id: [
|
||||||
|
"User not found",
|
||||||
|
"Cannot remove access from the last active administrator",
|
||||||
|
"You cannot remove your own administrator access",
|
||||||
|
"You cannot deactivate your own user",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
actionCopy,
|
||||||
|
),
|
||||||
|
).toEqual({
|
||||||
|
username: [actionCopy.duplicateUsername],
|
||||||
|
email: [actionCopy.duplicateEmail],
|
||||||
|
id: [
|
||||||
|
actionCopy.notFound,
|
||||||
|
actionCopy.lastActiveAdmin,
|
||||||
|
actionCopy.selfAdminAccess,
|
||||||
|
actionCopy.selfDeactivate,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("keeps unknown messages unchanged", () => {
|
||||||
|
expect(
|
||||||
|
localizeUserFieldErrors({ id: ["Unexpected user issue"] }, actionCopy),
|
||||||
|
).toEqual({ id: ["Unexpected user issue"] })
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns undefined for undefined input", () => {
|
||||||
|
expect(localizeUserFieldErrors(undefined, actionCopy)).toBeUndefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,200 @@
|
|||||||
|
import { describe, expect, it } from "vitest"
|
||||||
|
|
||||||
|
import { dictionaries, getDictionary } from "@/i18n/dictionaries"
|
||||||
|
|
||||||
|
describe("admin users dictionary", () => {
|
||||||
|
it("provides localized admin users copy for English", () => {
|
||||||
|
const users = getDictionary("en").admin.users
|
||||||
|
|
||||||
|
expect(users.list).toEqual({
|
||||||
|
title: "Users",
|
||||||
|
empty: "No users found.",
|
||||||
|
columns: {
|
||||||
|
name: "Name",
|
||||||
|
username: "Username",
|
||||||
|
email: "Email",
|
||||||
|
role: "Role",
|
||||||
|
status: "Status",
|
||||||
|
actions: "Actions",
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
edit: "Edit user",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(users.new).toEqual({ title: "New User" })
|
||||||
|
expect(users.edit).toEqual({ title: "Edit User" })
|
||||||
|
|
||||||
|
expect(users.form).toEqual({
|
||||||
|
nameLabel: "Name",
|
||||||
|
namePlaceholder: "Full name",
|
||||||
|
usernameLabel: "Username",
|
||||||
|
usernamePlaceholder: "Username",
|
||||||
|
emailLabel: "Email",
|
||||||
|
emailPlaceholder: "user@example.com",
|
||||||
|
passwordLabel: "Password",
|
||||||
|
passwordPlaceholder: "Minimum 8 characters",
|
||||||
|
roleLabel: "Role",
|
||||||
|
activeLabel: "Active user",
|
||||||
|
createSubmit: "Create User",
|
||||||
|
updateSubmit: "Update User",
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(users.resetPassword).toEqual({
|
||||||
|
title: "Reset password",
|
||||||
|
passwordLabel: "New password",
|
||||||
|
passwordPlaceholder: "Minimum 8 characters",
|
||||||
|
submit: "Reset Password",
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(users.roles).toEqual({
|
||||||
|
ADMIN: "Admin",
|
||||||
|
MANAGER: "Manager",
|
||||||
|
STAFF: "Staff",
|
||||||
|
VIEWER: "Viewer",
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(users.status).toEqual({
|
||||||
|
active: "Active",
|
||||||
|
inactive: "Inactive",
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(users.actions).toEqual({
|
||||||
|
createSuccess: "User created successfully",
|
||||||
|
createFailure: "Failed to create user",
|
||||||
|
updateSuccess: "User updated successfully",
|
||||||
|
updateFailure: "Failed to update user",
|
||||||
|
toggleStatusSuccess: "User status updated successfully",
|
||||||
|
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:
|
||||||
|
"Cannot remove access from the last active administrator",
|
||||||
|
selfAdminAccess: "You cannot remove your own administrator access",
|
||||||
|
selfDeactivate: "You cannot deactivate your own user",
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(users.schema).toEqual({
|
||||||
|
usernameRequired: "Username is required",
|
||||||
|
nameRequired: "Name is required",
|
||||||
|
emailInvalid: "Invalid email",
|
||||||
|
passwordMinLength: "Password must be at least 8 characters",
|
||||||
|
userIdRequired: "User id is required",
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(users.fallback).toEqual({
|
||||||
|
unknownRole: "Unknown role",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("provides localized admin users copy for Spanish", () => {
|
||||||
|
const users = getDictionary("es").admin.users
|
||||||
|
|
||||||
|
expect(users.list).toEqual({
|
||||||
|
title: "Usuarios",
|
||||||
|
empty: "No se encontraron usuarios.",
|
||||||
|
columns: {
|
||||||
|
name: "Nombre",
|
||||||
|
username: "Usuario",
|
||||||
|
email: "Correo electrónico",
|
||||||
|
role: "Rol",
|
||||||
|
status: "Estado",
|
||||||
|
actions: "Acciones",
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
edit: "Editar usuario",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(users.new).toEqual({ title: "Nuevo usuario" })
|
||||||
|
expect(users.edit).toEqual({ title: "Editar usuario" })
|
||||||
|
|
||||||
|
expect(users.form).toEqual({
|
||||||
|
nameLabel: "Nombre",
|
||||||
|
namePlaceholder: "Nombre completo",
|
||||||
|
usernameLabel: "Usuario",
|
||||||
|
usernamePlaceholder: "Usuario",
|
||||||
|
emailLabel: "Correo electrónico",
|
||||||
|
emailPlaceholder: "usuario@ejemplo.com",
|
||||||
|
passwordLabel: "Contraseña",
|
||||||
|
passwordPlaceholder: "Mínimo 8 caracteres",
|
||||||
|
roleLabel: "Rol",
|
||||||
|
activeLabel: "Usuario activo",
|
||||||
|
createSubmit: "Crear usuario",
|
||||||
|
updateSubmit: "Actualizar usuario",
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(users.resetPassword).toEqual({
|
||||||
|
title: "Restablecer contraseña",
|
||||||
|
passwordLabel: "Nueva contraseña",
|
||||||
|
passwordPlaceholder: "Mínimo 8 caracteres",
|
||||||
|
submit: "Restablecer contraseña",
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(users.roles).toEqual({
|
||||||
|
ADMIN: "Administrador",
|
||||||
|
MANAGER: "Gerente",
|
||||||
|
STAFF: "Personal",
|
||||||
|
VIEWER: "Visor",
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(users.status).toEqual({
|
||||||
|
active: "Activo",
|
||||||
|
inactive: "Inactivo",
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(users.actions).toEqual({
|
||||||
|
createSuccess: "Usuario creado correctamente",
|
||||||
|
createFailure: "Error al crear el usuario",
|
||||||
|
updateSuccess: "Usuario actualizado correctamente",
|
||||||
|
updateFailure: "Error al actualizar el usuario",
|
||||||
|
toggleStatusSuccess: "Estado del usuario actualizado correctamente",
|
||||||
|
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:
|
||||||
|
"No se puede eliminar el acceso del último administrador activo",
|
||||||
|
selfAdminAccess: "No puedes eliminar tu propio acceso de administrador",
|
||||||
|
selfDeactivate: "No puedes desactivar tu propio usuario",
|
||||||
|
})
|
||||||
|
|
||||||
|
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",
|
||||||
|
userIdRequired: "El ID de usuario es obligatorio",
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(users.fallback).toEqual({
|
||||||
|
unknownRole: "Rol desconocido",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("maintains structural parity between English and Spanish admin.users", () => {
|
||||||
|
const enKeys = extractKeyPaths(dictionaries.en.admin.users)
|
||||||
|
const esKeys = extractKeyPaths(dictionaries.es.admin.users)
|
||||||
|
|
||||||
|
expect(esKeys).toEqual(enKeys)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
function extractKeyPaths(value: unknown, prefix = ""): string[] {
|
||||||
|
if (!isPlainObject(value)) return [prefix]
|
||||||
|
|
||||||
|
return Object.keys(value)
|
||||||
|
.sort()
|
||||||
|
.flatMap((key) =>
|
||||||
|
extractKeyPaths(value[key], prefix ? `${prefix}.${key}` : key),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||||
|
return typeof value === "object" && value !== null && !Array.isArray(value)
|
||||||
|
}
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
import { describe, expect, it } from "vitest"
|
||||||
|
|
||||||
|
import {
|
||||||
|
buildCreateUserSchema,
|
||||||
|
buildResetUserPasswordSchema,
|
||||||
|
buildSetUserActiveSchema,
|
||||||
|
buildUpdateUserSchema,
|
||||||
|
createUserSchema,
|
||||||
|
defaultUserSchemaCopy,
|
||||||
|
type UserSchemaCopy,
|
||||||
|
updateUserSchema,
|
||||||
|
} 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",
|
||||||
|
userIdRequired: "El ID de usuario es obligatorio",
|
||||||
|
}
|
||||||
|
|
||||||
|
const validCreateData = {
|
||||||
|
username: "admin",
|
||||||
|
name: "Admin User",
|
||||||
|
email: "admin@example.test",
|
||||||
|
password: "password1",
|
||||||
|
role: "ADMIN" as const,
|
||||||
|
isActive: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
const validUpdateData = {
|
||||||
|
id: "user-id",
|
||||||
|
username: "admin",
|
||||||
|
name: "Admin User",
|
||||||
|
email: "admin@example.test",
|
||||||
|
role: "ADMIN" as const,
|
||||||
|
isActive: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
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",
|
||||||
|
role: "INVALID",
|
||||||
|
isActive: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
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(
|
||||||
|
"Password must be at least 8 characters",
|
||||||
|
)
|
||||||
|
// role error comes from z.enum, not our copy
|
||||||
|
expect(errors.role).toBeDefined()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it("buildCreateUserSchema with Spanish copy produces Spanish error messages", () => {
|
||||||
|
const result = buildCreateUserSchema(esCopy).safeParse({
|
||||||
|
username: "",
|
||||||
|
name: "",
|
||||||
|
email: "bad",
|
||||||
|
password: "short",
|
||||||
|
role: "INVALID",
|
||||||
|
isActive: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it("buildUpdateUserSchema with default copy produces same validation as updateUserSchema", () => {
|
||||||
|
const result = buildUpdateUserSchema(defaultUserSchemaCopy).safeParse({
|
||||||
|
id: "",
|
||||||
|
username: "",
|
||||||
|
name: "",
|
||||||
|
email: "bad",
|
||||||
|
role: "INVALID",
|
||||||
|
isActive: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.success).toBe(false)
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it("buildUpdateUserSchema with Spanish copy produces Spanish error messages", () => {
|
||||||
|
const result = buildUpdateUserSchema(esCopy).safeParse({
|
||||||
|
id: "",
|
||||||
|
username: "",
|
||||||
|
name: "",
|
||||||
|
email: "bad",
|
||||||
|
role: "INVALID",
|
||||||
|
isActive: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.success).toBe(false)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it("buildSetUserActiveSchema with default copy validates id requirement", () => {
|
||||||
|
const result = buildSetUserActiveSchema(defaultUserSchemaCopy).safeParse({
|
||||||
|
id: "",
|
||||||
|
isActive: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.success).toBe(false)
|
||||||
|
if (!result.success) {
|
||||||
|
expect(result.error.flatten().fieldErrors.id).toContain(
|
||||||
|
"User id is required",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it("buildResetUserPasswordSchema with default copy validates password and id", () => {
|
||||||
|
const result = buildResetUserPasswordSchema(
|
||||||
|
defaultUserSchemaCopy,
|
||||||
|
).safeParse({
|
||||||
|
id: "",
|
||||||
|
password: "short",
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.success).toBe(false)
|
||||||
|
if (!result.success) {
|
||||||
|
const errors = result.error.flatten().fieldErrors
|
||||||
|
expect(errors.id).toContain("User id is required")
|
||||||
|
expect(errors.password).toContain(
|
||||||
|
"Password must be at least 8 characters",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it("default exports match builder with default copy for valid data", () => {
|
||||||
|
expect(createUserSchema.safeParse(validCreateData).success).toBe(true)
|
||||||
|
expect(
|
||||||
|
buildCreateUserSchema(defaultUserSchemaCopy).safeParse(validCreateData)
|
||||||
|
.success,
|
||||||
|
).toBe(true)
|
||||||
|
|
||||||
|
expect(updateUserSchema.safeParse(validUpdateData).success).toBe(true)
|
||||||
|
expect(
|
||||||
|
buildUpdateUserSchema(defaultUserSchemaCopy).safeParse(validUpdateData)
|
||||||
|
.success,
|
||||||
|
).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user