diff --git a/src/i18n/dictionaries/en.ts b/src/i18n/dictionaries/en.ts index f63d687..f715094 100644 --- a/src/i18n/dictionaries/en.ts +++ b/src/i18n/dictionaries/en.ts @@ -36,7 +36,6 @@ export const en = { items: "Items", categories: "Categories", assets: "Assets", - recipients: "Recipients", people: "People", movements: "Movements", assignments: "Assignments", @@ -51,7 +50,6 @@ export const en = { category: "Category", item: "Item", asset: "Asset", - recipient: "Recipient", person: "Person", assignment: "Assignment", }, @@ -392,86 +390,7 @@ export const en = { departmentRequired: "Department is required", emailInvalid: "Email format is invalid", idRequired: "ID is required", - }, - }, - recipients: { - list: { - title: "Recipients", - addLabel: "Add Recipient", - empty: "No recipients found.", - columns: { - username: "Username", - name: "Name", - email: "Email", - phone: "Phone", - department: "Department", - actions: "Actions", - }, - actions: { - view: "View recipient", - edit: "Edit recipient", - }, - }, - detail: { - notFound: "Recipient not found", - labels: { - username: "Username", - email: "Email", - phone: "Phone", - department: "Department", - }, - }, - new: { - title: "Add Recipient", - }, - edit: { - title: "Edit Recipient", - notFound: "Recipient not found", - }, - form: { - usernameLabel: "Username", - usernamePlaceholder: "Username", - firstNameLabel: "First Name", - firstNamePlaceholder: "First name", - lastNameLabel: "Last Name", - lastNamePlaceholder: "Last name", - departmentLabel: "Department", - departmentPlaceholder: "Select a department", - emailLabel: "Email", - emailPlaceholder: "Email", - phoneLabel: "Phone", - phonePlaceholder: "Phone", - createSubmit: "Create Recipient", - updateSubmit: "Update Recipient", - }, - fallback: { - unknownDepartment: "Unknown department", - }, - departments: { - IT: "IT", - ENGINEERING: "Engineering", - LOGISTICS: "Logistics", - TRAFFIC: "Traffic", - DRIVER: "Driver", - ADMINISTRATION: "Administration", - SALES: "Sales", - OTHER: "Other", - }, - actions: { - createSuccess: "Recipient created successfully", - createFailure: "Failed to create recipient", - updateSuccess: "Recipient updated successfully", - updateFailure: "Failed to update recipient", - duplicateUsername: "Username already exists", - duplicateEmail: "Email already exists", - }, - schema: { - usernameRequired: "Username is required", - firstNameRequired: "First name is required", - lastNameRequired: "Last name is required", - departmentRequired: "Department is required", - emailInvalid: "Email format is invalid", - idRequired: "ID is required", + userIdInvalid: "User ID must be a valid UUID", }, }, movements: { @@ -608,10 +527,6 @@ export const en = { title: "Total Assets", countLabel: "Total", }, - recipients: { - title: "Total Recipients", - countLabel: "Total", - }, people: { title: "Total People", countLabel: "Total", diff --git a/src/i18n/dictionaries/es.ts b/src/i18n/dictionaries/es.ts index cdd5fee..3e4ce3d 100644 --- a/src/i18n/dictionaries/es.ts +++ b/src/i18n/dictionaries/es.ts @@ -38,7 +38,6 @@ export const es = { items: "Artículos", categories: "Categorías", assets: "Activos", - recipients: "Destinatarios", people: "Personas", movements: "Movimientos", assignments: "Asignaciones", @@ -53,7 +52,6 @@ export const es = { category: "Categoría", item: "Artículo", asset: "Activo", - recipient: "Destinatario", person: "Persona", assignment: "Asignación", }, @@ -397,86 +395,7 @@ export const es = { departmentRequired: "El departamento es obligatorio", emailInvalid: "El correo electrónico no es válido", idRequired: "El ID es obligatorio", - }, - }, - recipients: { - list: { - title: "Destinatarios", - addLabel: "Agregar destinatario", - empty: "No se encontraron destinatarios.", - columns: { - username: "Usuario", - name: "Nombre", - email: "Correo electrónico", - phone: "Teléfono", - department: "Departamento", - actions: "Acciones", - }, - actions: { - view: "Ver destinatario", - edit: "Editar destinatario", - }, - }, - detail: { - notFound: "Destinatario no encontrado", - labels: { - username: "Usuario", - email: "Correo electrónico", - phone: "Teléfono", - department: "Departamento", - }, - }, - new: { - title: "Agregar destinatario", - }, - edit: { - title: "Editar destinatario", - notFound: "Destinatario no encontrado", - }, - form: { - usernameLabel: "Usuario", - usernamePlaceholder: "Usuario", - firstNameLabel: "Nombre", - firstNamePlaceholder: "Nombre", - lastNameLabel: "Apellido", - lastNamePlaceholder: "Apellido", - departmentLabel: "Departamento", - departmentPlaceholder: "Selecciona un departamento", - emailLabel: "Correo electrónico", - emailPlaceholder: "Correo electrónico", - phoneLabel: "Teléfono", - phonePlaceholder: "Teléfono", - createSubmit: "Crear destinatario", - updateSubmit: "Actualizar destinatario", - }, - fallback: { - unknownDepartment: "Departamento desconocido", - }, - departments: { - IT: "IT", - ENGINEERING: "Ingeniería", - LOGISTICS: "Logística", - TRAFFIC: "Tráfico", - DRIVER: "Chofer", - ADMINISTRATION: "Administración", - SALES: "Ventas", - OTHER: "Otro", - }, - actions: { - createSuccess: "Destinatario creado correctamente", - createFailure: "Error al crear el destinatario", - updateSuccess: "Destinatario actualizado correctamente", - updateFailure: "Error al actualizar el destinatario", - duplicateUsername: "El nombre de usuario ya existe", - duplicateEmail: "El correo electrónico ya existe", - }, - schema: { - usernameRequired: "El usuario es obligatorio", - firstNameRequired: "El nombre es obligatorio", - lastNameRequired: "El apellido es obligatorio", - departmentRequired: "El departamento es obligatorio", - emailInvalid: "El correo electrónico no es válido", - idRequired: "El ID es obligatorio", + userIdInvalid: "El ID de usuario debe ser un UUID válido", }, }, movements: { @@ -613,10 +532,6 @@ export const es = { title: "Total de activos", countLabel: "Total", }, - recipients: { - title: "Total de destinatarios", - countLabel: "Total", - }, people: { title: "Total de personas", countLabel: "Total", diff --git a/src/schemas/person.schema.ts b/src/schemas/person.schema.ts index fbc3d69..4bd5c36 100644 --- a/src/schemas/person.schema.ts +++ b/src/schemas/person.schema.ts @@ -10,6 +10,7 @@ const defaultPersonSchemaCopy: PersonSchemaCopy = { departmentRequired: "Department is required", emailInvalid: "Email format is invalid", idRequired: "ID is required", + userIdInvalid: "User ID must be a valid UUID", } const personDepartments = [ @@ -37,7 +38,11 @@ function buildPersonBaseSchema(copy: PersonSchemaCopy) { }), email: z.string().optional().nullable(), phone: z.string().optional().nullable(), - userId: z.string().uuid().optional().nullable(), + userId: z + .string() + .uuid({ error: copy.userIdInvalid }) + .optional() + .nullable(), }) } diff --git a/tests/unit/actions/person.actions.test.ts b/tests/unit/actions/person.actions.test.ts new file mode 100644 index 0000000..59b6aad --- /dev/null +++ b/tests/unit/actions/person.actions.test.ts @@ -0,0 +1,112 @@ +import { beforeEach, describe, expect, it, vi } from "vitest" + +import { en } from "@/i18n/dictionaries/en" +import { es } from "@/i18n/dictionaries/es" + +const mocks = vi.hoisted(() => ({ + revalidatePath: vi.fn(), + getI18n: vi.fn(), + createPersonUseCase: vi.fn(), + updatePersonUseCase: vi.fn(), +})) + +vi.mock("next/cache", () => ({ + revalidatePath: mocks.revalidatePath, +})) + +vi.mock("@/i18n/server", () => ({ + getI18n: mocks.getI18n, +})) + +vi.mock("@/use-cases/person.use-cases", () => ({ + createPersonUseCase: mocks.createPersonUseCase, + updatePersonUseCase: mocks.updatePersonUseCase, +})) + +import { createNewPerson, updatePerson } from "@/actions/person.actions" + +describe("person actions localization", () => { + beforeEach(() => { + vi.clearAllMocks() + mocks.getI18n.mockResolvedValue({ dictionary: es, locale: "es" }) + }) + + it("returns localized schema validation errors for invalid create input (no username)", async () => { + const result = await createNewPerson({ + firstName: "", + lastName: "", + department: "", + email: "not-an-email", + } as unknown as Parameters[0]) + + expect(mocks.getI18n).toHaveBeenCalledOnce() + expect(mocks.createPersonUseCase).not.toHaveBeenCalled() + expect(result).toEqual({ + success: false, + errors: { + firstName: [es.inventory.people.schema.firstNameRequired], + lastName: [es.inventory.people.schema.lastNameRequired], + department: [es.inventory.people.schema.departmentRequired], + }, + }) + }) + + it("localizes mapped duplicate field errors for create failures (no duplicateUsername)", async () => { + mocks.createPersonUseCase.mockResolvedValue({ + success: false, + errors: { + email: ["Email already exists"], + }, + }) + + const result = await createNewPerson({ + firstName: "Ada", + lastName: "Lovelace", + department: "ENGINEERING", + email: "ada@example.test", + }) + + expect(result).toEqual({ + success: false, + errors: { + email: [es.inventory.people.actions.duplicateEmail], + }, + message: es.inventory.people.actions.createFailure, + }) + }) + + it("returns a localized update success message and revalidates /people", async () => { + mocks.getI18n.mockResolvedValue({ dictionary: en, locale: "en" }) + mocks.updatePersonUseCase.mockResolvedValue({ success: true }) + + const result = await updatePerson({ + id: "person-1", + firstName: "Ada", + lastName: "Lovelace", + department: "ENGINEERING", + email: "ada@example.test", + }) + + expect(result).toEqual({ + success: true, + message: en.inventory.people.actions.updateSuccess, + }) + expect(mocks.revalidatePath).toHaveBeenCalledWith("/people") + }) + + it("returns localized validation error for invalid userId UUID on create", async () => { + const result = await createNewPerson({ + firstName: "Ada", + lastName: "Lovelace", + department: "ENGINEERING", + userId: "not-a-uuid", + } as unknown as Parameters[0]) + + expect(result).toEqual({ + success: false, + errors: { + userId: [es.inventory.people.schema.userIdInvalid], + }, + }) + }) +}) diff --git a/tests/unit/actions/person.messages.test.ts b/tests/unit/actions/person.messages.test.ts new file mode 100644 index 0000000..fc36439 --- /dev/null +++ b/tests/unit/actions/person.messages.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, it } from "vitest" + +import { localizePersonFieldErrors } from "@/actions/person.messages" + +const actionCopy = { + createSuccess: "Persona creada correctamente", + createFailure: "Error al crear la persona", + updateSuccess: "Persona actualizada correctamente", + updateFailure: "Error al actualizar la persona", + duplicateEmail: "El correo electrónico ya existe", +} + +describe("person action message localization", () => { + it("localizes known person field errors (email only, no username)", () => { + expect( + localizePersonFieldErrors( + { + email: ["Email already exists"], + }, + actionCopy, + ), + ).toEqual({ + email: [actionCopy.duplicateEmail], + }) + }) + + it("keeps unknown messages unchanged", () => { + expect( + localizePersonFieldErrors( + { firstName: ["Unexpected person issue"] }, + actionCopy, + ), + ).toEqual({ firstName: ["Unexpected person issue"] }) + }) + + it("returns undefined when errors are undefined", () => { + expect(localizePersonFieldErrors(undefined, actionCopy)).toBeUndefined() + }) + + it("returns undefined when errors are empty", () => { + expect(localizePersonFieldErrors({}, actionCopy)).toEqual({}) + }) +}) diff --git a/tests/unit/actions/recipient.actions.test.ts b/tests/unit/actions/recipient.actions.test.ts deleted file mode 100644 index a70e6a3..0000000 --- a/tests/unit/actions/recipient.actions.test.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from "vitest" - -import { es } from "@/i18n/dictionaries/es" - -const mocks = vi.hoisted(() => ({ - revalidatePath: vi.fn(), - getI18n: vi.fn(), - createRecipientUseCase: vi.fn(), - updateRecipientUseCase: vi.fn(), -})) - -vi.mock("next/cache", () => ({ - revalidatePath: mocks.revalidatePath, -})) - -vi.mock("@/i18n/server", () => ({ - getI18n: mocks.getI18n, -})) - -vi.mock("@/use-cases/recipient.use-cases", () => ({ - createRecipientUseCase: mocks.createRecipientUseCase, - updateRecipientUseCase: mocks.updateRecipientUseCase, -})) - -import { - createNewRecipient, - updateRecipient, -} from "@/actions/recipient.actions" - -describe("recipient actions localization", () => { - beforeEach(() => { - vi.clearAllMocks() - mocks.getI18n.mockResolvedValue({ dictionary: es, locale: "es" }) - }) - - it("returns localized schema validation errors for invalid create input", async () => { - const result = await createNewRecipient({ - username: "", - firstName: "", - lastName: "", - department: "", - email: "not-an-email", - } as unknown as Parameters[0]) - - expect(mocks.getI18n).toHaveBeenCalledOnce() - expect(mocks.createRecipientUseCase).not.toHaveBeenCalled() - expect(result).toEqual({ - success: false, - errors: { - username: [es.inventory.recipients.schema.usernameRequired], - firstName: [es.inventory.recipients.schema.firstNameRequired], - lastName: [es.inventory.recipients.schema.lastNameRequired], - department: [es.inventory.recipients.schema.departmentRequired], - }, - }) - }) - - it("localizes mapped duplicate field errors for create failures", async () => { - mocks.createRecipientUseCase.mockResolvedValue({ - success: false, - errors: { - username: ["Username already exists"], - email: ["Email already exists"], - }, - }) - - const result = await createNewRecipient({ - username: "ada", - firstName: "Ada", - lastName: "Lovelace", - department: "ENGINEERING", - email: "ada@example.test", - }) - - expect(result).toEqual({ - success: false, - errors: { - username: [es.inventory.recipients.actions.duplicateUsername], - email: [es.inventory.recipients.actions.duplicateEmail], - }, - message: es.inventory.recipients.actions.createFailure, - }) - }) - - it("returns a localized update success message and revalidates recipients", async () => { - mocks.updateRecipientUseCase.mockResolvedValue({ success: true }) - - const result = await updateRecipient({ - id: "recipient-1", - username: "ada", - firstName: "Ada", - lastName: "Lovelace", - department: "ENGINEERING", - email: "ada@example.test", - }) - - expect(result).toEqual({ - success: true, - message: es.inventory.recipients.actions.updateSuccess, - }) - expect(mocks.revalidatePath).toHaveBeenCalledWith("/recipients") - }) -}) diff --git a/tests/unit/actions/recipient.messages.test.ts b/tests/unit/actions/recipient.messages.test.ts deleted file mode 100644 index 7d5eef6..0000000 --- a/tests/unit/actions/recipient.messages.test.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { describe, expect, it } from "vitest" - -import { localizeRecipientFieldErrors } from "@/actions/recipient.messages" - -const actionCopy = { - createSuccess: "Destinatario creado correctamente", - createFailure: "Error al crear el destinatario", - updateSuccess: "Destinatario actualizado correctamente", - updateFailure: "Error al actualizar el destinatario", - duplicateUsername: "El nombre de usuario ya existe", - duplicateEmail: "El correo electrónico ya existe", -} - -describe("recipient action message localization", () => { - it("localizes known recipient field errors", () => { - expect( - localizeRecipientFieldErrors( - { - username: ["Username already exists"], - email: ["Email already exists"], - }, - actionCopy, - ), - ).toEqual({ - username: [actionCopy.duplicateUsername], - email: [actionCopy.duplicateEmail], - }) - }) - - it("keeps unknown messages unchanged", () => { - expect( - localizeRecipientFieldErrors( - { username: ["Unexpected recipient issue"] }, - actionCopy, - ), - ).toEqual({ username: ["Unexpected recipient issue"] }) - }) -}) diff --git a/tests/unit/app/recipients/recipient-form-pages.test.ts b/tests/unit/app/recipients/recipient-form-pages.test.ts deleted file mode 100644 index f4dab57..0000000 --- a/tests/unit/app/recipients/recipient-form-pages.test.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { renderToStaticMarkup } from "react-dom/server" -import { beforeEach, describe, expect, it, vi } from "vitest" - -import { en } from "@/i18n/dictionaries/en" -import { es } from "@/i18n/dictionaries/es" - -const mocks = vi.hoisted(() => ({ - getI18n: vi.fn(), - findById: vi.fn(), - createNewRecipient: vi.fn(), - updateRecipient: vi.fn(), - push: vi.fn(), - toastError: vi.fn(), - toastSuccess: vi.fn(), -})) - -vi.mock("@/i18n/server", () => ({ - getI18n: mocks.getI18n, -})) - -vi.mock("@/services/recipient.service", () => ({ - RecipientService: { - findById: mocks.findById, - }, -})) - -vi.mock("@/actions/recipient.actions", () => ({ - createNewRecipient: mocks.createNewRecipient, - updateRecipient: mocks.updateRecipient, -})) - -vi.mock("next/navigation", () => ({ - useRouter: () => ({ - push: mocks.push, - }), -})) - -vi.mock("sonner", () => ({ - toast: { - error: mocks.toastError, - success: mocks.toastSuccess, - }, -})) - -describe("recipient form pages localization", () => { - beforeEach(() => { - vi.clearAllMocks() - mocks.getI18n.mockResolvedValue({ dictionary: es, locale: "es" }) - }) - - it("renders the new recipient page with localized form copy and canonical department option values", async () => { - const { default: NewRecipientPage } = await import( - "@/app/(dashboard)/recipients/new/page" - ) - - const html = renderToStaticMarkup(await NewRecipientPage()) - - expect(html).toContain("Agregar destinatario") - expect(html).toContain("Usuario") - expect(html).toContain('placeholder="Usuario"') - expect(html).toContain("Nombre") - expect(html).toContain("Apellido") - expect(html).toContain("Selecciona un departamento") - expect(html).toContain('option value="ENGINEERING"') - expect(html).toContain(">Ingeniería") - expect(html).toContain("Correo electrónico") - expect(html).toContain("Teléfono") - expect(html).toContain("Crear destinatario") - }) - - it("renders the edit recipient page with localized heading and submit text", async () => { - const { default: RecipientEditPage } = await import( - "@/app/(dashboard)/recipients/[recipientId]/edit/page" - ) - - mocks.findById.mockResolvedValue({ - id: "recipient-1", - username: "ada", - firstName: "Ada", - lastName: "Lovelace", - email: "ada@example.test", - phone: "1234", - department: "ENGINEERING", - }) - - const html = renderToStaticMarkup( - await RecipientEditPage({ - params: Promise.resolve({ recipientId: "recipient-1" }), - }), - ) - - expect(html).toContain("Editar destinatario") - expect(html).toContain("Actualizar destinatario") - expect(html).toContain('placeholder="Correo electrónico"') - expect(html).toContain(">Ingeniería") - }) - - it("renders a localized edit-page not-found message", async () => { - const { default: RecipientEditPage } = await import( - "@/app/(dashboard)/recipients/[recipientId]/edit/page" - ) - - mocks.findById.mockResolvedValue(null) - - const html = renderToStaticMarkup( - await RecipientEditPage({ - params: Promise.resolve({ recipientId: "missing-recipient" }), - }), - ) - - expect(html).toContain("Destinatario no encontrado") - }) - - it("wires English recipient form submit copy through the new page", async () => { - const { default: NewRecipientPage } = await import( - "@/app/(dashboard)/recipients/new/page" - ) - - mocks.getI18n.mockResolvedValueOnce({ dictionary: en, locale: "en" }) - - const html = renderToStaticMarkup(await NewRecipientPage()) - - expect(html).toContain("Create Recipient") - }) -}) diff --git a/tests/unit/app/recipients/recipient-form-wiring.test.ts b/tests/unit/app/recipients/recipient-form-wiring.test.ts deleted file mode 100644 index d6f07ea..0000000 --- a/tests/unit/app/recipients/recipient-form-wiring.test.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { createElement } from "react" -import { renderToStaticMarkup } from "react-dom/server" -import { beforeEach, describe, expect, it, vi } from "vitest" - -import { en } from "@/i18n/dictionaries/en" -import { es } from "@/i18n/dictionaries/es" - -const mocks = vi.hoisted(() => ({ - getI18n: vi.fn(), - findById: vi.fn(), - recipientForm: vi.fn(), -})) - -vi.mock("@/i18n/server", () => ({ - getI18n: mocks.getI18n, -})) - -vi.mock("@/services/recipient.service", () => ({ - RecipientService: { - findById: mocks.findById, - }, -})) - -vi.mock("@/app/(dashboard)/recipients/_components/recipient.form", () => ({ - default: (props: unknown) => { - mocks.recipientForm(props) - return createElement("div", null, "Recipient form") - }, -})) - -describe("recipient form schema wiring", () => { - beforeEach(() => { - vi.clearAllMocks() - }) - - it("passes server-resolved schema copy into the new recipient form boundary", async () => { - mocks.getI18n.mockResolvedValue({ dictionary: es, locale: "es" }) - - const { default: NewRecipientPage } = await import( - "@/app/(dashboard)/recipients/new/page" - ) - - renderToStaticMarkup(await NewRecipientPage()) - - expect(mocks.recipientForm).toHaveBeenCalledWith( - expect.objectContaining({ - mode: "create", - schemaCopy: es.inventory.recipients.schema, - }), - ) - }) - - it("passes server-resolved schema copy into the edit recipient form boundary", async () => { - mocks.getI18n.mockResolvedValue({ dictionary: en, locale: "en" }) - mocks.findById.mockResolvedValue({ - id: "recipient-1", - username: "ada", - firstName: "Ada", - lastName: "Lovelace", - department: "ENGINEERING", - email: "ada@example.test", - phone: "1234", - }) - - const { default: RecipientEditPage } = await import( - "@/app/(dashboard)/recipients/[recipientId]/edit/page" - ) - - renderToStaticMarkup( - await RecipientEditPage({ - params: Promise.resolve({ recipientId: "recipient-1" }), - }), - ) - - expect(mocks.recipientForm).toHaveBeenCalledWith( - expect.objectContaining({ - mode: "edit", - schemaCopy: en.inventory.recipients.schema, - }), - ) - }) -}) diff --git a/tests/unit/app/recipients/recipient-pages.test.ts b/tests/unit/app/recipients/recipient-pages.test.ts deleted file mode 100644 index 918969c..0000000 --- a/tests/unit/app/recipients/recipient-pages.test.ts +++ /dev/null @@ -1,165 +0,0 @@ -import { createElement } from "react" -import { renderToStaticMarkup } from "react-dom/server" -import { beforeEach, describe, expect, it, vi } from "vitest" - -import { es } from "@/i18n/dictionaries/es" - -const mocks = vi.hoisted(() => ({ - findAllPaginated: vi.fn(), - findById: vi.fn(), - findAllByRecipient: vi.fn(), - getI18n: vi.fn(), -})) - -vi.mock("@/i18n/server", () => ({ - getI18n: mocks.getI18n, -})) - -vi.mock("@/services/recipient.service", () => ({ - RecipientService: { - findAllPaginated: mocks.findAllPaginated, - findById: mocks.findById, - }, -})) - -vi.mock("@/services/assignment.service", () => ({ - AssignmentService: { - findAllByRecipient: mocks.findAllByRecipient, - }, -})) - -vi.mock("@/components/common/pageheader", () => ({ - default: ({ title, addLabel }: { title: string; addLabel?: string }) => - createElement( - "header", - null, - [title, addLabel].filter(Boolean).join(" | "), - ), -})) - -vi.mock("@/components/common/pagination", () => ({ - default: ({ totalPages }: { totalPages: number }) => - createElement("nav", { "aria-label": "Pagination" }, totalPages), -})) - -describe("recipient pages localization", () => { - beforeEach(() => { - vi.clearAllMocks() - mocks.getI18n.mockResolvedValue({ dictionary: es, locale: "es" }) - }) - - it("renders the recipient list in Spanish while keeping stored department values display-only", async () => { - const { default: RecipientsPage } = await import( - "@/app/(dashboard)/recipients/page" - ) - - mocks.findAllPaginated.mockResolvedValue({ - data: [ - { - id: "recipient-1", - username: "ada", - firstName: "Ada", - lastName: "Lovelace", - email: "ada@example.test", - phone: "1234", - department: "ENGINEERING", - }, - ], - totalPages: 1, - }) - - const html = renderToStaticMarkup( - await RecipientsPage({ searchParams: Promise.resolve({}) }), - ) - - expect(html).toContain("Destinatarios") - expect(html).toContain("Agregar destinatario") - expect(html).toContain("Usuario") - expect(html).toContain("Nombre") - expect(html).toContain("Correo electrónico") - expect(html).toContain("Teléfono") - expect(html).toContain("Departamento") - expect(html).toContain("Acciones") - expect(html).toContain("Ada Lovelace") - expect(html).toContain("Ingeniería") - expect(html).toContain('aria-label="Ver destinatario"') - expect(html).toContain('aria-label="Editar destinatario"') - expect(html).not.toContain(">ENGINEERING<") - }) - - it("renders the localized recipient empty state when no recipients exist", async () => { - const { default: RecipientsPage } = await import( - "@/app/(dashboard)/recipients/page" - ) - - mocks.findAllPaginated.mockResolvedValue({ - data: [], - totalPages: 0, - }) - - const html = renderToStaticMarkup( - await RecipientsPage({ searchParams: Promise.resolve({}) }), - ) - - expect(html).toContain("No se encontraron destinatarios.") - }) - - it("renders localized recipient-owned detail labels and localizes the embedded assignments title only", async () => { - const { default: RecipientInfoPage } = await import( - "@/app/(dashboard)/recipients/[recipientId]/page" - ) - - mocks.findById.mockResolvedValue({ - id: "recipient-1", - username: "ada", - firstName: "Ada", - lastName: "Lovelace", - email: "ada@example.test", - phone: "1234", - department: "DRIVER", - }) - mocks.findAllByRecipient.mockResolvedValue([ - { - id: "assignment-1", - item: { name: "Laptop" }, - asset: { serialNumber: "SN-001" }, - quantity: 1, - }, - ]) - - const html = renderToStaticMarkup( - await RecipientInfoPage({ - params: Promise.resolve({ recipientId: "recipient-1" }), - }), - ) - - expect(html).toContain("Usuario") - expect(html).toContain("Correo electrónico") - expect(html).toContain("Teléfono") - expect(html).toContain("Departamento") - expect(html).toContain("Chofer") - expect(html).toContain("ada") - expect(html).toContain("ada@example.test") - expect(html).toContain("Asignaciones") - expect(html).toContain("Laptop") - expect(html).not.toContain(">DRIVER<") - expect(html).not.toContain("Assignments") - }) - - it("renders a localized recipient detail not-found message", async () => { - const { default: RecipientInfoPage } = await import( - "@/app/(dashboard)/recipients/[recipientId]/page" - ) - - mocks.findById.mockResolvedValue(null) - mocks.findAllByRecipient.mockResolvedValue([]) - - const html = renderToStaticMarkup( - await RecipientInfoPage({ - params: Promise.resolve({ recipientId: "missing-recipient" }), - }), - ) - - expect(html).toContain("Destinatario no encontrado") - }) -}) diff --git a/tests/unit/i18n/dictionaries.test.ts b/tests/unit/i18n/dictionaries.test.ts index 97d49a0..05ae84b 100644 --- a/tests/unit/i18n/dictionaries.test.ts +++ b/tests/unit/i18n/dictionaries.test.ts @@ -52,7 +52,6 @@ describe("i18n dictionaries", () => { items: "Items", categories: "Categories", assets: "Assets", - recipients: "Recipients", people: "People", movements: "Movements", assignments: "Assignments", @@ -67,7 +66,6 @@ describe("i18n dictionaries", () => { category: "Category", item: "Item", asset: "Asset", - recipient: "Recipient", person: "Person", assignment: "Assignment", }, @@ -89,7 +87,6 @@ describe("i18n dictionaries", () => { items: "Artículos", categories: "Categorías", assets: "Activos", - recipients: "Destinatarios", people: "Personas", movements: "Movimientos", assignments: "Asignaciones", @@ -104,7 +101,6 @@ describe("i18n dictionaries", () => { category: "Categoría", item: "Artículo", asset: "Activo", - recipient: "Destinatario", person: "Persona", assignment: "Asignación", }, @@ -694,14 +690,13 @@ describe("i18n dictionaries", () => { }) }) - it("provides localized recipient UI copy for English and Spanish", () => { - expect(getDictionary("en").inventory.recipients).toEqual({ + it("provides localized people UI copy for English and Spanish", () => { + expect(getDictionary("en").inventory.people).toEqual({ list: { - title: "Recipients", - addLabel: "Add Recipient", - empty: "No recipients found.", + title: "People", + addLabel: "Add Person", + empty: "No people found.", columns: { - username: "Username", name: "Name", email: "Email", phone: "Phone", @@ -709,29 +704,26 @@ describe("i18n dictionaries", () => { actions: "Actions", }, actions: { - view: "View recipient", - edit: "Edit recipient", + view: "View person", + edit: "Edit person", }, }, detail: { - notFound: "Recipient not found", + notFound: "Person not found", labels: { - username: "Username", email: "Email", phone: "Phone", department: "Department", }, }, new: { - title: "Add Recipient", + title: "Add Person", }, edit: { - title: "Edit Recipient", - notFound: "Recipient not found", + title: "Edit Person", + notFound: "Person not found", }, form: { - usernameLabel: "Username", - usernamePlaceholder: "Username", firstNameLabel: "First Name", firstNamePlaceholder: "First name", lastNameLabel: "Last Name", @@ -742,8 +734,8 @@ describe("i18n dictionaries", () => { emailPlaceholder: "Email", phoneLabel: "Phone", phonePlaceholder: "Phone", - createSubmit: "Create Recipient", - updateSubmit: "Update Recipient", + createSubmit: "Create Person", + updateSubmit: "Update Person", }, fallback: { unknownDepartment: "Unknown department", @@ -759,30 +751,28 @@ describe("i18n dictionaries", () => { OTHER: "Other", }, actions: { - createSuccess: "Recipient created successfully", - createFailure: "Failed to create recipient", - updateSuccess: "Recipient updated successfully", - updateFailure: "Failed to update recipient", - duplicateUsername: "Username already exists", + createSuccess: "Person created successfully", + createFailure: "Failed to create person", + updateSuccess: "Person updated successfully", + updateFailure: "Failed to update person", duplicateEmail: "Email already exists", }, schema: { - usernameRequired: "Username is required", firstNameRequired: "First name is required", lastNameRequired: "Last name is required", departmentRequired: "Department is required", emailInvalid: "Email format is invalid", idRequired: "ID is required", + userIdInvalid: "User ID must be a valid UUID", }, }) - expect(getDictionary("es").inventory.recipients).toEqual({ + expect(getDictionary("es").inventory.people).toEqual({ list: { - title: "Destinatarios", - addLabel: "Agregar destinatario", - empty: "No se encontraron destinatarios.", + title: "Personas", + addLabel: "Agregar persona", + empty: "No se encontraron personas.", columns: { - username: "Usuario", name: "Nombre", email: "Correo electrónico", phone: "Teléfono", @@ -790,29 +780,26 @@ describe("i18n dictionaries", () => { actions: "Acciones", }, actions: { - view: "Ver destinatario", - edit: "Editar destinatario", + view: "Ver persona", + edit: "Editar persona", }, }, detail: { - notFound: "Destinatario no encontrado", + notFound: "Persona no encontrada", labels: { - username: "Usuario", email: "Correo electrónico", phone: "Teléfono", department: "Departamento", }, }, new: { - title: "Agregar destinatario", + title: "Agregar persona", }, edit: { - title: "Editar destinatario", - notFound: "Destinatario no encontrado", + title: "Editar persona", + notFound: "Persona no encontrada", }, form: { - usernameLabel: "Usuario", - usernamePlaceholder: "Usuario", firstNameLabel: "Nombre", firstNamePlaceholder: "Nombre", lastNameLabel: "Apellido", @@ -823,8 +810,8 @@ describe("i18n dictionaries", () => { emailPlaceholder: "Correo electrónico", phoneLabel: "Teléfono", phonePlaceholder: "Teléfono", - createSubmit: "Crear destinatario", - updateSubmit: "Actualizar destinatario", + createSubmit: "Crear persona", + updateSubmit: "Actualizar persona", }, fallback: { unknownDepartment: "Departamento desconocido", @@ -840,24 +827,34 @@ describe("i18n dictionaries", () => { OTHER: "Otro", }, actions: { - createSuccess: "Destinatario creado correctamente", - createFailure: "Error al crear el destinatario", - updateSuccess: "Destinatario actualizado correctamente", - updateFailure: "Error al actualizar el destinatario", - duplicateUsername: "El nombre de usuario ya existe", + createSuccess: "Persona creada correctamente", + createFailure: "Error al crear la persona", + updateSuccess: "Persona actualizada correctamente", + updateFailure: "Error al actualizar la persona", duplicateEmail: "El correo electrónico ya existe", }, schema: { - usernameRequired: "El usuario es obligatorio", firstNameRequired: "El nombre es obligatorio", lastNameRequired: "El apellido es obligatorio", departmentRequired: "El departamento es obligatorio", emailInvalid: "El correo electrónico no es válido", idRequired: "El ID es obligatorio", + userIdInvalid: "El ID de usuario debe ser un UUID válido", }, }) }) + it("does not have inventory.recipients or legacy recipient keys in either locale", () => { + expect("recipients" in getDictionary("en").inventory).toBe(false) + expect("recipients" in getDictionary("es").inventory).toBe(false) + expect("recipients" in getDictionary("en").layout.sidebar).toBe(false) + expect("recipient" in getDictionary("en").layout.addMenu).toBe(false) + expect("recipients" in getDictionary("es").layout.sidebar).toBe(false) + expect("recipient" in getDictionary("es").layout.addMenu).toBe(false) + expect("recipients" in getDictionary("en").dashboardHome.cards).toBe(false) + expect("recipients" in getDictionary("es").dashboardHome.cards).toBe(false) + }) + it("provides localized movement UI copy for English and Spanish", () => { expect(getDictionary("en").inventory.movements).toEqual({ list: { @@ -940,10 +937,6 @@ describe("i18n dictionaries", () => { title: "Total Assets", countLabel: "Total", }, - recipients: { - title: "Total Recipients", - countLabel: "Total", - }, people: { title: "Total People", countLabel: "Total", @@ -962,10 +955,6 @@ describe("i18n dictionaries", () => { title: "Total de activos", countLabel: "Total", }, - recipients: { - title: "Total de destinatarios", - countLabel: "Total", - }, people: { title: "Total de personas", countLabel: "Total", diff --git a/tests/unit/schemas/core-schemas.test.ts b/tests/unit/schemas/core-schemas.test.ts index 59e31d4..c0d8d9f 100644 --- a/tests/unit/schemas/core-schemas.test.ts +++ b/tests/unit/schemas/core-schemas.test.ts @@ -5,7 +5,7 @@ import { signInSchema } from "@/schemas/auth.schema" import { createCategorySchema } from "@/schemas/category.schema" import { createItemSchema, updateItemSchema } from "@/schemas/item.schema" import { createMovementSchema } from "@/schemas/movement.schema" -import { createRecipientSchema } from "@/schemas/recipient.schema" +import { createPersonSchema } from "@/schemas/person.schema" import { createUserSchema, updateUserSchema } from "@/schemas/user.schema" describe("core schemas", () => { @@ -120,32 +120,29 @@ describe("core schemas", () => { ).toBe(false) }) - it("validates recipient email only when present", () => { + it("validates person email only when present", () => { expect( - createRecipientSchema.safeParse({ - username: "recipient", - firstName: "Rec", - lastName: "Ipient", + createPersonSchema.safeParse({ + firstName: "Per", + lastName: "Son", department: "IT", - email: "recipient@example.test", + email: "person@example.test", }).success, ).toBe(true) expect( - createRecipientSchema.safeParse({ - username: "recipient", - firstName: "Rec", - lastName: "Ipient", + createPersonSchema.safeParse({ + firstName: "Per", + lastName: "Son", department: "IT", email: "not-an-email", }).success, ).toBe(false) expect( - createRecipientSchema.safeParse({ - username: "recipient", - firstName: "Rec", - lastName: "Ipient", + createPersonSchema.safeParse({ + firstName: "Per", + lastName: "Son", department: "IT", email: "", }).success, diff --git a/tests/unit/schemas/recipient.schema.test.ts b/tests/unit/schemas/person.schema.test.ts similarity index 54% rename from tests/unit/schemas/recipient.schema.test.ts rename to tests/unit/schemas/person.schema.test.ts index 21b68bc..ad16c5f 100644 --- a/tests/unit/schemas/recipient.schema.test.ts +++ b/tests/unit/schemas/person.schema.test.ts @@ -1,23 +1,22 @@ import { describe, expect, it } from "vitest" import { - buildCreateRecipientSchema, - buildUpdateRecipientSchema, -} from "@/schemas/recipient.schema" + buildCreatePersonSchema, + buildUpdatePersonSchema, +} from "@/schemas/person.schema" const schemaCopy = { - usernameRequired: "El usuario es obligatorio", firstNameRequired: "El nombre es obligatorio", lastNameRequired: "El apellido es obligatorio", departmentRequired: "El departamento es obligatorio", emailInvalid: "El correo electrónico no es válido", idRequired: "El ID es obligatorio", + userIdInvalid: "El ID de usuario debe ser un UUID válido", } -describe("recipient schema localization", () => { - it("uses localized required-field validation messages for create", () => { - const result = buildCreateRecipientSchema(schemaCopy).safeParse({ - username: "", +describe("person schema validation", () => { + it("uses localized required-field validation messages for create (no username)", () => { + const result = buildCreatePersonSchema(schemaCopy).safeParse({ firstName: "", lastName: "", department: "", @@ -27,7 +26,6 @@ describe("recipient schema localization", () => { if (!result.success) { const errors = result.error.flatten().fieldErrors - expect(errors.username).toContain(schemaCopy.usernameRequired) expect(errors.firstName).toContain(schemaCopy.firstNameRequired) expect(errors.lastName).toContain(schemaCopy.lastNameRequired) expect(errors.department).toContain(schemaCopy.departmentRequired) @@ -35,8 +33,7 @@ describe("recipient schema localization", () => { }) it("uses a localized invalid email message for create", () => { - const result = buildCreateRecipientSchema(schemaCopy).safeParse({ - username: "ada", + const result = buildCreatePersonSchema(schemaCopy).safeParse({ firstName: "Ada", lastName: "Lovelace", department: "ENGINEERING", @@ -51,10 +48,25 @@ describe("recipient schema localization", () => { } }) + it("uses a localized invalid userId message for create with non-UUID", () => { + const result = buildCreatePersonSchema(schemaCopy).safeParse({ + firstName: "Ada", + lastName: "Lovelace", + department: "ENGINEERING", + userId: "not-a-uuid", + }) + + expect(result.success).toBe(false) + if (!result.success) { + expect(result.error.flatten().fieldErrors.userId).toContain( + schemaCopy.userIdInvalid, + ) + } + }) + it("uses localized update identifier validation messages", () => { - const result = buildUpdateRecipientSchema(schemaCopy).safeParse({ + const result = buildUpdatePersonSchema(schemaCopy).safeParse({ id: "", - username: "ada", firstName: "Ada", lastName: "Lovelace", department: "ENGINEERING", @@ -69,19 +81,33 @@ describe("recipient schema localization", () => { } }) - it("preserves canonical department values and optional empty email semantics", () => { - const result = buildCreateRecipientSchema(schemaCopy).safeParse({ - username: "ada", + it("preserves canonical department values and accepts optional userId UUID", () => { + const result = buildCreatePersonSchema(schemaCopy).safeParse({ firstName: "Ada", lastName: "Lovelace", department: "ENGINEERING", email: "", + userId: "550e8400-e29b-41d4-a716-446655440000", }) expect(result.success).toBe(true) if (result.success) { expect(result.data.department).toBe("ENGINEERING") expect(result.data.email).toBe("") + expect(result.data.userId).toBe("550e8400-e29b-41d4-a716-446655440000") + } + }) + + it("accepts create without username field (removed from schema)", () => { + const result = buildCreatePersonSchema(schemaCopy).safeParse({ + firstName: "Ada", + lastName: "Lovelace", + department: "ENGINEERING", + }) + + expect(result.success).toBe(true) + if (result.success) { + expect("username" in result.data).toBe(false) } }) })