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 1fac906..aa99eef 100644 --- a/frontend/src/components/login/RegisterSide.tsx +++ b/frontend/src/components/login/RegisterSide.tsx @@ -1,16 +1,11 @@ -import { useState, FormEvent } from "react"; -import { Eye, EyeOff, ArrowLeft, Github, Linkedin } from "lucide-react"; +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { register } 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"; import PhoneInput from "react-phone-number-input"; - -import { api } from "@/services/api"; import "react-phone-number-input/style.css"; -import { useNavigate } from "react-router-dom"; - -interface ValidationError { - _errors?: string[]; -} const STATIC_STARS = Array.from({ length: 40 }).map((_, i) => { const random = (min: number, max: number) => Math.random() * (max - min) + min; @@ -54,8 +49,6 @@ function StarsBackground() { } export default function RegisterSide() { - const navigate = useNavigate(); - const [showPassword, setShowPassword] = useState(false); const [nome, setNome] = useState(""); const [email, setEmail] = useState(""); @@ -68,8 +61,9 @@ export default function RegisterSide() { const [telefoneError, setTelefoneError] = useState(""); const [passwordError, setPasswordError] = useState(""); const [cpfError, setCpfError] = useState(""); - const [globalError, setGlobalError] = useState(""); - const [isSubmitting, setIsSubmitting] = useState(false); + + const [isLoading, setIsLoading] = useState(false); + const [apiError, setApiError] = useState(""); const handleRevealPassword = () => setShowPassword((prev) => !prev); @@ -81,36 +75,6 @@ export default function RegisterSide() { return `${digits.slice(0, 3)}.${digits.slice(3, 6)}.${digits.slice(6, 9)}-${digits.slice(9)}`; }; - const handleOAuthLogin = async (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 retornada pelo servidor."); - } - } catch (error: unknown) { - console.error("Erro ao iniciar fluxo OAuth:", error); - - let apiError = `Não foi possível conectar ao provedor ${provider}.`; - - // Verifica de forma segura se o erro contém um objeto response vindo 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 handleSubmit = async (e: FormEvent) => { e.preventDefault(); setNomeError(""); @@ -118,106 +82,75 @@ export default function RegisterSide() { setTelefoneError(""); setPasswordError(""); setCpfError(""); - setGlobalError(""); + 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) return; - - setIsSubmitting(true); - - try { - const response = await api.post("/auth/register", { - name: nome, - email: email, - phone: telefone, - password: password, - cpf: cpf.replace(/\D/g, ""), - }); - - console.log("Cadastro efetuado com sucesso!", response.data); - navigate("/app"); - - } catch (error: unknown) { - console.error("Erro na requisição de cadastro:", error); - setIsSubmitting(false); - - if (error && typeof error === "object" && "response" in error) { - const axiosError = error as { - response?: { - data?: { - error?: string | Record; - message?: string; - }; - }; - }; - - const data = axiosError.response?.data; - - if (data && typeof data.error === "object") { - const validationErrors = data.error as Record; - - if (validationErrors.name?._errors?.[0]) setNomeError(validationErrors.name._errors[0]); - if (validationErrors.email?._errors?.[0]) setEmailError(validationErrors.email._errors[0]); - if (validationErrors.phone?._errors?.[0]) setTelefoneError(validationErrors.phone._errors[0]); - if (validationErrors.password?._errors?.[0]) setPasswordError(validationErrors.password._errors[0]); - if (validationErrors.cpf?._errors?.[0]) setCpfError(validationErrors.cpf._errors[0]); - } else { - const fallbackMsg = data?.message || (typeof data?.error === "string" ? data.error : "") || "Erro ao efetuar o cadastro."; - if (fallbackMsg.toLowerCase().includes("email")) { - setEmailError("Este endereço de e-mail já está em uso."); - } else { - setGlobalError(fallbackMsg); - } - } - } else { - setGlobalError("Ocorreu um erro inesperado de conexão."); + if (isValid) { + setIsLoading(true); + try { + await register({ + email: email, + password: password, + name: nome, + }); + 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); } } }; return (
-
-
-

< @@ -232,128 +165,48 @@ export default function RegisterSide() {

-
- - {globalError && ( -
- {globalError} + {apiError && ( +
+

{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
-
- - - - -
-
); -} +} \ 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() {

-
- - {globalError && ( -
- {globalError} + {apiError && ( +
+

{apiError}

)} -
-
{passwordError &&

{passwordError}

}
-
- - {isSubmitting ? "Entrando..." : "Entrar"} + {isLoading ? "Entrando..." : "Entrar"}
-
@@ -270,37 +217,20 @@ export default function RightSide() { Ou faça login com
-
- - - - -
-
- ) -} + ); +} \ 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) => ( + + ), 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",