chore: ajustar tipos e remover any em rotas e tickets

This commit is contained in:
Esdras Renan 2025-11-14 14:41:17 -03:00
parent 50a80f5244
commit 72b25fafab
6 changed files with 140 additions and 24 deletions

View file

@ -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<AdminSessionForTests>) | 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()
})

View file

@ -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<AdminSession>
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<AssertAdminSessionFn> {
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 })

View file

@ -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<TestMachineSession>) | 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<NextResponse>
__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<NextResponse>
__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)

View file

@ -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<MachineSession>
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<AssertSessionFn> {
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 })
}

View file

@ -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,

View file

@ -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([]),