From 515d1718a6e2e601fc55ba79ab82953ca1d03c66 Mon Sep 17 00:00:00 2001 From: Esdras Renan Date: Sun, 19 Oct 2025 15:36:00 -0300 Subject: [PATCH] fix: allow removing orphaned machine agents --- .../api/admin/machines/delete/route.test.ts | 100 ++++++++++++++++++ src/app/api/admin/machines/delete/route.ts | 10 +- vitest.config.mts | 2 +- 3 files changed, 107 insertions(+), 5 deletions(-) create mode 100644 src/app/api/admin/machines/delete/route.test.ts diff --git a/src/app/api/admin/machines/delete/route.test.ts b/src/app/api/admin/machines/delete/route.test.ts new file mode 100644 index 0000000..e2ff774 --- /dev/null +++ b/src/app/api/admin/machines/delete/route.test.ts @@ -0,0 +1,100 @@ +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", () => ({ + ConvexHttpClient: vi.fn().mockImplementation(() => ({ + mutation: mutationMock, + })), +})) + +vi.mock("@/lib/prisma", () => ({ + prisma: { + authUser: { + deleteMany: deleteManyMock, + }, + }, +})) + +vi.mock("@/lib/auth-server", () => ({ + assertAuthenticatedSession: assertAuthenticatedSession, +})) + +describe("POST /api/admin/machines/delete", () => { + const originalEnv = process.env.NEXT_PUBLIC_CONVEX_URL + + let restoreConsole: (() => void) | undefined + + beforeEach(() => { + process.env.NEXT_PUBLIC_CONVEX_URL = "https://convex.example" + mutationMock.mockReset() + deleteManyMock.mockReset() + assertAuthenticatedSession.mockReset() + assertAuthenticatedSession.mockResolvedValue({ + user: { + email: "admin@example.com", + name: "Admin", + role: "ADMIN", + tenantId: "tenant-1", + avatarUrl: null, + }, + }) + mutationMock.mockResolvedValueOnce({ _id: "user_123" }) + const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {}) + restoreConsole = () => consoleSpy.mockRestore() + }) + + afterEach(() => { + process.env.NEXT_PUBLIC_CONVEX_URL = originalEnv + restoreConsole?.() + }) + + it("returns ok when the machine removal succeeds", async () => { + mutationMock.mockResolvedValueOnce({ ok: true }) + const { POST } = await import("./route") + const response = await POST( + new Request("http://localhost/api/admin/machines/delete", { + method: "POST", + body: JSON.stringify({ machineId: "jn_machine" }), + }) + ) + + expect(response.status).toBe(200) + await expect(response.json()).resolves.toEqual({ ok: true, machineMissing: false }) + expect(deleteManyMock).toHaveBeenCalledWith({ where: { email: "machine-jn_machine@machines.local" } }) + expect(mutationMock).toHaveBeenNthCalledWith(1, expect.anything(), expect.objectContaining({ email: "admin@example.com" })) + expect(mutationMock).toHaveBeenNthCalledWith(2, expect.anything(), expect.objectContaining({ machineId: "jn_machine" })) + }) + + it("still succeeds when the Convex machine is already missing", async () => { + mutationMock.mockRejectedValueOnce(new Error("Máquina não encontrada")) + const { POST } = await import("./route") + const response = await POST( + new Request("http://localhost/api/admin/machines/delete", { + method: "POST", + body: JSON.stringify({ machineId: "jn_machine" }), + }) + ) + + expect(response.status).toBe(200) + await expect(response.json()).resolves.toEqual({ ok: true, machineMissing: true }) + expect(deleteManyMock).toHaveBeenCalledWith({ where: { email: "machine-jn_machine@machines.local" } }) + }) + + it("returns an error for other Convex failures", async () => { + mutationMock.mockRejectedValueOnce(new Error("timeout error")) + const { POST } = await import("./route") + const response = await POST( + new Request("http://localhost/api/admin/machines/delete", { + method: "POST", + body: JSON.stringify({ machineId: "jn_machine" }), + }) + ) + + expect(response.status).toBe(500) + await expect(response.json()).resolves.toEqual({ error: "Falha ao remover máquina no Convex" }) + expect(deleteManyMock).not.toHaveBeenCalled() + }) +}) diff --git a/src/app/api/admin/machines/delete/route.ts b/src/app/api/admin/machines/delete/route.ts index a75a964..8d07458 100644 --- a/src/app/api/admin/machines/delete/route.ts +++ b/src/app/api/admin/machines/delete/route.ts @@ -48,6 +48,7 @@ export async function POST(request: Request) { return NextResponse.json({ error: "Falha ao identificar o administrador" }, { status: 500 }) } + let machineMissing = false try { await convex.mutation(api.machines.remove, { machineId: parsed.data.machineId as Id<"machines">, @@ -56,16 +57,17 @@ export async function POST(request: Request) { } catch (error) { const message = error instanceof Error ? error.message : "" if (message.includes("Máquina não encontrada")) { - return NextResponse.json({ error: "Máquina não encontrada" }, { status: 404 }) + machineMissing = true + } else { + console.error("[machines.delete] Convex failure", error) + return NextResponse.json({ error: "Falha ao remover máquina no Convex" }, { status: 500 }) } - console.error("[machines.delete] Convex failure", error) - return NextResponse.json({ error: "Falha ao remover máquina no Convex" }, { status: 500 }) } const machineEmail = `machine-${parsed.data.machineId}@machines.local` await prisma.authUser.deleteMany({ where: { email: machineEmail } }) - return NextResponse.json({ ok: true }) + return NextResponse.json({ ok: true, machineMissing }) } catch (error) { console.error("[machines.delete] Falha ao excluir", error) return NextResponse.json({ error: "Falha ao excluir máquina" }, { status: 500 }) diff --git a/vitest.config.mts b/vitest.config.mts index 0b80e08..6a13589 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -12,7 +12,7 @@ export default defineConfig({ }, }, test: { - pool: "vmThreads", + pool: (process.env.VITEST_POOL as "threads" | "forks" | "vmThreads" | undefined) ?? "threads", environment: "node", globals: true, include: ["src/**/*.test.ts", "tests/**/*.test.ts"],