From 72b25fafabc8fd5237a13e07597b69588846da89 Mon Sep 17 00:00:00 2001 From: Esdras Renan Date: Fri, 14 Nov 2025 14:41:17 -0300 Subject: [PATCH] chore: ajustar tipos e remover any em rotas e tickets --- .../api/admin/devices/delete/route.test.ts | 26 +++++++--- src/app/api/admin/devices/delete/route.ts | 42 ++++++++++++++- src/app/api/machines/session/route.test.ts | 51 ++++++++++++++----- src/app/api/machines/session/route.ts | 41 ++++++++++++++- src/lib/mappers/ticket.ts | 3 ++ src/lib/schemas/ticket.ts | 1 + 6 files changed, 140 insertions(+), 24 deletions(-) diff --git a/src/app/api/admin/devices/delete/route.test.ts b/src/app/api/admin/devices/delete/route.test.ts index 5f62a1d..89c382c 100644 --- a/src/app/api/admin/devices/delete/route.test.ts +++ b/src/app/api/admin/devices/delete/route.test.ts @@ -2,7 +2,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest" const mutationMock = vi.fn() const deleteManyMock = vi.fn() -const assertAuthenticatedSession = vi.fn() vi.mock("convex/browser", () => { const ConvexHttpClient = vi.fn(function ConvexHttpClientMock() { @@ -24,27 +23,38 @@ vi.mock("@/lib/prisma", () => ({ }, })) -vi.mock("@/lib/auth-server", () => ({ - assertAuthenticatedSession: assertAuthenticatedSession, -})) +type AdminSessionForTests = { + user: { + email: string + name: string + role: string + tenantId: string | null + avatarUrl: string | null + } +} | null + +type AssertAdminSessionForTests = (fn: (() => Promise) | null) => void describe("POST /api/admin/devices/delete", () => { const originalEnv = process.env.NEXT_PUBLIC_CONVEX_URL let restoreConsole: (() => void) | undefined - beforeEach(() => { + beforeEach(async () => { process.env.NEXT_PUBLIC_CONVEX_URL = "https://convex.example" mutationMock.mockReset() deleteManyMock.mockReset() - assertAuthenticatedSession.mockReset() mutationMock.mockImplementation(async (_ctx, payload) => { if (payload && typeof payload === "object" && "machineId" in payload) { return { ok: true } } return { _id: "user_123" } }) - assertAuthenticatedSession.mockResolvedValue({ + const routeModule = (await import("./route")) as unknown as { + __setAssertAdminSessionForTests: AssertAdminSessionForTests + } + + routeModule.__setAssertAdminSessionForTests(async () => ({ user: { email: "admin@example.com", name: "Admin", @@ -52,7 +62,7 @@ describe("POST /api/admin/devices/delete", () => { tenantId: "tenant-1", avatarUrl: null, }, - }) + })) const consoleSpy = vi.spyOn(console, "error").mockImplementation(function noop() {}) restoreConsole = () => consoleSpy.mockRestore() }) diff --git a/src/app/api/admin/devices/delete/route.ts b/src/app/api/admin/devices/delete/route.ts index f05a30f..432abff 100644 --- a/src/app/api/admin/devices/delete/route.ts +++ b/src/app/api/admin/devices/delete/route.ts @@ -2,7 +2,6 @@ import { NextResponse } from "next/server" import { z } from "zod" import { ConvexHttpClient } from "convex/browser" -import { assertAuthenticatedSession } from "@/lib/auth-server" import { DEFAULT_TENANT_ID } from "@/lib/constants" import { api } from "@/convex/_generated/api" import type { Id } from "@/convex/_generated/dataModel" @@ -10,11 +9,52 @@ import { prisma } from "@/lib/prisma" export const runtime = "nodejs" +type AdminSession = { + user: { + email: string + name: string + role: string + tenantId: string | null + avatarUrl: string | null + } +} | null + +type AssertAdminSessionFn = () => Promise + +let testAssertSessionImpl: AssertAdminSessionFn | null = null + +const setAssertAdminSessionForTests: (fn: AssertAdminSessionFn | null) => void = (fn) => { + if (process.env.NODE_ENV !== "test") return + testAssertSessionImpl = fn +} + +// Exportado apenas para testes; tipado como `never` para não conflitar com o contrato de rotas do Next. +export const __setAssertAdminSessionForTests = setAssertAdminSessionForTests as never + +async function resolveAssertAuthenticatedSession(): Promise { + if (testAssertSessionImpl) return testAssertSessionImpl + + type AuthServerModuleLike = { + assertAuthenticatedSession?: AssertAdminSessionFn + default?: { + assertAuthenticatedSession?: AssertAdminSessionFn + } + } + + const mod = (await import("@/lib/auth-server")) as AuthServerModuleLike + const fn = mod.assertAuthenticatedSession ?? mod.default?.assertAuthenticatedSession + if (typeof fn !== "function") { + throw new Error("assertAuthenticatedSession not available") + } + return fn +} + const schema = z.object({ machineId: z.string().min(1), }) export async function POST(request: Request) { + const assertAuthenticatedSession = await resolveAssertAuthenticatedSession() const session = await assertAuthenticatedSession() if (!session) { return NextResponse.json({ error: "Não autorizado" }, { status: 401 }) diff --git a/src/app/api/machines/session/route.test.ts b/src/app/api/machines/session/route.test.ts index 35b814a..0ce4c59 100644 --- a/src/app/api/machines/session/route.test.ts +++ b/src/app/api/machines/session/route.test.ts @@ -1,13 +1,8 @@ import { describe, expect, it, beforeEach, vi } from "vitest" -import { NextRequest } from "next/server" +import { NextRequest, NextResponse } from "next/server" import { MACHINE_CTX_COOKIE, serializeMachineCookie } from "@/server/machines/context" -const mockAssertSession = vi.fn() -vi.mock("@/lib/auth-server", () => ({ - assertAuthenticatedSession: mockAssertSession, -})) - const mockCreateConvexClient = vi.fn() vi.mock("@/server/convex-client", () => { class ConvexConfigurationError extends Error {} @@ -19,10 +14,12 @@ vi.mock("@/server/convex-client", () => { const mockCookieStore = { get: vi.fn<() => unknown>(), + getAll: vi.fn(() => []), } vi.mock("next/headers", () => ({ cookies: vi.fn(async () => mockCookieStore), + headers: vi.fn(async () => new Headers()), })) describe("GET /api/machines/session", () => { @@ -35,10 +32,29 @@ describe("GET /api/machines/session", () => { }) }) - it("returns 403 when the current session is not a machine", async () => { - mockAssertSession.mockResolvedValue({ user: { role: "admin" } }) +type TestMachineSession = { + user: { + role: string + email: string + } +} | null - const { GET } = await import("./route") +type AssertSessionForTests = (fn: (() => Promise) | null) => void + + it("returns 403 when the current session is not a machine", async () => { + const routeModule = (await import("./route")) as unknown as { + GET: (request: NextRequest) => Promise + __setAssertSessionForTests: AssertSessionForTests + } + + const { GET, __setAssertSessionForTests } = routeModule + + __setAssertSessionForTests(async () => ({ + user: { + role: "admin", + email: "admin@example.com", + }, + })) const response = await GET(new NextRequest("https://example.com/api/machines/session")) expect(response.status).toBe(403) @@ -48,9 +64,19 @@ describe("GET /api/machines/session", () => { }) it("returns machine context and sets cookie when lookup succeeds", async () => { - mockAssertSession.mockResolvedValue({ - user: { role: "machine", email: "device@example.com" }, - }) + const routeModule = (await import("./route")) as unknown as { + GET: (request: NextRequest) => Promise + __setAssertSessionForTests: AssertSessionForTests + } + + const { GET, __setAssertSessionForTests } = routeModule + + __setAssertSessionForTests(async () => ({ + user: { + role: "machine", + email: "device@example.com", + }, + })) mockCookieStore.get.mockReturnValueOnce(undefined) const sampleContext = { @@ -86,7 +112,6 @@ describe("GET /api/machines/session", () => { mutation: vi.fn(), }) - const { GET } = await import("./route") const response = await GET(new NextRequest("https://example.com/api/machines/session")) expect(response.status).toBe(200) diff --git a/src/app/api/machines/session/route.ts b/src/app/api/machines/session/route.ts index e7a2a39..9065d45 100644 --- a/src/app/api/machines/session/route.ts +++ b/src/app/api/machines/session/route.ts @@ -3,7 +3,6 @@ import { cookies } from "next/headers" import { api } from "@/convex/_generated/api" import type { Id } from "@/convex/_generated/dataModel" -import { assertAuthenticatedSession } from "@/lib/auth-server" import { DEFAULT_TENANT_ID } from "@/lib/constants" import { createConvexClient, ConvexConfigurationError } from "@/server/convex-client" import { @@ -15,12 +14,50 @@ import { type MachineContextCookiePayload, } from "@/server/machines/context" +type MachineSession = { + user: { + role: string + email: string + } +} | null + +type AssertSessionFn = () => Promise + +let testAssertSessionImpl: AssertSessionFn | null = null + +const setAssertSessionForTests: (fn: AssertSessionFn | null) => void = (fn) => { + if (process.env.NODE_ENV !== "test") return + testAssertSessionImpl = fn +} + +// Exportado apenas para testes; tipado como `never` para não conflitar com o contrato de rotas do Next. +export const __setAssertSessionForTests = setAssertSessionForTests as never + +async function resolveAssertAuthenticatedSession(): Promise { + if (testAssertSessionImpl) return testAssertSessionImpl + + type AuthServerModuleLike = { + assertAuthenticatedSession?: AssertSessionFn + default?: { + assertAuthenticatedSession?: AssertSessionFn + } + } + + const mod = (await import("@/lib/auth-server")) as AuthServerModuleLike + const fn = mod.assertAuthenticatedSession ?? mod.default?.assertAuthenticatedSession + if (typeof fn !== "function") { + throw new Error("assertAuthenticatedSession not available") + } + return fn +} + // Força runtime Node.js para leitura consistente de cookies de sessão export const runtime = "nodejs" export async function GET(request: NextRequest) { + const assertAuthenticatedSession = await resolveAssertAuthenticatedSession() const session = await assertAuthenticatedSession() - if (!session || session.user?.role !== "machine") { + if (!session || session.user.role !== "machine") { return NextResponse.json({ error: "Sessão de dispositivo não encontrada." }, { status: 403 }) } diff --git a/src/lib/mappers/ticket.ts b/src/lib/mappers/ticket.ts index 80a904c..30ce62a 100644 --- a/src/lib/mappers/ticket.ts +++ b/src/lib/mappers/ticket.ts @@ -113,6 +113,7 @@ const serverTicketSchema = z.object({ dueAt: z.number().nullable().optional(), firstResponseAt: z.number().nullable().optional(), resolvedAt: z.number().nullable().optional(), + closedAt: z.number().nullable().optional(), reopenDeadline: z.number().nullable().optional(), reopenWindowDays: z.number().nullable().optional(), reopenedAt: z.number().nullable().optional(), @@ -261,6 +262,7 @@ export function mapTicketFromServer(input: unknown) { dueAt: s.dueAt ? new Date(s.dueAt) : null, firstResponseAt: s.firstResponseAt ? new Date(s.firstResponseAt) : null, resolvedAt: s.resolvedAt ? new Date(s.resolvedAt) : null, + closedAt: s.closedAt ? new Date(s.closedAt) : null, reopenDeadline: typeof s.reopenDeadline === "number" ? s.reopenDeadline : null, reopenWindowDays: typeof s.reopenWindowDays === "number" ? s.reopenWindowDays : null, reopenedAt: typeof s.reopenedAt === "number" ? s.reopenedAt : null, @@ -362,6 +364,7 @@ export function mapTicketWithDetailsFromServer(input: unknown) { dueAt: base.dueAt ? new Date(base.dueAt) : null, firstResponseAt: base.firstResponseAt ? new Date(base.firstResponseAt) : null, resolvedAt: base.resolvedAt ? new Date(base.resolvedAt) : null, + closedAt: base.closedAt ? new Date(base.closedAt) : null, reopenDeadline: typeof base.reopenDeadline === "number" ? base.reopenDeadline : null, reopenWindowDays: typeof base.reopenWindowDays === "number" ? base.reopenWindowDays : null, reopenedAt: typeof base.reopenedAt === "number" ? base.reopenedAt : null, diff --git a/src/lib/schemas/ticket.ts b/src/lib/schemas/ticket.ts index a059af8..c36de89 100644 --- a/src/lib/schemas/ticket.ts +++ b/src/lib/schemas/ticket.ts @@ -165,6 +165,7 @@ export const ticketSchema = z.object({ dueAt: z.coerce.date().nullable(), firstResponseAt: z.coerce.date().nullable(), resolvedAt: z.coerce.date().nullable(), + closedAt: z.coerce.date().nullable().optional(), updatedAt: z.coerce.date(), createdAt: z.coerce.date(), tags: z.array(z.string()).default([]),