mirror of
https://github.com/mgeeky/decode-spam-headers.git
synced 2026-02-22 13:33:30 +01:00
MAESTRO: add report component tests
This commit is contained in:
93
frontend/src/__tests__/report/HopChainVisualisation.test.tsx
Normal file
93
frontend/src/__tests__/report/HopChainVisualisation.test.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import type { ReactElement } from "react";
|
||||
import { act } from "react-dom/test-utils";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
|
||||
import HopChainVisualisation from "../../components/report/HopChainVisualisation";
|
||||
import type { HopChainNode } from "../../types/analysis";
|
||||
|
||||
type RenderResult = {
|
||||
container: HTMLDivElement;
|
||||
};
|
||||
|
||||
const cleanups: Array<() => void> = [];
|
||||
|
||||
const render = (ui: ReactElement): RenderResult => {
|
||||
const container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
const root = createRoot(container);
|
||||
|
||||
act(() => {
|
||||
root.render(ui);
|
||||
});
|
||||
|
||||
cleanups.push(() => {
|
||||
act(() => {
|
||||
root.unmount();
|
||||
});
|
||||
container.remove();
|
||||
});
|
||||
|
||||
return { container };
|
||||
};
|
||||
|
||||
const getByTestId = (container: HTMLElement, testId: string): HTMLElement => {
|
||||
const element = container.querySelector(`[data-testid="${testId}"]`);
|
||||
if (!element) {
|
||||
throw new Error(`Expected element ${testId} to be rendered.`);
|
||||
}
|
||||
return element as HTMLElement;
|
||||
};
|
||||
|
||||
const hopChain: HopChainNode[] = [
|
||||
{
|
||||
index: 0,
|
||||
hostname: "mail.sender.example",
|
||||
ip: "192.0.2.10",
|
||||
timestamp: "2026-02-17 10:00:01",
|
||||
serverInfo: "Postfix",
|
||||
},
|
||||
{
|
||||
index: 1,
|
||||
hostname: "mx.receiver.example",
|
||||
ip: "203.0.113.5",
|
||||
timestamp: "2026-02-17 10:00:04",
|
||||
serverInfo: "Exchange 2019",
|
||||
},
|
||||
];
|
||||
|
||||
afterEach(() => {
|
||||
while (cleanups.length > 0) {
|
||||
const cleanup = cleanups.pop();
|
||||
if (cleanup) {
|
||||
cleanup();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
describe("HopChainVisualisation", () => {
|
||||
it("renders hop nodes with server details", () => {
|
||||
const { container } = render(<HopChainVisualisation hopChain={hopChain} />);
|
||||
|
||||
const root = getByTestId(container, "hop-chain-visualisation");
|
||||
expect(root).toBeTruthy();
|
||||
|
||||
const firstHop = getByTestId(container, "hop-chain-node-0");
|
||||
expect(firstHop.textContent ?? "").toContain("mail.sender.example");
|
||||
expect(firstHop.textContent ?? "").toContain("192.0.2.10");
|
||||
|
||||
const secondHop = getByTestId(container, "hop-chain-node-1");
|
||||
expect(secondHop.textContent ?? "").toContain("mx.receiver.example");
|
||||
expect(secondHop.textContent ?? "").toContain("203.0.113.5");
|
||||
});
|
||||
|
||||
it("renders connectors between hop nodes", () => {
|
||||
const { container } = render(<HopChainVisualisation hopChain={hopChain} />);
|
||||
|
||||
const connectors = container.querySelectorAll(
|
||||
'[data-testid^="hop-chain-connector-"]',
|
||||
);
|
||||
|
||||
expect(connectors.length).toBe(hopChain.length - 1);
|
||||
});
|
||||
});
|
||||
163
frontend/src/__tests__/report/ReportContainer.test.tsx
Normal file
163
frontend/src/__tests__/report/ReportContainer.test.tsx
Normal file
@@ -0,0 +1,163 @@
|
||||
import type { ReactElement } from "react";
|
||||
import { act } from "react-dom/test-utils";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
|
||||
import ReportContainer from "../../components/report/ReportContainer";
|
||||
import type { AnalysisReport } from "../../types/analysis";
|
||||
|
||||
type RenderResult = {
|
||||
container: HTMLDivElement;
|
||||
};
|
||||
|
||||
const cleanups: Array<() => void> = [];
|
||||
|
||||
const render = (ui: ReactElement): RenderResult => {
|
||||
const container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
const root = createRoot(container);
|
||||
|
||||
act(() => {
|
||||
root.render(ui);
|
||||
});
|
||||
|
||||
cleanups.push(() => {
|
||||
act(() => {
|
||||
root.unmount();
|
||||
});
|
||||
container.remove();
|
||||
});
|
||||
|
||||
return { container };
|
||||
};
|
||||
|
||||
const getByTestId = (container: HTMLElement, testId: string): HTMLElement => {
|
||||
const element = container.querySelector(`[data-testid="${testId}"]`);
|
||||
if (!element) {
|
||||
throw new Error(`Expected element ${testId} to be rendered.`);
|
||||
}
|
||||
return element as HTMLElement;
|
||||
};
|
||||
|
||||
const report: AnalysisReport = {
|
||||
results: [
|
||||
{
|
||||
testId: 101,
|
||||
testName: "SpamAssassin Rule Hits",
|
||||
headerName: "X-Spam-Flag",
|
||||
headerValue: "YES",
|
||||
analysis: "Flagged by local rules.",
|
||||
description: "SpamAssassin rules matched.",
|
||||
severity: "spam",
|
||||
status: "error",
|
||||
error: "Timeout",
|
||||
},
|
||||
{
|
||||
testId: 202,
|
||||
testName: "Mimecast Fingerprint",
|
||||
headerName: "X-Mimecast-Spam-Info",
|
||||
headerValue: "none",
|
||||
analysis: "No fingerprint detected.",
|
||||
description: "No known fingerprint found.",
|
||||
severity: "clean",
|
||||
status: "success",
|
||||
error: null,
|
||||
},
|
||||
{
|
||||
testId: 303,
|
||||
testName: "Barracuda Reputation",
|
||||
headerName: "X-Barracuda",
|
||||
headerValue: "neutral",
|
||||
analysis: "Reputation check pending.",
|
||||
description: "Awaiting response.",
|
||||
severity: "suspicious",
|
||||
status: "success",
|
||||
error: null,
|
||||
},
|
||||
{
|
||||
testId: 404,
|
||||
testName: "Custom Header Check",
|
||||
headerName: "X-Custom",
|
||||
headerValue: "ok",
|
||||
analysis: "Custom info.",
|
||||
description: "Informational.",
|
||||
severity: "info",
|
||||
status: "success",
|
||||
error: null,
|
||||
},
|
||||
],
|
||||
hopChain: [
|
||||
{
|
||||
index: 0,
|
||||
hostname: "mail.sender.example",
|
||||
ip: "192.0.2.10",
|
||||
timestamp: "2026-02-17 10:00:01",
|
||||
serverInfo: "Postfix",
|
||||
},
|
||||
{
|
||||
index: 1,
|
||||
hostname: "mx.receiver.example",
|
||||
ip: "203.0.113.5",
|
||||
timestamp: "2026-02-17 10:00:04",
|
||||
serverInfo: "Exchange 2019",
|
||||
},
|
||||
],
|
||||
securityAppliances: [
|
||||
{
|
||||
name: "Mimecast Email Security",
|
||||
vendor: "Mimecast",
|
||||
headers: ["X-Mimecast-Spam-Info"],
|
||||
},
|
||||
],
|
||||
metadata: {
|
||||
totalTests: 4,
|
||||
passedTests: 3,
|
||||
failedTests: 1,
|
||||
skippedTests: 0,
|
||||
elapsedMs: 4200,
|
||||
timedOut: false,
|
||||
incompleteTests: [],
|
||||
},
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
while (cleanups.length > 0) {
|
||||
const cleanup = cleanups.pop();
|
||||
if (cleanup) {
|
||||
cleanup();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
describe("ReportContainer", () => {
|
||||
it("renders the full report with summary stats and sections", () => {
|
||||
const { container } = render(<ReportContainer report={report} />);
|
||||
|
||||
getByTestId(container, "report-container");
|
||||
|
||||
const total = getByTestId(container, "report-summary-total");
|
||||
const passed = getByTestId(container, "report-summary-passed");
|
||||
const failed = getByTestId(container, "report-summary-failed");
|
||||
const spam = getByTestId(container, "report-summary-severity-spam");
|
||||
const suspicious = getByTestId(container, "report-summary-severity-suspicious");
|
||||
const clean = getByTestId(container, "report-summary-severity-clean");
|
||||
const info = getByTestId(container, "report-summary-severity-info");
|
||||
|
||||
expect(total.textContent ?? "").toContain("4");
|
||||
expect(passed.textContent ?? "").toContain("3");
|
||||
expect(failed.textContent ?? "").toContain("1");
|
||||
expect(spam.textContent ?? "").toContain("1");
|
||||
expect(suspicious.textContent ?? "").toContain("1");
|
||||
expect(clean.textContent ?? "").toContain("1");
|
||||
expect(info.textContent ?? "").toContain("1");
|
||||
|
||||
getByTestId(container, "report-search-bar");
|
||||
getByTestId(container, "report-export");
|
||||
getByTestId(container, "security-appliances-summary");
|
||||
getByTestId(container, "hop-chain-visualisation");
|
||||
|
||||
report.results.forEach((result) => {
|
||||
getByTestId(container, `test-result-card-${result.testId}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
147
frontend/src/__tests__/report/ReportExport.test.tsx
Normal file
147
frontend/src/__tests__/report/ReportExport.test.tsx
Normal file
@@ -0,0 +1,147 @@
|
||||
import type { ReactElement } from "react";
|
||||
import { act } from "react-dom/test-utils";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import ReportExport from "../../components/report/ReportExport";
|
||||
import type { AnalysisReport } from "../../types/analysis";
|
||||
|
||||
type RenderResult = {
|
||||
container: HTMLDivElement;
|
||||
};
|
||||
|
||||
type UrlStatics = {
|
||||
createObjectURL?: (blob: Blob) => string;
|
||||
revokeObjectURL?: (url: string) => void;
|
||||
};
|
||||
|
||||
const cleanups: Array<() => void> = [];
|
||||
|
||||
const render = (ui: ReactElement): RenderResult => {
|
||||
const container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
const root = createRoot(container);
|
||||
|
||||
act(() => {
|
||||
root.render(ui);
|
||||
});
|
||||
|
||||
cleanups.push(() => {
|
||||
act(() => {
|
||||
root.unmount();
|
||||
});
|
||||
container.remove();
|
||||
});
|
||||
|
||||
return { container };
|
||||
};
|
||||
|
||||
const getByTestId = (container: HTMLElement, testId: string): HTMLElement => {
|
||||
const element = container.querySelector(`[data-testid="${testId}"]`);
|
||||
if (!element) {
|
||||
throw new Error(`Expected element ${testId} to be rendered.`);
|
||||
}
|
||||
return element as HTMLElement;
|
||||
};
|
||||
|
||||
const report: AnalysisReport = {
|
||||
results: [
|
||||
{
|
||||
testId: 101,
|
||||
testName: "SpamAssassin Rule Hits",
|
||||
headerName: "X-Spam-Flag",
|
||||
headerValue: "YES",
|
||||
analysis: "Flagged by local rules.",
|
||||
description: "SpamAssassin rules matched during analysis.",
|
||||
severity: "spam",
|
||||
status: "error",
|
||||
error: "Timeout",
|
||||
},
|
||||
],
|
||||
hopChain: [],
|
||||
securityAppliances: [],
|
||||
metadata: {
|
||||
totalTests: 1,
|
||||
passedTests: 0,
|
||||
failedTests: 1,
|
||||
skippedTests: 0,
|
||||
elapsedMs: 1200,
|
||||
timedOut: false,
|
||||
incompleteTests: [],
|
||||
},
|
||||
};
|
||||
|
||||
let createObjectUrlSpy: ReturnType<typeof vi.spyOn> | null = null;
|
||||
let revokeObjectUrlSpy: ReturnType<typeof vi.spyOn> | null = null;
|
||||
let clickSpy: ReturnType<typeof vi.spyOn> | null = null;
|
||||
|
||||
const originalUrlStatics: UrlStatics = {
|
||||
createObjectURL: (URL as unknown as UrlStatics).createObjectURL,
|
||||
revokeObjectURL: (URL as unknown as UrlStatics).revokeObjectURL,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
const urlStatics = URL as unknown as UrlStatics;
|
||||
|
||||
if (!urlStatics.createObjectURL) {
|
||||
urlStatics.createObjectURL = () => "blob:report";
|
||||
}
|
||||
if (!urlStatics.revokeObjectURL) {
|
||||
urlStatics.revokeObjectURL = () => undefined;
|
||||
}
|
||||
|
||||
createObjectUrlSpy = vi
|
||||
.spyOn(URL, "createObjectURL")
|
||||
.mockReturnValue("blob:report");
|
||||
revokeObjectUrlSpy = vi.spyOn(URL, "revokeObjectURL").mockImplementation(() => {
|
||||
return undefined;
|
||||
});
|
||||
clickSpy = vi
|
||||
.spyOn(HTMLAnchorElement.prototype, "click")
|
||||
.mockImplementation(() => undefined);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
while (cleanups.length > 0) {
|
||||
const cleanup = cleanups.pop();
|
||||
if (cleanup) {
|
||||
cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
createObjectUrlSpy?.mockRestore();
|
||||
revokeObjectUrlSpy?.mockRestore();
|
||||
clickSpy?.mockRestore();
|
||||
|
||||
const urlStatics = URL as unknown as UrlStatics;
|
||||
urlStatics.createObjectURL = originalUrlStatics.createObjectURL;
|
||||
urlStatics.revokeObjectURL = originalUrlStatics.revokeObjectURL;
|
||||
});
|
||||
|
||||
describe("ReportExport", () => {
|
||||
it("triggers a JSON download", () => {
|
||||
const { container } = render(<ReportExport report={report} />);
|
||||
|
||||
const jsonButton = getByTestId(container, "report-export-json");
|
||||
|
||||
act(() => {
|
||||
jsonButton.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
});
|
||||
|
||||
expect(createObjectUrlSpy).toHaveBeenCalled();
|
||||
expect(clickSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("triggers an HTML download", () => {
|
||||
const { container } = render(<ReportExport report={report} />);
|
||||
|
||||
const htmlButton = getByTestId(container, "report-export-html");
|
||||
|
||||
act(() => {
|
||||
htmlButton.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
});
|
||||
|
||||
expect(createObjectUrlSpy).toHaveBeenCalled();
|
||||
expect(clickSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
161
frontend/src/__tests__/report/ReportSearchBar.test.tsx
Normal file
161
frontend/src/__tests__/report/ReportSearchBar.test.tsx
Normal file
@@ -0,0 +1,161 @@
|
||||
import type { ReactElement } from "react";
|
||||
import { useState } from "react";
|
||||
import { act } from "react-dom/test-utils";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
|
||||
import ReportSearchBar from "../../components/report/ReportSearchBar";
|
||||
import type { TestResult } from "../../types/analysis";
|
||||
|
||||
type RenderResult = {
|
||||
container: HTMLDivElement;
|
||||
};
|
||||
|
||||
const cleanups: Array<() => void> = [];
|
||||
|
||||
const render = (ui: ReactElement): RenderResult => {
|
||||
const container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
const root = createRoot(container);
|
||||
|
||||
act(() => {
|
||||
root.render(ui);
|
||||
});
|
||||
|
||||
cleanups.push(() => {
|
||||
act(() => {
|
||||
root.unmount();
|
||||
});
|
||||
container.remove();
|
||||
});
|
||||
|
||||
return { container };
|
||||
};
|
||||
|
||||
const getByTestId = (container: HTMLElement, testId: string): HTMLElement => {
|
||||
const element = container.querySelector(`[data-testid="${testId}"]`);
|
||||
if (!element) {
|
||||
throw new Error(`Expected element ${testId} to be rendered.`);
|
||||
}
|
||||
return element as HTMLElement;
|
||||
};
|
||||
|
||||
const sampleResults: TestResult[] = [
|
||||
{
|
||||
testId: 101,
|
||||
testName: "SpamAssassin Rule Hits",
|
||||
headerName: "X-Spam-Flag",
|
||||
headerValue: "YES",
|
||||
analysis: "Flagged by local rules.",
|
||||
description: "SpamAssassin rules matched.",
|
||||
severity: "spam",
|
||||
status: "error",
|
||||
error: "Timeout",
|
||||
},
|
||||
{
|
||||
testId: 202,
|
||||
testName: "Mimecast Fingerprint",
|
||||
headerName: "X-Mimecast-Spam-Info",
|
||||
headerValue: "none",
|
||||
analysis: "No fingerprint detected.",
|
||||
description: "No known fingerprint found.",
|
||||
severity: "clean",
|
||||
status: "success",
|
||||
error: null,
|
||||
},
|
||||
{
|
||||
testId: 303,
|
||||
testName: "Barracuda Reputation",
|
||||
headerName: "X-Barracuda",
|
||||
headerValue: "neutral",
|
||||
analysis: "Reputation check pending.",
|
||||
description: "Awaiting response.",
|
||||
severity: "suspicious",
|
||||
status: "success",
|
||||
error: null,
|
||||
},
|
||||
];
|
||||
|
||||
const filterResults = (query: string, results: TestResult[]): TestResult[] => {
|
||||
const normalizedQuery = query.trim().toLowerCase();
|
||||
if (!normalizedQuery) {
|
||||
return results;
|
||||
}
|
||||
return results.filter((result) => {
|
||||
return (
|
||||
result.testName.toLowerCase().includes(normalizedQuery) ||
|
||||
result.headerName.toLowerCase().includes(normalizedQuery) ||
|
||||
result.analysis.toLowerCase().includes(normalizedQuery)
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const ReportSearchHarness = () => {
|
||||
const [query, setQuery] = useState("");
|
||||
const filtered = filterResults(query, sampleResults);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ReportSearchBar
|
||||
query={query}
|
||||
matchCount={filtered.length}
|
||||
totalCount={sampleResults.length}
|
||||
onQueryChange={setQuery}
|
||||
/>
|
||||
<ul data-testid="filtered-results">
|
||||
{filtered.map((result) => (
|
||||
<li key={result.testId}>{result.testName}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
while (cleanups.length > 0) {
|
||||
const cleanup = cleanups.pop();
|
||||
if (cleanup) {
|
||||
cleanup();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
describe("ReportSearchBar", () => {
|
||||
it("filters results when the search query changes", () => {
|
||||
const { container } = render(<ReportSearchHarness />);
|
||||
|
||||
const input = getByTestId(container, "report-search-input") as HTMLInputElement;
|
||||
|
||||
act(() => {
|
||||
input.value = "mime";
|
||||
input.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
});
|
||||
|
||||
const filteredList = getByTestId(container, "filtered-results");
|
||||
expect(filteredList.textContent ?? "").toContain("Mimecast Fingerprint");
|
||||
expect(filteredList.textContent ?? "").not.toContain("SpamAssassin Rule Hits");
|
||||
|
||||
const count = getByTestId(container, "report-search-count");
|
||||
expect(count.textContent ?? "").toMatch(/1/);
|
||||
});
|
||||
|
||||
it("clears the query on Escape", () => {
|
||||
const { container } = render(<ReportSearchHarness />);
|
||||
|
||||
const input = getByTestId(container, "report-search-input") as HTMLInputElement;
|
||||
|
||||
act(() => {
|
||||
input.value = "spam";
|
||||
input.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
});
|
||||
|
||||
act(() => {
|
||||
input.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true }));
|
||||
});
|
||||
|
||||
expect(input.value).toBe("");
|
||||
const filteredList = getByTestId(container, "filtered-results");
|
||||
expect(filteredList.textContent ?? "").toContain("SpamAssassin Rule Hits");
|
||||
expect(filteredList.textContent ?? "").toContain("Mimecast Fingerprint");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,84 @@
|
||||
import type { ReactElement } from "react";
|
||||
import { act } from "react-dom/test-utils";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
|
||||
import SecurityAppliancesSummary from "../../components/report/SecurityAppliancesSummary";
|
||||
import type { SecurityAppliance } from "../../types/analysis";
|
||||
|
||||
type RenderResult = {
|
||||
container: HTMLDivElement;
|
||||
};
|
||||
|
||||
const cleanups: Array<() => void> = [];
|
||||
|
||||
const render = (ui: ReactElement): RenderResult => {
|
||||
const container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
const root = createRoot(container);
|
||||
|
||||
act(() => {
|
||||
root.render(ui);
|
||||
});
|
||||
|
||||
cleanups.push(() => {
|
||||
act(() => {
|
||||
root.unmount();
|
||||
});
|
||||
container.remove();
|
||||
});
|
||||
|
||||
return { container };
|
||||
};
|
||||
|
||||
const getByTestId = (container: HTMLElement, testId: string): HTMLElement => {
|
||||
const element = container.querySelector(`[data-testid="${testId}"]`);
|
||||
if (!element) {
|
||||
throw new Error(`Expected element ${testId} to be rendered.`);
|
||||
}
|
||||
return element as HTMLElement;
|
||||
};
|
||||
|
||||
const sampleAppliances: SecurityAppliance[] = [
|
||||
{
|
||||
name: "Mimecast Email Security",
|
||||
vendor: "Mimecast",
|
||||
headers: ["X-Mimecast-Spam-Info"],
|
||||
},
|
||||
{
|
||||
name: "Proofpoint Protection",
|
||||
vendor: "Proofpoint",
|
||||
headers: ["X-Proofpoint-Verdict"],
|
||||
},
|
||||
];
|
||||
|
||||
afterEach(() => {
|
||||
while (cleanups.length > 0) {
|
||||
const cleanup = cleanups.pop();
|
||||
if (cleanup) {
|
||||
cleanup();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
describe("SecurityAppliancesSummary", () => {
|
||||
it("renders detected security appliances as badges", () => {
|
||||
const { container } = render(
|
||||
<SecurityAppliancesSummary appliances={sampleAppliances} />,
|
||||
);
|
||||
|
||||
const summary = getByTestId(container, "security-appliances-summary");
|
||||
expect(summary.textContent ?? "").toContain("Mimecast Email Security");
|
||||
expect(summary.textContent ?? "").toContain("Proofpoint Protection");
|
||||
|
||||
const firstBadge = getByTestId(container, "security-appliance-0");
|
||||
expect(firstBadge.textContent ?? "").toContain("Mimecast");
|
||||
});
|
||||
|
||||
it("renders an empty state when no appliances are detected", () => {
|
||||
const { container } = render(<SecurityAppliancesSummary appliances={[]} />);
|
||||
|
||||
const emptyState = getByTestId(container, "security-appliances-empty");
|
||||
expect(emptyState.textContent ?? "").toMatch(/No security appliances detected/i);
|
||||
});
|
||||
});
|
||||
133
frontend/src/__tests__/report/TestResultCard.test.tsx
Normal file
133
frontend/src/__tests__/report/TestResultCard.test.tsx
Normal file
@@ -0,0 +1,133 @@
|
||||
import type { ReactElement } from "react";
|
||||
import { act } from "react-dom/test-utils";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
|
||||
import TestResultCard from "../../components/report/TestResultCard";
|
||||
import type { TestResult, TestSeverity } from "../../types/analysis";
|
||||
|
||||
type RenderResult = {
|
||||
container: HTMLDivElement;
|
||||
};
|
||||
|
||||
const cleanups: Array<() => void> = [];
|
||||
|
||||
const render = (ui: ReactElement): RenderResult => {
|
||||
const container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
const root = createRoot(container);
|
||||
|
||||
act(() => {
|
||||
root.render(ui);
|
||||
});
|
||||
|
||||
cleanups.push(() => {
|
||||
act(() => {
|
||||
root.unmount();
|
||||
});
|
||||
container.remove();
|
||||
});
|
||||
|
||||
return { container };
|
||||
};
|
||||
|
||||
const getByTestId = (container: HTMLElement, testId: string): HTMLElement => {
|
||||
const element = container.querySelector(`[data-testid="${testId}"]`);
|
||||
if (!element) {
|
||||
throw new Error(`Expected element ${testId} to be rendered.`);
|
||||
}
|
||||
return element as HTMLElement;
|
||||
};
|
||||
|
||||
const buildResult = (overrides: Partial<TestResult>): TestResult => ({
|
||||
testId: 101,
|
||||
testName: "SpamAssassin Rule Hits",
|
||||
headerName: "X-Spam-Flag",
|
||||
headerValue: "YES",
|
||||
analysis: "Flagged by local rules.",
|
||||
description: "SpamAssassin rules matched during analysis.",
|
||||
severity: "spam",
|
||||
status: "success",
|
||||
error: null,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
while (cleanups.length > 0) {
|
||||
const cleanup = cleanups.pop();
|
||||
if (cleanup) {
|
||||
cleanup();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
describe("TestResultCard", () => {
|
||||
it("renders severity labels with mapped styles", () => {
|
||||
const severityCases: Array<{
|
||||
severity: TestSeverity;
|
||||
label: string;
|
||||
className: string;
|
||||
}> = [
|
||||
{ severity: "spam", label: "Spam", className: "text-spam" },
|
||||
{ severity: "suspicious", label: "Suspicious", className: "text-suspicious" },
|
||||
{ severity: "clean", label: "Clean", className: "text-clean" },
|
||||
{ severity: "info", label: "Info", className: "text-accent" },
|
||||
];
|
||||
|
||||
severityCases.forEach((severityCase, index) => {
|
||||
const result = buildResult({
|
||||
testId: 200 + index,
|
||||
testName: `Severity ${severityCase.label}`,
|
||||
severity: severityCase.severity,
|
||||
});
|
||||
|
||||
const { container } = render(<TestResultCard result={result} />);
|
||||
|
||||
const severityBadge = getByTestId(
|
||||
container,
|
||||
`test-result-severity-${result.testId}`,
|
||||
);
|
||||
|
||||
expect(severityBadge.textContent ?? "").toContain(severityCase.label);
|
||||
expect(severityBadge.className).toContain(severityCase.className);
|
||||
});
|
||||
});
|
||||
|
||||
it("expands and collapses on click and keyboard", () => {
|
||||
const result = buildResult({ testId: 311, testName: "Header Anomaly" });
|
||||
const { container } = render(<TestResultCard result={result} />);
|
||||
|
||||
const toggle = getByTestId(container, `test-result-toggle-${result.testId}`);
|
||||
expect(toggle.getAttribute("aria-expanded")).toBe("false");
|
||||
|
||||
act(() => {
|
||||
toggle.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
});
|
||||
|
||||
expect(toggle.getAttribute("aria-expanded")).toBe("true");
|
||||
|
||||
act(() => {
|
||||
toggle.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true }));
|
||||
});
|
||||
|
||||
expect(toggle.getAttribute("aria-expanded")).toBe("false");
|
||||
|
||||
act(() => {
|
||||
toggle.dispatchEvent(new KeyboardEvent("keydown", { key: " ", bubbles: true }));
|
||||
});
|
||||
|
||||
expect(toggle.getAttribute("aria-expanded")).toBe("true");
|
||||
});
|
||||
|
||||
it("shows error indicators for failed tests", () => {
|
||||
const result = buildResult({
|
||||
testId: 404,
|
||||
status: "error",
|
||||
error: "SpamAssassin database timeout.",
|
||||
});
|
||||
const { container } = render(<TestResultCard result={result} />);
|
||||
|
||||
const errorIndicator = getByTestId(container, `test-result-error-${result.testId}`);
|
||||
expect(errorIndicator.textContent ?? "").toMatch(/SpamAssassin database timeout/);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user