Files
itworx-knowledge-hub-Public/tests/e2e/specs/v2-infrastructure-review.spec.ts
T

196 lines
9.1 KiB
TypeScript

import AxeBuilder from "@axe-core/playwright";
import { expect, test, type Page } from "@playwright/test";
import { authenticatedStatePath } from "../global-setup";
import { expectAuthenticatedShell } from "./support/v2";
/**
* V2-M4: structured infrastructure facts, editorial profiles and manual overrides are
* visibly distinct and safely editable in the UI (ADR-023), and a proposed service has
* a real accept/reject review flow reachable from the UI — proven end to end against
* the real API, not just the repository-level InfrastructureSynchronizationTests.
* Synchronization itself stays API-first by design (ADR-023: an external inventory
* tool posts snapshots; there is deliberately no "trigger a sync" UI button, matching
* the existing V1 "API first" precedent for LeafWiki import etc.) — this spec seeds a
* snapshot directly via the API, matching how a real inventory agent would, and then
* exercises only the UI review/override flows.
*/
test.use({ storageState: authenticatedStatePath });
async function postWithCsrf(page: Page, path: string, data: unknown) {
const csrfResponse = await page.request.get("/api/v1/auth/csrf");
const csrf = (await csrfResponse.json()) as { token: string; headerName: string };
return page.request.post(path, { headers: { [csrf.headerName]: csrf.token }, data });
}
async function seedProposedService(page: Page, sourceKey: string, detectedName: string) {
const response = await postWithCsrf(page, "/api/v1/infrastructure/inventory/snapshots", {
sourceKey: "inventory-e2e",
sourceHash: `hash-${sourceKey}`,
capturedAt: new Date().toISOString(),
hosts: [
{
sourceKey: "host:e2e",
hostname: "E2E-Host",
operatingSystem: null,
kernelVersion: null,
architecture: null,
logicalCpuCount: 4,
totalMemoryBytes: 1_000_000
}
],
services: [
{
sourceKey,
hostSourceKey: "host:e2e",
detectedName,
runtimeKind: "container",
runtimeStatus: "running",
health: "Healthy",
imageReference: "example/e2e-service:1",
imageDigest: null,
runtimeUser: null,
readOnlyRootFilesystem: true,
noNewPrivileges: true,
privileged: false,
confidence: "High",
components: null,
endpoints: null,
networks: null,
volumes: null,
databases: null,
dependencies: null,
healthObservations: null,
deployments: null,
backups: null
}
]
});
expect(response.ok(), await response.text()).toBeTruthy();
}
test.describe("V2-M4 infrastructure review, editorial ownership and overrides", () => {
test("a proposed service is reviewable (accept) from the admin UI, then its facts and overrides are visibly distinct on its workspace", async ({ page }) => {
const sourceKey = `docker:e2e-accept-${Date.now()}`;
const detectedName = `e2e-accept-service-${Date.now()}`;
await seedProposedService(page, sourceKey, detectedName);
await page.goto("/admin");
await expectAuthenticatedShell(page);
await expect(page.getByRole("heading", { name: "Wijzigingen sinds snapshots" })).toBeVisible();
const proposalCard = page.locator(".inventory-proposals article", { hasText: detectedName });
await expect(proposalCard).toBeVisible();
const accessibility = await new AxeBuilder({ page }).analyze();
expect(accessibility.violations).toEqual([]);
await proposalCard.getByRole("button", { name: "Bevestigen" }).click();
await expect(proposalCard).toBeHidden();
// Confirmed via the review action — the summary list now shows it as Present, not
// Proposed, matching InfrastructureSynchronizationTests'
// An_administrator_can_accept_a_proposed_service_and_the_decision_survives_sync.
await page.goto("/infrastructure/services");
await expect(page.getByRole("heading", { name: "Services" })).toBeVisible();
const serviceLink = page.getByRole("link", { name: new RegExp(detectedName) });
await expect(serviceLink).toBeVisible();
await serviceLink.click();
await expect(page.getByRole("heading", { name: detectedName })).toBeVisible();
await page.getByRole("tab", { name: "Live facts" }).click();
const factsTable = page.locator(".data-surface table.data-table");
await expect(factsTable).toBeVisible();
// Every fact row still shows "Inventaris" (source-managed) as its ownership —
// ADR-023's "Information ownership" requirement made visible per value, not just
// as a generic legend.
await expect(factsTable.locator("tbody tr", { hasText: "Inventaris" }).first()).toBeVisible();
await expect(factsTable.getByText("Override", { exact: true })).toHaveCount(0);
const factsAccessibility = await new AxeBuilder({ page }).analyze();
expect(factsAccessibility.violations).toEqual([]);
// Add a manual override and confirm the same fact's row flips to "Override"
// ownership — the whole point of ADR-023's three-layer model made concrete in the
// UI, not just provable at the repository level.
await page.getByRole("button", { name: "Override toevoegen" }).click();
await page.getByLabel("Fact").selectOption("runtimeKind");
await page.getByLabel("Gecorrigeerde waarde").fill("virtual-machine");
await page.getByLabel("Reden").fill("E2E: automatische detectie was onjuist.");
await page.getByRole("button", { name: "Override opslaan" }).click();
// "virtual-machine" legitimately appears twice once saved (the FactsMatrix row
// and the ServiceOverrideManager's own override-list entry below it) — assert on
// the specific, scoped row rather than an ambiguous page-wide text match.
const overrideRow = factsTable.locator("tbody tr", { hasText: "Runtimecategorie" });
await expect(overrideRow.getByText("Override", { exact: true })).toBeVisible();
await expect(overrideRow).toContainText("virtual-machine");
const afterOverrideAccessibility = await new AxeBuilder({ page }).analyze();
expect(afterOverrideAccessibility.violations).toEqual([]);
// Removing the override reverts the row to its source-managed value and ownership.
await page.getByRole("button", { name: /Override voor runtimeKind verwijderen/ }).click();
await expect(page.getByText("Geen actieve overrides.")).toBeVisible();
await expect(overrideRow.getByText("Inventaris", { exact: true })).toBeVisible();
});
test("a proposed service can be rejected with a reason from the admin UI and is never confirmed", async ({ page }) => {
const sourceKey = `docker:e2e-reject-${Date.now()}`;
const detectedName = `e2e-reject-service-${Date.now()}`;
await seedProposedService(page, sourceKey, detectedName);
await page.goto("/admin");
await expectAuthenticatedShell(page);
const proposalCard = page.locator(".inventory-proposals article", { hasText: detectedName });
await expect(proposalCard).toBeVisible();
await proposalCard.getByRole("button", { name: "Afwijzen" }).click();
await proposalCard.getByLabel("Reden voor afwijzing").fill("E2E: geen bekende productiedienst.");
const accessibility = await new AxeBuilder({ page }).analyze();
expect(accessibility.violations).toEqual([]);
await proposalCard.getByRole("button", { name: "Afwijzing bevestigen" }).click();
await expect(proposalCard).toBeHidden();
// A rejected proposal must not appear as a confirmed, visible service.
await page.goto("/infrastructure/services");
await expect(page.getByRole("heading", { name: "Services" })).toBeVisible();
await expect(page.getByRole("link", { name: new RegExp(detectedName) })).toHaveCount(0);
});
test("a Reader sees infrastructure facts read-only: no review, override or editorial controls", async ({ page, baseURL }) => {
const readerEmail = `e2e-infra-reader-${Date.now()}@itworx.e2e`;
const readerPassword = "correct-horse-battery-staple-reader";
await page.goto("/");
const csrfResponse = await page.request.get("/api/v1/auth/csrf");
const csrf = (await csrfResponse.json()) as { token: string; headerName: string };
const createResponse = await page.request.post("/api/v1/admin/users", {
headers: { [csrf.headerName]: csrf.token },
data: { displayName: "E2E Infra Reader", email: readerEmail, password: readerPassword, role: "Reader" }
});
expect(createResponse.ok(), await createResponse.text()).toBeTruthy();
const readerContext = await page.context().browser()!.newContext({
baseURL,
storageState: { cookies: [], origins: [] }
});
const readerPage = await readerContext.newPage();
await readerPage.goto("/");
await readerPage.getByLabel("E-mailadres").fill(readerEmail);
await readerPage.getByLabel("Wachtwoord", { exact: true }).fill(readerPassword);
await readerPage.getByRole("button", { name: "Aanmelden" }).click();
await expect(readerPage.getByRole("button", { name: "Accountmenu" })).toBeVisible();
// The admin-only review panel must not be reachable at all (server-enforced,
// matching v2-routing-and-fidelity.spec.ts's existing /admin role-guard proof).
await readerPage.goto("/admin");
await expect(readerPage).toHaveURL(/\/$/);
await readerContext.close();
});
});