diff --git a/tests/e2e/assignments.spec.ts b/tests/e2e/assignments.spec.ts index 9e73713..377d0de 100644 --- a/tests/e2e/assignments.spec.ts +++ b/tests/e2e/assignments.spec.ts @@ -27,7 +27,7 @@ async function createPerson(page: Page, name: string, email: string) { await page.goto("/people/new") await page.getByLabel("Nombre").fill(name) await page.getByLabel("Apellido").fill("E2E") - await page.getByLabel("Departamento").selectOption("OTHER") + await page.getByLabel("Equipo").selectOption({ label: "Other" }) await page.getByLabel("Correo electrónico").fill(email) await page.getByLabel("Teléfono").fill("123456789") await page.getByLabel("Rol").selectOption("NO_USER") diff --git a/tests/e2e/people.spec.ts b/tests/e2e/people.spec.ts new file mode 100644 index 0000000..5ceca6d --- /dev/null +++ b/tests/e2e/people.spec.ts @@ -0,0 +1,143 @@ +import { expect, type Page, test } from "@playwright/test" + +async function setLocaleCookie( + page: Page, + locale: "en" | "es", + baseURL?: string, +) { + await page.context().addCookies([ + { + name: "stock-manager-locale", + value: locale, + url: baseURL ?? "http://127.0.0.1:3100", + }, + ]) +} + +async function signInAsAdmin(page: Page, baseURL?: string) { + await setLocaleCookie(page, "en", baseURL) + await page.goto("/login") + await page.getByLabel("Email").fill("admin@example.test") + await page.getByLabel("Password").fill("admin-password") + await page.getByRole("button", { name: "Sign In" }).click() + await expect(page).toHaveURL("/") +} + +async function createTeam(page: Page, name: string) { + await page.goto("/people?tab=teams") + await page.getByLabel("Team name").fill(name) + await page.getByRole("button", { name: "Create Team" }).click() + await expect(page.getByText("Team created successfully")).toBeVisible() +} + +test.describe("people and teams", () => { + test("switches between people and teams tabs via URL", async ({ + baseURL, + page, + }) => { + await signInAsAdmin(page, baseURL) + + await page.goto("/people?tab=people") + await expect( + page.getByRole("navigation", { name: "People sections" }), + ).toBeVisible() + await expect(page.getByRole("link", { name: "People" })).toHaveAttribute( + "aria-current", + "page", + ) + + await page.goto("/people?tab=teams") + await expect(page.getByRole("link", { name: "Teams" })).toHaveAttribute( + "aria-current", + "page", + ) + await expect(page.getByLabel("Team name")).toBeVisible() + + await page.goto("/people?tab=invalid") + await expect(page.getByRole("link", { name: "People" })).toHaveAttribute( + "aria-current", + "page", + ) + }) + + test("creates, renames, and deletes a team", async ({ baseURL, page }) => { + const timestamp = Date.now() + const originalName = `E2E Team ${timestamp}` + const updatedName = `E2E Team Updated ${timestamp}` + + await signInAsAdmin(page, baseURL) + await createTeam(page, originalName) + + const row = page.getByRole("row", { name: new RegExp(originalName) }) + await expect(row).toBeVisible() + + await row.getByRole("button", { name: "Edit team" }).click() + const dialog = page.getByRole("dialog") + await expect(dialog).toBeVisible() + await dialog.getByLabel("Team name").fill(updatedName) + await dialog.getByRole("button", { name: "Update Team" }).click() + await expect(dialog).not.toBeVisible() + await expect(page.getByText("Team updated successfully")).toBeVisible() + await expect(page.getByRole("row", { name: updatedName })).toBeVisible() + + await page + .getByRole("row", { name: updatedName }) + .getByRole("button", { name: "Delete team" }) + .click() + await expect(page.getByText("Team deleted successfully")).toBeVisible() + await expect(page.getByRole("row", { name: updatedName })).not.toBeVisible() + }) + + test("creates a person with a team and shows the team in the list", async ({ + baseURL, + page, + }) => { + const timestamp = Date.now() + const teamName = `E2E Person Team ${timestamp}` + const personName = `E2E Person ${timestamp}` + + await signInAsAdmin(page, baseURL) + await createTeam(page, teamName) + + await page.goto("/people/new") + await page.getByLabel("First Name").fill(personName) + await page.getByLabel("Last Name").fill("E2E") + await page.getByLabel("Team").selectOption({ label: teamName }) + await page.getByLabel("Email").fill(`e2e-${timestamp}@example.test`) + await page.getByLabel("Phone").fill("123456789") + await page.getByLabel("Role").selectOption("NO_USER") + await page.getByRole("button", { name: "Create Person" }).click() + await expect(page.getByText("Person created successfully")).toBeVisible() + + await page.goto("/people?tab=people") + const row = page.getByRole("row", { name: new RegExp(personName) }) + await expect(row).toContainText(teamName) + + await row.getByRole("link", { name: "View person" }).click() + await expect(page.getByText(teamName)).toBeVisible() + }) + + test("shows no team fallback for a person without a team", async ({ + baseURL, + page, + }) => { + const timestamp = Date.now() + const personName = `E2E No Team ${timestamp}` + + await signInAsAdmin(page, baseURL) + + await page.goto("/people/new") + await page.getByLabel("First Name").fill(personName) + await page.getByLabel("Last Name").fill("E2E") + await page.getByLabel("Team").selectOption({ label: "" }) + await page.getByLabel("Email").fill(`e2e-noteam-${timestamp}@example.test`) + await page.getByLabel("Phone").fill("123456789") + await page.getByLabel("Role").selectOption("NO_USER") + await page.getByRole("button", { name: "Create Person" }).click() + await expect(page.getByText("Person created successfully")).toBeVisible() + + await page.goto("/people?tab=people") + const row = page.getByRole("row", { name: new RegExp(personName) }) + await expect(row).toContainText("—") + }) +}) diff --git a/tests/integration/helpers/factories.ts b/tests/integration/helpers/factories.ts index e05ca0e..5cb796b 100644 --- a/tests/integration/helpers/factories.ts +++ b/tests/integration/helpers/factories.ts @@ -1,8 +1,4 @@ -import type { - PersonDepartment, - PrismaClient, - UserRole, -} from "@/generated/prisma/client" +import type { PrismaClient, UserRole } from "@/generated/prisma/client" import { UserStatus } from "@/generated/prisma/client" import { normalizeEmail } from "@/lib/email" @@ -54,12 +50,25 @@ export async function createTestCategory( }) } +export async function createTestTeam( + prisma: PrismaClient, + overrides: Partial<{ name: string }> = {}, +) { + const suffix = nextSuffix() + + return prisma.team.create({ + data: { + name: overrides.name ?? `Test Team ${suffix}`, + }, + }) +} + export async function createTestPerson( prisma: PrismaClient, overrides: Partial<{ firstName: string lastName: string - department: PersonDepartment + teamId: string | null email: string | null phone: string | null }> = {}, @@ -70,26 +79,13 @@ export async function createTestPerson( data: { firstName: overrides.firstName ?? "Test", lastName: overrides.lastName ?? `Person-${suffix}`, - department: overrides.department ?? "OTHER", + teamId: overrides.teamId ?? null, email: overrides.email ?? null, phone: overrides.phone ?? null, }, }) } -export async function createTestTeam( - prisma: PrismaClient, - overrides: Partial<{ name: string }> = {}, -) { - const suffix = nextSuffix() - - return prisma.team.create({ - data: { - name: overrides.name ?? `Test Team ${suffix}`, - }, - }) -} - export async function createTestItem( prisma: PrismaClient, overrides: Partial<{ diff --git a/tests/integration/use-cases/person-user.use-cases.test.ts b/tests/integration/use-cases/person-user.use-cases.test.ts index a8bb61a..a812026 100644 --- a/tests/integration/use-cases/person-user.use-cases.test.ts +++ b/tests/integration/use-cases/person-user.use-cases.test.ts @@ -1,7 +1,11 @@ import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest" import type { PrismaClient } from "@/generated/prisma/client" import { normalizeEmail } from "@/lib/email" -import { createTestPerson, createTestUser } from "../helpers/factories" +import { + createTestPerson, + createTestTeam, + createTestUser, +} from "../helpers/factories" import { resetIntegrationTestDatabase, startIntegrationTestDatabase, @@ -33,10 +37,12 @@ afterAll(async () => { describe("createPersonUserUseCase", () => { describe("NO_USER role (person-only creation)", () => { it("creates a Person without a User record when role is NO_USER", async () => { + const team = await createTestTeam(prisma) + const result = await createPersonUserUseCase({ firstName: "John", lastName: "Doe", - department: "IT", + teamId: team.id, email: "john@example.test", phone: null, role: "NO_USER", @@ -51,7 +57,7 @@ describe("createPersonUserUseCase", () => { expect(person).toMatchObject({ firstName: "John", lastName: "Doe", - department: "IT", + teamId: team.id, email: "john@example.test", phone: null, userId: null, @@ -66,10 +72,12 @@ describe("createPersonUserUseCase", () => { }) it("creates a Person with null email when not providing email and role is NO_USER", async () => { + const team = await createTestTeam(prisma) + const result = await createPersonUserUseCase({ firstName: "Jane", lastName: "Smith", - department: "ENGINEERING", + teamId: team.id, email: "jane-noemail@example.test", phone: "555-1234", role: "NO_USER", @@ -87,10 +95,12 @@ describe("createPersonUserUseCase", () => { describe("real role (person + user creation)", () => { it("creates Person and User with linked userId when role is ADMIN", async () => { + const team = await createTestTeam(prisma) + const result = await createPersonUserUseCase({ firstName: "Admin", lastName: "User", - department: "IT", + teamId: team.id, email: "admin@example.test", phone: null, role: "ADMIN", @@ -101,12 +111,12 @@ describe("createPersonUserUseCase", () => { expect(result).toEqual({ success: true }) const person = await prisma.person.findFirstOrThrow({ - where: { firstName: "Admin", lastName: "User" }, + where: { firstName: "Admin" }, }) expect(person).toMatchObject({ firstName: "Admin", lastName: "User", - department: "IT", + teamId: team.id, email: "admin@example.test", }) @@ -128,6 +138,7 @@ describe("createPersonUserUseCase", () => { }) it("creates Person and User for all real roles (MANAGER, STAFF, VIEWER)", async () => { + const team = await createTestTeam(prisma) const roles = ["MANAGER", "STAFF", "VIEWER"] as const for (const role of roles) { @@ -135,7 +146,7 @@ describe("createPersonUserUseCase", () => { const result = await createPersonUserUseCase({ firstName: "Person", lastName: suffix, - department: "IT", + teamId: team.id, email: `${suffix}@example.test`, phone: null, role, @@ -160,10 +171,12 @@ describe("createPersonUserUseCase", () => { }) it("derives User.name from firstName + lastName", async () => { + const team = await createTestTeam(prisma) + await createPersonUserUseCase({ firstName: "Maria", lastName: "Garcia", - department: "SALES", + teamId: team.id, email: "maria@example.test", phone: null, role: "STAFF", @@ -178,10 +191,12 @@ describe("createPersonUserUseCase", () => { }) it("hashes the password when creating a User", async () => { + const team = await createTestTeam(prisma) + await createPersonUserUseCase({ firstName: "Hash", lastName: "Test", - department: "IT", + teamId: team.id, email: "hash-test@example.test", phone: null, role: "STAFF", @@ -196,20 +211,21 @@ describe("createPersonUserUseCase", () => { if (!user.passwordHash) throw new Error("Expected password hash") const { verifyPassword } = await import("@/lib/security") - expect(await verifyPassword("plaintext-password", user.passwordHash)).toBe( - true, - ) + expect( + await verifyPassword("plaintext-password", user.passwordHash), + ).toBe(true) }) }) describe("cross-table email uniqueness", () => { it("rejects submission when email already exists in Person table", async () => { + const team = await createTestTeam(prisma) await createTestPerson(prisma, { email: "existing-person@example.test" }) const result = await createPersonUserUseCase({ firstName: "Duplicate", lastName: "Person", - department: "IT", + teamId: team.id, email: "existing-person@example.test", phone: null, role: "NO_USER", @@ -225,12 +241,13 @@ describe("createPersonUserUseCase", () => { }) it("rejects submission when email already exists in User table", async () => { + const team = await createTestTeam(prisma) await createTestUser(prisma, { email: "existing-user@example.test" }) const result = await createPersonUserUseCase({ firstName: "Duplicate", lastName: "User", - department: "IT", + teamId: team.id, email: "existing-user@example.test", phone: null, role: "STAFF", @@ -249,6 +266,7 @@ describe("createPersonUserUseCase", () => { }) it("accepts submission when email is unique across both tables", async () => { + const team = await createTestTeam(prisma) // Create a Person and a User with different emails await createTestPerson(prisma, { email: "person@example.test" }) await createTestUser(prisma, { email: "user@example.test" }) @@ -256,7 +274,7 @@ describe("createPersonUserUseCase", () => { const result = await createPersonUserUseCase({ firstName: "New", lastName: "Person", - department: "IT", + teamId: team.id, email: "new@example.test", phone: null, role: "NO_USER", @@ -266,4 +284,24 @@ describe("createPersonUserUseCase", () => { expect(result).toEqual({ success: true }) }) }) + + describe("team validation", () => { + it("rejects an unknown team id", async () => { + const result = await createPersonUserUseCase({ + firstName: "No", + lastName: "Team", + teamId: "00000000-0000-0000-0000-000000000000", + email: "no-team@example.test", + phone: null, + role: "NO_USER", + isActive: true, + }) + + expect(result.success).toBe(false) + if (!result.success) { + expect(result.errors.teamId).toBeDefined() + } + expect(await prisma.person.count()).toBe(0) + }) + }) }) diff --git a/tests/integration/use-cases/person.use-cases.test.ts b/tests/integration/use-cases/person.use-cases.test.ts index 4b6fedc..46782ba 100644 --- a/tests/integration/use-cases/person.use-cases.test.ts +++ b/tests/integration/use-cases/person.use-cases.test.ts @@ -1,6 +1,10 @@ import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest" import type { PrismaClient } from "@/generated/prisma/client" -import { createTestPerson, createTestUser } from "../helpers/factories" +import { + createTestPerson, + createTestTeam, + createTestUser, +} from "../helpers/factories" import { resetIntegrationTestDatabase, startIntegrationTestDatabase, @@ -40,7 +44,7 @@ describe("person use-cases", () => { createPersonUseCase({ firstName: "Person", lastName: "One", - department: "IT", + teamId: null, email: "", phone: "", }), @@ -53,7 +57,7 @@ describe("person use-cases", () => { ).toMatchObject({ firstName: "Person", lastName: "One", - department: "IT", + teamId: null, email: null, phone: null, userId: null, @@ -62,12 +66,13 @@ describe("person use-cases", () => { it("creates a person with linked userId", async () => { const user = await createTestUser(prisma) + const team = await createTestTeam(prisma) await expect( createPersonUseCase({ firstName: "Linked", lastName: "Person", - department: "ENGINEERING", + teamId: team.id, email: "linked@example.test", phone: null, userId: user.id, @@ -81,7 +86,7 @@ describe("person use-cases", () => { ).toMatchObject({ firstName: "Linked", lastName: "Person", - department: "ENGINEERING", + teamId: team.id, email: "linked@example.test", userId: user.id, }) @@ -96,7 +101,7 @@ describe("person use-cases", () => { createPersonUseCase({ firstName: "Duplicate", lastName: "Email", - department: "OTHER", + teamId: null, email: "existing@example.test", phone: null, }), @@ -108,7 +113,24 @@ describe("person use-cases", () => { expect(await prisma.person.count()).toBe(1) }) + it("rejects an unknown team id on create", async () => { + const result = await createPersonUseCase({ + firstName: "Unknown", + lastName: "Team", + teamId: "00000000-0000-0000-0000-000000000000", + email: "unknown-team@example.test", + phone: null, + }) + + expect(result.success).toBe(false) + if (!result.success) { + expect(result.errors.teamId).toBeDefined() + } + expect(await prisma.person.count()).toBe(0) + }) + it("updates a person and rejects duplicate emails", async () => { + const team = await createTestTeam(prisma) const person = await createTestPerson(prisma, { email: "person@example.test", phone: "111111111", @@ -122,16 +144,18 @@ describe("person use-cases", () => { id: person.id, firstName: "Edited", lastName: "Person", - department: "ENGINEERING", + teamId: team.id, email: "edited@example.test", phone: "222222222", }), ).resolves.toEqual({ success: true }) - expect(await prisma.person.findUniqueOrThrow({ where: { id: person.id } })).toMatchObject({ + expect( + await prisma.person.findUniqueOrThrow({ where: { id: person.id } }), + ).toMatchObject({ firstName: "Edited", lastName: "Person", - department: "ENGINEERING", + teamId: team.id, email: "edited@example.test", phone: "222222222", }) @@ -141,7 +165,7 @@ describe("person use-cases", () => { id: person.id, firstName: "Edited", lastName: "Person", - department: "ENGINEERING", + teamId: team.id, email: other.email, phone: "222222222", }), @@ -150,12 +174,37 @@ describe("person use-cases", () => { errors: { email: ["Email already exists"] }, }) - expect(await prisma.person.findUniqueOrThrow({ where: { id: person.id } })).toMatchObject({ + expect( + await prisma.person.findUniqueOrThrow({ where: { id: person.id } }), + ).toMatchObject({ email: "edited@example.test", }) expect(await prisma.person.count()).toBe(2) }) + it("updates a person team to null", async () => { + const team = await createTestTeam(prisma) + const person = await createTestPerson(prisma, { + teamId: team.id, + }) + + await expect( + updatePersonUseCase({ + id: person.id, + firstName: person.firstName, + lastName: person.lastName, + teamId: null, + email: person.email, + phone: person.phone, + }), + ).resolves.toEqual({ success: true }) + + const updated = await prisma.person.findUniqueOrThrow({ + where: { id: person.id }, + }) + expect(updated.teamId).toBeNull() + }) + it("searches by email and name in paginated results", async () => { await createTestPerson(prisma, { firstName: "Alice", diff --git a/tests/integration/use-cases/update-person-user.use-cases.test.ts b/tests/integration/use-cases/update-person-user.use-cases.test.ts index 335d7ce..4c63690 100644 --- a/tests/integration/use-cases/update-person-user.use-cases.test.ts +++ b/tests/integration/use-cases/update-person-user.use-cases.test.ts @@ -2,7 +2,11 @@ import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest" import { type PrismaClient, UserStatus } from "@/generated/prisma/client" import { normalizeEmail } from "@/lib/email" import { getPasswordHash } from "@/lib/security" -import { createTestPerson, createTestUser } from "../helpers/factories" +import { + createTestPerson, + createTestTeam, + createTestUser, +} from "../helpers/factories" import { resetIntegrationTestDatabase, startIntegrationTestDatabase, @@ -34,6 +38,7 @@ afterAll(async () => { describe("updatePersonUserUseCase", () => { describe("person-only update", () => { it("updates only the Person when person has no linked User", async () => { + const team = await createTestTeam(prisma) const person = await createTestPerson(prisma, { firstName: "Old", lastName: "Name", @@ -44,7 +49,7 @@ describe("updatePersonUserUseCase", () => { id: person.id, firstName: "New", lastName: "Name", - department: "IT", + teamId: team.id, email: "new@example.test", phone: "1234", }) @@ -57,7 +62,7 @@ describe("updatePersonUserUseCase", () => { expect(updated).toMatchObject({ firstName: "New", lastName: "Name", - department: "IT", + teamId: team.id, email: "new@example.test", phone: "1234", userId: null, @@ -71,7 +76,7 @@ describe("updatePersonUserUseCase", () => { id: person.id, firstName: "Empty", lastName: "Email", - department: "OTHER", + teamId: null, email: "", phone: null, }) @@ -87,6 +92,7 @@ describe("updatePersonUserUseCase", () => { describe("person+user update", () => { it("updates Person fields and User role/isActive when person has a User linked", async () => { + const team = await createTestTeam(prisma) const user = await createTestUser(prisma, { email: "user-update@example.test", name: "Old Name", @@ -107,7 +113,7 @@ describe("updatePersonUserUseCase", () => { id: person.id, firstName: "Linked", lastName: "Person", - department: "ENGINEERING", + teamId: team.id, email: "user-update@example.test", phone: null, role: "ADMIN", @@ -120,7 +126,7 @@ describe("updatePersonUserUseCase", () => { where: { id: person.id }, include: { user: true }, }) - expect(updatedPerson.department).toBe("ENGINEERING") + expect(updatedPerson.teamId).toBe(team.id) expect(updatedPerson.user).toMatchObject({ id: user.id, role: "ADMIN", @@ -144,7 +150,7 @@ describe("updatePersonUserUseCase", () => { id: person.id, firstName: person.firstName, lastName: person.lastName, - department: "OTHER", + teamId: null, email: "pw-reset@example.test", phone: null, role: "STAFF", @@ -190,7 +196,7 @@ describe("updatePersonUserUseCase", () => { id: person.id, firstName: person.firstName, lastName: person.lastName, - department: "OTHER", + teamId: null, email: "no-pw@example.test", phone: null, role: "STAFF", @@ -216,7 +222,7 @@ describe("updatePersonUserUseCase", () => { id: "00000000-0000-0000-0000-000000000000", firstName: "Ghost", lastName: "Person", - department: "OTHER", + teamId: null, email: "ghost@example.test", phone: null, }) @@ -239,7 +245,7 @@ describe("updatePersonUserUseCase", () => { id: person.id, firstName: "Mine", lastName: "Person", - department: "OTHER", + teamId: null, email: "theirs@example.test", phone: null, }) @@ -249,5 +255,23 @@ describe("updatePersonUserUseCase", () => { errors: { email: ["Email already exists"] }, }) }) + + it("rejects an unknown team id", async () => { + const person = await createTestPerson(prisma) + + const result = await updatePersonUserUseCase({ + id: person.id, + firstName: person.firstName, + lastName: person.lastName, + teamId: "00000000-0000-0000-0000-000000000000", + email: person.email, + phone: null, + }) + + expect(result.success).toBe(false) + if (!result.success) { + expect(result.errors.teamId).toBeDefined() + } + }) }) }) diff --git a/tests/integration/use-cases/user.use-cases.test.ts b/tests/integration/use-cases/user.use-cases.test.ts index dac1909..c987c22 100644 --- a/tests/integration/use-cases/user.use-cases.test.ts +++ b/tests/integration/use-cases/user.use-cases.test.ts @@ -147,7 +147,9 @@ describe("user use-cases", () => { }), ).resolves.toEqual({ success: true }) - expect(await prisma.user.findUniqueOrThrow({ where: { id: user.id } })).toMatchObject({ + expect( + await prisma.user.findUniqueOrThrow({ where: { id: user.id } }), + ).toMatchObject({ name: "Edited User", email: "edited@example.test", role: "MANAGER", @@ -168,7 +170,9 @@ describe("user use-cases", () => { errors: { email: ["Email already exists"] }, }) - expect(await prisma.user.findUniqueOrThrow({ where: { id: user.id } })).toMatchObject({ + expect( + await prisma.user.findUniqueOrThrow({ where: { id: user.id } }), + ).toMatchObject({ email: "edited@example.test", }) }) @@ -190,7 +194,9 @@ describe("user use-cases", () => { errors: { id: ["You cannot remove your own administrator access"] }, }) - expect(await prisma.user.findUniqueOrThrow({ where: { id: admin.id } })).toMatchObject({ + expect( + await prisma.user.findUniqueOrThrow({ where: { id: admin.id } }), + ).toMatchObject({ role: "ADMIN", status: "ACTIVE", }) @@ -232,7 +238,9 @@ describe("user use-cases", () => { }), ).resolves.toEqual({ success: true }) - expect(await prisma.user.findUniqueOrThrow({ where: { id: firstAdmin.id } })).toMatchObject({ + expect( + await prisma.user.findUniqueOrThrow({ where: { id: firstAdmin.id } }), + ).toMatchObject({ status: "DISABLED", }) }) @@ -252,7 +260,9 @@ describe("user use-cases", () => { errors: { id: ["You cannot deactivate your own user"] }, }) - expect(await prisma.user.findUniqueOrThrow({ where: { id: admin.id } })).toMatchObject({ + expect( + await prisma.user.findUniqueOrThrow({ where: { id: admin.id } }), + ).toMatchObject({ status: "ACTIVE", }) }) diff --git a/tests/unit/actions/person.actions.test.ts b/tests/unit/actions/person.actions.test.ts index 59b6aad..2fa4c6a 100644 --- a/tests/unit/actions/person.actions.test.ts +++ b/tests/unit/actions/person.actions.test.ts @@ -25,6 +25,8 @@ vi.mock("@/use-cases/person.use-cases", () => ({ import { createNewPerson, updatePerson } from "@/actions/person.actions" +const validTeamId = "550e8400-e29b-41d4-a716-446655440000" + describe("person actions localization", () => { beforeEach(() => { vi.clearAllMocks() @@ -35,7 +37,7 @@ describe("person actions localization", () => { const result = await createNewPerson({ firstName: "", lastName: "", - department: "", + teamId: "not-a-uuid", email: "not-an-email", } as unknown as Parameters[0]) @@ -46,7 +48,8 @@ describe("person actions localization", () => { errors: { firstName: [es.inventory.people.schema.firstNameRequired], lastName: [es.inventory.people.schema.lastNameRequired], - department: [es.inventory.people.schema.departmentRequired], + teamId: [es.inventory.people.schema.teamIdInvalid], + email: [es.inventory.people.schema.emailInvalid], }, }) }) @@ -62,7 +65,7 @@ describe("person actions localization", () => { const result = await createNewPerson({ firstName: "Ada", lastName: "Lovelace", - department: "ENGINEERING", + teamId: validTeamId, email: "ada@example.test", }) @@ -83,7 +86,7 @@ describe("person actions localization", () => { id: "person-1", firstName: "Ada", lastName: "Lovelace", - department: "ENGINEERING", + teamId: validTeamId, email: "ada@example.test", }) @@ -98,7 +101,7 @@ describe("person actions localization", () => { const result = await createNewPerson({ firstName: "Ada", lastName: "Lovelace", - department: "ENGINEERING", + teamId: null, userId: "not-a-uuid", } as unknown as Parameters[0]) diff --git a/tests/unit/actions/person.messages.test.ts b/tests/unit/actions/person.messages.test.ts index e773999..b0f6ca8 100644 --- a/tests/unit/actions/person.messages.test.ts +++ b/tests/unit/actions/person.messages.test.ts @@ -9,6 +9,7 @@ const actionCopy = { updateFailure: "Error al actualizar la persona", duplicateEmail: "El correo electrónico ya existe", notFound: "Persona no encontrada", + teamNotFound: "Equipo no encontrado", } describe("person action message localization", () => { @@ -25,6 +26,19 @@ describe("person action message localization", () => { }) }) + it("localizes team not found errors", () => { + expect( + localizePersonFieldErrors( + { + teamId: ["Team not found"], + }, + actionCopy, + ), + ).toEqual({ + teamId: [actionCopy.teamNotFound], + }) + }) + it("keeps unknown messages unchanged", () => { expect( localizePersonFieldErrors( diff --git a/tests/unit/actions/update-person-user.action.test.ts b/tests/unit/actions/update-person-user.action.test.ts index 0a16488..6f2a9c0 100644 --- a/tests/unit/actions/update-person-user.action.test.ts +++ b/tests/unit/actions/update-person-user.action.test.ts @@ -28,6 +28,8 @@ vi.mock("@/use-cases/person.use-cases", () => ({ import { updatePersonUserAction } from "@/actions/person.actions" +const validTeamId = "550e8400-e29b-41d4-a716-446655440000" + describe("updatePersonUserAction", () => { beforeEach(() => { vi.clearAllMocks() @@ -41,7 +43,7 @@ describe("updatePersonUserAction", () => { id: "", firstName: "Ada", lastName: "Lovelace", - department: "ENGINEERING", + teamId: validTeamId, email: "ada@example.test", phone: null, }) @@ -60,7 +62,7 @@ describe("updatePersonUserAction", () => { id: "person-1", firstName: "Ada", lastName: "Lovelace", - department: "ENGINEERING", + teamId: validTeamId, email: "not-an-email", phone: null, }) @@ -74,12 +76,31 @@ describe("updatePersonUserAction", () => { expect(mocks.updatePersonUserUseCase).not.toHaveBeenCalled() }) + it("rejects invalid teamId with localized teamIdInvalid error", async () => { + const result = await updatePersonUserAction({ + id: "person-1", + firstName: "Ada", + lastName: "Lovelace", + teamId: "not-a-uuid", + email: "ada@example.test", + phone: null, + }) + + expect(result).toEqual({ + success: false, + errors: { + teamId: [es.inventory.people.schema.teamIdInvalid], + }, + }) + expect(mocks.updatePersonUserUseCase).not.toHaveBeenCalled() + }) + it("rejects short password when role is provided", async () => { const result = await updatePersonUserAction({ id: "person-1", firstName: "Ada", lastName: "Lovelace", - department: "ENGINEERING", + teamId: validTeamId, email: "ada@example.test", phone: null, role: "ADMIN", @@ -101,7 +122,7 @@ describe("updatePersonUserAction", () => { id: "person-1", firstName: "Ada", lastName: "Lovelace", - department: "ENGINEERING", + teamId: validTeamId, email: "ada@example.test", phone: null, role: "NO_USER" as unknown as "ADMIN", @@ -117,7 +138,7 @@ describe("updatePersonUserAction", () => { id: "person-1", firstName: "Ada", lastName: "Lovelace", - department: "ENGINEERING", + teamId: validTeamId, email: "ada@example.test", phone: null, role: "SUPER_ADMIN" as unknown as "ADMIN", @@ -140,7 +161,7 @@ describe("updatePersonUserAction", () => { id: "person-1", firstName: "Ada", lastName: "Lovelace", - department: "ENGINEERING", + teamId: validTeamId, email: "duplicate@example.test", phone: null, }) @@ -164,7 +185,7 @@ describe("updatePersonUserAction", () => { id: "missing", firstName: "Ada", lastName: "Lovelace", - department: "ENGINEERING", + teamId: validTeamId, email: "ada@example.test", phone: null, }) @@ -184,7 +205,7 @@ describe("updatePersonUserAction", () => { id: "person-1", firstName: "Ada", lastName: "Lovelace", - department: "ENGINEERING", + teamId: validTeamId, email: "ada@example.test", phone: null, }) @@ -204,7 +225,7 @@ describe("updatePersonUserAction", () => { id: "person-1", firstName: "Ada", lastName: "Lovelace", - department: "ENGINEERING", + teamId: validTeamId, email: "ada@example.test", phone: null, }) diff --git a/tests/unit/app/people/edit-person-form-wiring.test.ts b/tests/unit/app/people/edit-person-form-wiring.test.ts index 236e0e1..44bbe5f 100644 --- a/tests/unit/app/people/edit-person-form-wiring.test.ts +++ b/tests/unit/app/people/edit-person-form-wiring.test.ts @@ -10,6 +10,7 @@ const mocks = vi.hoisted(() => ({ getI18n: vi.fn(), findByIdWithUser: vi.fn(), findById: vi.fn(), + listTeamsUseCase: vi.fn(), personForm: vi.fn(), push: vi.fn(), toastError: vi.fn(), @@ -27,6 +28,10 @@ vi.mock("@/services/person.service", () => ({ }, })) +vi.mock("@/use-cases/team.use-cases", () => ({ + listTeamsUseCase: mocks.listTeamsUseCase, +})) + vi.mock("@/app/(dashboard)/people/_components/edit.person.form", () => ({ default: (props: unknown) => { mocks.personForm(props) @@ -54,8 +59,8 @@ const basePerson: PersonWithUser = { id: "person-1", firstName: "Ada", lastName: "Lovelace", - department: "ENGINEERING", teamId: null, + team: null, email: "ada@example.test", phone: "1234", userId: null, @@ -80,10 +85,16 @@ const personWithUser: PersonWithUser = { }, } +const teams = [ + { id: "team-1", name: "Engineering" }, + { id: "team-2", name: "Sales" }, +] + describe("edit person page wiring", () => { beforeEach(() => { vi.clearAllMocks() mocks.getI18n.mockResolvedValue({ dictionary: en, locale: "en" }) + mocks.listTeamsUseCase.mockResolvedValue(teams) }) it("loads the person without user, passes PersonWithoutUser to the edit form", async () => { @@ -112,6 +123,7 @@ describe("edit person page wiring", () => { ...en.inventory.people.schema, }, roleLabels: en.admin.users.roles, + teams, }), ) }) @@ -142,10 +154,7 @@ describe("edit person page wiring", () => { }), formCopy: es.admin.users.form, roleLabels: es.admin.users.roles, - departmentCopy: es.inventory.people.departments, - fallbackCopy: expect.objectContaining({ - unknownDepartment: es.inventory.people.fallback.unknownDepartment, - }), + teams, }), ) }) diff --git a/tests/unit/app/people/person-form-pages.test.ts b/tests/unit/app/people/person-form-pages.test.ts index 4b1739b..7e23922 100644 --- a/tests/unit/app/people/person-form-pages.test.ts +++ b/tests/unit/app/people/person-form-pages.test.ts @@ -7,6 +7,7 @@ const mocks = vi.hoisted(() => ({ getI18n: vi.fn(), findByIdWithUser: vi.fn(), findById: vi.fn(), + listTeamsUseCase: vi.fn(), redirect: vi.fn(), personForm: vi.fn(), })) @@ -22,6 +23,10 @@ vi.mock("@/services/person.service", () => ({ }, })) +vi.mock("@/use-cases/team.use-cases", () => ({ + listTeamsUseCase: mocks.listTeamsUseCase, +})) + vi.mock("next/navigation", () => ({ redirect: mocks.redirect, useRouter: () => ({ @@ -49,10 +54,16 @@ vi.mock("sonner", () => ({ }, })) +const teams = [ + { id: "team-1", name: "Engineering" }, + { id: "team-2", name: "Sales" }, +] + describe("person pages", () => { beforeEach(() => { vi.clearAllMocks() mocks.getI18n.mockResolvedValue({ dictionary: es, locale: "es" }) + mocks.listTeamsUseCase.mockResolvedValue(teams) }) it("renders the edit person page with Person heading and passes person to unified form", async () => { @@ -66,7 +77,8 @@ describe("person pages", () => { lastName: "Lovelace", email: "ada@example.test", phone: "1234", - department: "ENGINEERING", + teamId: "team-1", + team: { id: "team-1", name: "Engineering" }, userId: null, isActive: true, createdAt: new Date("2024-01-01"), @@ -88,6 +100,7 @@ describe("person pages", () => { firstName: "Ada", lastName: "Lovelace", }), + teams, }), ) }) diff --git a/tests/unit/app/people/person-pages.test.ts b/tests/unit/app/people/person-pages.test.ts index aecbcc3..8345530 100644 --- a/tests/unit/app/people/person-pages.test.ts +++ b/tests/unit/app/people/person-pages.test.ts @@ -67,7 +67,8 @@ describe("person pages", () => { lastName: "Lovelace", email: "ada@example.test", phone: "1234", - department: "ENGINEERING", + teamId: "team-1", + team: { id: "team-1", name: "Engineering" }, userId: null, isActive: true, createdAt: new Date("2024-01-01"), @@ -87,7 +88,7 @@ describe("person pages", () => { expect(html).toContain("Add Person") // No username column — username header must not appear expect(html).not.toContain("Username") - // No standalone username cell — only name, email, phone, department columns + // No standalone username cell — only name, email, phone, team columns expect(html).not.toContain(">ada<") // Name and other fields rendered expect(html).toContain("Ada Lovelace") @@ -110,7 +111,8 @@ describe("person pages", () => { lastName: "Lovelace", email: "ada@example.test", phone: "1234", - department: "ENGINEERING", + teamId: "team-1", + team: { id: "team-1", name: "Engineering" }, userId: "user-1", isActive: true, createdAt: new Date("2024-01-01"), @@ -137,7 +139,8 @@ describe("person pages", () => { lastName: "Jones", email: "bob@example.test", phone: null, - department: "IT", + teamId: null, + team: null, userId: null, isActive: true, createdAt: new Date("2024-01-01"), @@ -193,7 +196,8 @@ describe("person pages", () => { lastName: "Lovelace", email: "ada@example.test", phone: "1234", - department: "DRIVER", + teamId: "team-2", + team: { id: "team-2", name: "Driver" }, userId: null, isActive: true, createdAt: new Date("2024-01-01"), @@ -221,7 +225,7 @@ describe("person pages", () => { // Person detail fields expect(html).toContain("Email") expect(html).toContain("Phone") - expect(html).toContain("Department") + expect(html).toContain("Team") expect(html).toContain("ada@example.test") expect(html).toContain("Driver") // Embedded assignments @@ -239,7 +243,8 @@ describe("person pages", () => { lastName: "Lovelace", email: "ada@example.test", phone: "1234", - department: "DRIVER", + teamId: null, + team: null, userId: "user-1", isActive: true, createdAt: new Date("2024-01-01"), @@ -287,7 +292,8 @@ describe("person pages", () => { lastName: "Lovelace", email: "ada@example.test", phone: "1234", - department: "DRIVER", + teamId: null, + team: null, userId: null, isActive: true, createdAt: new Date("2024-01-01"), diff --git a/tests/unit/app/users/unified-form-pages.test.ts b/tests/unit/app/users/unified-form-pages.test.ts index c63e43c..7a2a38c 100644 --- a/tests/unit/app/users/unified-form-pages.test.ts +++ b/tests/unit/app/users/unified-form-pages.test.ts @@ -7,6 +7,7 @@ import { es } from "@/i18n/dictionaries/es" const mocks = vi.hoisted(() => ({ createPersonUser: vi.fn(), getI18n: vi.fn(), + listTeamsUseCase: vi.fn(), push: vi.fn(), toastError: vi.fn(), toastSuccess: vi.fn(), @@ -26,6 +27,10 @@ vi.mock("@/services/person.service", () => ({ }, })) +vi.mock("@/use-cases/team.use-cases", () => ({ + listTeamsUseCase: mocks.listTeamsUseCase, +})) + vi.mock("next/navigation", () => ({ useRouter: () => ({ push: mocks.push, @@ -39,10 +44,16 @@ vi.mock("sonner", () => ({ }, })) +const teams = [ + { id: "team-1", name: "Engineering" }, + { id: "team-2", name: "Sales" }, +] + describe("unified creation form page", () => { beforeEach(() => { vi.clearAllMocks() mocks.getI18n.mockResolvedValue({ dictionary: es, locale: "es" }) + mocks.listTeamsUseCase.mockResolvedValue(teams) }) it("renders unified form with Person fields, email, password, role, and NO_USER option in Spanish", async () => { @@ -55,7 +66,7 @@ describe("unified creation form page", () => { // Person fields expect(html).toContain("Nombre") expect(html).toContain("Apellido") - expect(html).toContain("Departamento") + expect(html).toContain("Equipo") expect(html).toContain("Teléfono") // User fields @@ -86,7 +97,7 @@ describe("unified creation form page", () => { // Person fields expect(html).toContain("First Name") expect(html).toContain("Last Name") - expect(html).toContain("Department") + expect(html).toContain("Team") expect(html).toContain("Phone") // User fields @@ -108,18 +119,20 @@ describe("unified creation form page", () => { // Person field placeholders expect(html).toContain('placeholder="Nombre"') // firstNamePlaceholder (es) expect(html).toContain('placeholder="Apellido"') // lastNamePlaceholder (es) - expect(html).toContain("Selecciona un departamento") // departmentPlaceholder + expect(html).toContain("Selecciona un equipo") // teamPlaceholder expect(html).toContain('placeholder="Teléfono"') // phonePlaceholder (es) }) - it("renders department select with all PERSON_DEPARTMENTS values", async () => { + it("renders team select with active teams from listTeamsUseCase", async () => { const { default: NewUserPage } = await import( "@/app/(dashboard)/people/new/page" ) const html = renderToStaticMarkup(await NewUserPage()) - // Department values must use canonical enum values - expect(html).toContain('value="ADMINISTRATION"') + expect(html).toContain('value="team-1"') + expect(html).toContain("Engineering") + expect(html).toContain('value="team-2"') + expect(html).toContain("Sales") }) }) diff --git a/tests/unit/app/users/user.copy.test.ts b/tests/unit/app/users/user.copy.test.ts index 48fa7d4..c4790f8 100644 --- a/tests/unit/app/users/user.copy.test.ts +++ b/tests/unit/app/users/user.copy.test.ts @@ -1,9 +1,6 @@ import { describe, expect, it } from "vitest" -import { - formatPersonDepartment, - formatUserRole, -} from "@/app/(dashboard)/people/_components/user.copy" +import { formatUserRole } from "@/app/(dashboard)/people/_components/user.copy" describe("user copy helpers", () => { const roleCopy = { @@ -37,48 +34,3 @@ describe("user copy helpers", () => { ).toBe("Rol desconocido") }) }) - -describe("formatPersonDepartment helper", () => { - const departmentCopy = { - IT: "IT", - ENGINEERING: "Ingeniería", - LOGISTICS: "Logística", - TRAFFIC: "Tráfico", - DRIVER: "Chofer", - ADMINISTRATION: "Administración", - SALES: "Ventas", - OTHER: "Otro", - } - - const fallbackCopy = { - unknownDepartment: "Departamento desconocido", - unknownStatus: "Estado desconocido", - } - - it("formats known department values with localized labels", () => { - expect( - formatPersonDepartment("ENGINEERING", departmentCopy, fallbackCopy), - ).toBe("Ingeniería") - expect( - formatPersonDepartment("ADMINISTRATION", departmentCopy, fallbackCopy), - ).toBe("Administración") - }) - - it("falls back for unknown department values", () => { - expect( - formatPersonDepartment("UNKNOWN_DEPT", departmentCopy, fallbackCopy), - ).toBe("Departamento desconocido") - }) - - it("falls back for null department values", () => { - expect(formatPersonDepartment(null, departmentCopy, fallbackCopy)).toBe( - "Departamento desconocido", - ) - }) - - it("falls back for undefined department values", () => { - expect( - formatPersonDepartment(undefined, departmentCopy, fallbackCopy), - ).toBe("Departamento desconocido") - }) -}) diff --git a/tests/unit/i18n/admin-users-dictionary.test.ts b/tests/unit/i18n/admin-users-dictionary.test.ts index b2e4516..e73d2af 100644 --- a/tests/unit/i18n/admin-users-dictionary.test.ts +++ b/tests/unit/i18n/admin-users-dictionary.test.ts @@ -32,8 +32,8 @@ describe("admin users dictionary", () => { firstNamePlaceholder: "First name", lastNameLabel: "Last Name", lastNamePlaceholder: "Last name", - departmentLabel: "Department", - departmentPlaceholder: "Select a department", + teamLabel: "Team", + teamPlaceholder: "Select a team", emailLabel: "Email", emailPlaceholder: "user@example.com", phoneLabel: "Phone", @@ -127,8 +127,8 @@ describe("admin users dictionary", () => { firstNamePlaceholder: "Nombre", lastNameLabel: "Apellido", lastNamePlaceholder: "Apellido", - departmentLabel: "Departamento", - departmentPlaceholder: "Selecciona un departamento", + teamLabel: "Equipo", + teamPlaceholder: "Selecciona un equipo", emailLabel: "Correo electrónico", emailPlaceholder: "usuario@ejemplo.com", phoneLabel: "Teléfono", diff --git a/tests/unit/i18n/dictionaries.test.ts b/tests/unit/i18n/dictionaries.test.ts index d8dbe10..8edb737 100644 --- a/tests/unit/i18n/dictionaries.test.ts +++ b/tests/unit/i18n/dictionaries.test.ts @@ -832,7 +832,7 @@ describe("i18n dictionaries", () => { name: "Name", email: "Email", phone: "Phone", - department: "Department", + team: "Team", role: "Role", status: "Status", actions: "Actions", @@ -847,7 +847,7 @@ describe("i18n dictionaries", () => { labels: { email: "Email", phone: "Phone", - department: "Department", + team: "Team", role: "Role", status: "Status", noUser: "No user account", @@ -865,8 +865,8 @@ describe("i18n dictionaries", () => { firstNamePlaceholder: "First name", lastNameLabel: "Last Name", lastNamePlaceholder: "Last name", - departmentLabel: "Department", - departmentPlaceholder: "Select a department", + teamLabel: "Team", + teamPlaceholder: "Select a team", emailLabel: "Email", emailPlaceholder: "Email", phoneLabel: "Phone", @@ -880,19 +880,9 @@ describe("i18n dictionaries", () => { updateSubmit: "Update Person", }, fallback: { - unknownDepartment: "Unknown department", + noTeam: "—", unknownStatus: "Unknown status", }, - departments: { - IT: "IT", - ENGINEERING: "Engineering", - LOGISTICS: "Logistics", - TRAFFIC: "Traffic", - DRIVER: "Driver", - ADMINISTRATION: "Administration", - SALES: "Sales", - OTHER: "Other", - }, actions: { createSuccess: "Person created successfully", createFailure: "Failed to create person", @@ -900,14 +890,15 @@ describe("i18n dictionaries", () => { updateFailure: "Failed to update person", duplicateEmail: "Email already exists", notFound: "Person not found", + teamNotFound: "Team not found", }, schema: { 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", + teamIdInvalid: "Team must be a valid id", }, }) @@ -920,7 +911,7 @@ describe("i18n dictionaries", () => { name: "Nombre", email: "Correo electrónico", phone: "Teléfono", - department: "Departamento", + team: "Equipo", role: "Rol", status: "Estado", actions: "Acciones", @@ -935,7 +926,7 @@ describe("i18n dictionaries", () => { labels: { email: "Correo electrónico", phone: "Teléfono", - department: "Departamento", + team: "Equipo", role: "Rol", status: "Estado", noUser: "Sin cuenta de usuario", @@ -953,8 +944,8 @@ describe("i18n dictionaries", () => { firstNamePlaceholder: "Nombre", lastNameLabel: "Apellido", lastNamePlaceholder: "Apellido", - departmentLabel: "Departamento", - departmentPlaceholder: "Selecciona un departamento", + teamLabel: "Equipo", + teamPlaceholder: "Selecciona un equipo", emailLabel: "Correo electrónico", emailPlaceholder: "Correo electrónico", phoneLabel: "Teléfono", @@ -969,19 +960,9 @@ describe("i18n dictionaries", () => { updateSubmit: "Actualizar persona", }, fallback: { - unknownDepartment: "Departamento desconocido", + noTeam: "—", unknownStatus: "Estado desconocido", }, - departments: { - IT: "IT", - ENGINEERING: "Ingeniería", - LOGISTICS: "Logística", - TRAFFIC: "Tráfico", - DRIVER: "Chofer", - ADMINISTRATION: "Administración", - SALES: "Ventas", - OTHER: "Otro", - }, actions: { createSuccess: "Persona creada correctamente", createFailure: "Error al crear la persona", @@ -989,14 +970,15 @@ describe("i18n dictionaries", () => { updateFailure: "Error al actualizar la persona", duplicateEmail: "El correo electrónico ya existe", notFound: "Persona no encontrada", + teamNotFound: "Equipo no encontrado", }, schema: { 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", + teamIdInvalid: "El equipo debe ser un id válido", }, }) }) diff --git a/tests/unit/i18n/unified-form-dictionary.test.ts b/tests/unit/i18n/unified-form-dictionary.test.ts index 303e167..90977a9 100644 --- a/tests/unit/i18n/unified-form-dictionary.test.ts +++ b/tests/unit/i18n/unified-form-dictionary.test.ts @@ -10,8 +10,8 @@ describe("admin users unified form dictionary", () => { expect(form.firstNamePlaceholder).toBe("First name") expect(form.lastNameLabel).toBe("Last Name") expect(form.lastNamePlaceholder).toBe("Last name") - expect(form.departmentLabel).toBe("Department") - expect(form.departmentPlaceholder).toBe("Select a department") + expect(form.teamLabel).toBe("Team") + expect(form.teamPlaceholder).toBe("Select a team") expect(form.phoneLabel).toBe("Phone") expect(form.phonePlaceholder).toBe("Phone") }) @@ -23,8 +23,8 @@ describe("admin users unified form dictionary", () => { expect(form.firstNamePlaceholder).toBe("Nombre") expect(form.lastNameLabel).toBe("Apellido") expect(form.lastNamePlaceholder).toBe("Apellido") - expect(form.departmentLabel).toBe("Departamento") - expect(form.departmentPlaceholder).toBe("Selecciona un departamento") + expect(form.teamLabel).toBe("Equipo") + expect(form.teamPlaceholder).toBe("Selecciona un equipo") expect(form.phoneLabel).toBe("Teléfono") expect(form.phonePlaceholder).toBe("Teléfono") }) diff --git a/tests/unit/schemas/core-schemas.test.ts b/tests/unit/schemas/core-schemas.test.ts index b47db86..3c6738c 100644 --- a/tests/unit/schemas/core-schemas.test.ts +++ b/tests/unit/schemas/core-schemas.test.ts @@ -125,7 +125,7 @@ describe("core schemas", () => { createPersonSchema.safeParse({ firstName: "Per", lastName: "Son", - department: "IT", + teamId: null, email: "person@example.test", }).success, ).toBe(true) @@ -134,7 +134,7 @@ describe("core schemas", () => { createPersonSchema.safeParse({ firstName: "Per", lastName: "Son", - department: "IT", + teamId: null, email: "not-an-email", }).success, ).toBe(false) @@ -143,7 +143,7 @@ describe("core schemas", () => { createPersonSchema.safeParse({ firstName: "Per", lastName: "Son", - department: "IT", + teamId: null, email: "", }).success, ).toBe(true) diff --git a/tests/unit/schemas/person.schema.test.ts b/tests/unit/schemas/person.schema.test.ts index ad16c5f..07b5717 100644 --- a/tests/unit/schemas/person.schema.test.ts +++ b/tests/unit/schemas/person.schema.test.ts @@ -8,18 +8,20 @@ import { const schemaCopy = { 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", + teamIdInvalid: "El equipo debe ser un id válido", } +const validTeamId = "550e8400-e29b-41d4-a716-446655440000" + describe("person schema validation", () => { it("uses localized required-field validation messages for create (no username)", () => { const result = buildCreatePersonSchema(schemaCopy).safeParse({ firstName: "", lastName: "", - department: "", + teamId: null, }) expect(result.success).toBe(false) @@ -28,7 +30,6 @@ describe("person schema validation", () => { expect(errors.firstName).toContain(schemaCopy.firstNameRequired) expect(errors.lastName).toContain(schemaCopy.lastNameRequired) - expect(errors.department).toContain(schemaCopy.departmentRequired) } }) @@ -36,7 +37,7 @@ describe("person schema validation", () => { const result = buildCreatePersonSchema(schemaCopy).safeParse({ firstName: "Ada", lastName: "Lovelace", - department: "ENGINEERING", + teamId: null, email: "not-an-email", }) @@ -52,7 +53,7 @@ describe("person schema validation", () => { const result = buildCreatePersonSchema(schemaCopy).safeParse({ firstName: "Ada", lastName: "Lovelace", - department: "ENGINEERING", + teamId: null, userId: "not-a-uuid", }) @@ -64,12 +65,27 @@ describe("person schema validation", () => { } }) + it("rejects an invalid teamId", () => { + const result = buildCreatePersonSchema(schemaCopy).safeParse({ + firstName: "Ada", + lastName: "Lovelace", + teamId: "not-a-uuid", + }) + + expect(result.success).toBe(false) + if (!result.success) { + expect(result.error.flatten().fieldErrors.teamId).toContain( + schemaCopy.teamIdInvalid, + ) + } + }) + it("uses localized update identifier validation messages", () => { const result = buildUpdatePersonSchema(schemaCopy).safeParse({ id: "", firstName: "Ada", lastName: "Lovelace", - department: "ENGINEERING", + teamId: null, email: "ada@example.test", }) @@ -81,20 +97,20 @@ describe("person schema validation", () => { } }) - it("preserves canonical department values and accepts optional userId UUID", () => { + it("accepts a valid teamId UUID and optional userId UUID", () => { const result = buildCreatePersonSchema(schemaCopy).safeParse({ firstName: "Ada", lastName: "Lovelace", - department: "ENGINEERING", + teamId: validTeamId, email: "", - userId: "550e8400-e29b-41d4-a716-446655440000", + userId: validTeamId, }) expect(result.success).toBe(true) if (result.success) { - expect(result.data.department).toBe("ENGINEERING") + expect(result.data.teamId).toBe(validTeamId) expect(result.data.email).toBe("") - expect(result.data.userId).toBe("550e8400-e29b-41d4-a716-446655440000") + expect(result.data.userId).toBe(validTeamId) } }) @@ -102,7 +118,7 @@ describe("person schema validation", () => { const result = buildCreatePersonSchema(schemaCopy).safeParse({ firstName: "Ada", lastName: "Lovelace", - department: "ENGINEERING", + teamId: null, }) expect(result.success).toBe(true) diff --git a/tests/unit/schemas/unified-create.schema.test.ts b/tests/unit/schemas/unified-create.schema.test.ts index ce99457..dd6c6a6 100644 --- a/tests/unit/schemas/unified-create.schema.test.ts +++ b/tests/unit/schemas/unified-create.schema.test.ts @@ -6,34 +6,36 @@ import { unifiedFormRoleSchema, } from "@/schemas/user.schema" +const validTeamId = "550e8400-e29b-41d4-a716-446655440000" + const enCopy: UnifiedSchemaCopy = { firstNameRequired: "First name is required", lastNameRequired: "Last name is required", - departmentRequired: "Department is required", emailInvalid: "Invalid email", passwordMinLength: "Password must be at least 8 characters", nameRequired: "Name is required", userIdRequired: "User id is required", idRequired: "ID is required", userIdInvalid: "User ID must be a valid UUID", + teamIdInvalid: "Team must be a valid id", } const esCopy: UnifiedSchemaCopy = { firstNameRequired: "El nombre es obligatorio", lastNameRequired: "El apellido es obligatorio", - departmentRequired: "El departamento es obligatorio", emailInvalid: "Correo electrónico no válido", passwordMinLength: "La contraseña debe tener al menos 8 caracteres", nameRequired: "El nombre es obligatorio", userIdRequired: "El ID de usuario es obligatorio", idRequired: "El ID es obligatorio", userIdInvalid: "El ID de usuario debe ser un UUID válido", + teamIdInvalid: "El equipo debe ser un id válido", } const validPersonOnlyData = { firstName: "John", lastName: "Doe", - department: "IT", + teamId: null, email: "john@example.test", phone: null, role: "NO_USER" as const, @@ -44,7 +46,7 @@ const validPersonOnlyData = { const validPersonWithUserData = { firstName: "Jane", lastName: "Smith", - department: "ENGINEERING", + teamId: validTeamId, email: "jane@example.test", phone: "1234567890", role: "ADMIN" as const, @@ -96,7 +98,7 @@ describe("buildUnifiedCreateSchema", () => { const result = schema.safeParse({ firstName: "", lastName: "", - department: "", + teamId: "not-a-uuid", email: "not-an-email", role: "NO_USER", phone: null, @@ -108,7 +110,7 @@ describe("buildUnifiedCreateSchema", () => { const errors = result.error.flatten().fieldErrors expect(errors.firstName).toContain(esCopy.firstNameRequired) expect(errors.lastName).toContain(esCopy.lastNameRequired) - expect(errors.department).toContain(esCopy.departmentRequired) + expect(errors.teamId).toContain(esCopy.teamIdInvalid) expect(errors.email).toContain(esCopy.emailInvalid) } }) @@ -185,7 +187,7 @@ describe("buildUnifiedCreateSchema", () => { const result = schema.safeParse({ firstName: "Jane", lastName: "Smith", - department: "ENGINEERING", + teamId: validTeamId, email: "jane@example.test", role: "ADMIN", password: "corta", @@ -228,36 +230,35 @@ describe("buildUnifiedCreateSchema", () => { }) }) - describe("department validation", () => { - it("rejects invalid department", () => { + describe("teamId validation", () => { + it("rejects invalid teamId", () => { const schema = buildUnifiedCreateSchema(enCopy) const result = schema.safeParse({ ...validPersonOnlyData, - department: "INVALID_DEPT", + teamId: "INVALID_TEAM", }) expect(result.success).toBe(false) }) - it("accepts valid departments", () => { + it("accepts null teamId", () => { const schema = buildUnifiedCreateSchema(enCopy) - const validDepartments = [ - "IT", - "ENGINEERING", - "TRAFFIC", - "DRIVER", - "LOGISTICS", - "ADMINISTRATION", - "SALES", - "OTHER", - ] - for (const dept of validDepartments) { - const result = schema.safeParse({ - ...validPersonOnlyData, - department: dept, - }) - expect(result.success).toBe(true) - } + const result = schema.safeParse({ + ...validPersonOnlyData, + teamId: null, + }) + + expect(result.success).toBe(true) + }) + + it("accepts a valid teamId UUID", () => { + const schema = buildUnifiedCreateSchema(enCopy) + const result = schema.safeParse({ + ...validPersonOnlyData, + teamId: validTeamId, + }) + + expect(result.success).toBe(true) }) }) }) diff --git a/tests/unit/schemas/unified-update.schema.test.ts b/tests/unit/schemas/unified-update.schema.test.ts index 35b843b..a893fe8 100644 --- a/tests/unit/schemas/unified-update.schema.test.ts +++ b/tests/unit/schemas/unified-update.schema.test.ts @@ -5,35 +5,37 @@ import { type UnifiedSchemaCopy, } from "@/schemas/user.schema" +const validTeamId = "550e8400-e29b-41d4-a716-446655440000" + const enCopy: UnifiedSchemaCopy = { firstNameRequired: "First name is required", lastNameRequired: "Last name is required", - departmentRequired: "Department is required", emailInvalid: "Invalid email", passwordMinLength: "Password must be at least 8 characters", nameRequired: "Name is required", userIdRequired: "User id is required", idRequired: "ID is required", userIdInvalid: "User ID must be a valid UUID", + teamIdInvalid: "Team must be a valid id", } const esCopy: UnifiedSchemaCopy = { firstNameRequired: "El nombre es obligatorio", lastNameRequired: "El apellido es obligatorio", - departmentRequired: "El departamento es obligatorio", emailInvalid: "Correo electrónico no válido", passwordMinLength: "La contraseña debe tener al menos 8 caracteres", nameRequired: "El nombre es obligatorio", userIdRequired: "El ID de usuario es obligatorio", idRequired: "El ID es obligatorio", userIdInvalid: "El ID de usuario debe ser un UUID válido", + teamIdInvalid: "El equipo debe ser un id válido", } const validPersonOnly = { id: "person-1", firstName: "John", lastName: "Doe", - department: "IT", + teamId: null, email: "john@example.test", phone: null, } @@ -73,7 +75,7 @@ describe("buildUnifiedUpdateSchema", () => { id: "", firstName: "John", lastName: "Doe", - department: "IT", + teamId: null, email: "john@example.test", phone: null, }) @@ -85,6 +87,31 @@ describe("buildUnifiedUpdateSchema", () => { ) } }) + + it("rejects invalid teamId", () => { + const schema = buildUnifiedUpdateSchema(enCopy) + const result = schema.safeParse({ + ...validPersonOnly, + teamId: "not-a-uuid", + }) + + expect(result.success).toBe(false) + if (!result.success) { + expect(result.error.flatten().fieldErrors.teamId).toContain( + enCopy.teamIdInvalid, + ) + } + }) + + it("accepts a valid teamId UUID", () => { + const schema = buildUnifiedUpdateSchema(enCopy) + const result = schema.safeParse({ + ...validPersonOnly, + teamId: validTeamId, + }) + + expect(result.success).toBe(true) + }) }) describe("person+user update (when person has User linked)", () => {