mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-11-02 01:15:31 +00:00
497 lines
19 KiB
TypeScript
497 lines
19 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
|
|
|
import {
|
|
controlledRegistryFixture,
|
|
FIXTURE_REGISTRY_KEY,
|
|
} from "./controlled-registry-fixture";
|
|
import { RegistryPage } from "./registry-page";
|
|
|
|
const fixtureMode = process.env.E2E_REGISTRY_ACCEPTANCE_MODE === "fixture";
|
|
const enabledProject = "registry";
|
|
const flagOffProject = "registry-flag-off";
|
|
const localProject = "registry-local";
|
|
const mobileProject = "registry-mobile";
|
|
|
|
function skipUnlessProject(projectName: string) {
|
|
test.skip(
|
|
test.info().project.name !== projectName,
|
|
`This scenario runs in the ${projectName} fixture profile.`,
|
|
);
|
|
}
|
|
|
|
test.describe.serial("Registry", () => {
|
|
test.setTimeout(60_000);
|
|
test.use({ storageState: "playwright/.auth/manage_registry_user.json" });
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
test.skip(
|
|
!fixtureMode,
|
|
"Registry browser acceptance is available only through the self-contained fixture profile.",
|
|
);
|
|
// The saved manager state carries the first-run marker for the enabled
|
|
// profile's origin only; the flag-off and Local profiles serve other ports.
|
|
await new RegistryPage(page).skipFirstRunRedirect();
|
|
await controlledRegistryFixture.reset();
|
|
// Exercise the real media CSP without contacting the Registry service.
|
|
await page.route(
|
|
"https://media.registry.dev.prowler.com/fixture-owner.svg",
|
|
(route) =>
|
|
route.fulfill({
|
|
contentType: "image/svg+xml",
|
|
body: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20"><circle cx="10" cy="10" r="10" fill="#2563eb"/></svg>',
|
|
}),
|
|
);
|
|
});
|
|
|
|
test(
|
|
"detects keys embedded in request URLs and browser storage JSON",
|
|
{ tag: ["@critical", "@e2e", "@registry", "@REGISTRY-E2E-011"] },
|
|
async ({ page }) => {
|
|
skipUnlessProject(enabledProject);
|
|
const registryPage = new RegistryPage(page);
|
|
const key = "synthetic-registry-disclosure-check";
|
|
await registryPage.goto();
|
|
await registryPage.verifyOnboarding();
|
|
|
|
await expect(
|
|
registryPage.verifyKeyIsNotDisclosed(key, [
|
|
`https://registry.test/request?key=${key}&source=test`,
|
|
]),
|
|
).rejects.toThrow();
|
|
|
|
for (const storage of ["localStorage", "sessionStorage"] as const) {
|
|
await page.evaluate(
|
|
({ storage, key }) => {
|
|
window[storage].setItem(
|
|
"disclosure-regression",
|
|
JSON.stringify({ nested: { key } }),
|
|
);
|
|
},
|
|
{ storage, key },
|
|
);
|
|
try {
|
|
await expect(
|
|
registryPage.verifyKeyIsNotDisclosed(key, []),
|
|
).rejects.toThrow();
|
|
} finally {
|
|
await page.evaluate((storage) => {
|
|
window[storage].removeItem("disclosure-regression");
|
|
}, storage);
|
|
}
|
|
}
|
|
await registryPage.verifyKeyIsNotDisclosed(key, []);
|
|
},
|
|
);
|
|
|
|
test(
|
|
"fails closed in Local and Registry-flag-off process profiles",
|
|
{ tag: ["@critical", "@e2e", "@registry", "@REGISTRY-E2E-001"] },
|
|
async ({ page }) => {
|
|
test.skip(
|
|
![flagOffProject, localProject].includes(test.info().project.name),
|
|
"This assertion requires the Local or Registry-flag-off fixture profile.",
|
|
);
|
|
const registryPage = new RegistryPage(page);
|
|
|
|
await page.goto("/");
|
|
await registryPage.verifyRegistryNavigationHidden();
|
|
await registryPage.goto();
|
|
await registryPage.verifyDirectRouteDenied();
|
|
await registryPage.verifyProviderSelectorWithoutRegistry();
|
|
},
|
|
);
|
|
|
|
test(
|
|
"shows the New Registry navigation entry only in the enabled manager profile",
|
|
{ tag: ["@critical", "@e2e", "@registry", "@REGISTRY-E2E-002"] },
|
|
async ({ page }) => {
|
|
skipUnlessProject(enabledProject);
|
|
const registryPage = new RegistryPage(page);
|
|
|
|
await page.goto("/");
|
|
await registryPage.verifyRegistryNavigationVisible();
|
|
await expect(
|
|
registryPage.registryLink.getByText("New", { exact: true }),
|
|
).toBeVisible();
|
|
},
|
|
);
|
|
|
|
test(
|
|
"denies stale manager storage after controlled current-authority revocation",
|
|
{ tag: ["@critical", "@e2e", "@registry", "@REGISTRY-E2E-003"] },
|
|
async ({ page }) => {
|
|
skipUnlessProject(enabledProject);
|
|
const registryPage = new RegistryPage(page);
|
|
|
|
await page.goto("/");
|
|
await registryPage.verifyRegistryNavigationVisible();
|
|
await controlledRegistryFixture.revokeCurrentAuthority();
|
|
// No client-side lease machinery: revocation is enforced by the API and
|
|
// lands on the next server-rendered request.
|
|
await page.goto("/");
|
|
await registryPage.verifyRegistryNavigationHidden();
|
|
await registryPage.goto();
|
|
await registryPage.verifyDirectRouteDenied();
|
|
},
|
|
);
|
|
|
|
test(
|
|
"lets provider managers create an installed Registry provider account without Registry management access",
|
|
{ tag: ["@critical", "@e2e", "@registry", "@REGISTRY-E2E-012"] },
|
|
async ({ page }) => {
|
|
skipUnlessProject(enabledProject);
|
|
const registry = new RegistryPage(page);
|
|
await registry.goto();
|
|
await registry.connectFixtureRegistry();
|
|
await registry.addLatest("Fixture network audit");
|
|
|
|
// The synthetic current-user response keeps manage_providers=true while
|
|
// revoking manage_registry. Live API authorization needs separate coverage.
|
|
await controlledRegistryFixture.revokeCurrentAuthority();
|
|
await registry.goto();
|
|
await registry.verifyDirectRouteDenied();
|
|
await registry.verifyRegistryNavigationHidden();
|
|
|
|
await page.goto("/providers");
|
|
await registry.dismissWelcomeDialog();
|
|
await registry.verifyRegistryNavigationHidden();
|
|
await page.getByRole("button", { name: /Add (a )?Provider/i }).click();
|
|
await page.getByRole("tab", { name: "Registry", exact: true }).click();
|
|
const provider = page.getByRole("option", {
|
|
name: "Fixture Cloud Registry",
|
|
});
|
|
await expect(provider).toBeVisible();
|
|
await provider.click();
|
|
await page
|
|
.getByLabel("Provider UID", { exact: true })
|
|
.fill("fixture-provider-manager-account");
|
|
await page
|
|
.getByLabel("Provider alias (optional)")
|
|
.fill("Provider manager Registry account");
|
|
await page.getByRole("button", { name: "Next", exact: true }).click();
|
|
await expect(
|
|
page.getByLabel("API token", { exact: false }),
|
|
).toBeVisible();
|
|
expect(await controlledRegistryFixture.snapshot()).toMatchObject({
|
|
providerCreated: true,
|
|
secretSaved: false,
|
|
});
|
|
|
|
await page.goto("/providers");
|
|
await expect(
|
|
page.getByRole("row").filter({
|
|
hasText: "Provider manager Registry account",
|
|
}),
|
|
).toBeVisible();
|
|
await registry.verifyRegistryNavigationHidden();
|
|
},
|
|
);
|
|
|
|
test(
|
|
"keeps an onboarding key write-only while 202 validation settles through an authoritative read",
|
|
{ tag: ["@critical", "@e2e", "@registry", "@REGISTRY-E2E-004"] },
|
|
async ({ page }) => {
|
|
skipUnlessProject(enabledProject);
|
|
const registryPage = new RegistryPage(page);
|
|
const requestUrls: string[] = [];
|
|
page.on("request", (request) => requestUrls.push(request.url()));
|
|
|
|
await registryPage.goto();
|
|
await registryPage.verifyOnboarding();
|
|
await controlledRegistryFixture.holdCredentialTask(true);
|
|
await registryPage.submitRegistryKey(FIXTURE_REGISTRY_KEY);
|
|
// The form stays visible while the task watcher tracks validation: the
|
|
// submit control flips to a disabled Connecting… state.
|
|
await expect(
|
|
page.getByRole("button", { name: "Connecting…" }),
|
|
).toBeDisabled();
|
|
await expect(page.getByLabel("Registry key")).toBeDisabled();
|
|
await registryPage.verifyKeyIsNotDisclosed(
|
|
FIXTURE_REGISTRY_KEY,
|
|
requestUrls,
|
|
);
|
|
await controlledRegistryFixture.holdCredentialTask(false);
|
|
await registryPage.verifyMarketplaceReady();
|
|
await expect(
|
|
page.getByText("Registry connected", { exact: true }),
|
|
).toBeVisible();
|
|
|
|
const snapshot = await controlledRegistryFixture.snapshot();
|
|
expect(snapshot.credentialAccepted).toBe(true);
|
|
expect(snapshot.credentialReadCount).toBeGreaterThanOrEqual(2);
|
|
expect(snapshot.taskReadCount).toBeGreaterThanOrEqual(2);
|
|
},
|
|
);
|
|
|
|
test(
|
|
"uses complete catalog data for recovery, direct card Add, and confirmed Remove",
|
|
{ tag: ["@critical", "@e2e", "@registry", "@REGISTRY-E2E-005"] },
|
|
async ({ page }) => {
|
|
skipUnlessProject(enabledProject);
|
|
await page.setViewportSize({ height: 900, width: 1440 });
|
|
const registryPage = new RegistryPage(page);
|
|
|
|
await registryPage.goto();
|
|
await registryPage.connectFixtureRegistry();
|
|
await registryPage.dismissWelcomeDialog();
|
|
await registryPage.verifyCompleteCatalogSearchAndFilters();
|
|
await registryPage.verifyOwnerRows();
|
|
await registryPage.captureEvidence("registry-catalog-desktop-dark");
|
|
await page.getByRole("switch", { name: "Switch to light mode" }).click();
|
|
await registryPage.captureEvidence("registry-catalog-desktop-light");
|
|
await page.setViewportSize({ width: 800, height: 1000 });
|
|
await registryPage.captureEvidence("registry-catalog-tablet-light");
|
|
await page.setViewportSize({ width: 1440, height: 900 });
|
|
await registryPage.verifyBuiltInArtifactHasNoAdd(
|
|
"Fixture built-in provider",
|
|
);
|
|
await expect(
|
|
registryPage.addButtonFor("Fixture shared policy"),
|
|
).toBeHidden();
|
|
const artifactSnapshotBefore = await controlledRegistryFixture.snapshot();
|
|
await registryPage.addLatest("Fixture network audit");
|
|
const artifactSnapshotAfter = await controlledRegistryFixture.snapshot();
|
|
expect(artifactSnapshotAfter.artifactSubmissionCount).toBe(
|
|
artifactSnapshotBefore.artifactSubmissionCount + 1,
|
|
);
|
|
expect(artifactSnapshotAfter.artifactTaskReadCount).toBe(2);
|
|
expect(artifactSnapshotAfter.artifactReadCount).toBeGreaterThan(
|
|
artifactSnapshotBefore.artifactReadCount,
|
|
);
|
|
expect(
|
|
artifactSnapshotAfter.artifactEvents.slice(
|
|
artifactSnapshotBefore.artifactEvents.length,
|
|
),
|
|
).toEqual(["submission", "task-poll", "task-poll", "authoritative-read"]);
|
|
await registryPage.verifyAddedInMyArtifacts("Fixture network audit");
|
|
await registryPage.removeArtifact("Fixture network audit");
|
|
await page.reload();
|
|
await registryPage.verifyMarketplaceReady();
|
|
await controlledRegistryFixture.setDiscoveryMode("reconnect");
|
|
await page.reload();
|
|
await expect(
|
|
page.getByRole("heading", { name: "Reconnect Registry" }),
|
|
).toBeVisible();
|
|
await controlledRegistryFixture.setDiscoveryMode("unavailable");
|
|
await page.reload();
|
|
await expect(
|
|
page.getByRole("heading", { name: "Registry is unavailable" }),
|
|
).toBeVisible();
|
|
await controlledRegistryFixture.setDiscoveryMode("error");
|
|
await page.reload();
|
|
await expect(
|
|
page.getByRole("heading", { name: "Registry could not be loaded" }),
|
|
).toBeVisible();
|
|
},
|
|
);
|
|
|
|
test(
|
|
"keeps keyboard and reduced-motion Registry browsing usable on Pixel 5",
|
|
{ tag: ["@high", "@e2e", "@registry", "@REGISTRY-E2E-006"] },
|
|
async ({ page }) => {
|
|
skipUnlessProject(mobileProject);
|
|
const registryPage = new RegistryPage(page);
|
|
await page.emulateMedia({ reducedMotion: "reduce" });
|
|
expect(
|
|
await page.evaluate(
|
|
() => window.matchMedia("(prefers-reduced-motion: reduce)").matches,
|
|
),
|
|
).toBe(true);
|
|
|
|
await registryPage.goto();
|
|
await registryPage.connectFixtureRegistry();
|
|
await registryPage.dismissWelcomeDialog();
|
|
// With no detail panel, direct card actions are the keyboard path.
|
|
await registryPage.captureEvidence("registry-catalog-mobile-dark");
|
|
const addButton = registryPage.addButtonFor("Fixture network audit");
|
|
await addButton.focus();
|
|
await addButton.press("Enter");
|
|
await expect(
|
|
page.getByText("Artifact added", { exact: true }),
|
|
).toBeVisible();
|
|
},
|
|
);
|
|
test(
|
|
"installs an external provider and completes the existing account, credentials, connection and scan wizard",
|
|
{ tag: ["@critical", "@e2e", "@registry", "@REGISTRY-E2E-007"] },
|
|
async ({ page }) => {
|
|
skipUnlessProject(enabledProject);
|
|
const registry = new RegistryPage(page);
|
|
await registry.goto();
|
|
await registry.connectFixtureRegistry();
|
|
await registry.addLatest("Fixture network audit");
|
|
await registry.connectInstalledProviderAndScan();
|
|
const snapshot = await controlledRegistryFixture.snapshot();
|
|
expect(snapshot).toMatchObject({
|
|
providerCreated: true,
|
|
secretSaved: true,
|
|
connected: true,
|
|
scanCreated: true,
|
|
});
|
|
const browserStorage = await page.evaluate(() =>
|
|
JSON.stringify({
|
|
local: { ...localStorage },
|
|
session: { ...sessionStorage },
|
|
}),
|
|
);
|
|
expect(browserStorage).not.toContain(
|
|
"fixture-provider-token-not-a-secret",
|
|
);
|
|
await registry.goto();
|
|
await registry.removeArtifact("Fixture network audit");
|
|
await page.goto("/providers");
|
|
await expect(
|
|
page.getByRole("row").filter({ hasText: "Registry test account" }),
|
|
).toBeVisible();
|
|
const beforeOpen = await controlledRegistryFixture.snapshot();
|
|
await page.getByRole("button", { name: /Add (a )?Provider/i }).click();
|
|
await expect
|
|
.poll(
|
|
async () =>
|
|
(await controlledRegistryFixture.snapshot()).artifactReadCount,
|
|
)
|
|
.toBeGreaterThan(beforeOpen.artifactReadCount);
|
|
await expect(
|
|
page.getByRole("option", { name: "Fixture Cloud Registry" }),
|
|
).toBeHidden();
|
|
},
|
|
);
|
|
test(
|
|
"updates and downgrades the installed version, including failure and reload recovery",
|
|
{ tag: ["@critical", "@e2e", "@registry", "@REGISTRY-E2E-009"] },
|
|
async ({ page }) => {
|
|
skipUnlessProject(enabledProject);
|
|
test.setTimeout(90_000);
|
|
const registry = new RegistryPage(page);
|
|
const name = "Fixture network audit";
|
|
await registry.goto();
|
|
await registry.connectFixtureRegistry();
|
|
await registry.addLatest(name);
|
|
await controlledRegistryFixture.publishVersion("1.3.0");
|
|
await page.reload();
|
|
await expect(registry.updateButtonFor(name, "1.3.0")).toBeVisible();
|
|
await expect(registry.artifactCardFor(name)).toContainText("1.2.3");
|
|
await controlledRegistryFixture.setArtifactTaskError(
|
|
"This version has been withdrawn.",
|
|
);
|
|
await registry.updateButtonFor(name, "1.3.0").click();
|
|
await expect(
|
|
page.getByText("Artifact could not be updated", { exact: true }),
|
|
).toBeVisible();
|
|
await expect(page.locator("body")).toContainText(
|
|
"The artifact could not be installed.",
|
|
);
|
|
await expect(page.locator("body")).not.toContainText(
|
|
"This version has been withdrawn.",
|
|
);
|
|
await expect(registry.updateButtonFor(name, "1.3.0")).toBeEnabled();
|
|
expect(
|
|
(await controlledRegistryFixture.snapshot()).installedVersion,
|
|
).toBe("1.2.3");
|
|
await controlledRegistryFixture.setArtifactTaskError(null);
|
|
await controlledRegistryFixture.holdArtifactTask(true);
|
|
await registry.updateButtonFor(name, "1.3.0").click();
|
|
await expect(registry.removeButtonFor(name)).toBeDisabled();
|
|
await expect
|
|
.poll(() => page.evaluate(() => localStorage.getItem("task-watcher")))
|
|
.toContain('"expectedVersion":"1.3.0"');
|
|
await page.reload();
|
|
await registry.verifyMarketplaceReady();
|
|
await expect(registry.updateButtonFor(name, "1.3.0")).toBeDisabled();
|
|
await controlledRegistryFixture.holdArtifactTask(false);
|
|
await expect(
|
|
page.getByText("Artifact updated", { exact: true }),
|
|
).toHaveCount(1);
|
|
await expect(
|
|
registry.artifactCardFor(name).getByText("Added", { exact: true }),
|
|
).toBeVisible();
|
|
const upgraded = await controlledRegistryFixture.snapshot();
|
|
expect(upgraded.installedVersion).toBe("1.3.0");
|
|
expect(upgraded.artifactTaskVersionSpec).toBe("1.3.0");
|
|
expect(upgraded.artifactSubmissionCount).toBe(3);
|
|
await controlledRegistryFixture.publishVersion("1.2.3");
|
|
await page.reload();
|
|
await registry.myArtifactsTab.click();
|
|
await registry.updateButtonFor(name, "1.2.3").click();
|
|
await expect(
|
|
registry.artifactCardFor(name).getByText("Added", { exact: true }),
|
|
).toBeVisible();
|
|
expect(
|
|
(await controlledRegistryFixture.snapshot()).installedVersion,
|
|
).toBe("1.2.3");
|
|
await registry.captureEvidence("registry-version-updated");
|
|
},
|
|
);
|
|
|
|
test(
|
|
"refreshes publications on tab changes, manually and when returning focus",
|
|
{ tag: ["@high", "@e2e", "@registry", "@REGISTRY-E2E-010"] },
|
|
async ({ page }) => {
|
|
skipUnlessProject(enabledProject);
|
|
const registry = new RegistryPage(page);
|
|
await registry.goto();
|
|
await registry.connectFixtureRegistry();
|
|
await registry.searchInput.fill("Fixture");
|
|
await controlledRegistryFixture.publishArtifact(
|
|
"Fixture tab publication",
|
|
);
|
|
await registry.myArtifactsTab.click();
|
|
await registry.exploreTab.click();
|
|
await expect(
|
|
registry.artifactCardFor("Fixture tab publication"),
|
|
).toBeVisible();
|
|
await controlledRegistryFixture.publishArtifact(
|
|
"Fixture manual publication",
|
|
);
|
|
await registry.refreshButton.click();
|
|
await expect(
|
|
registry.artifactCardFor("Fixture manual publication"),
|
|
).toBeVisible();
|
|
const beforeFocus = await controlledRegistryFixture.snapshot();
|
|
await controlledRegistryFixture.publishArtifact(
|
|
"Fixture focus publication",
|
|
);
|
|
// Synthetic focus dispatch also works in headless browsers, which do not
|
|
// reliably dispatch window focus when bringing an OS window to the front.
|
|
await page.evaluate(() => window.dispatchEvent(new Event("focus")));
|
|
await expect(
|
|
registry.artifactCardFor("Fixture focus publication"),
|
|
).toBeVisible();
|
|
await registry.verifySearchPreserved("Fixture");
|
|
expect(
|
|
(await controlledRegistryFixture.snapshot()).catalogReadCount,
|
|
).toBeGreaterThan(beforeFocus.catalogReadCount);
|
|
await registry.captureEvidence("registry-refreshed-publications");
|
|
},
|
|
);
|
|
|
|
test(
|
|
"resumes an installation after reload with one confirmation notification",
|
|
{ tag: ["@critical", "@e2e", "@registry", "@REGISTRY-E2E-008"] },
|
|
async ({ page }) => {
|
|
skipUnlessProject(enabledProject);
|
|
const registry = new RegistryPage(page);
|
|
await registry.goto();
|
|
await registry.connectFixtureRegistry();
|
|
await controlledRegistryFixture.holdArtifactTask(true);
|
|
await registry.addButtonFor("Fixture network audit").click();
|
|
await expect
|
|
.poll(() => page.evaluate(() => localStorage.getItem("task-watcher")))
|
|
.toContain('"kind":"registry-artifact-add"');
|
|
await page.reload();
|
|
await registry.verifyMarketplaceReady();
|
|
await expect(
|
|
page.getByText("Artifact added", { exact: true }),
|
|
).toBeHidden();
|
|
await controlledRegistryFixture.holdArtifactTask(false);
|
|
await expect(
|
|
page.getByText("Artifact added", { exact: true }),
|
|
).toHaveCount(1);
|
|
await registry.verifyAddedInMyArtifacts("Fixture network audit");
|
|
expect(
|
|
(await controlledRegistryFixture.snapshot()).artifactSubmissionCount,
|
|
).toBe(1);
|
|
},
|
|
);
|
|
});
|