diff --git a/frontend/src/pages/admin/AdminImpersonationSafeguards.test.tsx b/frontend/src/pages/admin/AdminImpersonationSafeguards.test.tsx new file mode 100644 index 00000000..b6c8f19c --- /dev/null +++ b/frontend/src/pages/admin/AdminImpersonationSafeguards.test.tsx @@ -0,0 +1,115 @@ +import { describe, it, expect, beforeEach } from "vitest"; +import { render, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { http, HttpResponse } from "msw"; +import { server } from "../../test/mocks/server"; +import { MemoryRouter } from "react-router-dom"; +import AdminImpersonationSafeguards from "./AdminImpersonationSafeguards"; + +const mockSession = { + id: "session-1", + adminId: "admin-security-chief", + impersonatedUserId: "user-target-88", + reason: "Investigating user issue ticket #SUP-4412", + approvalTicketId: "SUP-4412", + status: "ACTIVE" as const, + tokenHash: "hash-123", + maxDurationMinutes: 30, + expiresAt: new Date(Date.now() + 1800000).toISOString(), + endedAt: null, + ipAddress: "127.0.0.1", + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), +}; + +const mockAuditLogs = [ + { + id: "log-1", + sessionId: "session-1", + requestMethod: "GET", + requestPath: "/api/v1/user/profile", + actorAdminId: "admin-security-chief", + targetUserId: "user-target-88", + ipAddress: "127.0.0.1", + timestamp: new Date().toISOString(), + }, +]; + +let currentSessions = [mockSession]; + +function setup() { + currentSessions = [mockSession]; + server.use( + http.get("/api/v1/admin/impersonation/sessions", () => + HttpResponse.json({ sessions: currentSessions }) + ), + http.post("/api/v1/admin/impersonation/start", () => + HttpResponse.json({ + session: mockSession, + token: "impersonation-token-123", + }) + ), + http.post("/api/v1/admin/impersonation/stop", () => { + currentSessions = [{ ...mockSession, status: "ENDED" }]; + return HttpResponse.json({ success: true }); + }), + http.get("/api/v1/admin/impersonation/audit-logs", () => + HttpResponse.json({ auditLogs: mockAuditLogs }) + ) + ); + window.localStorage.setItem("bridge-watch:admin-api-key:v1", JSON.stringify("test-token")); +} + +function renderPage() { + return render( + + + + ); +} + +describe("AdminImpersonationSafeguards page", () => { + beforeEach(() => { + setup(); + }); + + it("renders heading and active impersonation session", async () => { + renderPage(); + expect( + screen.getByRole("heading", { name: /admin impersonation safeguards/i }) + ).toBeInTheDocument(); + + await waitFor(() => { + expect(screen.getByText(/ACTIVE IMPERSONATION SESSION/i)).toBeInTheDocument(); + }); + expect(screen.getAllByText("user-target-88").length).toBeGreaterThan(0); + }); + + it("handles stopping an impersonation session", async () => { + renderPage(); + + const stopButton = await screen.findByRole("button", { name: /end session now/i }); + expect(stopButton).toBeInTheDocument(); + await userEvent.click(stopButton); + + await waitFor(() => { + expect(screen.queryByText(/ACTIVE IMPERSONATION SESSION/i)).not.toBeInTheDocument(); + }); + }); + + it("fetches audit logs when session detail is requested", async () => { + renderPage(); + + await waitFor(() => { + expect(screen.getAllByText("user-target-88").length).toBeGreaterThan(0); + }); + + const auditTrailButtons = screen.getAllByRole("button", { name: /audit trail/i }); + if (auditTrailButtons.length > 0) { + await userEvent.click(auditTrailButtons[0]); + await waitFor(() => { + expect(screen.getByText("/api/v1/user/profile")).toBeInTheDocument(); + }); + } + }); +}); diff --git a/frontend/src/pages/admin/AllowlistManagement.test.tsx b/frontend/src/pages/admin/AllowlistManagement.test.tsx new file mode 100644 index 00000000..66c95486 --- /dev/null +++ b/frontend/src/pages/admin/AllowlistManagement.test.tsx @@ -0,0 +1,103 @@ +import { describe, it, expect, beforeEach } from "vitest"; +import { render, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { http, HttpResponse } from "msw"; +import { server } from "../../test/mocks/server"; +import { MemoryRouter } from "react-router-dom"; +import AllowlistManagement from "./AllowlistManagement"; + +const mockAllowlist = [ + { + id: "entry-1", + contractAddress: "CCW67TSB3SSS4ZXGBOI2CCBGH2QPBDH2VMKCEW47GFZ2G5J57TL2JG3V", + addedBy: "admin-1", + addedAt: new Date().toISOString(), + isActive: true, + }, +]; + +const mockRequests = [ + { + id: "req-1", + contractAddress: "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMWAXA26TXTXFX", + action: "add" as const, + reason: "New liquidity pool integration", + requestedBy: "op-lead", + status: "pending" as const, + reviewedBy: null, + reviewComment: null, + reviewedAt: null, + createdAt: new Date().toISOString(), + }, +]; + +function setup() { + server.use( + http.get("/api/v1/admin/allowlist", () => + HttpResponse.json({ allowlist: mockAllowlist }) + ), + http.get("/api/v1/admin/allowlist/change-requests", () => + HttpResponse.json({ changeRequests: mockRequests }) + ), + http.post("/api/v1/admin/allowlist/change-requests", () => + HttpResponse.json({ success: true }, { status: 201 }) + ), + http.post("/api/v1/admin/allowlist/change-requests/:id/review", () => + HttpResponse.json({ success: true }) + ) + ); +} + +function renderPage() { + return render( + + + + ); +} + +describe("AllowlistManagement page", () => { + beforeEach(() => { + setup(); + }); + + it("renders page heading and allowlist entries", async () => { + renderPage(); + expect( + screen.getByRole("heading", { name: /contract allowlist management/i }) + ).toBeInTheDocument(); + + await waitFor(() => { + expect( + screen.getByText("CCW67TSB3SSS4ZXGBOI2CCBGH2QPBDH2VMKCEW47GFZ2G5J57TL2JG3V") + ).toBeInTheDocument(); + }); + }); + + it("switches tabs and displays change requests", async () => { + renderPage(); + const requestsTabButton = screen.getByRole("button", { name: /change requests/i }); + await userEvent.click(requestsTabButton); + + await waitFor(() => { + expect( + screen.getByText("CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMWAXA26TXTXFX") + ).toBeInTheDocument(); + }); + }); + + it("allows submitting a new change request", async () => { + const { container } = renderPage(); + const addressInput = screen.getByPlaceholderText("0x..."); + const textareaReason = container.querySelector("textarea")!; + const submitBtn = screen.getByRole("button", { name: /submit request/i }); + + await userEvent.type(addressInput, "CABC1234567890TESTCONTRACTADDRESSEXAMPLE"); + await userEvent.type(textareaReason, "Testing submission flow"); + await userEvent.click(submitBtn); + + await waitFor(() => { + expect(addressInput).toHaveValue(""); + }); + }); +}); diff --git a/frontend/src/pages/admin/ParseQuarantineQueue.test.tsx b/frontend/src/pages/admin/ParseQuarantineQueue.test.tsx new file mode 100644 index 00000000..01e5cae7 --- /dev/null +++ b/frontend/src/pages/admin/ParseQuarantineQueue.test.tsx @@ -0,0 +1,119 @@ +import { describe, it, expect, beforeEach } from "vitest"; +import { render, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { http, HttpResponse } from "msw"; +import { server } from "../../test/mocks/server"; +import { MemoryRouter } from "react-router-dom"; +import ParseQuarantineQueue from "./ParseQuarantineQueue"; + +const mockRecord = { + id: "qr-101", + source: "horizon-ingestor", + dataType: "transaction_meta", + rawPayload: { hash: "0x123abc" }, + parseError: "Unexpected end of JSON input", + errorCode: "ERR_JSON_PARSE", + status: "quarantined" as const, + retryCount: 1, + priority: 5, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), +}; + +const mockStats = { + total: 10, + byStatus: { + quarantined: 4, + in_review: 2, + resolved: 3, + disposed: 1, + failed: 0, + }, + bySource: { + "horizon-ingestor": 10, + }, +}; + +function setup() { + server.use( + http.get("/api/v1/admin/quarantine", () => + HttpResponse.json({ records: [mockRecord] }) + ), + http.get("/api/v1/admin/quarantine/stats", () => + HttpResponse.json({ stats: mockStats }) + ), + http.post("/api/v1/admin/quarantine", () => + HttpResponse.json({ record: mockRecord }, { status: 201 }) + ), + http.post("/api/v1/admin/quarantine/:id/retry", () => + HttpResponse.json({ record: { ...mockRecord, status: "resolved" } }) + ), + http.post("/api/v1/admin/quarantine/:id/resolve", () => + HttpResponse.json({ record: { ...mockRecord, status: "resolved" } }) + ), + http.delete("/api/v1/admin/quarantine/:id", () => + HttpResponse.json({ success: true }) + ) + ); + window.localStorage.setItem("bridge-watch:admin-api-key:v1", JSON.stringify("test-token")); +} + +function renderPage() { + return render( + + + + ); +} + +describe("ParseQuarantineQueue page", () => { + beforeEach(() => { + setup(); + }); + + it("renders heading and quarantine queue items", async () => { + renderPage(); + expect( + screen.getByRole("heading", { name: /failed parse quarantine/i }) + ).toBeInTheDocument(); + + await waitFor(() => { + expect(screen.getByText("horizon-ingestor")).toBeInTheDocument(); + }); + expect(screen.getByText(/ERR_JSON_PARSE/i)).toBeInTheDocument(); + }); + + it("handles enqueueing a new quarantine item", async () => { + renderPage(); + + const sourceInput = screen.getByPlaceholderText("some-exchange"); + const dataTypeInput = screen.getByPlaceholderText("asset"); + const parseErrorInput = screen.getByPlaceholderText("Unexpected field 'foo'"); + + await userEvent.type(sourceInput, "horizon-stream"); + await userEvent.type(dataTypeInput, "ledger_header"); + await userEvent.type(parseErrorInput, "SyntaxError"); + + const enqueueBtn = screen.getByRole("button", { name: /enqueue record/i }); + await userEvent.click(enqueueBtn); + + await waitFor(() => { + expect(sourceInput).toHaveValue(""); + }); + }); + + it("retries and resolves quarantine items", async () => { + renderPage(); + + await waitFor(() => { + expect(screen.getByText("horizon-ingestor")).toBeInTheDocument(); + }); + + const retryBtn = screen.getByRole("button", { name: /retry/i }); + await userEvent.click(retryBtn); + + await waitFor(() => { + expect(screen.getByRole("heading", { name: /failed parse quarantine/i })).toBeInTheDocument(); + }); + }); +}); diff --git a/frontend/src/pages/admin/SecurityEventCorrelation.test.tsx b/frontend/src/pages/admin/SecurityEventCorrelation.test.tsx new file mode 100644 index 00000000..422cc2be --- /dev/null +++ b/frontend/src/pages/admin/SecurityEventCorrelation.test.tsx @@ -0,0 +1,56 @@ +import { describe, it, expect } from "vitest"; +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { MemoryRouter } from "react-router-dom"; +import SecurityEventCorrelation from "./SecurityEventCorrelation"; + +function renderPage() { + return render( + + + + ); +} + +describe("SecurityEventCorrelation page", () => { + it("renders page heading and initial security correlations", () => { + renderPage(); + expect( + screen.getByRole("heading", { name: /security event correlation/i }) + ).toBeInTheDocument(); + + expect( + screen.getByText("Multiple Failed Webhook & Auth Signatures") + ).toBeInTheDocument(); + expect( + screen.getByText("Sensitive Field Bulk Export Anomaly") + ).toBeInTheDocument(); + }); + + it("filters correlations by search term", async () => { + renderPage(); + const searchInput = screen.getByPlaceholderText(/search correlation views/i); + await userEvent.type(searchInput, "Webhook"); + + expect( + screen.getByText("Multiple Failed Webhook & Auth Signatures") + ).toBeInTheDocument(); + expect( + screen.queryByText("Sensitive Field Bulk Export Anomaly") + ).not.toBeInTheDocument(); + }); + + it("opens create modal and adds a correlation rule", async () => { + const { container } = renderPage(); + const createBtn = screen.getByRole("button", { name: /\+ Create Correlation View/i }); + await userEvent.click(createBtn); + + const titleInput = container.querySelector('form input[type="text"]')!; + await userEvent.type(titleInput, "Unusual Geo IP Spike"); + + const submitBtn = screen.getByRole("button", { name: /save correlation view/i }); + await userEvent.click(submitBtn); + + expect(screen.getByText("Unusual Geo IP Spike")).toBeInTheDocument(); + }); +}); diff --git a/frontend/src/pages/admin/SignedRequestVerification.test.tsx b/frontend/src/pages/admin/SignedRequestVerification.test.tsx new file mode 100644 index 00000000..55d40958 --- /dev/null +++ b/frontend/src/pages/admin/SignedRequestVerification.test.tsx @@ -0,0 +1,46 @@ +import { describe, it, expect } from "vitest"; +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { MemoryRouter } from "react-router-dom"; +import SignedRequestVerification from "./SignedRequestVerification"; + +function renderPage() { + return render( + + + + ); +} + +describe("SignedRequestVerification page", () => { + it("renders page title and active signing keys", () => { + renderPage(); + expect( + screen.getByRole("heading", { name: /signed request verification middleware/i }) + ).toBeInTheDocument(); + expect(screen.getByText("payment-gateway")).toBeInTheDocument(); + expect(screen.getByText("partner-oracle")).toBeInTheDocument(); + }); + + it("opens create key modal and adds a new key", async () => { + renderPage(); + const createBtn = screen.getByRole("button", { name: /\+ Issue New Signing Key/i }); + await userEvent.click(createBtn); + + const ownerInput = screen.getByPlaceholderText(/e\.g\. payment-processor/i); + await userEvent.type(ownerInput, "analytics-worker"); + + const submitBtn = screen.getByRole("button", { name: /generate key credentials/i }); + await userEvent.click(submitBtn); + + expect(screen.getByText("analytics-worker")).toBeInTheDocument(); + }); + + it("runs the signature verification sandbox", async () => { + renderPage(); + const verifyBtn = screen.getByRole("button", { name: /compute & verify test signature/i }); + await userEvent.click(verifyBtn); + + expect(screen.getByText(/Generated Signature Header/i)).toBeInTheDocument(); + }); +}); diff --git a/frontend/src/pages/admin/WebhookIpAllowlist.test.tsx b/frontend/src/pages/admin/WebhookIpAllowlist.test.tsx new file mode 100644 index 00000000..f2c5c6ee --- /dev/null +++ b/frontend/src/pages/admin/WebhookIpAllowlist.test.tsx @@ -0,0 +1,54 @@ +import { describe, it, expect } from "vitest"; +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { MemoryRouter } from "react-router-dom"; +import WebhookIpAllowlist from "./WebhookIpAllowlist"; + +function renderPage() { + return render( + + + + ); +} + +describe("WebhookIpAllowlist page", () => { + it("renders page title and initial allowlist entries", () => { + renderPage(); + expect( + screen.getByRole("heading", { name: /webhook ip allowlist/i }) + ).toBeInTheDocument(); + + expect(screen.getByText("192.168.1.0/24")).toBeInTheDocument(); + expect(screen.getByText("10.0.4.50")).toBeInTheDocument(); + }); + + it("adds a new IP allowlist entry via modal", async () => { + const { container } = renderPage(); + const addBtn = screen.getByRole("button", { name: /\+ Add Allowlist Entry/i }); + await userEvent.click(addBtn); + + const ipInput = screen.getByPlaceholderText(/e\.g\. 192\.168\.1\.0\/24/i); + const descTextarea = container.querySelector("textarea")!; + + await userEvent.type(ipInput, "172.16.0.0/16"); + await userEvent.type(descTextarea, "VPN Subnet"); + + const submitBtn = screen.getByRole("button", { name: /add rule/i }); + await userEvent.click(submitBtn); + + expect(screen.getByText("172.16.0.0/16")).toBeInTheDocument(); + expect(screen.getByText("VPN Subnet")).toBeInTheDocument(); + }); + + it("tests an IP against active rules", async () => { + renderPage(); + const testIpInput = screen.getByPlaceholderText(/Enter test IP/i); + const runTestBtn = screen.getByRole("button", { name: /Evaluate Access/i }); + + await userEvent.type(testIpInput, "192.168.1.50"); + await userEvent.click(runTestBtn); + + expect(screen.getByText(/Matched rule/i)).toBeInTheDocument(); + }); +});