import { beforeEach, describe, expect, it, vi } from "vitest"; const actionState = vi.hoisted(() => ({ authorized: 0, selected: [] as unknown[], transactionSelected: [] as unknown[], updates: [] as Record[], inserts: [] as unknown[], audits: [] as Array<{ admin: unknown; subject: string; type: string; data: Record; correlationId?: string }>, auditFailure: false, executions: [] as Array<{ connection: Record; command: string }>, gatewayResult: { ok: true, response: "private response" } as | { ok: true; response: string } | { ok: false; reason: "busy" | "timeout" | "unavailable" }, })); vi.mock("@/lib/auth/require-admin", () => ({ requireAdminSession: async () => { actionState.authorized += 1; return { email: "admin@example.test", name: "Admin" }; }, })); vi.mock("next/headers", () => ({ headers: async () => new Headers() })); vi.mock("next/navigation", () => ({ redirect: (path: string) => { throw new Error(`REDIRECT:${path}`); }, })); vi.mock("@/lib/database", () => { function selection(result: unknown[]) { const chain = { from: () => chain, where: () => chain, limit: async () => result, }; return chain; } const tx = { execute: async () => undefined, select: () => selection(actionState.transactionSelected), update: () => ({ set: (value: Record) => ({ where: async () => { actionState.updates.push(value); }, }), }), insert: () => ({ values: async (value: unknown) => { actionState.inserts.push(value); }, }), }; return { db: { select: () => selection(actionState.selected), transaction: async (callback: (transaction: typeof tx) => Promise) => callback(tx), }, }; }); vi.mock("@/lib/rcon-validation", () => ({ validateRconCommand: (value: unknown) => typeof value === "string" && value.trim() ? value.trim() : null, validateRconConnection: (input: { name?: string; host?: string; port?: number; password?: string }) => { if (!input.name || !input.host || !input.port) return null; return input; }, })); vi.mock("@/lib/rcon-credentials", () => ({ decryptRconPassword: () => "decrypted-password", encryptRconPassword: vi.fn(), rconCommandDigest: () => "hmac-sha256:v1:digest", })); vi.mock("@/lib/rcon-gateway", () => ({ executeRcon: async (connection: Record, command: string) => { actionState.executions.push({ connection, command }); return actionState.gatewayResult; }, testRconConnection: vi.fn(), })); vi.mock("@/lib/audit", () => ({ recordAdminSubjectEvent: async ( admin: unknown, subject: string, type: string, data: Record, options?: { correlationId?: string }, ) => { if (actionState.auditFailure) throw new Error("audit unavailable"); actionState.audits.push({ admin, subject, type, data, correlationId: options?.correlationId }); return "22222222-2222-4222-8222-222222222222"; }, })); import { createRconServer, deleteRconServer, executeRconCommand, setRconServerEnabled, testSavedRconServer, updateRconServer, } from "./actions"; const serverId = "11111111-1111-4111-8111-111111111111"; const savedServer = { id: serverId, name: "Season 4", host: "season4.somc.svc.cluster.local", port: 25575, encryptedPassword: "ciphertext", enabled: true, createdAt: new Date(), updatedAt: new Date(), }; describe("RCON server actions", () => { beforeEach(() => { actionState.authorized = 0; actionState.selected = []; actionState.transactionSelected = []; actionState.updates = []; actionState.inserts = []; actionState.audits = []; actionState.auditFailure = false; actionState.executions = []; actionState.gatewayResult = { ok: true, response: "private response" }; }); it("independently authorizes every exported operation before accepting input", async () => { await expect(createRconServer(new FormData())).rejects.toThrow("REDIRECT:/admin/rcon?error=invalid-connection"); await expect(updateRconServer(new FormData())).rejects.toThrow("REDIRECT:/admin/rcon?error=invalid-connection"); await expect(setRconServerEnabled(new FormData())).rejects.toThrow("REDIRECT:/admin/rcon?error=unknown-connection"); await expect(deleteRconServer(new FormData())).rejects.toThrow("REDIRECT:/admin/rcon?error=confirmation-required"); await expect(testSavedRconServer(new FormData())).rejects.toThrow("REDIRECT:/admin/rcon?error=connection-unavailable"); await expect(executeRconCommand({ status: "idle", message: "", serverId: "" }, new FormData())).resolves.toEqual({ status: "error", message: "Enter one command of at most 1,024 bytes without control characters.", serverId: "", }); expect(actionState.authorized).toBe(6); }); it("preserves the encrypted password on an unrelated connection update", async () => { actionState.transactionSelected = [savedServer]; const formData = new FormData(); formData.set("serverId", serverId); formData.set("name", "Renamed server"); formData.set("host", "season4.somc.svc.cluster.local"); formData.set("port", "25575"); formData.set("password", ""); formData.set("enabled", "yes"); await expect(updateRconServer(formData)).rejects.toThrow("REDIRECT:/admin/rcon?saved=updated"); expect(actionState.updates).toEqual([ expect.objectContaining({ encryptedPassword: "ciphertext", enabled: true }), ]); expect(JSON.stringify(actionState.inserts)).not.toContain("ciphertext"); expect(JSON.stringify(actionState.inserts)).not.toContain("decrypted-password"); }); it("rechecks enabled saved state and records complete command lifecycle audits", async () => { actionState.selected = [savedServer]; const formData = new FormData(); formData.set("serverId", serverId); formData.set("command", "say private value"); await expect(executeRconCommand({ status: "idle", message: "", serverId: "" }, formData)).resolves.toEqual({ status: "success", message: "private response", serverId, }); expect(actionState.authorized).toBe(1); expect(actionState.executions).toEqual([{ connection: expect.objectContaining({ id: serverId, enabled: true, password: "decrypted-password" }), command: "say private value", }]); expect(actionState.audits).toEqual([ expect.objectContaining({ admin: { email: "admin@example.test", name: "Admin" }, subject: `rcon-server/${serverId}`, type: "games.minecraft.account-manager.rcon.command.requested", data: expect.objectContaining({ command: "say private value", verb: "say", commandDigest: "hmac-sha256:v1:digest" }), correlationId: expect.stringMatching(/^[0-9a-f-]{36}$/), }), expect.objectContaining({ subject: `rcon-server/${serverId}`, type: "games.minecraft.account-manager.rcon.command.completed", data: expect.objectContaining({ success: true, durationMs: expect.any(Number) }), correlationId: expect.stringMatching(/^[0-9a-f-]{36}$/), }), ]); const serializedAudits = JSON.stringify(actionState.audits); expect(actionState.audits[0]?.correlationId).toBe(actionState.audits[1]?.correlationId); expect(serializedAudits).toContain("private value"); expect(serializedAudits).not.toContain("private response"); expect(serializedAudits).not.toContain("decrypted-password"); }); it.each([ ["busy", "Another command is already running for this server."], ["timeout", "The RCON request timed out."], ["unavailable", "The RCON server was unavailable or rejected authentication."], ] as const)("returns a safe %s failure without exposing transport details", async (reason, message) => { actionState.selected = [savedServer]; actionState.gatewayResult = { ok: false, reason }; const formData = new FormData(); formData.set("serverId", serverId); formData.set("command", "list"); await expect(executeRconCommand({ status: "idle", message: "", serverId: "" }, formData)).resolves.toEqual({ status: "error", message, serverId, }); expect(JSON.stringify(actionState.audits)).not.toContain("decrypted-password"); }); it("does not send a command when its requested audit cannot be recorded", async () => { actionState.selected = [savedServer]; actionState.auditFailure = true; const formData = new FormData(); formData.set("serverId", serverId); formData.set("command", "list"); await expect(executeRconCommand({ status: "idle", message: "", serverId: "" }, formData)).resolves.toEqual({ status: "error", message: "Command not sent because its audit record could not be created.", serverId, }); expect(actionState.executions).toEqual([]); }); it("does not execute or audit when the enabled connection is unavailable", async () => { const formData = new FormData(); formData.set("serverId", serverId); formData.set("command", "list"); await expect(executeRconCommand({ status: "idle", message: "", serverId: "" }, formData)).resolves.toEqual({ status: "error", message: "That RCON connection is disabled or unavailable.", serverId, }); expect(actionState.executions).toEqual([]); expect(actionState.audits).toEqual([]); }); });