Files
prowler/ui/lib/lighthouse/context/context.test.ts
2026-07-22 17:18:10 +02:00

559 lines
14 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { compileLighthouseContext } from "./compiler";
import { lighthouseContextEnvelopeSchema } from "./schema";
describe("lighthouseContextEnvelopeSchema", () => {
describe("when validating an inline page context", () => {
it("should accept a valid version 1 envelope", () => {
// Given
const envelope = {
schemaVersion: 1,
transport: "inline",
items: [
{
kind: "page",
id: "findings",
source: "automatic",
scopeKey: "findings:/findings",
label: "Findings",
path: "/findings",
filters: { severity: ["critical"] },
},
],
};
// When
const result = lighthouseContextEnvelopeSchema.safeParse(envelope);
// Then
expect(result.success).toBe(true);
});
it("should accept every supported contextual item kind", () => {
// Given
const envelope = {
schemaVersion: 1,
transport: "inline",
items: [
{
kind: "finding",
id: "finding-1",
source: "selection",
scopeKey: "findings:/findings",
label: "Selected finding",
findingId: "finding-1",
checkId: "check-1",
severity: "critical",
},
{
kind: "resource",
id: "resource-1",
source: "selection",
scopeKey: "resources:/resources",
label: "Selected resource",
resourceId: "resource-1",
service: "s3",
failedFindingsCount: 4,
},
{
kind: "compliance",
id: "cis-1.5",
source: "automatic",
scopeKey: "compliance:/compliance",
label: "CIS 1.5",
framework: "cis_1.5_aws",
score: 82,
totals: { passed: 82, failed: 18, total: 100 },
},
{
kind: "attack_path",
id: "query-1",
source: "automatic",
scopeKey: "attack-paths:/attack-paths/query-builder",
label: "Attack path query",
scanId: "scan-1",
queryId: "query-1",
parameters: { region: "eu-west-1", limit: 10 },
nodeCount: 12,
edgeCount: 11,
},
{
kind: "scan",
id: "scans-summary",
source: "automatic",
scopeKey: "scans:/scans",
label: "Visible scans",
total: 25,
},
{
kind: "provider",
id: "providers-summary",
source: "automatic",
scopeKey: "providers:/providers",
label: "Visible providers",
total: 7,
},
],
};
// When
const result = lighthouseContextEnvelopeSchema.safeParse(envelope);
// Then
expect(result.success).toBe(true);
});
});
describe("when context exceeds transport limits", () => {
it("should reject filters containing more than 20 values", () => {
// Given
const envelope = {
schemaVersion: 1,
transport: "inline",
items: [
{
kind: "page",
id: "findings",
source: "automatic",
scopeKey: "findings:/findings",
label: "Findings",
path: "/findings",
filters: {
severity: Array.from({ length: 21 }, (_, index) => `${index}`),
},
},
],
};
// When
const result = lighthouseContextEnvelopeSchema.safeParse(envelope);
// Then
expect(result.success).toBe(false);
});
it("should reject more than eight context items", () => {
// Given
const item = {
kind: "page",
id: "findings",
source: "automatic",
scopeKey: "findings:/findings",
label: "Findings",
path: "/findings",
};
// When
const result = lighthouseContextEnvelopeSchema.safeParse({
schemaVersion: 1,
transport: "inline",
items: Array.from({ length: 9 }, (_, index) => ({
...item,
id: `page-${index}`,
})),
});
// Then
expect(result.success).toBe(false);
});
it("should reject strings longer than 256 characters", () => {
// Given / When
const result = lighthouseContextEnvelopeSchema.safeParse({
schemaVersion: 1,
transport: "inline",
items: [
{
kind: "page",
id: "findings",
source: "automatic",
scopeKey: "findings:/findings",
label: "x".repeat(257),
path: "/findings",
},
],
});
// Then
expect(result.success).toBe(false);
});
it("should reject unknown item kinds", () => {
// Given / When
const result = lighthouseContextEnvelopeSchema.safeParse({
schemaVersion: 1,
transport: "inline",
items: [
{
kind: "secret",
id: "credentials",
source: "automatic",
scopeKey: "findings:/findings",
label: "Credentials",
},
],
});
// Then
expect(result.success).toBe(false);
});
});
});
describe("compileLighthouseContext", () => {
describe("when multiple contributors describe the same entity", () => {
it("should deduplicate items by kind and id", () => {
// Given
const scopeKey = "findings:/findings";
const items = [
{
kind: "page",
id: "findings",
source: "automatic",
scopeKey,
label: "Findings",
path: "/findings",
},
{
kind: "finding",
id: "finding-1",
source: "selection",
scopeKey,
label: "Selected finding",
findingId: "finding-1",
severity: "critical",
},
{
kind: "finding",
id: "finding-1",
source: "selection",
scopeKey,
label: "Duplicate finding",
findingId: "finding-1",
severity: "critical",
},
];
// When
const context = compileLighthouseContext(items, scopeKey);
// Then
expect(context?.items.map((item) => `${item.kind}:${item.id}`)).toEqual([
"page:findings",
"finding:finding-1",
]);
});
it("should retain the highest-priority duplicate", () => {
// Given
const scopeKey = "findings:/findings";
const items = [
{
kind: "page",
id: "findings",
source: "automatic",
scopeKey,
label: "Findings",
path: "/findings",
},
{
kind: "finding",
id: "finding-1",
source: "selection",
scopeKey,
label: "Selected finding",
findingId: "finding-1",
},
{
kind: "finding",
id: "finding-1",
source: "focused",
scopeKey,
label: "Focused finding",
findingId: "finding-1",
},
];
// When
const context = compileLighthouseContext(items, scopeKey);
// Then
expect(context?.items[1]).toMatchObject({
id: "finding-1",
source: "focused",
label: "Focused finding",
});
});
});
describe("when contributors arrive in render order", () => {
it("should order page, focused, selection, and summary items deterministically", () => {
// Given
const scopeKey = "findings:/findings";
const items = [
{
kind: "finding",
id: "findings-summary",
source: "automatic",
scopeKey,
label: "Visible findings",
findingId: "summary",
total: 42,
},
{
kind: "finding",
id: "finding-1",
source: "selection",
scopeKey,
label: "Selected finding",
findingId: "finding-1",
},
{
kind: "finding",
id: "finding-focused",
source: "focused",
scopeKey,
label: "Focused finding",
findingId: "finding-focused",
},
{
kind: "page",
id: "findings",
source: "automatic",
scopeKey,
label: "Findings",
path: "/findings",
},
];
// When
const context = compileLighthouseContext(items, scopeKey);
// Then
expect(context?.items.map((item) => item.id)).toEqual([
"findings",
"finding-focused",
"finding-1",
"findings-summary",
]);
});
});
describe("when serialized context exceeds 2 KiB", () => {
it("should drop lowest-priority items until the context fits", () => {
// Given
const scopeKey = "findings:/findings";
const page = {
kind: "page",
id: "findings",
source: "automatic",
scopeKey,
label: "Findings",
path: "/findings",
};
const selection = {
kind: "finding",
id: "finding-1",
source: "selection",
scopeKey,
label: "Selected finding",
findingId: "finding-1",
};
const summaries = Array.from({ length: 6 }, (_, index) => ({
kind: "finding",
id: `summary-${index}`,
source: "automatic",
scopeKey,
label: `Summary ${index} ${"x".repeat(240)}`,
findingId: `summary-${index}`,
checkId: `check-${index}-${"y".repeat(240)}`,
providerUid: `provider-${index}-${"z".repeat(237)}`,
total: index,
}));
// When
const context = compileLighthouseContext(
[page, selection, ...summaries],
scopeKey,
);
// Then
expect(context?.items.map((item) => item.id)).toEqual([
"findings",
"finding-1",
"summary-0",
]);
});
it("should preserve only the page when selection data is still too large", () => {
// Given
const scopeKey = "findings:/findings";
const page = {
kind: "page",
id: "findings",
source: "automatic",
scopeKey,
label: "Findings",
path: "/findings",
};
const selection = {
kind: "finding",
id: "finding-1",
source: "selection",
scopeKey,
label: "x".repeat(256),
findingId: "y".repeat(256),
checkId: "z".repeat(256),
severity: "s".repeat(256),
status: "t".repeat(256),
providerUid: "p".repeat(256),
resourceUid: "r".repeat(256),
region: "g".repeat(256),
};
// When
const context = compileLighthouseContext([selection, page], scopeKey);
// Then
expect(context?.items.map((item) => item.id)).toEqual(["findings"]);
});
});
describe("when context exceeds the eight-item limit", () => {
it("should progressively drop only the lowest-priority items", () => {
// Given
const scopeKey = "findings:/findings";
const page = {
kind: "page",
id: "findings",
source: "automatic",
scopeKey,
label: "Findings",
path: "/findings",
};
const focused = {
kind: "finding",
id: "focused",
source: "focused",
scopeKey,
label: "Focused finding",
findingId: "focused",
};
const selections = Array.from({ length: 2 }, (_, index) => ({
kind: "finding",
id: `selection-${index}`,
source: "selection",
scopeKey,
label: `Selected finding ${index}`,
findingId: `selection-${index}`,
}));
const summaries = Array.from({ length: 6 }, (_, index) => ({
kind: "finding",
id: `summary-${index}`,
source: "automatic",
scopeKey,
label: `Summary ${index}`,
findingId: `summary-${index}`,
total: index,
}));
// When
const context = compileLighthouseContext(
[page, ...summaries, ...selections, focused],
scopeKey,
);
// Then
expect(context?.items.map((item) => item.id)).toEqual([
"findings",
"focused",
"selection-0",
"selection-1",
"summary-0",
"summary-1",
"summary-2",
"summary-3",
]);
});
});
describe("when contributors belong to another page", () => {
it("should ignore stale scoped data", () => {
// Given / When
const context = compileLighthouseContext(
[
{
kind: "page",
id: "resources",
source: "automatic",
scopeKey: "resources:/resources",
label: "Resources",
path: "/resources",
},
{
kind: "finding",
id: "finding-1",
source: "selection",
scopeKey: "findings:/findings",
label: "Old finding",
findingId: "finding-1",
},
],
"resources:/resources",
);
// Then
expect(context?.items.map((item) => item.id)).toEqual(["resources"]);
});
});
describe("when current context is invalid", () => {
it("should return no context so sending remains available", () => {
// Given / When
const context = compileLighthouseContext(
[
{
kind: "finding",
id: "finding-1",
source: "selection",
scopeKey: "findings:/findings",
label: "Invalid finding without findingId",
},
],
"findings:/findings",
);
// Then
expect(context).toBeUndefined();
});
it("should discard valid items together with an invalid same-scope item", () => {
// Given / When
const context = compileLighthouseContext(
[
{
kind: "page",
id: "findings",
source: "automatic",
scopeKey: "findings:/findings",
label: "Findings",
path: "/findings",
},
{
kind: "finding",
id: "finding-1",
source: "selection",
scopeKey: "findings:/findings",
label: "Invalid finding without findingId",
},
],
"findings:/findings",
);
// Then
expect(context).toBeUndefined();
});
});
});