diff --git a/frontend/index.html b/frontend/index.html index e0b3e5d..12ad152 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -2,8 +2,7 @@ - - + - Painel de Vagas + <Cand!Date!>
diff --git a/frontend/public/icone.png b/frontend/public/icone.png new file mode 100644 index 0000000..d61420d Binary files /dev/null and b/frontend/public/icone.png differ diff --git a/frontend/src/components/login/LeftSide.tsx b/frontend/src/components/login/LeftSide.tsx index 833ed71..aa684e0 100644 --- a/frontend/src/components/login/LeftSide.tsx +++ b/frontend/src/components/login/LeftSide.tsx @@ -1,5 +1,5 @@ import { Image } from "@unpic/react" -import { Instagram, Linkedin, Search, Bell, TrendingUp } from "lucide-react" +import { Bell, Instagram, Linkedin, Search, TrendingUp } from "lucide-react" export default function LeftSide() { return ( @@ -11,7 +11,7 @@ export default function LeftSide() { alt="Profissionais de tecnologia" layout="fullWidth" className="h-full w-full object-cover opacity-[0.85] dark:opacity-30 mix-blend-multiply dark:mix-blend-luminosity" - priority + priority={true} />
@@ -77,4 +77,4 @@ export default function LeftSide() { ) -} +} \ No newline at end of file diff --git a/frontend/src/components/login/RegisterSide.tsx b/frontend/src/components/login/RegisterSide.tsx index 71eb5d8..8a53386 100644 --- a/frontend/src/components/login/RegisterSide.tsx +++ b/frontend/src/components/login/RegisterSide.tsx @@ -1,7 +1,9 @@ -import { useState, FormEvent } from "react"; -import { Eye, EyeOff, ArrowLeft } from "lucide-react"; +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { register } from "@/services/authService"; // ← MUDE para register import { Image } from "@unpic/react"; import { motion } from "framer-motion"; +import { ArrowLeft, Eye, EyeOff } from "lucide-react"; +import { FormEvent, useState } from "react"; import PhoneInput from "react-phone-number-input"; import "react-phone-number-input/style.css"; @@ -59,6 +61,9 @@ export default function RegisterSide() { const [telefoneError, setTelefoneError] = useState(""); const [passwordError, setPasswordError] = useState(""); const [cpfError, setCpfError] = useState(""); + + const [isLoading, setIsLoading] = useState(false); + const [apiError, setApiError] = useState(""); const handleRevealPassword = () => setShowPassword((prev) => !prev); @@ -70,41 +75,67 @@ export default function RegisterSide() { return `${digits.slice(0, 3)}.${digits.slice(3, 6)}.${digits.slice(6, 9)}-${digits.slice(9)}`; }; - const handleSubmit = (e: FormEvent) => { + const handleSubmit = async (e: FormEvent) => { e.preventDefault(); setNomeError(""); setEmailError(""); setTelefoneError(""); setPasswordError(""); setCpfError(""); + setApiError(""); let isValid = true; - if (!nome) { setNomeError("O campo de nome é obrigatório."); isValid = false; } + if (!nome) { + setNomeError("O campo de nome é obrigatório."); + isValid = false; + } if (!email) { - setEmailError("O campo de e-mail é obrigatório."); isValid = false; + setEmailError("O campo de e-mail é obrigatório."); + isValid = false; } else { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - if (!emailRegex.test(email)) { setEmailError("Por favor, insira um e-mail válido."); isValid = false; } + if (!emailRegex.test(email)) { + setEmailError("Por favor, insira um e-mail válido."); + isValid = false; + } } - if (!telefone) { setTelefoneError("O campo de telefone é obrigatório."); isValid = false; } + if (!telefone) { + setTelefoneError("O campo de telefone é obrigatório."); + isValid = false; + } if (!password) { - setPasswordError("O campo de senha é obrigatório."); isValid = false; + setPasswordError("O campo de senha é obrigatório."); + isValid = false; } else if (password.length < 6) { - setPasswordError("A senha precisa conter pelo menos 6 caracteres."); isValid = false; + setPasswordError("A senha precisa conter pelo menos 6 caracteres."); + isValid = false; } - if (!cpf) { - setCpfError("O campo de CPF é obrigatório."); isValid = false; - } else if (cpf.replace(/\D/g, "").length < 11) { - setCpfError("Por favor, insira um CPF válido."); isValid = false; + if (cpf && cpf.replace(/\D/g, "").length > 0 && cpf.replace(/\D/g, "").length !== 11) { + setCpfError("CPF inválido. Deve conter 11 dígitos."); + isValid = false; } if (isValid) { - console.log("Cadastro válido!", { nome, email, telefone, password, cpf }); + setIsLoading(true); + try { + const result = await register({ + email: email, + password: password, + name: nome, + }); + console.log("Cadastro bem sucedido:", result); + window.location.href = "/login?registered=true"; + } catch (error: any) { + console.error("Erro no cadastro:", error); + setApiError(error.message || "Erro ao cadastrar. Tente novamente."); + } finally { + setIsLoading(false); + } } }; @@ -144,6 +175,12 @@ export default function RegisterSide() {
+ {apiError && ( +
+

{apiError}

+
+ )} +
@@ -170,10 +208,11 @@ export default function RegisterSide() { type="email" value={email} onChange={(e) => setEmail(e.target.value)} - placeholder="lbene17@gmail.com" + placeholder="benevanio@dev.com.br" + disabled={isLoading} className={`w-full px-4 py-3.5 rounded-xl border bg-white/80 dark:bg-neutral-800/80 backdrop-blur-sm text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-neutral-500 focus:outline-none focus:ring-2 transition-all shadow-sm ${ emailError ? "border-red-500 focus:ring-red-500" : "border-gray-200 dark:border-neutral-700 focus:ring-blue-500 focus:border-transparent" - }`} + } ${isLoading ? "opacity-50 cursor-not-allowed" : ""}`} /> {emailError &&

{emailError}

} @@ -184,12 +223,13 @@ export default function RegisterSide() {
+ } ${isLoading ? "opacity-50" : ""}`}> @@ -208,13 +248,15 @@ export default function RegisterSide() { value={password} onChange={(e) => setPassword(e.target.value)} placeholder="Ex: ••••••••••••" + disabled={isLoading} className={`w-full px-4 py-3.5 rounded-xl border bg-white/80 dark:bg-neutral-800/80 backdrop-blur-sm text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-neutral-500 focus:outline-none focus:ring-2 transition-all shadow-sm ${ passwordError ? "border-red-500 focus:ring-red-500" : "border-gray-200 dark:border-neutral-700 focus:ring-blue-500 focus:border-transparent" - }`} + } ${isLoading ? "opacity-50 cursor-not-allowed" : ""}`} />
- {/* Botões Sociais Atualizados com Hover translúcido e bordas combinando */}
- - -
+ + {apiError && ( +
+

{apiError}

+
+ )} +
@@ -152,15 +176,17 @@ export default function RightSide() { value={password} onChange={(e) => setPassword(e.target.value)} placeholder="Ex: ••••••••••••" + disabled={isLoading} className={`w-full px-4 py-3.5 rounded-xl border bg-white/80 dark:bg-neutral-800/80 backdrop-blur-sm text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-neutral-500 focus:outline-none focus:ring-2 transition-all shadow-sm ${ passwordError ? "border-red-500 focus:ring-red-500" : "border-gray-200 dark:border-neutral-700 focus:ring-blue-500 focus:border-transparent" - }`} + } ${isLoading ? "opacity-50 cursor-not-allowed" : ""}`} />
- - , + button: ({ children, ...props }: any) => ( + + ), }, })); vi.mock("react-phone-number-input", () => ({ - default: ({ value, onChange, placeholder }: any) => ( + default: ({ value, onChange, placeholder, disabled }: any) => ( onChange(e.target.value)} placeholder={placeholder} - onChange={(event) => onChange(event.target.value)} + disabled={disabled} /> ), })); import RegisterSide from "@/components/login/RegisterSide"; +// ===================== +// TEST SUITE +// ===================== describe("RegisterSide", () => { - const logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + const originalLocation = window.location; beforeEach(() => { - logSpy.mockClear(); + mockRegister.mockReset(); + + Object.defineProperty(window, "location", { + configurable: true, + value: { href: "" }, + }); + }); + + afterEach(() => { + Object.defineProperty(window, "location", { + configurable: true, + value: originalLocation, + }); }); - it("renderiza formulário completo de cadastro", () => { + it("renderiza formulário e alterna visibilidade da senha", () => { render(); - expect(screen.getByLabelText(/nome/i)).toBeInTheDocument(); - expect(screen.getByLabelText(/email/i)).toBeInTheDocument(); - expect(screen.getByLabelText(/^telefone$/i)).toBeInTheDocument(); - expect(screen.getByLabelText(/senha/i)).toBeInTheDocument(); - expect(screen.getByLabelText(/cpf/i)).toBeInTheDocument(); + const passwordInput = screen.getByLabelText(/senha/i) as HTMLInputElement; + expect(passwordInput.type).toBe("password"); + + const toggleButtons = screen.getAllByRole("button"); + fireEvent.click(toggleButtons[0]); + + expect(screen.getByLabelText(/senha/i)).toHaveAttribute("type", "text"); }); - it("mostra erros obrigatórios quando submit vazio", () => { + it("mostra erros obrigatórios quando submit vazio", async () => { + render(); + + fireEvent.click(screen.getByRole("button", { name: /cadastrar/i })); + + expect( + await screen.findByText(/campo de nome.*obrigatório/i) + ).toBeInTheDocument(); + expect( + await screen.findByText(/campo de e-mail.*obrigatório/i) + ).toBeInTheDocument(); + expect( + await screen.findByText(/campo de telefone.*obrigatório/i) + ).toBeInTheDocument(); + expect( + await screen.findByText(/campo de senha.*obrigatório/i) + ).toBeInTheDocument(); + // CPF é campo opcional — não dispara erro de obrigatório + }); + + + it("valida CPF quando preenchido incorretamente", async () => { + render(); + + fireEvent.change(screen.getByLabelText(/nome/i), { + target: { value: "Usuário Teste" }, + }); + + fireEvent.change(screen.getByLabelText(/email/i), { + target: { value: "teste@email.com" }, + }); + + fireEvent.change(screen.getByPlaceholderText(/\(34\)/i), { + target: { value: "+5534999999999" }, + }); + + fireEvent.change(screen.getByLabelText(/senha/i), { + target: { value: "123456" }, + }); + + fireEvent.change(screen.getByLabelText(/cpf/i), { + target: { value: "123" }, + }); + + fireEvent.click(screen.getByRole("button", { name: /cadastrar/i })); + + // Componente exibe: "CPF inválido. Deve conter 11 dígitos." + expect(await screen.findByText(/cpf inv[aá]lido/i)).toBeInTheDocument(); + }); + + it("envia formulário válido com CPF opcional", async () => { + mockRegister.mockResolvedValueOnce({ + message: "Usuário criado", + user: { id: 1 }, + }); + render(); + fireEvent.change(screen.getByLabelText(/nome/i), { + target: { value: "Usuário Teste" }, + }); + + fireEvent.change(screen.getByLabelText(/email/i), { + target: { value: "teste@email.com" }, + }); + + fireEvent.change(screen.getByPlaceholderText(/\(34\)/i), { + target: { value: "+5534999999999" }, + }); + + fireEvent.change(screen.getByLabelText(/senha/i), { + target: { value: "123456" }, + }); + fireEvent.click(screen.getByRole("button", { name: /cadastrar/i })); - expect(screen.getByText(/campo de nome.*obrigatório/i)).toBeInTheDocument(); - expect(screen.getByText(/campo de e-mail.*obrigatório/i)).toBeInTheDocument(); - expect(screen.getByText(/campo de telefone.*obrigatório/i)).toBeInTheDocument(); - expect(screen.getByText(/campo de senha.*obrigatório/i)).toBeInTheDocument(); - expect(screen.getByText(/campo de cpf.*obrigatório/i)).toBeInTheDocument(); + await waitFor(() => { + expect(mockRegister).toHaveBeenCalled(); + }); }); - it("valida email, senha e cpf inválidos", () => { + it("exibe erro quando API falha", async () => { + mockRegister.mockRejectedValueOnce(new Error("Email já cadastrado")); + render(); - fireEvent.change(screen.getByLabelText(/nome/i), { target: { value: "Bene" } }); - fireEvent.change(screen.getByLabelText(/email/i), { target: { value: "bene@teste" } }); - fireEvent.change(screen.getByLabelText(/^telefone$/i), { target: { value: "+5534999999999" } }); - fireEvent.change(screen.getByLabelText(/senha/i), { target: { value: "123" } }); - fireEvent.change(screen.getByLabelText(/cpf/i), { target: { value: "123" } }); + fireEvent.change(screen.getByLabelText(/nome/i), { + target: { value: "Bene" }, + }); + + fireEvent.change(screen.getByLabelText(/email/i), { + target: { value: "bene@teste.com" }, + }); + + fireEvent.change(screen.getByPlaceholderText(/\(34\)/i), { + target: { value: "+5534999999999" }, + }); + + fireEvent.change(screen.getByLabelText(/senha/i), { + target: { value: "123456" }, + }); fireEvent.click(screen.getByRole("button", { name: /cadastrar/i })); - expect(screen.getByText(/insira um e-mail válido/i)).toBeInTheDocument(); - expect(screen.getByText(/pelo menos 6 caracteres/i)).toBeInTheDocument(); - expect(screen.getByText(/insira um cpf válido/i)).toBeInTheDocument(); + expect( + await screen.findByText(/email já cadastrado/i) + ).toBeInTheDocument(); }); - it("formata cpf e envia submit válido", () => { + it("mostra loading durante requisição", async () => { + mockRegister.mockImplementation( + () => new Promise((resolve) => setTimeout(resolve, 200)) + ); + render(); - fireEvent.change(screen.getByLabelText(/nome/i), { target: { value: "Bene" } }); - fireEvent.change(screen.getByLabelText(/email/i), { target: { value: "bene@teste.com" } }); - fireEvent.change(screen.getByLabelText(/^telefone$/i), { target: { value: "+5534999999999" } }); - fireEvent.change(screen.getByLabelText(/senha/i), { target: { value: "123456" } }); - fireEvent.change(screen.getByLabelText(/cpf/i), { target: { value: "12345678901" } }); + fireEvent.change(screen.getByLabelText(/nome/i), { + target: { value: "Bene" }, + }); - expect(screen.getByLabelText(/cpf/i)).toHaveValue("123.456.789-01"); + fireEvent.change(screen.getByLabelText(/email/i), { + target: { value: "bene@teste.com" }, + }); + + fireEvent.change(screen.getByPlaceholderText(/\(34\)/i), { + target: { value: "+5534999999999" }, + }); + + fireEvent.change(screen.getByLabelText(/senha/i), { + target: { value: "123456" }, + }); fireEvent.click(screen.getByRole("button", { name: /cadastrar/i })); - expect(logSpy).toHaveBeenCalledWith("Cadastro válido!", { - nome: "Bene", - email: "bene@teste.com", - telefone: "+5534999999999", - password: "123456", - cpf: "123.456.789-01", + expect( + await screen.findByRole("button", { name: /cadastrando/i }) + ).toBeDisabled(); + }); + + it("desabilita inputs durante loading", async () => { + mockRegister.mockImplementation( + () => new Promise((resolve) => setTimeout(resolve, 200)) + ); + + render(); + + const nome = screen.getByLabelText(/nome/i); + const email = screen.getByLabelText(/email/i); + const senha = screen.getByLabelText(/senha/i); + + fireEvent.change(nome, { target: { value: "Bene" } }); + fireEvent.change(email, { target: { value: "bene@teste.com" } }); + fireEvent.change(senha, { target: { value: "123456" } }); + + fireEvent.change(screen.getByPlaceholderText(/\(34\)/i), { + target: { value: "+5534999999999" }, }); + + fireEvent.click(screen.getByRole("button", { name: /cadastrar/i })); + + expect( + await screen.findByRole("button", { name: /cadastrando/i }) + ).toBeDisabled(); }); }); diff --git a/frontend/tests/unit/components/login/RigthSide.test.tsx b/frontend/tests/unit/components/login/RigthSide.test.tsx index fd852de..6658157 100644 --- a/frontend/tests/unit/components/login/RigthSide.test.tsx +++ b/frontend/tests/unit/components/login/RigthSide.test.tsx @@ -1,6 +1,12 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { fireEvent, render, screen } from "@testing-library/react"; -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +const mockLogin = vi.fn(); + +vi.mock("@/services/authService", () => ({ + login: (...args: any[]) => mockLogin(...args), +})); vi.mock("@unpic/react", () => ({ Image: (props: any) => {props.alt}, @@ -16,54 +22,163 @@ vi.mock("framer-motion", () => ({ import RigthSide from "@/components/login/RigthSide"; describe("RigthSide", () => { - const logSpy = vi.spyOn(console, "log").mockImplementation(() => {}); + const originalLocation = window.location; beforeEach(() => { - logSpy.mockClear(); + mockLogin.mockReset(); + Object.defineProperty(window, "location", { + configurable: true, + value: { href: "" }, + }); + localStorage.clear(); + }); + + afterEach(() => { + Object.defineProperty(window, "location", { + configurable: true, + value: originalLocation, + }); + }); + + it("renderiza formulário corretamente", () => { + render(); + + expect(screen.getByLabelText(/email/i)).toBeInTheDocument(); + expect(screen.getByLabelText(/senha/i)).toBeInTheDocument(); + expect(screen.getByRole("button", { name: /entrar/i })).toBeInTheDocument(); + expect(screen.getByText(/cadastre-se/i)).toBeInTheDocument(); + expect(screen.getByText(/ou faça login com/i)).toBeInTheDocument(); }); - it("renderiza formulário e alterna visibilidade da senha", () => { + it("alterna visibilidade da senha ao clicar no botão", () => { render(); const passwordInput = screen.getByLabelText(/senha/i) as HTMLInputElement; expect(passwordInput.type).toBe("password"); - const revealButtons = screen.getAllByRole("button", { name: "" }); - fireEvent.click(revealButtons[0]); + const toggleButtons = screen.getAllByRole("button", { name: "" }); + const eyeButton = toggleButtons.find(btn => btn.querySelector('svg')); + if (eyeButton) fireEvent.click(eyeButton); - expect((screen.getByLabelText(/senha/i) as HTMLInputElement).type).toBe("text"); + expect(screen.getByLabelText(/senha/i)).toHaveAttribute("type", "text"); }); - it("mostra erros para submit invalido", () => { + it("mostra erro de email obrigatório ao submeter vazio", async () => { render(); - + fireEvent.click(screen.getByRole("button", { name: /entrar/i })); - - expect(screen.getByText(/campo de e-mail.*obrigatório/i)).toBeInTheDocument(); + + expect(await screen.findByText(/campo de e-mail.*obrigatório/i)).toBeInTheDocument(); expect(screen.getByText(/campo de senha.*obrigatório/i)).toBeInTheDocument(); }); - it("valida formato de email e tamanho minimo da senha", () => { - render(); - fireEvent.change(screen.getByLabelText(/email/i), { target: { value: "qa@teste" } }); + it("valida tamanho mínimo da senha", async () => { + render(); + + fireEvent.change(screen.getByLabelText(/email/i), { target: { value: "teste@email.com" } }); fireEvent.change(screen.getByLabelText(/senha/i), { target: { value: "123" } }); fireEvent.click(screen.getByRole("button", { name: /entrar/i })); + + expect(await screen.findByText(/pelo menos 6 caracteres/i)).toBeInTheDocument(); + }); + + it("envia formulário válido e redireciona após login", async () => { + mockLogin.mockResolvedValueOnce({ token: "fake-token-123", user: { id: 1, email: "teste@email.com" } }); + + render(); + + fireEvent.change(screen.getByLabelText(/email/i), { target: { value: "teste@email.com" } }); + fireEvent.change(screen.getByLabelText(/senha/i), { target: { value: "123456" } }); + fireEvent.click(screen.getByRole("button", { name: /entrar/i })); + + await waitFor(() => { + expect(mockLogin).toHaveBeenCalledWith({ + email: "teste@email.com", + password: "123456", + }); + }); + + expect(localStorage.getItem("token")).toBe("fake-token-123"); + expect(window.location.href).toBe("/dashboard"); + }); - expect(screen.getByText(/insira um e-mail válido/i)).toBeInTheDocument(); - expect(screen.getByText(/pelo menos 6 caracteres/i)).toBeInTheDocument(); + it("exibe erro quando API retorna erro", async () => { + mockLogin.mockRejectedValueOnce(new Error("Credenciais inválidas")); + + render(); + + fireEvent.change(screen.getByLabelText(/email/i), { target: { value: "teste@email.com" } }); + fireEvent.change(screen.getByLabelText(/senha/i), { target: { value: "senhaerrada" } }); + fireEvent.click(screen.getByRole("button", { name: /entrar/i })); + + expect(await screen.findByText(/Credenciais inválidas/i)).toBeInTheDocument(); }); - it("envia formulário valido", () => { + it("exibe erro genérico quando API retorna erro sem mensagem", async () => { + mockLogin.mockRejectedValueOnce(new Error("")); + render(); + + fireEvent.change(screen.getByLabelText(/email/i), { target: { value: "teste@email.com" } }); + fireEvent.change(screen.getByLabelText(/senha/i), { target: { value: "senhaerrada" } }); + fireEvent.click(screen.getByRole("button", { name: /entrar/i })); + + expect(await screen.findByText(/Erro ao fazer login. Verifique suas credenciais./i)).toBeInTheDocument(); + }); - fireEvent.change(screen.getByLabelText(/email/i), { target: { value: "qa@teste.com" } }); + it("mostra loading durante requisição", async () => { + mockLogin.mockImplementation(() => new Promise(resolve => setTimeout(resolve, 200))); + + render(); + + fireEvent.change(screen.getByLabelText(/email/i), { target: { value: "teste@email.com" } }); fireEvent.change(screen.getByLabelText(/senha/i), { target: { value: "123456" } }); fireEvent.click(screen.getByRole("button", { name: /entrar/i })); + + expect(await screen.findByRole("button", { name: /entrando\.\.\./i })).toBeDisabled(); + }); + + it("desabilita inputs durante loading", async () => { + mockLogin.mockImplementation(() => new Promise(resolve => setTimeout(resolve, 200))); + + render(); + + const emailInput = screen.getByLabelText(/email/i); + const passwordInput = screen.getByLabelText(/senha/i); + + fireEvent.change(emailInput, { target: { value: "teste@email.com" } }); + fireEvent.change(passwordInput, { target: { value: "123456" } }); + fireEvent.click(screen.getByRole("button", { name: /entrar/i })); + + await waitFor(() => { + expect(emailInput).toBeDisabled(); + expect(passwordInput).toBeDisabled(); + }); + }); - expect(logSpy).toHaveBeenCalledWith("Formulário válido!", { - email: "qa@teste.com", - password: "123456", + it("não envia formulário quando validação falha", async () => { + render(); + + fireEvent.click(screen.getByRole("button", { name: /entrar/i })); + + await waitFor(() => { + expect(mockLogin).not.toHaveBeenCalled(); }); }); -}); + + it("checkbox de lembrete está presente", () => { + render(); + + const checkbox = screen.getByRole("checkbox"); + expect(checkbox).toBeInTheDocument(); + expect(checkbox).toBeChecked(); + }); + + it("botão de esqueceu a senha está presente", () => { + render(); + + expect(screen.getByText(/esqueceu a senha/i)).toBeInTheDocument(); + }); + +}); \ No newline at end of file diff --git a/frontend/tests/unit/services/auth.service.test.tsx b/frontend/tests/unit/services/auth.service.test.tsx new file mode 100644 index 0000000..4826905 --- /dev/null +++ b/frontend/tests/unit/services/auth.service.test.tsx @@ -0,0 +1,359 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const fetchMock = vi.fn(); +globalThis.fetch = fetchMock as any; + +import * as auth from "@/services/authService"; + +function mockResponse({ + ok = true, + status = 200, + statusText = "OK", + jsonData = {}, + textData = "", + contentType = "application/json", +}: any) { + return { + ok, + status, + statusText, + headers: { + get: (key: string) => { + if (key === "content-type") return contentType; + return null; + }, + }, + json: vi.fn().mockResolvedValue(jsonData), + text: vi.fn().mockResolvedValue(textData), + }; +} + +describe("authService", () => { + beforeEach(() => { + fetchMock.mockReset(); + vi.restoreAllMocks(); + }); + + describe("login", () => { + it("should login successfully", async () => { + fetchMock.mockResolvedValueOnce( + mockResponse({ + jsonData: { token: "abc", user: { id: 1, email: "test@test.com" } }, + }) + ); + + const result = await auth.login({ + email: "test@test.com", + password: "123456", + }); + + expect(fetchMock).toHaveBeenCalledWith( + expect.stringContaining("/api/auth/login"), + expect.objectContaining({ + method: "POST", + headers: { "Content-Type": "application/json" }, + credentials: "include", + }) + ); + expect(result.token).toBe("abc"); + expect(result.user.email).toBe("test@test.com"); + }); + + it("should throw error on login failure with message", async () => { + fetchMock.mockResolvedValueOnce( + mockResponse({ + ok: false, + status: 401, + jsonData: { message: "Invalid credentials" }, + }) + ); + + await expect( + auth.login({ email: "x@x.com", password: "wrong" }) + ).rejects.toThrow("Invalid credentials"); + }); + + it("should throw error on login failure without message", async () => { + fetchMock.mockResolvedValueOnce( + mockResponse({ + ok: false, + status: 500, + jsonData: {}, + }) + ); + + await expect( + auth.login({ email: "x@x.com", password: "wrong" }) + ).rejects.toThrow("Falha ao fazer login."); + }); + + it("should handle text response fallback", async () => { + fetchMock.mockResolvedValueOnce( + mockResponse({ + contentType: "text/plain", + textData: "Server error occurred", + }) + ); + + const result = await auth.login({ + email: "test@test.com", + password: "123456", + }); + + expect(result).toEqual({ message: "Server error occurred" }); + }); + + it("should handle empty text response", async () => { + fetchMock.mockResolvedValueOnce( + mockResponse({ + contentType: "text/plain", + textData: "", + }) + ); + + const result = await auth.login({ + email: "test@test.com", + password: "123456", + }); + + expect(result).toEqual({}); + }); + + it("should handle invalid JSON response", async () => { + const mockResponseObj = { + ok: true, + headers: { + get: (key: string) => { + if (key === "content-type") return "application/json"; + return null; + }, + }, + json: vi.fn().mockRejectedValue(new Error("Invalid JSON")), + text: vi.fn().mockResolvedValue(""), + }; + fetchMock.mockResolvedValueOnce(mockResponseObj as any); + + const result = await auth.login({ + email: "test@test.com", + password: "123456", + }); + + expect(result).toEqual({}); + }); + }); + + describe("register", () => { + it("should register successfully", async () => { + fetchMock.mockResolvedValueOnce( + mockResponse({ + jsonData: { message: "User created", user: { id: 1, email: "new@test.com" } }, + }) + ); + + const result = await auth.register({ + email: "new@test.com", + password: "123456", + name: "New User", + }); + + expect(fetchMock).toHaveBeenCalledWith( + expect.stringContaining("/api/auth/register"), + expect.objectContaining({ + method: "POST", + body: JSON.stringify({ + email: "new@test.com", + password: "123456", + name: "New User", + }), + }) + ); + expect(result.user.id).toBe(1); + }); + + it("should register with optional fields", async () => { + fetchMock.mockResolvedValueOnce( + mockResponse({ + jsonData: { message: "User created", user: { id: 1 } }, + }) + ); + + const result = await auth.register({ + email: "new@test.com", + password: "123456", + name: "New User", + phone: "+5511999999999", + cpf: "12345678901", + }); + + expect(result.user.id).toBe(1); + const callArgs = fetchMock.mock.calls[0][1]; + expect(JSON.parse(callArgs.body)).toEqual({ + email: "new@test.com", + password: "123456", + name: "New User", + }); + }); + + it("should throw error on register failure with message", async () => { + fetchMock.mockResolvedValueOnce( + mockResponse({ + ok: false, + status: 400, + jsonData: { message: "Email already exists" }, + }) + ); + + await expect( + auth.register({ + email: "existing@test.com", + password: "123456", + name: "Existing", + }) + ).rejects.toThrow("Email already exists"); + }); + + it("should throw error on register failure without message", async () => { + fetchMock.mockResolvedValueOnce( + mockResponse({ + ok: false, + status: 500, + jsonData: {}, + }) + ); + + await expect( + auth.register({ + email: "test@test.com", + password: "123456", + name: "Test", + }) + ).rejects.toThrow("Falha ao cadastrar: 500 OK"); + }); + + it("should handle text response on register", async () => { + fetchMock.mockResolvedValueOnce( + mockResponse({ + ok: true, + contentType: "text/plain", + textData: "Registration successful", + }) + ); + + const result = await auth.register({ + email: "test@test.com", + password: "123456", + name: "Test User", + }); + + expect(result).toEqual({ message: "Registration successful" }); + }); + }); + + describe("logout", () => { + it("should logout successfully", async () => { + fetchMock.mockResolvedValueOnce( + mockResponse({ + jsonData: {}, + }) + ); + + await expect(auth.logout()).resolves.toBeUndefined(); + expect(fetchMock).toHaveBeenCalledWith( + expect.stringContaining("/api/auth/logout"), + expect.objectContaining({ + method: "POST", + credentials: "include", + }) + ); + }); + + it("should throw on logout failure with message", async () => { + fetchMock.mockResolvedValueOnce( + mockResponse({ + ok: false, + status: 500, + jsonData: { message: "Session expired" }, + }) + ); + + await expect(auth.logout()).rejects.toThrow("Session expired"); + }); + + it("should throw on logout failure without message", async () => { + fetchMock.mockResolvedValueOnce( + mockResponse({ + ok: false, + status: 500, + jsonData: {}, + }) + ); + + await expect(auth.logout()).rejects.toThrow("Falha ao fazer logout."); + }); + }); + + describe("getCurrentUser", () => { + it("should get current user successfully", async () => { + fetchMock.mockResolvedValueOnce( + mockResponse({ + jsonData: { id: 1, name: "Bene", email: "bene@test.com" }, + }) + ); + + const user = await auth.getCurrentUser(); + expect(user.name).toBe("Bene"); + expect(user.email).toBe("bene@test.com"); + expect(fetchMock).toHaveBeenCalledWith( + expect.stringContaining("/api/auth/me"), + { credentials: "include" } + ); + }); + + it("should throw when getCurrentUser fails with message", async () => { + fetchMock.mockResolvedValueOnce( + mockResponse({ + ok: false, + status: 401, + jsonData: { message: "Not authenticated" }, + }) + ); + + await expect(auth.getCurrentUser()).rejects.toThrow("Not authenticated"); + }); + + it("should throw when getCurrentUser fails without message", async () => { + fetchMock.mockResolvedValueOnce( + mockResponse({ + ok: false, + status: 403, + jsonData: {}, + }) + ); + + await expect(auth.getCurrentUser()).rejects.toThrow("Falha ao carregar usuário."); + }); + }); + + describe("buildApiUrl with base URL", () => { + it("should use VITE_API_BASE_URL when configured", async () => { + const originalEnv = import.meta.env.VITE_API_BASE_URL; + import.meta.env.VITE_API_BASE_URL = "https://api.example.com"; + + fetchMock.mockResolvedValueOnce( + mockResponse({ + jsonData: { token: "abc" }, + }) + ); + + await auth.login({ email: "test@test.com", password: "123456" }); + + expect(fetchMock).toHaveBeenCalledWith( + "https://api.example.com/api/auth/login", + expect.any(Object) + ); + + import.meta.env.VITE_API_BASE_URL = originalEnv; + }); + }); +}); \ No newline at end of file diff --git a/frontend/vite.config.js b/frontend/vite.config.js index c910189..45fb667 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -4,9 +4,7 @@ import { fileURLToPath } from 'url' import { defineConfig } from 'vite' const __dirname = path.dirname(fileURLToPath(import.meta.url)) -const apiProxyTarget = process.env.VITE_API_PROXY_TARGET || process.env.VITE_API_BASE_URL || 'http://localhost:3001' -// https://vite.dev/config/ export default defineConfig({ plugins: [react()], resolve: { @@ -15,8 +13,22 @@ export default defineConfig({ }, }, server: { + port: 5173, proxy: { - '/api': apiProxyTarget, - }, - }, -}) + '/api': { + target: 'https://jobsglobalscraper.ddns.net', + changeOrigin: true, + secure: false, + rewrite: (path) => path, + configure: (proxy, options) => { + proxy.on('error', (err, req, res) => { + console.log('proxy error', err); + }); + proxy.on('proxyReq', (proxyReq, req, res) => { + console.log('Proxying:', req.method, req.url); + }); + } + } + } + } +}) \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index dffb34e..eca391e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,7 +34,7 @@ "@commitlint/config-conventional": "^21.0.2", "@types/cors": "^2.8.19", "@types/express": "^5.0.6", - "@types/node": "^25.9.1", + "@types/node": "^25.9.2", "@types/pdfkit": "^0.17.6", "@types/pg": "^8.20.0", "@types/pino": "^7.0.5", diff --git a/package.json b/package.json index f14230e..55ef619 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "@commitlint/config-conventional": "^21.0.2", "@types/cors": "^2.8.19", "@types/express": "^5.0.6", - "@types/node": "^25.9.1", + "@types/node": "^25.9.2", "@types/pdfkit": "^0.17.6", "@types/pg": "^8.20.0", "@types/pino": "^7.0.5",