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 mutationMock = vi.fn()
const deleteManyMock = vi.fn() const deleteManyMock = vi.fn()
const assertAuthenticatedSession = vi.fn()
vi.mock("convex/browser", () => { vi.mock("convex/browser", () => {
const ConvexHttpClient = vi.fn(function ConvexHttpClientMock() { const ConvexHttpClient = vi.fn(function ConvexHttpClientMock() {
@ -24,27 +23,38 @@ vi.mock("@/lib/prisma", () => ({
}, },
})) }))
vi.mock("@/lib/auth-server", () => ({ type AdminSessionForTests = {
assertAuthenticatedSession: assertAuthenticatedSession, 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", () => { describe("POST /api/admin/devices/delete", () => {
const originalEnv = process.env.NEXT_PUBLIC_CONVEX_URL const originalEnv = process.env.NEXT_PUBLIC_CONVEX_URL
let restoreConsole: (() => void) | undefined let restoreConsole: (() => void) | undefined
beforeEach(() => { beforeEach(async () => {
process.env.NEXT_PUBLIC_CONVEX_URL = "https://convex.example" process.env.NEXT_PUBLIC_CONVEX_URL = "https://convex.example"
mutationMock.mockReset() mutationMock.mockReset()
deleteManyMock.mockReset() deleteManyMock.mockReset()
assertAuthenticatedSession.mockReset()
mutationMock.mockImplementation(async (_ctx, payload) => { mutationMock.mockImplementation(async (_ctx, payload) => {
if (payload && typeof payload === "object" && "machineId" in payload) { if (payload && typeof payload === "object" && "machineId" in payload) {
return { ok: true } return { ok: true }
} }
return { _id: "user_123" } return { _id: "user_123" }
}) })
assertAuthenticatedSession.mockResolvedValue({ const routeModule = (await import("./route")) as unknown as {
__setAssertAdminSessionForTests: AssertAdminSessionForTests
}
routeModule.__setAssertAdminSessionForTests(async () => ({
user: { user: {
email: "admin@example.com", email: "admin@example.com",
name: "Admin", name: "Admin",
@ -52,7 +62,7 @@ describe("POST /api/admin/devices/delete", () => {
tenantId: "tenant-1", tenantId: "tenant-1",
avatarUrl: null, avatarUrl: null,
}, },
}) }))
const consoleSpy = vi.spyOn(console, "error").mockImplementation(function noop() {}) const consoleSpy = vi.spyOn(console, "error").mockImplementation(function noop() {})
restoreConsole = () => consoleSpy.mockRestore() restoreConsole = () => consoleSpy.mockRestore()
}) })

View file

@ -2,7 +2,6 @@ import { NextResponse } from "next/server"
import { z } from "zod" import { z } from "zod"
import { ConvexHttpClient } from "convex/browser" import { ConvexHttpClient } from "convex/browser"
import { assertAuthenticatedSession } from "@/lib/auth-server"
import { DEFAULT_TENANT_ID } from "@/lib/constants" import { DEFAULT_TENANT_ID } from "@/lib/constants"
import { api } from "@/convex/_generated/api" import { api } from "@/convex/_generated/api"
import type { Id } from "@/convex/_generated/dataModel" import type { Id } from "@/convex/_generated/dataModel"
@ -10,11 +9,52 @@ import { prisma } from "@/lib/prisma"
export const runtime = "nodejs" 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({ const schema = z.object({
machineId: z.string().min(1), machineId: z.string().min(1),
}) })
export async function POST(request: Request) { export async function POST(request: Request) {
const assertAuthenticatedSession = await resolveAssertAuthenticatedSession()
const session = await assertAuthenticatedSession() const session = await assertAuthenticatedSession()
if (!session) { if (!session) {
return NextResponse.json({ error: "Não autorizado" }, { status: 401 }) return NextResponse.json({ error: "Não autorizado" }, { status: 401 })

View file

@ -1,13 +1,8 @@
import { describe, expect, it, beforeEach, vi } from "vitest" 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" import { MACHINE_CTX_COOKIE, serializeMachineCookie } from "@/server/machines/context"
const mockAssertSession = vi.fn()
vi.mock("@/lib/auth-server", () => ({
assertAuthenticatedSession: mockAssertSession,
}))
const mockCreateConvexClient = vi.fn() const mockCreateConvexClient = vi.fn()
vi.mock("@/server/convex-client", () => { vi.mock("@/server/convex-client", () => {
class ConvexConfigurationError extends Error {} class ConvexConfigurationError extends Error {}
@ -19,10 +14,12 @@ vi.mock("@/server/convex-client", () => {
const mockCookieStore = { const mockCookieStore = {
get: vi.fn<() => unknown>(), get: vi.fn<() => unknown>(),
getAll: vi.fn(() => []),
} }
vi.mock("next/headers", () => ({ vi.mock("next/headers", () => ({
cookies: vi.fn(async () => mockCookieStore), cookies: vi.fn(async () => mockCookieStore),
headers: vi.fn(async () => new Headers()),
})) }))
describe("GET /api/machines/session", () => { 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 () => { type TestMachineSession = {
mockAssertSession.mockResolvedValue({ user: { role: "admin" } }) 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")) const response = await GET(new NextRequest("https://example.com/api/machines/session"))
expect(response.status).toBe(403) 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 () => { it("returns machine context and sets cookie when lookup succeeds", async () => {
mockAssertSession.mockResolvedValue({ const routeModule = (await import("./route")) as unknown as {
user: { role: "machine", email: "device@example.com" }, GET: (request: NextRequest) => Promise<NextResponse>
}) __setAssertSessionForTests: AssertSessionForTests
}
const { GET, __setAssertSessionForTests } = routeModule
__setAssertSessionForTests(async () => ({
user: {
role: "machine",
email: "device@example.com",
},
}))
mockCookieStore.get.mockReturnValueOnce(undefined) mockCookieStore.get.mockReturnValueOnce(undefined)
const sampleContext = { const sampleContext = {
@ -86,7 +112,6 @@ describe("GET /api/machines/session", () => {
mutation: vi.fn(), mutation: vi.fn(),
}) })
const { GET } = await import("./route")
const response = await GET(new NextRequest("https://example.com/api/machines/session")) const response = await GET(new NextRequest("https://example.com/api/machines/session"))
expect(response.status).toBe(200) expect(response.status).toBe(200)

View file

@ -3,7 +3,6 @@ import { cookies } from "next/headers"
import { api } from "@/convex/_generated/api" import { api } from "@/convex/_generated/api"
import type { Id } from "@/convex/_generated/dataModel" import type { Id } from "@/convex/_generated/dataModel"
import { assertAuthenticatedSession } from "@/lib/auth-server"
import { DEFAULT_TENANT_ID } from "@/lib/constants" import { DEFAULT_TENANT_ID } from "@/lib/constants"
import { createConvexClient, ConvexConfigurationError } from "@/server/convex-client" import { createConvexClient, ConvexConfigurationError } from "@/server/convex-client"
import { import {
@ -15,12 +14,50 @@ import {
type MachineContextCookiePayload, type MachineContextCookiePayload,
} from "@/server/machines/context" } 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 // Força runtime Node.js para leitura consistente de cookies de sessão
export const runtime = "nodejs" export const runtime = "nodejs"
export async function GET(request: NextRequest) { export async function GET(request: NextRequest) {
const assertAuthenticatedSession = await resolveAssertAuthenticatedSession()
const session = await assertAuthenticatedSession() 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 }) 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(), dueAt: z.number().nullable().optional(),
firstResponseAt: z.number().nullable().optional(), firstResponseAt: z.number().nullable().optional(),
resolvedAt: z.number().nullable().optional(), resolvedAt: z.number().nullable().optional(),
closedAt: z.number().nullable().optional(),
reopenDeadline: z.number().nullable().optional(), reopenDeadline: z.number().nullable().optional(),
reopenWindowDays: z.number().nullable().optional(), reopenWindowDays: z.number().nullable().optional(),
reopenedAt: 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, dueAt: s.dueAt ? new Date(s.dueAt) : null,
firstResponseAt: s.firstResponseAt ? new Date(s.firstResponseAt) : null, firstResponseAt: s.firstResponseAt ? new Date(s.firstResponseAt) : null,
resolvedAt: s.resolvedAt ? new Date(s.resolvedAt) : 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, reopenDeadline: typeof s.reopenDeadline === "number" ? s.reopenDeadline : null,
reopenWindowDays: typeof s.reopenWindowDays === "number" ? s.reopenWindowDays : null, reopenWindowDays: typeof s.reopenWindowDays === "number" ? s.reopenWindowDays : null,
reopenedAt: typeof s.reopenedAt === "number" ? s.reopenedAt : 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, dueAt: base.dueAt ? new Date(base.dueAt) : null,
firstResponseAt: base.firstResponseAt ? new Date(base.firstResponseAt) : null, firstResponseAt: base.firstResponseAt ? new Date(base.firstResponseAt) : null,
resolvedAt: base.resolvedAt ? new Date(base.resolvedAt) : 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, reopenDeadline: typeof base.reopenDeadline === "number" ? base.reopenDeadline : null,
reopenWindowDays: typeof base.reopenWindowDays === "number" ? base.reopenWindowDays : null, reopenWindowDays: typeof base.reopenWindowDays === "number" ? base.reopenWindowDays : null,
reopenedAt: typeof base.reopenedAt === "number" ? base.reopenedAt : 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(), dueAt: z.coerce.date().nullable(),
firstResponseAt: z.coerce.date().nullable(), firstResponseAt: z.coerce.date().nullable(),
resolvedAt: z.coerce.date().nullable(), resolvedAt: z.coerce.date().nullable(),
closedAt: z.coerce.date().nullable().optional(),
updatedAt: z.coerce.date(), updatedAt: z.coerce.date(),
createdAt: z.coerce.date(), createdAt: z.coerce.date(),
tags: z.array(z.string()).default([]), tags: z.array(z.string()).default([]),