- {globalError}
+ {apiError && (
+
)}
-
-
-
setNome(e.target.value)}
- placeholder="Bene Santos"
- 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 ${
- nomeError ? "border-red-500 focus:ring-red-500" : "border-gray-200 dark:border-neutral-700 focus:ring-blue-500 focus:border-transparent"
- }`}
- />
+
+
setNome(e.target.value)} placeholder="benevanio" 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 ${nomeError ? "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" : ""}`} />
{nomeError &&
{nomeError}
}
-
-
-
setEmail(e.target.value)}
- placeholder="lbene17@gmail.com"
- 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"
- }`}
- />
+
+
setEmail(e.target.value)} 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}
}
-
-
-
-
+
+
{telefoneError &&
{telefoneError}
}
-
-
+
- setPassword(e.target.value)}
- placeholder="Ex: ••••••••••••"
- 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"
- }`}
- />
-
{passwordError &&
{passwordError}
}
-
-
-
setCpf(formatCpf(e.target.value))}
- placeholder="091.000.000-00"
- 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 ${
- cpfError ? "border-red-500 focus:ring-red-500" : "border-gray-200 dark:border-neutral-700 focus:ring-blue-500 focus:border-transparent"
- }`}
- />
+
+
setCpf(formatCpf(e.target.value))} placeholder="091.000.000-00" 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 ${cpfError ? "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" : ""}`} />
{cpfError &&
{cpfError}
}
-
-
- {isSubmitting ? "Cadastrando..." : "Cadastrar"}
+
+ {isLoading ? "Cadastrando..." : "Cadastrar"}
-
@@ -363,37 +216,20 @@ export default function RegisterSide() {
Ou cadastre-se com
-
-
handleOAuthLogin("google")}
- className="flex justify-center items-center py-3 px-4 border border-gray-200 dark:border-neutral-800 rounded-xl bg-white/50 dark:bg-neutral-800/50 hover:bg-gray-50 dark:hover:bg-neutral-700/50 transition-all shadow-sm cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
- >
+
-
- handleOAuthLogin("linkedin")}
- className="flex justify-center items-center py-3 px-4 border border-gray-200 dark:border-neutral-800 rounded-xl bg-white/50 dark:bg-neutral-800/50 hover:bg-gray-50 dark:hover:bg-neutral-700/50 transition-all shadow-sm cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
- >
-
+
+
-
- handleOAuthLogin("github")}
- className="flex justify-center items-center py-3 px-4 border border-gray-200 dark:border-neutral-800 rounded-xl bg-white/50 dark:bg-neutral-800/50 hover:bg-gray-50 dark:hover:bg-neutral-700/50 transition-all shadow-sm cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
- >
-
+
+
-
);
-}
+}
\ No newline at end of file
diff --git a/frontend/src/components/login/RigthSide.tsx b/frontend/src/components/login/RigthSide.tsx
index fb7db33..a7e06e3 100644
--- a/frontend/src/components/login/RigthSide.tsx
+++ b/frontend/src/components/login/RigthSide.tsx
@@ -1,10 +1,9 @@
-import { useState, FormEvent } from "react"
-import { Eye, EyeOff, ArrowLeft, Github, Linkedin } from "lucide-react"
-import { Image } from "@unpic/react"
-import { motion } from "framer-motion"
-import { useNavigate } from "react-router-dom"
-import { useAuth } from "@/context/AuthContext"
-import { api } from "@/services/api"
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import { login } from "@/services/authService";
+import { Image } from "@unpic/react";
+import { motion } from "framer-motion";
+import { ArrowLeft, Eye, EyeOff } from "lucide-react";
+import { FormEvent, useState } from "react";
const STATIC_STARS = Array.from({ length: 40 }).map((_, i) => {
const random = (min: number, max: number) => Math.random() * (max - min) + min;
@@ -48,122 +47,75 @@ function StarsBackground() {
}
export default function RightSide() {
- const navigate = useNavigate();
- const { login } = useAuth();
+ const [showPassword, setShowPassword] = useState(false);
+ const [email, setEmail] = useState("");
+ const [password, setPassword] = useState("");
+ const [emailError, setEmailError] = useState("");
+ const [passwordError, setPasswordError] = useState("");
+ const [isLoading, setIsLoading] = useState(false);
+ const [apiError, setApiError] = useState("");
- const [showPassword, setShowPassword] = useState(false)
- const [email, setEmail] = useState("")
- const [password, setPassword] = useState("")
-
- const [emailError, setEmailError] = useState("")
- const [passwordError, setPasswordError] = useState("")
- const [globalError, setGlobalError] = useState("")
- const [isSubmitting, setIsSubmitting] = useState(false)
-
- const handleRevealPassword = () => setShowPassword((prev) => !prev)
-
- async function handleOAuthLogin(provider: "google" | "github" | "linkedin") {
- try {
- setGlobalError("");
- setIsSubmitting(true);
-
- const response = await api.get(`/auth/${provider}/url`);
-
- if (response.data?.url) {
- window.location.href = response.data.url;
- } else {
- throw new Error("URL de redirecionamento não encontrada.");
- }
- } catch (error: unknown) {
- console.error(`Erro ao iniciar OAuth com ${provider}:`, error);
-
- let apiError = `Não foi possível conectar ao provedor de login com o ${provider}.`;
-
- // Validação defensiva do erro do Axios
- if (error && typeof error === "object" && "response" in error) {
- const axiosError = error as { response?: { data?: { error?: string } } };
- if (typeof axiosError.response?.data?.error === "string") {
- apiError = axiosError.response.data.error;
- }
- }
-
- setGlobalError(apiError);
- setIsSubmitting(false);
- }
- }
+ const handleRevealPassword = () => setShowPassword((prev) => !prev);
const handleSubmit = async (e: FormEvent) => {
- e.preventDefault()
+ e.preventDefault();
- setEmailError("")
- setPasswordError("")
- setGlobalError("")
+ setEmailError("");
+ setPasswordError("");
+ setApiError("");
- let isValid = true
+ let isValid = true;
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@]+$/
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
- setEmailError("Por favor, insira um e-mail válido.")
- isValid = false
+ setEmailError("Por favor, insira um e-mail válido.");
+ 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 (!isValid) return;
-
- setIsSubmitting(true)
-
- try {
- await login({ email, password });
- navigate("/app");
- } catch (err: unknown) {
- console.error(err);
- let errorMessage = "Credenciais inválidas ou erro de conexão.";
-
- // Validação defensiva do erro repassado pelo Contexto/Axios
- if (err && typeof err === "object" && "response" in err) {
- const axiosError = err as { response?: { data?: { error?: string } } };
- if (typeof axiosError.response?.data?.error === "string") {
- errorMessage = axiosError.response.data.error;
+ if (isValid) {
+ setIsLoading(true);
+ try {
+ const result = await login({ email, password });
+
+ if (result.token) {
+ localStorage.setItem("token", result.token);
}
+
+ window.location.href = "/dashboard";
+ } catch (error: any) {
+ setApiError(error.message || "Erro ao fazer login. Verifique suas credenciais.");
+ } finally {
+ setIsLoading(false);
}
-
- setGlobalError(errorMessage);
- } finally {
- setIsSubmitting(false)
}
- }
+ };
return (
-
-
-
<
@@ -178,15 +130,12 @@ export default function RightSide() {
-
-
@@ -270,37 +217,20 @@ export default function RightSide() {
Ou faça login com
-
-
handleOAuthLogin("google")}
- className="flex justify-center items-center py-3 px-4 border border-gray-200 dark:border-neutral-800 rounded-xl bg-white/50 dark:bg-neutral-800/50 hover:bg-gray-50 dark:hover:bg-neutral-700/50 transition-all shadow-sm cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
- >
+
-
- handleOAuthLogin("linkedin")}
- className="flex justify-center items-center py-3 px-4 border border-gray-200 dark:border-neutral-800 rounded-xl bg-white/50 dark:bg-neutral-800/50 hover:bg-gray-50 dark:hover:bg-neutral-700/50 transition-all shadow-sm cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
- >
-
+
+
-
- handleOAuthLogin("github")}
- className="flex justify-center items-center py-3 px-4 border border-gray-200 dark:border-neutral-800 rounded-xl bg-white/50 dark:bg-neutral-800/50 hover:bg-gray-50 dark:hover:bg-neutral-700/50 transition-all shadow-sm cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
- >
-
+
+
-
- )
-}
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/services/authService.ts b/frontend/src/services/authService.ts
new file mode 100644
index 0000000..23a6d58
--- /dev/null
+++ b/frontend/src/services/authService.ts
@@ -0,0 +1,187 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+
+function normalizeBaseUrl(value: string | undefined): string {
+ return typeof value === "string" ? value.trim().replace(/\/+$/, "") : "";
+}
+
+function getApiBaseUrl(): string {
+ const configuredBaseUrl = normalizeBaseUrl(import.meta.env.VITE_API_BASE_URL);
+ if (configuredBaseUrl) {
+ console.log("Usando VITE_API_BASE_URL:", configuredBaseUrl);
+ return configuredBaseUrl;
+ }
+ console.log(" Sem VITE_API_BASE_URL, usando caminho relativo");
+ return "";
+}
+
+function buildApiUrl(path: string): string {
+ const normalizedPath = path.startsWith("/") ? path : `/${path}`;
+ const baseUrl = getApiBaseUrl();
+ const fullUrl = baseUrl ? `${baseUrl}${normalizedPath}` : normalizedPath;
+ console.log(" Construindo URL:", { path, baseUrl, fullUrl });
+ return fullUrl;
+}
+
+async function readPayload(response: Response): Promise> {
+ const contentType = response.headers?.get?.("content-type") ?? "";
+ console.log(" Content-Type:", contentType);
+
+ if (!contentType || contentType.includes("application/json")) {
+ try {
+ const payload = await response.json();
+ console.log(" Payload JSON:", payload);
+ return payload && typeof payload === "object"
+ ? (payload as Record)
+ : {};
+ } catch (error) {
+ console.log(" Erro ao parsear JSON:", error);
+ return {};
+ }
+ }
+
+ if (typeof response.text === "function") {
+ const text = await response.text();
+ console.log("Resposta texto:", text);
+ return text ? { message: text } : {};
+ }
+
+ return {};
+}
+
+function buildError(message: unknown, fallback: string): Error {
+ const errorMsg = typeof message === "string" && message ? message : fallback;
+ console.error("Erro construído:", errorMsg);
+ return new Error(errorMsg);
+}
+
+function readMessage(payload: unknown): string | undefined {
+ if (payload && typeof payload === "object" && "message" in payload) {
+ const message = (payload as { message?: unknown }).message;
+ if (typeof message === "string") return message;
+ }
+ return undefined;
+}
+
+export interface LoginCredentials {
+ email: string;
+ password: string;
+}
+
+export interface RegisterData {
+ email: string;
+ password: string;
+ name: string;
+ phone?: string;
+ cpf?: string;
+}
+
+export async function login(credentials: LoginCredentials): Promise<{ token?: string; user?: any; message?: string }> {
+ console.log("Iniciando login com:", { email: credentials.email });
+ const url = buildApiUrl("/api/auth/login");
+ console.log("URL:", url);
+ console.log("Método: POST");
+
+ const response = await fetch(url, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(credentials),
+ credentials: "include",
+ });
+
+ console.log("Status da resposta:", response.status);
+ console.log("Status text:", response.statusText);
+
+ const payload = await readPayload(response);
+
+ if (!response.ok) {
+ console.error("Login falhou:", payload);
+ throw buildError(readMessage(payload), "Falha ao fazer login.");
+ }
+
+ console.log("Login bem sucedido:", payload);
+ return payload;
+}
+
+export async function register(userData: RegisterData): Promise<{ message?: string; user?: any }> {
+ console.log("Iniciando registro com:", { email: userData.email, name: userData.name });
+ const url = buildApiUrl("/api/auth/register");
+ console.log("URL:", url);
+ console.log("Método: POST");
+
+ const bodyData = {
+ email: userData.email,
+ password: userData.password,
+ name: userData.name,
+ };
+ console.log("Body enviado:", bodyData);
+
+ const response = await fetch(url, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(bodyData),
+ credentials: "include",
+ });
+
+ console.log("Status da resposta:", response.status);
+ console.log("Status text:", response.statusText);
+ console.log("Headers:", {
+ contentType: response.headers.get("content-type"),
+ cors: response.headers.get("access-control-allow-origin"),
+ });
+
+ const payload = await readPayload(response);
+
+ if (!response.ok) {
+ console.error(" Registro falhou:", { status: response.status, payload });
+ throw buildError(readMessage(payload), `Falha ao cadastrar: ${response.status} ${response.statusText}`);
+ }
+
+ console.log(" Registro bem sucedido:", payload);
+ return payload;
+}
+
+export async function logout(): Promise {
+ console.log(" Iniciando logout");
+ const url = buildApiUrl("/api/auth/logout");
+ console.log(" URL:", url);
+
+ const response = await fetch(url, {
+ method: "POST",
+ credentials: "include",
+ });
+
+ console.log(" Status:", response.status);
+
+ if (!response.ok) {
+ const payload = await readPayload(response);
+ console.error(" Logout falhou:", payload);
+ throw buildError(readMessage(payload), "Falha ao fazer logout.");
+ }
+
+ console.log("Logout bem sucedido");
+}
+
+export async function getCurrentUser(): Promise {
+ console.log("🚀 Buscando usuário atual");
+ const url = buildApiUrl("/api/auth/me");
+ console.log("URL:", url);
+
+ const response = await fetch(url, {
+ credentials: "include",
+ });
+
+ console.log("📡 Status:", response.status);
+ const payload = await readPayload(response);
+
+ if (!response.ok) {
+ console.error(" Busca falhou:", payload);
+ throw buildError(readMessage(payload), "Falha ao carregar usuário.");
+ }
+
+ console.log(" Usuário encontrado:", payload);
+ return payload;
+}
\ No newline at end of file
diff --git a/frontend/tests/unit/components/login/RegisterSide.test.tsx b/frontend/tests/unit/components/login/RegisterSide.test.tsx
index 5fc15fe..81a23a6 100644
--- a/frontend/tests/unit/components/login/RegisterSide.test.tsx
+++ b/frontend/tests/unit/components/login/RegisterSide.test.tsx
@@ -1,4 +1,16 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
+
+import { fireEvent, render, screen, waitFor } from "@testing-library/react";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+
+// =====================
+// MOCKS
+// =====================
+const mockRegister = vi.fn();
+
+vi.mock("@/services/authService", () => ({
+ register: (...args: any[]) => mockRegister(...args),
+}));
/* eslint-disable @typescript-eslint/no-unused-vars */
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
@@ -10,6 +22,10 @@ vi.mock("@unpic/react", () => ({
vi.mock("framer-motion", () => ({
motion: {
+ div: ({ children, ...props }: any) => {children}
,
+ button: ({ children, ...props }: any) => (
+ {children}
+ ),
div: ({ children, whileHover, whileTap, animate, transition, ...props }: any) =>
{children}
,
button: ({ children, whileHover, whileTap, animate, transition, ...props }: any) =>
@@ -18,12 +34,13 @@ vi.mock("framer-motion", () => ({
}));
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}
/>
),
}));
@@ -43,51 +60,163 @@ vi.mock("@/services/api", () => ({
import RegisterSide from "@/components/login/RegisterSide";
+// =====================
+// TEST SUITE
+// =====================
describe("RegisterSide", () => {
+ const originalLocation = window.location;
+
+ beforeEach(() => {
+ mockRegister.mockReset();
+
+ Object.defineProperty(window, "location", {
+ configurable: true,
+ value: { href: "" },
+ });
+ });
+
+ afterEach(() => {
+ Object.defineProperty(window, "location", {
+ configurable: true,
+ value: originalLocation,
+ });
beforeEach(() => {
mockApiPost.mockClear();
mockApiGet.mockClear();
});
- 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(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();
+ 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 email, senha e cpf inválidos", () => {
+
+ it("valida CPF quando preenchido incorretamente", async () => {
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: "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 }));
+
+ await waitFor(() => {
+ expect(mockRegister).toHaveBeenCalled();
+ });
+ });
+
+ 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.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("mostra loading durante requisição", async () => {
+ mockRegister.mockImplementation(
+ () => new Promise((resolve) => setTimeout(resolve, 200))
+ );
+
+ render();
+
+ fireEvent.change(screen.getByLabelText(/nome/i), {
+ target: { value: "Bene" },
+ });
it("formata cpf e envia submit válido", async () => {
mockApiPost.mockResolvedValueOnce({ data: {} });
@@ -98,10 +227,42 @@ describe("RegisterSide", () => {
fireEvent.change(screen.getByLabelText(/senha/i), { target: { value: "123456" } });
fireEvent.change(screen.getByLabelText(/cpf/i), { target: { value: "12345678901" } });
- 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(
+ 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" },
await waitFor(() => {
expect(mockApiPost).toHaveBeenCalledWith("/auth/register", {
name: "Bene",
@@ -111,5 +272,11 @@ describe("RegisterSide", () => {
cpf: "12345678901",
});
});
+
+ 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 e5a5a9c..1000c48 100644
--- a/frontend/tests/unit/components/login/RigthSide.test.tsx
+++ b/frontend/tests/unit/components/login/RigthSide.test.tsx
@@ -1,4 +1,12 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
+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),
+}));
import { fireEvent, render, screen } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import "@testing-library/jest-dom/vitest";
@@ -34,52 +42,170 @@ vi.mock("@/services/api", () => ({
import RigthSide from "@/components/login/RigthSide";
describe("RigthSide", () => {
+ const originalLocation = window.location;
+
+ beforeEach(() => {
+ 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();
beforeEach(() => {
mockLogin.mockClear();
});
- 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("exibe erro genérico quando API retorna erro sem mensagem", async () => {
+ mockLogin.mockRejectedValueOnce(new Error(""));
+
it("envia formulário valido", async () => {
mockLogin.mockResolvedValueOnce(undefined);
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();
+ });
+ });
+
+ 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();
expect(mockLogin).toHaveBeenCalledWith({
email: "qa@teste.com",
password: "123456",
});
});
-});
+
+ 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 e5a4617..c4a9395 100644
--- a/frontend/vite.config.js
+++ b/frontend/vite.config.js
@@ -4,9 +4,9 @@ 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/
+const isDev = process.env.NODE_ENV === 'development'
+
export default defineConfig({
plugins: [react()],
resolve: {
@@ -14,28 +14,14 @@ export default defineConfig({
'@': path.resolve(__dirname, './src'),
},
},
- server: {
+ server: isDev ? {
+ port: 5173,
proxy: {
'/api': {
- target: apiProxyTarget,
- changeOrigin: true,
- secure: false,
- },
- '/auth': {
- target: apiProxyTarget,
+ target: 'https://jobsglobalscraper.ddns.net',
changeOrigin: true,
secure: false,
- configure: (proxy) => {
- proxy.on('error', (err) => {
- console.error('Erro no proxy do Vite para /auth:', err.message);
- });
- },
- },
- },
- },
- test: {
- globals: true,
- environment: 'jsdom',
- setupFiles: ['./tests/setup.js'],
- },
-})
+ }
+ }
+ } : undefined,
+})
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 79e69c5..5564abb 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",