chore: ajustar tipos e remover any em rotas e tickets
This commit is contained in:
parent
50a80f5244
commit
72b25fafab
6 changed files with 140 additions and 24 deletions
|
|
@ -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()
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -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 })
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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([]),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue