From 1f9a764c1482191ff3a2eba92f070a169ecb1c84 Mon Sep 17 00:00:00 2001 From: gustavo Date: Wed, 10 Jun 2026 03:50:38 -0300 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20corrige=20CORS,=20sess=C3=A3o=20cros?= =?UTF-8?q?s-site=20e=20fluxo=20de=20autentica=C3=A7=C3=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend: - cors.ts: adiciona PATCH/DELETE/OPTIONS, regex para preview deploys Vercel - session.ts: sameSite 'none' em produção para funcionar cross-site - app.ts: wrapper do cors para propagar erros ao errorHandler - errorHandler.ts: startsWith para capturar erros de CORS corretamente - credentials.controller.ts: /me retorna user completo via findById - credentials.service.ts: adiciona findById para busca por ID Frontend: - App.tsx: adiciona ProtectedRoute na rota /app - RegisterSide.tsx: login automático após cadastro via AuthContext - AuthContext.tsx: fetchCurrentUser reutilizável chamado após login Tests: - auth.routes.test.ts: findById mockado, /me espera { user } - credentials.controller.test.ts: findById no mock, assertions atualizadas --- backend/src/app.ts | 12 +++- backend/src/lib/session.ts | 6 +- backend/src/middleware/cors.ts | 17 +++-- backend/src/middleware/errorHandler.ts | 2 +- .../modules/auth/credentials.controller.ts | 9 ++- .../src/modules/auth/credentials.service.ts | 62 ++++++------------ .../integration/routes/auth.routes.test.ts | 64 ++++++------------- .../auth/credentials.controller.test.ts | 37 ++++++++--- frontend/src/App.tsx | 21 +++++- .../src/components/login/RegisterSide.tsx | 14 ++-- 10 files changed, 130 insertions(+), 114 deletions(-) diff --git a/backend/src/app.ts b/backend/src/app.ts index 09b9d76..6528da3 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -1,5 +1,5 @@ import cors from "cors"; -import express from "express"; +import express, { NextFunction, Request, Response } from "express"; import { corsOptions } from "./middleware/cors"; import { errorHandler } from "./middleware/errorHandler"; import { requireAuth } from "./middleware/requireAuth"; @@ -17,9 +17,15 @@ export function createJobsApiApp() { app.disable("x-powered-by"); app.use(express.json({ limit: "16kb" })); app.use(securityHeaders); - app.use(cors(corsOptions)); - //Confia no proxy reverso (nginx) para lidar com HTTPS e IPs reais dos clientes + // um wrapper para garantir se o CORS chega com erro 403 + app.use((req: Request, res: Response, next: NextFunction) => { + cors(corsOptions)(req, res, (err) => { + if (err) return next(err); + next(); + }); + }); + app.set("trust proxy", 1); app.use("/api/auth", withSession, authRoutes); diff --git a/backend/src/lib/session.ts b/backend/src/lib/session.ts index ebd3b85..a972a1f 100644 --- a/backend/src/lib/session.ts +++ b/backend/src/lib/session.ts @@ -1,12 +1,14 @@ import type { SessionOptions } from "iron-session"; +const isProd = process.env.NODE_ENV === "production"; + export const sessionOptions: SessionOptions = { password: process.env.SESSION_SECRET!, cookieName: "vagas_session", cookieOptions: { - secure: process.env.NODE_ENV === "production", + secure: isProd, httpOnly: true, - sameSite: "lax", + sameSite: isProd ? "none" : "lax", }, }; diff --git a/backend/src/middleware/cors.ts b/backend/src/middleware/cors.ts index a0aa227..afb37f9 100644 --- a/backend/src/middleware/cors.ts +++ b/backend/src/middleware/cors.ts @@ -2,7 +2,6 @@ import cors from "cors"; const DEFAULT_ALLOWED_ORIGINS = [ "https://painel-vagas-lake.vercel.app", - "https://painel-vagas-m6hbzlqeh-bene-teslas-projects.vercel.app", "https://jobsglobalscraper.ddns.net", "http://jobsglobalscraper.ddns.net", "http://localhost:5173", @@ -22,11 +21,19 @@ export const corsOptions: cors.CorsOptions = { const allowedOrigins = parseAllowedOrigins( process.env.CORS_ALLOWED_ORIGINS, ); - if (!origin || allowedOrigins.has(origin)) return callback(null, true); - callback(new Error("Origin not allowed by CORS")); + + if (!origin) return callback(null, true); + + if (/^https:\/\/painel-vagas-[a-z0-9-]+\.vercel\.app$/.test(origin)) { + return callback(null, true); + } + + if (allowedOrigins.has(origin)) return callback(null, true); + + callback(new Error(`Origin not allowed by CORS: ${origin}`)); }, - methods: ["GET", "POST", "OPTIONS"], - allowedHeaders: ["Content-Type", "Authorization"], + methods: ["GET", "POST", "PATCH", "DELETE", "OPTIONS"], + allowedHeaders: ["Content-Type", "Authorization", "X-Requested-With"], credentials: true, maxAge: 86400, }; diff --git a/backend/src/middleware/errorHandler.ts b/backend/src/middleware/errorHandler.ts index f9985f1..c54ccf5 100644 --- a/backend/src/middleware/errorHandler.ts +++ b/backend/src/middleware/errorHandler.ts @@ -6,7 +6,7 @@ export function errorHandler( res: Response, _next: NextFunction, ): void { - if (error.message === "Origin not allowed by CORS") { + if (error.message.startsWith("Origin not allowed by CORS")) { res.status(403).json({ message: "Origem não permitida." }); return; } diff --git a/backend/src/modules/auth/credentials.controller.ts b/backend/src/modules/auth/credentials.controller.ts index 29f1f3e..e82adf8 100644 --- a/backend/src/modules/auth/credentials.controller.ts +++ b/backend/src/modules/auth/credentials.controller.ts @@ -53,6 +53,13 @@ export class CredentialsController { if (!req.session.userId) { return res.status(401).json({ error: "Não autenticado" }); } - return res.json({ userId: req.session.userId }); + + const user = await this.service.findById(req.session.userId); + if (!user) { + await req.session.destroy(); + return res.status(401).json({ error: "Não autenticado" }); + } + + return res.json({ user }); } } diff --git a/backend/src/modules/auth/credentials.service.ts b/backend/src/modules/auth/credentials.service.ts index 0a228db..ca69b73 100644 --- a/backend/src/modules/auth/credentials.service.ts +++ b/backend/src/modules/auth/credentials.service.ts @@ -16,12 +16,19 @@ import { const argonOptions = { type: argon2.argon2id, - memoryCost: 65536, // 64 MB de memória - timeCost: 3, // 3 iterações - parallelism: 4, // Usa 4 threads paralelas + memoryCost: 65536, + timeCost: 3, + parallelism: 4, }; export class CredentialsService { + async findById(id: string): Promise { + const user = await db.query.users.findFirst({ + where: eq(users.id, id), + }); + return user ?? null; + } + async register( input: RegisterInput, ): Promise<{ user: User; session: Session }> { @@ -30,44 +37,25 @@ export class CredentialsService { const existingCredential = await db.query.credentials.findFirst({ where: eq(credentials.email, email), }); - - if (existingCredential) { - throw new Error("Email já cadastrado"); - } + if (existingCredential) throw new Error("Email já cadastrado"); const existingUser = await db.query.users.findFirst({ where: eq(users.email, email), }); - - if (existingUser) { - throw new Error("Email já cadastrado"); - } + if (existingUser) throw new Error("Email já cadastrado"); const passwordHash = await argon2.hash(password, argonOptions); const username = await generateUsername(name ?? email.split("@")[0], db); const [user] = await db .insert(users) - .values({ - email, - displayName: name, - username, - emailVerified: false, - }) + .values({ email, displayName: name, username, emailVerified: false }) .returning(); - await db.insert(credentials).values({ - userId: user.id, - email, - passwordHash, - }); - + await db.insert(credentials).values({ userId: user.id, email, passwordHash }); await db.insert(userPreferences).values({ userId: user.id }); - return { - user, - session: { userId: user.id }, - }; + return { user, session: { userId: user.id } }; } async login(input: LoginInput): Promise<{ user: User; session: Session }> { @@ -76,28 +64,16 @@ export class CredentialsService { const credential = await db.query.credentials.findFirst({ where: eq(credentials.email, email), }); - - if (!credential) { - throw new Error("Credenciais inválidas"); - } + if (!credential) throw new Error("Credenciais inválidas"); const valid = await argon2.verify(credential.passwordHash, password); - - if (!valid) { - throw new Error("Credenciais inválidas"); - } + if (!valid) throw new Error("Credenciais inválidas"); const user = await db.query.users.findFirst({ where: eq(users.id, credential.userId), }); + if (!user) throw new Error("Usuário não encontrado"); - if (!user) { - throw new Error("Usuário não encontrado"); - } - - return { - user, - session: { userId: user.id }, - }; + return { user, session: { userId: user.id } }; } } diff --git a/backend/tests/integration/routes/auth.routes.test.ts b/backend/tests/integration/routes/auth.routes.test.ts index e964233..a0cd64b 100644 --- a/backend/tests/integration/routes/auth.routes.test.ts +++ b/backend/tests/integration/routes/auth.routes.test.ts @@ -21,6 +21,7 @@ vi.mock("../../../src/modules/auth/auth.service", () => ({ const mockCredentialsService = vi.hoisted(() => ({ register: vi.fn(), login: vi.fn(), + findById: vi.fn(), // ← fix: adicionado })); vi.mock("../../../src/modules/auth/credentials.service", () => ({ @@ -109,6 +110,8 @@ describe("Integration - Auth Routes", () => { session: { userId: fixtureUser.id }, }); + mockCredentialsService.findById.mockResolvedValue(fixtureUser); + app = createJobsApiApp(); }); @@ -252,10 +255,7 @@ describe("Integration - Auth Routes", () => { .get(`${BASE}/twitter/callback?code=abc&state=valid-state-abc123`) .expect(400); - expect(res.body).toHaveProperty( - "error", - "Parâmetros de callback inválidos", - ); + expect(res.body).toHaveProperty("error", "Parâmetros de callback inválidos"); }); it("retorna 400 quando code esta ausente (ZodError)", async () => { @@ -288,10 +288,7 @@ describe("Integration - Auth Routes", () => { describe("POST /register", () => { it("cria usuario e retorna 201", async () => { - // CredentialsController usa req.session — injetado pelo withSession mock - vi.mocked(getIronSession).mockResolvedValue( - fixtureCredentialsSession as any, - ); + vi.mocked(getIronSession).mockResolvedValue(fixtureCredentialsSession as any); const res = await request(app) .post(`${BASE}/register`) @@ -303,9 +300,7 @@ describe("Integration - Auth Routes", () => { }); it("chama register com email, password e name", async () => { - vi.mocked(getIronSession).mockResolvedValue( - fixtureCredentialsSession as any, - ); + vi.mocked(getIronSession).mockResolvedValue(fixtureCredentialsSession as any); await request(app).post(`${BASE}/register`).send(registerPayload); @@ -319,9 +314,7 @@ describe("Integration - Auth Routes", () => { }); it("retorna 400 para email invalido (ZodError)", async () => { - vi.mocked(getIronSession).mockResolvedValue( - fixtureCredentialsSession as any, - ); + vi.mocked(getIronSession).mockResolvedValue(fixtureCredentialsSession as any); const res = await request(app) .post(`${BASE}/register`) @@ -332,9 +325,7 @@ describe("Integration - Auth Routes", () => { }); it("retorna 400 para senha curta (ZodError)", async () => { - vi.mocked(getIronSession).mockResolvedValue( - fixtureCredentialsSession as any, - ); + vi.mocked(getIronSession).mockResolvedValue(fixtureCredentialsSession as any); await request(app) .post(`${BASE}/register`) @@ -343,9 +334,7 @@ describe("Integration - Auth Routes", () => { }); it("retorna 409 quando email ja esta cadastrado", async () => { - vi.mocked(getIronSession).mockResolvedValue( - fixtureCredentialsSession as any, - ); + vi.mocked(getIronSession).mockResolvedValue(fixtureCredentialsSession as any); mockCredentialsService.register.mockRejectedValueOnce( new Error("Email já cadastrado"), ); @@ -359,9 +348,7 @@ describe("Integration - Auth Routes", () => { }); it("retorna 500 para erro inesperado no register", async () => { - vi.mocked(getIronSession).mockResolvedValue( - fixtureCredentialsSession as any, - ); + vi.mocked(getIronSession).mockResolvedValue(fixtureCredentialsSession as any); mockCredentialsService.register.mockRejectedValueOnce( new Error("db connection failed"), ); @@ -377,9 +364,7 @@ describe("Integration - Auth Routes", () => { describe("POST /login", () => { it("autentica e retorna 200 com user e session", async () => { - vi.mocked(getIronSession).mockResolvedValue( - fixtureCredentialsSession as any, - ); + vi.mocked(getIronSession).mockResolvedValue(fixtureCredentialsSession as any); const res = await request(app) .post(`${BASE}/login`) @@ -391,9 +376,7 @@ describe("Integration - Auth Routes", () => { }); it("chama login com email e password corretos", async () => { - vi.mocked(getIronSession).mockResolvedValue( - fixtureCredentialsSession as any, - ); + vi.mocked(getIronSession).mockResolvedValue(fixtureCredentialsSession as any); await request(app).post(`${BASE}/login`).send(loginPayload); @@ -403,9 +386,7 @@ describe("Integration - Auth Routes", () => { }); it("retorna 400 para email invalido (ZodError)", async () => { - vi.mocked(getIronSession).mockResolvedValue( - fixtureCredentialsSession as any, - ); + vi.mocked(getIronSession).mockResolvedValue(fixtureCredentialsSession as any); await request(app) .post(`${BASE}/login`) @@ -414,9 +395,7 @@ describe("Integration - Auth Routes", () => { }); it("retorna 400 para senha ausente (ZodError)", async () => { - vi.mocked(getIronSession).mockResolvedValue( - fixtureCredentialsSession as any, - ); + vi.mocked(getIronSession).mockResolvedValue(fixtureCredentialsSession as any); await request(app) .post(`${BASE}/login`) @@ -425,9 +404,7 @@ describe("Integration - Auth Routes", () => { }); it("retorna 401 para credenciais invalidas", async () => { - vi.mocked(getIronSession).mockResolvedValue( - fixtureCredentialsSession as any, - ); + vi.mocked(getIronSession).mockResolvedValue(fixtureCredentialsSession as any); mockCredentialsService.login.mockRejectedValueOnce( new Error("Credenciais inválidas"), ); @@ -441,9 +418,7 @@ describe("Integration - Auth Routes", () => { }); it("retorna 500 para erro inesperado no login", async () => { - vi.mocked(getIronSession).mockResolvedValue( - fixtureCredentialsSession as any, - ); + vi.mocked(getIronSession).mockResolvedValue(fixtureCredentialsSession as any); mockCredentialsService.login.mockRejectedValueOnce( new Error("db timeout"), ); @@ -486,15 +461,18 @@ describe("Integration - Auth Routes", () => { // ── GET /me ─────────────────────────────────────────────────────────────── describe("GET /me", () => { - it("retorna userId quando autenticado", async () => { + it("retorna user completo quando autenticado", async () => { vi.mocked(getIronSession).mockResolvedValue({ userId: "user-1", save: vi.fn(), + destroy: vi.fn(), } as any); const res = await request(app).get(`${BASE}/me`).expect(200); - expect(res.body).toEqual({ userId: "user-1" }); + expect(res.body).toHaveProperty("user"); + expect(res.body.user).toHaveProperty("id", fixtureUser.id); + expect(res.body.user).toHaveProperty("email", fixtureUser.email); }); it("retorna 401 quando nao autenticado", async () => { diff --git a/backend/tests/unit/modules/auth/credentials.controller.test.ts b/backend/tests/unit/modules/auth/credentials.controller.test.ts index 32948d1..c6ce3e4 100644 --- a/backend/tests/unit/modules/auth/credentials.controller.test.ts +++ b/backend/tests/unit/modules/auth/credentials.controller.test.ts @@ -1,7 +1,7 @@ import { Request, Response } from "express"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { z } from "zod"; -import { CredentialsController } from "../../../../src/modules/auth/credentials.controller"; // Ajuste se usar path aliases (ex: @/modules/...) +import { CredentialsController } from "../../../../src/modules/auth/credentials.controller"; // Mock do módulo de tipos usando o mesmo padrão de caminho do import do seu controller vi.mock("../../../../src/modules/types/credentials.types", () => ({ @@ -46,6 +46,12 @@ describe("CredentialsController", () => { user: { id: "user_123", email: "dev@teste.com" }, session: { id: "sess_abc" }, }), + // ← fix: findById agora existe no mock + findById: vi.fn().mockResolvedValue({ + id: "user_789", + email: "auth@teste.com", + displayName: "Teste", + }), }; controller = new CredentialsController(serviceMock); @@ -108,9 +114,7 @@ describe("CredentialsController", () => { ); expect(resMock.status).toHaveBeenCalledWith(409); - expect(resMock.json).toHaveBeenCalledWith({ - error: "Email já cadastrado", - }); + expect(resMock.json).toHaveBeenCalledWith({ error: "Email já cadastrado" }); }); }); @@ -140,9 +144,7 @@ describe("CredentialsController", () => { ); expect(resMock.status).toHaveBeenCalledWith(401); - expect(resMock.json).toHaveBeenCalledWith({ - error: "Credenciais inválidas", - }); + expect(resMock.json).toHaveBeenCalledWith({ error: "Credenciais inválidas" }); }); }); @@ -158,15 +160,32 @@ describe("CredentialsController", () => { }); describe("me", () => { - it("deve retornar o userId se o usuário estiver autenticado na sessão", async () => { + it("deve retornar o user completo se o usuário estiver autenticado na sessão", async () => { reqMock.session.userId = "user_789"; + + await controller.me(reqMock as unknown as Request, resMock as Response); + + expect(serviceMock.findById).toHaveBeenCalledWith("user_789"); + expect(resMock.json).toHaveBeenCalledWith({ + user: { id: "user_789", email: "auth@teste.com", displayName: "Teste" }, + }); + }); + + it("deve retornar 401 se findById não encontrar o usuário", async () => { + reqMock.session.userId = "user_inexistente"; + serviceMock.findById.mockResolvedValue(null); + await controller.me(reqMock as unknown as Request, resMock as Response); - expect(resMock.json).toHaveBeenCalledWith({ userId: "user_789" }); + + expect(resMock.status).toHaveBeenCalledWith(401); + expect(reqMock.session.destroy).toHaveBeenCalled(); }); it("deve retornar 401 se não houver userId armazenado na sessão", async () => { reqMock.session.userId = undefined; + await controller.me(reqMock as unknown as Request, resMock as Response); + expect(resMock.status).toHaveBeenCalledWith(401); }); }); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 57f0659..26fae95 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,11 +1,19 @@ import { useState, useEffect } from "react"; -import { Route, Routes } from "react-router-dom"; +import { Navigate, Route, Routes } from "react-router-dom"; import LandingPage from "./pages/dashboard/LandingPage"; import Dashboard from "./pages/dashboard/Dashboard"; import NotFound from "./not_found"; import Loading from "./Loading"; import LoginPage from "./pages/login/LoginPage"; import RegisterPage from "./pages/register/RegisterPage"; +import { useAuth } from "@/context/AuthContext"; + +function ProtectedRoute({ children }: { children: React.ReactNode }) { + const { user, isLoading } = useAuth(); + if (isLoading) return ; + if (!user) return ; + return <>{children}; +} function App() { const [appCarregando, setAppCarregando] = useState(true); @@ -25,9 +33,16 @@ function App() { return ( } /> - } /> + + + + } + /> } /> - }/> + } /> } /> ); diff --git a/frontend/src/components/login/RegisterSide.tsx b/frontend/src/components/login/RegisterSide.tsx index 1fac906..e20346c 100644 --- a/frontend/src/components/login/RegisterSide.tsx +++ b/frontend/src/components/login/RegisterSide.tsx @@ -7,6 +7,7 @@ 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"; +import { useAuth } from "@/context/AuthContext"; interface ValidationError { _errors?: string[]; @@ -55,6 +56,7 @@ function StarsBackground() { export default function RegisterSide() { const navigate = useNavigate(); + const { login } = useAuth(); const [showPassword, setShowPassword] = useState(false); const [nome, setNome] = useState(""); @@ -150,15 +152,19 @@ export default function RegisterSide() { setIsSubmitting(true); try { - const response = await api.post("/auth/register", { + // 1. Cadastra o usuário + await api.post("/auth/register", { name: nome, - email: email, + email, phone: telefone, - password: password, + password, cpf: cpf.replace(/\D/g, ""), }); - console.log("Cadastro efetuado com sucesso!", response.data); + // 2. Loga automaticamente após cadastro — popula o AuthContext corretamente + await login({ email, password }); + + // 3. Redireciona para o app navigate("/app"); } catch (error: unknown) { From 7a05cf3b0a9f73333ff4e675564010fd46fee8ae Mon Sep 17 00:00:00 2001 From: gustavo Date: Wed, 10 Jun 2026 04:06:38 -0300 Subject: [PATCH 2/2] test(frontend): fix authentication mock path in register side extended test --- .../login/RegisterSide.extended.test.tsx | 31 ++++++++++++++++--- .../components/login/RegisterSide.test.tsx | 4 ++- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/frontend/tests/unit/components/login/RegisterSide.extended.test.tsx b/frontend/tests/unit/components/login/RegisterSide.extended.test.tsx index 1b2ba37..d02c15d 100644 --- a/frontend/tests/unit/components/login/RegisterSide.extended.test.tsx +++ b/frontend/tests/unit/components/login/RegisterSide.extended.test.tsx @@ -28,9 +28,23 @@ vi.mock("react-phone-number-input", () => ({ ), })); +// Mock estável do react-router-dom protegendo contra hoisting const mockNavigate = vi.fn(); -vi.mock("react-router-dom", () => ({ - useNavigate: () => mockNavigate, +vi.mock("react-router-dom", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + useNavigate: () => mockNavigate, + }; +}); + +const mockLogin = vi.fn().mockResolvedValue(undefined); +vi.mock("@/context/AuthContext", () => ({ + useAuth: () => ({ + login: mockLogin, + user: null, + isLoading: false, + }), })); const mockApiPost = vi.fn(); @@ -58,15 +72,24 @@ describe("RegisterSide — branches extras", () => { mockApiPost.mockReset(); mockApiGet.mockReset(); mockNavigate.mockReset(); + mockLogin.mockReset(); }); // ── Branch: submit com sucesso → navega para /app ──────────────────────── it("navega para /app após cadastro bem-sucedido", async () => { - mockApiPost.mockResolvedValueOnce({ data: {} }); + mockApiPost.mockResolvedValueOnce({ data: { user: {}, token: "fake-token" } }); + mockLogin.mockResolvedValueOnce(undefined); + render(); fillValidForm(); + + // Dispara o clique no botão de cadastrar fireEvent.click(screen.getByRole("button", { name: /cadastrar/i })); - await waitFor(() => expect(mockNavigate).toHaveBeenCalledWith("/app")); + + // Aguarda as promessas resolverem e valida a navegação esperada + await waitFor(() => { + expect(mockNavigate).toHaveBeenCalledWith("/app"); + }); }); // ── Branch: API retorna erro de validação por campo (data.error é objeto) ─ diff --git a/frontend/tests/unit/components/login/RegisterSide.test.tsx b/frontend/tests/unit/components/login/RegisterSide.test.tsx index 5fc15fe..43394d7 100644 --- a/frontend/tests/unit/components/login/RegisterSide.test.tsx +++ b/frontend/tests/unit/components/login/RegisterSide.test.tsx @@ -28,8 +28,9 @@ vi.mock("react-phone-number-input", () => ({ ), })); +const mockNavigate = vi.fn(); vi.mock("react-router-dom", () => ({ - useNavigate: () => vi.fn(), + useNavigate: () => mockNavigate, })); const mockApiPost = vi.fn(); @@ -48,6 +49,7 @@ describe("RegisterSide", () => { beforeEach(() => { mockApiPost.mockClear(); mockApiGet.mockClear(); + mockNavigate.mockClear(); }); it("renderiza formulário completo de cadastro", () => {