From 346ba81ede35d8025665b753fb00420c501b4247 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Wed, 8 Jul 2026 12:26:13 +0100 Subject: [PATCH] Rewrite platform-connection specs against the consolidated workspace The per-platform connection workspaces under /settings/infrastructure/platforms/* are retired compatibility paths; connections now live in the consolidated Connected systems workspace, whose table reads the unified /api/connections endpoint and whose add dialogs still speak the unchanged per-platform REST APIs. The TrueNAS and VMware specs are rewritten against that surface: table listing from /api/connections, the add dialog's draft Test connection payload, the monitored-system impact preview copy from monitoredSystemPresentation, the capacity-denial alert (the server explanation now surfaces as a notification while the dialog stays open), and the structured unsupported-vCenter draft-test guidance, which kept its testid and copy in the ConnectionEditor. The demo-boundary spec's settings heading follows the same rename (Infrastructure Operations -> Infrastructure); its remaining retired-route references are tracked for the demo-contract pass. --- .../21-truenas-connections-workspace.spec.ts | 359 +++++ ...enas-settings-platform-connections.spec.ts | 1296 ---------------- .../22-vmware-connections-workspace.spec.ts | 240 +++ ...ware-settings-platform-connections.spec.ts | 1347 ----------------- .../53-demo-mode-commercial-boundary.spec.ts | 4 +- 5 files changed, 601 insertions(+), 2645 deletions(-) create mode 100644 tests/integration/tests/21-truenas-connections-workspace.spec.ts delete mode 100644 tests/integration/tests/21-truenas-settings-platform-connections.spec.ts create mode 100644 tests/integration/tests/22-vmware-connections-workspace.spec.ts delete mode 100644 tests/integration/tests/22-vmware-settings-platform-connections.spec.ts diff --git a/tests/integration/tests/21-truenas-connections-workspace.spec.ts b/tests/integration/tests/21-truenas-connections-workspace.spec.ts new file mode 100644 index 000000000..f8a1f2753 --- /dev/null +++ b/tests/integration/tests/21-truenas-connections-workspace.spec.ts @@ -0,0 +1,359 @@ +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { test as base, expect, type Page } from "@playwright/test"; +import { createAuthenticatedStorageState } from "./helpers"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +type WorkerFixtures = { + authStorageStatePath: string; +}; + +const test = base.extend<{}, WorkerFixtures>({ + storageState: async ({ authStorageStatePath }, use) => { + await use(authStorageStatePath); + }, + authStorageStatePath: [ + async ({ browser }, use, workerInfo) => { + const storageStatePath = path.resolve( + __dirname, + "..", + "..", + "tmp", + "playwright-auth", + `truenas-connections-workspace-${workerInfo.project.name}.json`, + ); + fs.mkdirSync(path.dirname(storageStatePath), { recursive: true }); + await createAuthenticatedStorageState(browser, storageStatePath); + try { + await use(storageStatePath); + } finally { + fs.rmSync(storageStatePath, { force: true }); + } + }, + { scope: "worker" }, + ], +}); + +const healthyAt = () => new Date(Date.now() - 5 * 60_000).toISOString(); +const failingAt = () => new Date(Date.now() - 2 * 60_000).toISOString(); + +const buildTrueNASConnectionRows = () => ({ + connections: [ + { + id: "truenas-truenas-1", + type: "truenas", + name: "Tower NAS", + address: "tower.local", + state: "active", + stateReason: "", + enabled: true, + surfaces: ["storage", "recovery"], + scope: {}, + lastSeen: healthyAt(), + lastError: null, + source: "manual", + capabilities: { supportsPause: true, supportsScope: true, supportsTest: true }, + }, + { + id: "truenas-truenas-2", + type: "truenas", + name: "Backup Vault", + address: "vault.local", + state: "unauthorized", + stateReason: "authentication failed", + enabled: false, + surfaces: ["storage"], + scope: {}, + lastSeen: failingAt(), + lastError: { message: "authentication failed", at: failingAt() }, + source: "manual", + capabilities: { supportsPause: true, supportsScope: true, supportsTest: true }, + }, + ], + systems: [], +}); + +const buildSafeTrueNASAdmissionPreview = () => ({ + current_count: 1, + projected_count: 1, + additional_count: 0, + limit: 10, + would_exceed_limit: false, + effect: "attaches_existing", + current_systems: [], + projected_systems: [ + { + name: "tower", + type: "truenas-system", + status: "online", + status_explanation: { summary: "", reasons: [] }, + latest_included_signal: { + name: "tower", + type: "truenas-system", + source: "truenas", + at: new Date().toISOString(), + }, + source: "truenas", + explanation: { summary: "", reasons: [], surfaces: [] }, + }, + ], + current_system: null, + projected_system: null, +}); + +const buildExceededTrueNASAdmissionPreview = () => ({ + current_count: 9, + projected_count: 10, + additional_count: 1, + limit: 9, + would_exceed_limit: true, + effect: "creates_new", + current_systems: [], + projected_systems: [ + { + name: "tower", + type: "truenas-system", + status: "online", + status_explanation: { summary: "", reasons: [] }, + latest_included_signal: { + name: "tower", + type: "truenas-system", + source: "truenas", + at: new Date().toISOString(), + }, + source: "truenas", + explanation: { summary: "", reasons: [], surfaces: [] }, + }, + ], + current_system: null, + projected_system: null, +}); + +const openAddTrueNASDialog = async (page: Page) => { + await page.goto("/settings/infrastructure", { waitUntil: "domcontentloaded" }); + await expect( + page.getByRole("heading", { level: 2, name: "Connected systems" }), + ).toBeVisible(); + await page.getByRole("button", { name: "Add infrastructure" }).first().click(); + const picker = page.getByRole("dialog", { name: "Add infrastructure" }); + await expect(picker).toBeVisible(); + await picker.getByRole("button", { name: /TrueNAS SCALE/ }).click(); + const dialog = page.getByRole("dialog", { name: "Add TrueNAS SCALE" }); + await expect(dialog).toBeVisible(); + return dialog; +}; + +test.describe("TrueNAS connections in the consolidated workspace", () => { + test.setTimeout(180_000); + + test("lists TrueNAS connections in the Connected systems table", async ({ + page, + }, testInfo) => { + test.skip( + testInfo.project.name.startsWith("mobile-"), + "Desktop-only connections workspace coverage", + ); + + await page.route("**/api/connections", async (route) => { + if (route.request().method() !== "GET") { + await route.continue(); + return; + } + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify(buildTrueNASConnectionRows()), + }); + }); + + await page.goto("/settings/infrastructure", { + waitUntil: "domcontentloaded", + }); + await expect( + page.getByRole("heading", { level: 1, name: "Infrastructure" }), + ).toBeVisible(); + await expect( + page.getByRole("heading", { level: 2, name: "Connected systems" }), + ).toBeVisible(); + + const towerRow = page.locator("tr").filter({ hasText: "Tower NAS" }).first(); + await expect(towerRow).toBeVisible(); + await expect(towerRow).toContainText("tower.local"); + + const vaultRow = page.locator("tr").filter({ hasText: "Backup Vault" }).first(); + await expect(vaultRow).toBeVisible(); + await expect(vaultRow).toContainText("vault.local"); + + await expect( + towerRow.getByRole("button", { name: "Manage" }), + ).toBeVisible(); + }); + + test("adds a TrueNAS connection with draft test and impact preview", async ({ + page, + }, testInfo) => { + test.skip( + testInfo.project.name.startsWith("mobile-"), + "Desktop-only connections workspace coverage", + ); + + let draftTestPayload: Record | null = null; + let createPayload: Record | null = null; + + await page.route("**/api/truenas/connections**", async (route) => { + const request = route.request(); + const method = request.method(); + const pathname = new URL(request.url()).pathname; + + if (pathname === "/api/truenas/connections" && method === "GET") { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify([]), + }); + return; + } + + if (pathname === "/api/truenas/connections/test" && method === "POST") { + draftTestPayload = JSON.parse(request.postData() || "{}"); + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ success: true }), + }); + return; + } + + if (pathname === "/api/truenas/connections/preview" && method === "POST") { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify(buildSafeTrueNASAdmissionPreview()), + }); + return; + } + + if (pathname === "/api/truenas/connections" && method === "POST") { + createPayload = JSON.parse(request.postData() || "{}"); + await route.fulfill({ + status: 201, + contentType: "application/json", + body: JSON.stringify({ + id: "conn-1", + name: createPayload.name, + host: createPayload.host, + port: 443, + apiKey: "********", + useHttps: true, + insecureSkipVerify: false, + enabled: true, + pollIntervalSeconds: 60, + }), + }); + return; + } + + await route.continue(); + }); + + const dialog = await openAddTrueNASDialog(page); + + await dialog.getByRole("textbox", { name: "Name" }).fill("Tower NAS"); + await dialog.getByRole("textbox", { name: "Host" }).fill("tower.local"); + await dialog.getByRole("textbox", { name: "API key" }).fill("secret-api-key"); + + await dialog.getByRole("button", { name: "Test connection" }).click(); + await expect.poll(() => draftTestPayload).not.toBeNull(); + expect(draftTestPayload).toMatchObject({ + host: "tower.local", + apiKey: "secret-api-key", + }); + + await dialog.getByRole("button", { name: "Preview impact" }).click(); + await expect( + dialog.getByText("This change keeps monitored-system count unchanged"), + ).toBeVisible(); + await expect( + dialog.getByText(/Pulse currently counts 1 monitored system/), + ).toBeVisible(); + + await dialog.getByRole("button", { name: "Add connection" }).click(); + await expect.poll(() => createPayload).not.toBeNull(); + expect(createPayload).toMatchObject({ + name: "Tower NAS", + host: "tower.local", + apiKey: "secret-api-key", + useHttps: true, + enabled: true, + }); + await expect(dialog).not.toBeVisible(); + }); + + test("surfaces the canonical monitored-system denial when a TrueNAS save is rejected", async ({ + page, + }, testInfo) => { + test.skip( + testInfo.project.name.startsWith("mobile-"), + "Desktop-only connections workspace coverage", + ); + + await page.route("**/api/truenas/connections**", async (route) => { + const request = route.request(); + const method = request.method(); + const pathname = new URL(request.url()).pathname; + + if (pathname === "/api/truenas/connections" && method === "GET") { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify([]), + }); + return; + } + + if (pathname === "/api/truenas/connections/preview" && method === "POST") { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify(buildSafeTrueNASAdmissionPreview()), + }); + return; + } + + if (pathname === "/api/truenas/connections" && method === "POST") { + await route.fulfill({ + status: 402, + contentType: "application/json", + body: JSON.stringify({ + error: "license_required", + message: "Monitored-system capacity reached (10/9)", + feature: "max_monitored_systems", + monitored_system_preview: buildExceededTrueNASAdmissionPreview(), + }), + }); + return; + } + + await route.continue(); + }); + + const dialog = await openAddTrueNASDialog(page); + + await dialog.getByRole("textbox", { name: "Name" }).fill("Tower NAS"); + await dialog.getByRole("textbox", { name: "Host" }).fill("tower.local"); + await dialog.getByRole("textbox", { name: "API key" }).fill("secret-api-key"); + + await dialog.getByRole("button", { name: "Add connection" }).click(); + + // The rejection surfaces the server's monitored-system explanation as an + // alert while the dialog stays open for the user to adjust or cancel. + await expect( + page + .getByRole("alert") + .getByRole("heading", { name: "Monitored-system capacity reached (10/9)" }), + ).toBeVisible(); + await expect(dialog).toBeVisible(); + }); +}); diff --git a/tests/integration/tests/21-truenas-settings-platform-connections.spec.ts b/tests/integration/tests/21-truenas-settings-platform-connections.spec.ts deleted file mode 100644 index 351bd8dff..000000000 --- a/tests/integration/tests/21-truenas-settings-platform-connections.spec.ts +++ /dev/null @@ -1,1296 +0,0 @@ -import fs from "node:fs"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; -import { test as base, expect } from "@playwright/test"; -import { createAuthenticatedStorageState } from "./helpers"; - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); - -type WorkerFixtures = { - authStorageStatePath: string; -}; - -const SCREENSHOT_PATH = path.resolve( - __dirname, - "..", - "..", - "tmp", - "truenas-settings-platform-connections.png", -); -const OPERATIONS_SCREENSHOT_PATH = path.resolve( - __dirname, - "..", - "..", - "tmp", - "truenas-settings-operations-summary.png", -); -const WORKFLOW_SCREENSHOT_PATH = path.resolve( - __dirname, - "..", - "..", - "tmp", - "truenas-settings-platform-workflow.png", -); -const HEALTH_REFRESH_SCREENSHOT_PATH = path.resolve( - __dirname, - "..", - "..", - "tmp", - "truenas-settings-health-refresh.png", -); - -const buildSafeTrueNASAdmissionPreview = () => ({ - current_count: 1, - projected_count: 1, - additional_count: 0, - limit: 10, - would_exceed_limit: false, - effect: "attaches_existing", - current_systems: [], - projected_systems: [ - { - name: "tower", - type: "truenas-system", - status: "online", - status_explanation: { summary: "", reasons: [] }, - latest_included_signal: { - name: "tower", - type: "truenas-system", - source: "truenas", - at: new Date().toISOString(), - }, - source: "truenas", - explanation: { summary: "", reasons: [], surfaces: [] }, - }, - ], - current_system: null, - projected_system: null, -}); - -const test = base.extend<{}, WorkerFixtures>({ - storageState: async ({ authStorageStatePath }, use) => { - await use(authStorageStatePath); - }, - authStorageStatePath: [ - async ({ browser }, use, workerInfo) => { - const storageStatePath = path.resolve( - __dirname, - "..", - "..", - "tmp", - "playwright-auth", - `truenas-settings-platform-connections-${workerInfo.project.name}.json`, - ); - fs.mkdirSync(path.dirname(storageStatePath), { recursive: true }); - await createAuthenticatedStorageState(browser, storageStatePath); - try { - await use(storageStatePath); - } finally { - fs.rmSync(storageStatePath, { force: true }); - } - }, - { scope: "worker" }, - ], -}); - -test.describe("TrueNAS platform connections settings", () => { - test.setTimeout(180_000); - - test("renders the platform-connections workspace with the TrueNAS integration shell", async ({ - page, - }) => { - const healthyAt = new Date(Date.now() - 5 * 60_000).toISOString(); - const failingAt = new Date(Date.now() - 2 * 60_000).toISOString(); - - await page.route("**/api/truenas/connections", async (route) => { - if (route.request().method() !== "GET") { - await route.continue(); - return; - } - - await route.fulfill({ - status: 200, - contentType: "application/json", - body: JSON.stringify([ - { - id: "truenas-1", - name: "Tower NAS", - host: "tower.local", - port: 443, - apiKey: "********", - useHttps: true, - insecureSkipVerify: false, - fingerprint: "", - enabled: true, - pollIntervalSeconds: 60, - poll: { - intervalSeconds: 60, - lastSuccessAt: healthyAt, - }, - observed: { - host: "tower", - resourceId: "tower", - collectedAt: healthyAt, - systems: 1, - storagePools: 2, - datasets: 12, - apps: 4, - disks: 8, - recoveryArtifacts: 18, - }, - }, - { - id: "truenas-2", - name: "Backup Vault", - host: "vault.local", - port: 443, - username: "admin", - password: "********", - useHttps: true, - insecureSkipVerify: true, - fingerprint: "sha256:example", - enabled: false, - pollIntervalSeconds: 300, - poll: { - intervalSeconds: 300, - lastAttemptAt: failingAt, - consecutiveFailures: 2, - lastError: { - at: failingAt, - message: "authentication failed", - category: "auth", - }, - }, - observed: { - host: "vault", - resourceId: "vault", - collectedAt: healthyAt, - systems: 1, - storagePools: 1, - datasets: 6, - apps: 0, - disks: 12, - recoveryArtifacts: 24, - }, - }, - ]), - }); - }); - - await page.goto("/settings/infrastructure/platforms/truenas", { - waitUntil: "domcontentloaded", - }); - await page.waitForURL(/\/settings\/infrastructure\/platforms\/truenas/, { - timeout: 15_000, - }); - - await expect( - page.getByRole("heading", { - level: 1, - name: "Infrastructure Operations", - }), - ).toBeVisible(); - await expect( - page.getByRole("tab", { name: "Platform connections" }), - ).toHaveAttribute("aria-selected", "true"); - await expect(page.getByRole("tab", { name: "TrueNAS" })).toHaveAttribute( - "aria-selected", - "true", - ); - await expect(page.getByRole("tab", { name: "Proxmox" })).toHaveAttribute( - "aria-selected", - "false", - ); - - await expect(page.getByText("TrueNAS platform integration")).toBeVisible(); - await expect( - page.getByRole("button", { name: "Add TrueNAS connection" }), - ).toBeVisible(); - await expect(page.getByText("Tower NAS")).toBeVisible(); - await expect(page.getByText("Backup Vault")).toBeVisible(); - await expect(page.getByText("API key auth")).toBeVisible(); - await expect(page.getByText("Username/password auth")).toBeVisible(); - await expect(page.getByText("Healthy")).toBeVisible(); - await expect(page.getByText("Paused", { exact: true })).toBeVisible(); - await expect(page.getByText("Poll every 1 minute")).toBeVisible(); - await expect(page.getByText("Poll every 5 minutes")).toBeVisible(); - await expect(page.getByText("2 pools")).toBeVisible(); - await expect(page.getByText("12 datasets")).toBeVisible(); - await expect( - page.getByTestId("truenas-connection-truenas-1-infrastructure"), - ).toHaveAttribute("href", "/infrastructure?source=truenas&resource=tower"); - await expect( - page.getByTestId("truenas-connection-truenas-1-workloads"), - ).toHaveAttribute( - "href", - "/workloads?type=app-container&platform=truenas&agent=tower", - ); - await expect( - page.getByTestId("truenas-connection-truenas-1-storage"), - ).toHaveAttribute("href", "/storage?source=truenas&node=tower"); - await expect( - page.getByTestId("truenas-connection-truenas-1-recovery"), - ).toHaveAttribute("href", "/recovery?platform=truenas&node=tower"); - - fs.mkdirSync(path.dirname(SCREENSHOT_PATH), { recursive: true }); - await page.screenshot({ path: SCREENSHOT_PATH, fullPage: true }); - }); - - test("adds, edits, retests, and deletes TrueNAS connections through the canonical settings workflow", async ({ - page, - }) => { - const syncedAt = new Date(Date.now() - 60_000).toISOString(); - let connections: Record[] = []; - let draftTestPayload: Record | null = null; - let createPayload: Record | null = null; - let updatePayload: Record | null = null; - let draftTestCalls = 0; - const savedTestRequests: Array<{ - path: string; - payload: Record | null; - }> = []; - const deletePaths: string[] = []; - - await page.route("**/api/truenas/connections**", async (route) => { - const request = route.request(); - const method = request.method(); - const pathname = new URL(request.url()).pathname; - - if (pathname === "/api/truenas/connections" && method === "GET") { - await route.fulfill({ - status: 200, - contentType: "application/json", - body: JSON.stringify(connections), - }); - return; - } - - if (pathname === "/api/truenas/connections/test" && method === "POST") { - draftTestCalls += 1; - draftTestPayload = JSON.parse(request.postData() || "{}"); - await route.fulfill({ - status: 200, - contentType: "application/json", - body: JSON.stringify({ success: true }), - }); - return; - } - - if ( - pathname === "/api/truenas/connections/preview" && - method === "POST" - ) { - await route.fulfill({ - status: 200, - contentType: "application/json", - body: JSON.stringify(buildSafeTrueNASAdmissionPreview()), - }); - return; - } - - if (pathname === "/api/truenas/connections" && method === "POST") { - createPayload = JSON.parse(request.postData() || "{}"); - connections = [ - { - id: "conn-1", - name: createPayload.name, - host: createPayload.host, - port: createPayload.port, - apiKey: "********", - useHttps: createPayload.useHttps, - insecureSkipVerify: createPayload.insecureSkipVerify, - fingerprint: createPayload.fingerprint, - enabled: createPayload.enabled, - pollIntervalSeconds: createPayload.pollIntervalSeconds, - poll: { - intervalSeconds: createPayload.pollIntervalSeconds, - lastSuccessAt: syncedAt, - }, - observed: { - host: "tower", - resourceId: "tower", - collectedAt: syncedAt, - systems: 1, - storagePools: 2, - datasets: 12, - apps: 4, - disks: 8, - recoveryArtifacts: 18, - }, - }, - ]; - await route.fulfill({ - status: 201, - contentType: "application/json", - body: JSON.stringify(connections[0]), - }); - return; - } - - if ( - pathname === "/api/truenas/connections/conn-1/test" && - method === "POST" - ) { - savedTestRequests.push({ - path: pathname, - payload: request.postData() - ? JSON.parse(request.postData() || "{}") - : null, - }); - await route.fulfill({ - status: 200, - contentType: "application/json", - body: JSON.stringify({ success: true }), - }); - return; - } - - if ( - pathname === "/api/truenas/connections/conn-1/preview" && - method === "POST" - ) { - await route.fulfill({ - status: 200, - contentType: "application/json", - body: JSON.stringify(buildSafeTrueNASAdmissionPreview()), - }); - return; - } - - if (pathname === "/api/truenas/connections/conn-1" && method === "PUT") { - updatePayload = JSON.parse(request.postData() || "{}"); - connections = [ - { - id: "conn-1", - name: updatePayload.name, - host: updatePayload.host, - port: updatePayload.port, - apiKey: "********", - useHttps: updatePayload.useHttps, - insecureSkipVerify: updatePayload.insecureSkipVerify, - fingerprint: updatePayload.fingerprint, - enabled: updatePayload.enabled, - pollIntervalSeconds: updatePayload.pollIntervalSeconds, - poll: { - intervalSeconds: updatePayload.pollIntervalSeconds, - lastSuccessAt: syncedAt, - }, - observed: { - host: "tower", - resourceId: "tower", - collectedAt: syncedAt, - systems: 1, - storagePools: 2, - datasets: 12, - apps: 4, - disks: 8, - recoveryArtifacts: 18, - }, - }, - ]; - await route.fulfill({ - status: 200, - contentType: "application/json", - body: JSON.stringify(connections[0]), - }); - return; - } - - if ( - pathname === "/api/truenas/connections/conn-1" && - method === "DELETE" - ) { - deletePaths.push(pathname); - connections = []; - await route.fulfill({ - status: 200, - contentType: "application/json", - body: JSON.stringify({ success: true, id: "conn-1" }), - }); - return; - } - - await route.continue(); - }); - - await page.goto("/settings/infrastructure/platforms/truenas", { - waitUntil: "domcontentloaded", - }); - await page.waitForURL(/\/settings\/infrastructure\/platforms\/truenas/, { - timeout: 15_000, - }); - - await expect(page.getByText("No TrueNAS connections yet")).toBeVisible(); - - await page.getByRole("button", { name: "Add TrueNAS connection" }).click(); - const dialog = page.getByRole("dialog", { name: "Add TrueNAS connection" }); - await expect(dialog).toBeVisible(); - - await dialog.getByPlaceholder("tower").fill("Tower NAS"); - await dialog.getByPlaceholder("truenas.local").fill("tower.local"); - await dialog.getByPlaceholder("443").fill("443"); - await dialog.getByPlaceholder("60").fill("90"); - await dialog - .locator('input[type="password"]') - .first() - .fill("secret-api-key"); - - await dialog.getByRole("button", { name: "Test connection" }).click(); - await expect - .poll(() => draftTestPayload) - .toMatchObject({ - name: "Tower NAS", - host: "tower.local", - port: 443, - apiKey: "secret-api-key", - useHttps: true, - enabled: true, - pollIntervalSeconds: 90, - }); - - await expect( - dialog.getByText("Preview monitored-system impact before saving"), - ).toBeVisible(); - await expect( - dialog.getByRole("button", { name: "Add connection" }), - ).toBeDisabled(); - await dialog.getByRole("button", { name: "Preview impact" }).click(); - await expect(dialog.getByText(/Current usage 1 \/ 10/)).toBeVisible(); - await expect( - dialog.getByRole("button", { name: "Add connection" }), - ).toBeEnabled(); - await dialog.getByRole("button", { name: "Add connection" }).click(); - - const connectionCard = page.getByTestId("truenas-connection-conn-1"); - await expect(connectionCard).toBeVisible(); - await expect(connectionCard).toContainText("Tower NAS"); - await expect(connectionCard).toContainText("Healthy"); - await expect - .poll(() => createPayload) - .toMatchObject({ - name: "Tower NAS", - host: "tower.local", - port: 443, - apiKey: "secret-api-key", - useHttps: true, - enabled: true, - pollIntervalSeconds: 90, - }); - - await connectionCard.getByRole("button", { name: "Edit" }).click(); - const editDialog = page.getByRole("dialog", { - name: "Edit TrueNAS connection", - }); - await expect(editDialog).toBeVisible(); - await expect( - editDialog.getByPlaceholder("Saved API key retained unless replaced"), - ).toBeVisible(); - - await editDialog.getByPlaceholder("tower").fill("Tower NAS Edited"); - await editDialog - .getByPlaceholder("truenas.local") - .fill("tower-edited.local"); - await editDialog.getByPlaceholder("60").fill("120"); - - await editDialog.getByRole("button", { name: "Test connection" }).click(); - await expect - .poll(() => savedTestRequests[0]) - .toMatchObject({ - path: "/api/truenas/connections/conn-1/test", - payload: { - name: "Tower NAS Edited", - host: "tower-edited.local", - port: 443, - apiKey: "********", - useHttps: true, - enabled: true, - pollIntervalSeconds: 120, - }, - }); - await expect.poll(() => draftTestCalls).toBe(1); - - await expect( - editDialog.getByRole("button", { name: "Save connection" }), - ).toBeDisabled(); - await editDialog.getByRole("button", { name: "Preview impact" }).click(); - await expect(editDialog.getByText(/Current usage 1 \/ 10/)).toBeVisible(); - await expect( - editDialog.getByRole("button", { name: "Save connection" }), - ).toBeEnabled(); - await editDialog.getByRole("button", { name: "Save connection" }).click(); - await expect - .poll(() => updatePayload) - .toMatchObject({ - name: "Tower NAS Edited", - host: "tower-edited.local", - port: 443, - apiKey: "********", - useHttps: true, - enabled: true, - pollIntervalSeconds: 120, - }); - await expect(connectionCard).toContainText("Tower NAS Edited"); - - await connectionCard.getByRole("button", { name: "Test" }).click(); - await expect - .poll(() => savedTestRequests) - .toEqual([ - expect.objectContaining({ - path: "/api/truenas/connections/conn-1/test", - payload: expect.objectContaining({ - host: "tower-edited.local", - apiKey: "********", - pollIntervalSeconds: 120, - }), - }), - { path: "/api/truenas/connections/conn-1/test", payload: null }, - ]); - await expect.poll(() => draftTestCalls).toBe(1); - - await connectionCard.getByRole("button", { name: "Delete" }).click(); - await page.getByRole("button", { name: "Delete connection" }).click(); - await expect - .poll(() => deletePaths) - .toEqual(["/api/truenas/connections/conn-1"]); - await expect(page.getByText("No TrueNAS connections yet")).toBeVisible(); - - fs.mkdirSync(path.dirname(WORKFLOW_SCREENSHOT_PATH), { recursive: true }); - await page.screenshot({ path: WORKFLOW_SCREENSHOT_PATH, fullPage: true }); - }); - - test("previews monitored-system impact before creating a TrueNAS connection", async ({ - page, - }) => { - let previewPayload: Record | null = null; - let createCalls = 0; - - await page.route("**/api/truenas/connections**", async (route) => { - const request = route.request(); - const method = request.method(); - const pathname = new URL(request.url()).pathname; - - if (pathname === "/api/truenas/connections" && method === "GET") { - await route.fulfill({ - status: 200, - contentType: "application/json", - body: JSON.stringify([]), - }); - return; - } - - if ( - pathname === "/api/truenas/connections/preview" && - method === "POST" - ) { - previewPayload = JSON.parse(request.postData() || "{}"); - await route.fulfill({ - status: 200, - contentType: "application/json", - body: JSON.stringify({ - current_count: 9, - projected_count: 10, - additional_count: 1, - limit: 9, - would_exceed_limit: true, - effect: "creates_new", - current_systems: [ - { - name: "tower-agent", - type: "agent", - status: "online", - status_explanation: { summary: "", reasons: [] }, - latest_included_signal: { - name: "tower-agent", - type: "agent", - source: "agent", - at: new Date(Date.now() - 60_000).toISOString(), - }, - source: "agent", - explanation: { summary: "", reasons: [], surfaces: [] }, - }, - ], - projected_systems: [ - { - name: "tower", - type: "truenas-system", - status: "online", - status_explanation: { summary: "", reasons: [] }, - latest_included_signal: { - name: "tower", - type: "truenas-system", - source: "truenas", - at: new Date().toISOString(), - }, - source: "truenas", - explanation: { summary: "", reasons: [], surfaces: [] }, - }, - ], - current_system: null, - projected_system: null, - }), - }); - return; - } - - if (pathname === "/api/truenas/connections" && method === "POST") { - createCalls += 1; - await route.fulfill({ - status: 201, - contentType: "application/json", - body: JSON.stringify({}), - }); - return; - } - - await route.continue(); - }); - - await page.goto("/settings/infrastructure/platforms/truenas", { - waitUntil: "domcontentloaded", - }); - await page.waitForURL(/\/settings\/infrastructure\/platforms\/truenas/, { - timeout: 15_000, - }); - - await page.getByRole("button", { name: "Add TrueNAS connection" }).click(); - const dialog = page.getByRole("dialog", { name: "Add TrueNAS connection" }); - await expect(dialog).toBeVisible(); - - await dialog.getByPlaceholder("tower").fill("Tower NAS"); - await dialog.getByPlaceholder("truenas.local").fill("tower.local"); - await dialog.getByPlaceholder("443").fill("443"); - await dialog - .locator('input[type="password"]') - .first() - .fill("secret-api-key"); - - await dialog.getByRole("button", { name: "Preview impact" }).click(); - - await expect - .poll(() => previewPayload) - .toMatchObject({ - name: "Tower NAS", - host: "tower.local", - port: 443, - apiKey: "secret-api-key", - useHttps: true, - enabled: true, - }); - await expect( - dialog.getByText("This change exceeds your monitored-system limit"), - ).toBeVisible(); - await expect(dialog.getByText(/Current usage 9 \/ 9/)).toBeVisible(); - await expect(dialog.getByText("Current matched systems")).toBeVisible(); - await expect( - dialog.getByText("tower-agent (Host via Agent)"), - ).toBeVisible(); - await expect(dialog.getByText("Projected systems")).toBeVisible(); - await expect( - dialog.getByText("tower (TrueNAS System via TrueNAS)"), - ).toBeVisible(); - await expect( - dialog.getByRole("button", { name: "Add connection" }), - ).toBeDisabled(); - expect(createCalls).toBe(0); - }); - - test("shows no monitored-system increase when adding a disabled TrueNAS connection", async ({ - page, - }) => { - let previewPayload: Record | null = null; - - await page.route("**/api/truenas/connections**", async (route) => { - const request = route.request(); - const method = request.method(); - const pathname = new URL(request.url()).pathname; - - if (pathname === "/api/truenas/connections" && method === "GET") { - await route.fulfill({ - status: 200, - contentType: "application/json", - body: JSON.stringify([]), - }); - return; - } - - if ( - pathname === "/api/truenas/connections/preview" && - method === "POST" - ) { - previewPayload = JSON.parse(request.postData() || "{}"); - await route.fulfill({ - status: 200, - contentType: "application/json", - body: JSON.stringify({ - current_count: 3, - projected_count: 3, - additional_count: 0, - limit: 10, - would_exceed_limit: false, - effect: "no_change", - current_systems: [], - projected_systems: [], - current_system: null, - projected_system: null, - }), - }); - return; - } - - await route.continue(); - }); - - await page.goto("/settings/infrastructure/platforms/truenas", { - waitUntil: "domcontentloaded", - }); - await page.waitForURL(/\/settings\/infrastructure\/platforms\/truenas/, { - timeout: 15_000, - }); - - await page.getByRole("button", { name: "Add TrueNAS connection" }).click(); - const dialog = page.getByRole("dialog", { name: "Add TrueNAS connection" }); - await expect(dialog).toBeVisible(); - - await dialog.getByPlaceholder("tower").fill("Archive NAS"); - await dialog.getByPlaceholder("truenas.local").fill("archive.local"); - await dialog - .locator('input[type="password"]') - .first() - .fill("secret-api-key"); - await dialog.getByLabel("Enable polling immediately").uncheck(); - - await expect( - dialog.getByRole("button", { name: "Add connection" }), - ).toBeDisabled(); - await dialog.getByRole("button", { name: "Preview impact" }).click(); - - await expect - .poll(() => previewPayload) - .toMatchObject({ - name: "Archive NAS", - host: "archive.local", - apiKey: "secret-api-key", - useHttps: true, - enabled: false, - }); - await expect( - dialog.getByText( - "This change reuses your current monitored-system capacity", - ), - ).toBeVisible(); - await expect( - dialog.getByText( - "Current usage 3 / 10. Saving this change keeps usage at 3 / 10.", - ), - ).toBeVisible(); - await expect( - dialog.getByRole("button", { name: "Add connection" }), - ).toBeEnabled(); - }); - - test("reuses the canonical monitored-system explanation when a TrueNAS save is denied", async ({ - page, - }) => { - let createPayload: Record | null = null; - - await page.route("**/api/truenas/connections**", async (route) => { - const request = route.request(); - const method = request.method(); - const pathname = new URL(request.url()).pathname; - - if (pathname === "/api/truenas/connections" && method === "GET") { - await route.fulfill({ - status: 200, - contentType: "application/json", - body: JSON.stringify([]), - }); - return; - } - - if ( - pathname === "/api/truenas/connections/preview" && - method === "POST" - ) { - await route.fulfill({ - status: 200, - contentType: "application/json", - body: JSON.stringify(buildSafeTrueNASAdmissionPreview()), - }); - return; - } - - if (pathname === "/api/truenas/connections" && method === "POST") { - createPayload = JSON.parse(request.postData() || "{}"); - await route.fulfill({ - status: 402, - contentType: "application/json", - body: JSON.stringify({ - error: "license_required", - message: "Monitored-system capacity reached (10/9)", - feature: "max_monitored_systems", - monitored_system_preview: { - current_count: 9, - projected_count: 10, - additional_count: 1, - limit: 9, - would_exceed_limit: true, - effect: "creates_new", - current_systems: [], - projected_systems: [ - { - name: "tower", - type: "truenas-system", - status: "online", - status_explanation: { summary: "", reasons: [] }, - latest_included_signal: { - name: "tower", - type: "truenas-system", - source: "truenas", - at: new Date().toISOString(), - }, - source: "truenas", - explanation: { summary: "", reasons: [], surfaces: [] }, - }, - ], - current_system: null, - projected_system: null, - }, - }), - }); - return; - } - - await route.continue(); - }); - - await page.goto("/settings/infrastructure/platforms/truenas", { - waitUntil: "domcontentloaded", - }); - await page.waitForURL(/\/settings\/infrastructure\/platforms\/truenas/, { - timeout: 15_000, - }); - - await page.getByRole("button", { name: "Add TrueNAS connection" }).click(); - const dialog = page.getByRole("dialog", { name: "Add TrueNAS connection" }); - await expect(dialog).toBeVisible(); - - await dialog.getByPlaceholder("tower").fill("Tower NAS"); - await dialog.getByPlaceholder("truenas.local").fill("tower.local"); - await dialog - .locator('input[type="password"]') - .first() - .fill("secret-api-key"); - await expect( - dialog.getByRole("button", { name: "Add connection" }), - ).toBeDisabled(); - await dialog.getByRole("button", { name: "Preview impact" }).click(); - await expect(dialog.getByText(/Current usage 1 \/ 10/)).toBeVisible(); - await expect( - dialog.getByRole("button", { name: "Add connection" }), - ).toBeEnabled(); - await dialog.getByRole("button", { name: "Add connection" }).click(); - - await expect - .poll(() => createPayload) - .toMatchObject({ - name: "Tower NAS", - host: "tower.local", - apiKey: "secret-api-key", - useHttps: true, - enabled: true, - }); - await expect( - dialog.getByText("This change exceeds your monitored-system limit"), - ).toBeVisible(); - await expect(dialog.getByText(/Current usage 9 \/ 9/)).toBeVisible(); - await expect(dialog.getByText("Projected systems")).toBeVisible(); - await expect( - dialog.getByText("tower (TrueNAS System via TrueNAS)"), - ).toBeVisible(); - await expect( - dialog.getByRole("button", { name: "Add connection" }), - ).toBeDisabled(); - }); - - test("blocks TrueNAS save while monitored-system usage is still settling", async ({ - page, - }) => { - let previewPayload: Record | null = null; - let createCalls = 0; - - await page.route("**/api/truenas/connections**", async (route) => { - const request = route.request(); - const method = request.method(); - const pathname = new URL(request.url()).pathname; - - if (pathname === "/api/truenas/connections" && method === "GET") { - await route.fulfill({ - status: 200, - contentType: "application/json", - body: JSON.stringify([]), - }); - return; - } - - if ( - pathname === "/api/truenas/connections/preview" && - method === "POST" - ) { - previewPayload = JSON.parse(request.postData() || "{}"); - await route.fulfill({ - status: 503, - contentType: "application/json", - body: JSON.stringify({ - error: "Unable to verify monitored-system capacity right now", - code: "monitored_system_usage_unavailable", - status_code: 503, - details: { - reason: "supplemental_inventory_unsettled", - }, - }), - }); - return; - } - - if (pathname === "/api/truenas/connections" && method === "POST") { - createCalls += 1; - await route.fulfill({ - status: 201, - contentType: "application/json", - body: JSON.stringify({}), - }); - return; - } - - await route.continue(); - }); - - await page.goto("/settings/infrastructure/platforms/truenas", { - waitUntil: "domcontentloaded", - }); - await page.waitForURL(/\/settings\/infrastructure\/platforms\/truenas/, { - timeout: 15_000, - }); - - await page.getByRole("button", { name: "Add TrueNAS connection" }).click(); - const dialog = page.getByRole("dialog", { name: "Add TrueNAS connection" }); - await expect(dialog).toBeVisible(); - - await dialog.getByPlaceholder("tower").fill("Tower NAS"); - await dialog.getByPlaceholder("truenas.local").fill("tower.local"); - await dialog - .locator('input[type="password"]') - .first() - .fill("secret-api-key"); - - await dialog.getByRole("button", { name: "Preview impact" }).click(); - - await expect - .poll(() => previewPayload) - .toMatchObject({ - name: "Tower NAS", - host: "tower.local", - apiKey: "secret-api-key", - useHttps: true, - enabled: true, - }); - await expect( - dialog.getByText("Monitored-system capacity is temporarily unavailable"), - ).toBeVisible(); - await expect( - dialog.getByText( - "Pulse is still settling provider-owned inventory for this platform connection, so the monitored-system check is not safe yet. Retry preview after the first baseline finishes.", - ), - ).toBeVisible(); - await expect( - dialog.getByRole("button", { name: "Add connection" }), - ).toBeDisabled(); - expect(createCalls).toBe(0); - }); - - test("saved connection tests refresh runtime health in the settings card", async ({ - page, - }) => { - const failedAt = new Date(Date.now() - 5 * 60_000).toISOString(); - const recoveredAt = new Date().toISOString(); - let savedTestCalls = 0; - let connection = { - id: "conn-1", - name: "Tower NAS", - host: "tower.local", - port: 443, - apiKey: "********", - useHttps: true, - insecureSkipVerify: false, - enabled: true, - pollIntervalSeconds: 60, - poll: { - intervalSeconds: 60, - lastAttemptAt: failedAt, - consecutiveFailures: 2, - lastError: { - at: failedAt, - message: "authentication failed", - category: "auth", - }, - }, - observed: { - host: "tower", - resourceId: "tower", - collectedAt: failedAt, - systems: 1, - storagePools: 2, - datasets: 12, - apps: 4, - disks: 8, - recoveryArtifacts: 18, - }, - }; - - await page.route("**/api/truenas/connections**", async (route) => { - const request = route.request(); - const method = request.method(); - const pathname = new URL(request.url()).pathname; - - if (pathname === "/api/truenas/connections" && method === "GET") { - await route.fulfill({ - status: 200, - contentType: "application/json", - body: JSON.stringify([connection]), - }); - return; - } - - if ( - pathname === "/api/truenas/connections/conn-1/test" && - method === "POST" - ) { - savedTestCalls += 1; - connection = { - ...connection, - poll: { - intervalSeconds: 60, - lastSuccessAt: recoveredAt, - }, - }; - await route.fulfill({ - status: 200, - contentType: "application/json", - body: JSON.stringify({ success: true }), - }); - return; - } - - await route.continue(); - }); - - await page.goto("/settings/infrastructure/platforms/truenas", { - waitUntil: "domcontentloaded", - }); - await page.waitForURL(/\/settings\/infrastructure\/platforms\/truenas/, { - timeout: 15_000, - }); - - const connectionCard = page.getByTestId("truenas-connection-conn-1"); - await expect(connectionCard).toContainText("Sync failing"); - await expect(connectionCard).toContainText("authentication failed"); - - await connectionCard.getByRole("button", { name: "Test" }).click(); - - await expect.poll(() => savedTestCalls).toBe(1); - await expect(connectionCard).toContainText("Healthy"); - await expect(connectionCard).not.toContainText("authentication failed"); - - fs.mkdirSync(path.dirname(HEALTH_REFRESH_SCREENSHOT_PATH), { - recursive: true, - }); - await page.screenshot({ - path: HEALTH_REFRESH_SCREENSHOT_PATH, - fullPage: true, - }); - }); - - test("counts TrueNAS alongside the other platform connections on the operations summary", async ({ - page, - }) => { - await page.route("**/api/config/nodes", async (route) => { - if (route.request().method() !== "GET") { - await route.continue(); - return; - } - - await route.fulfill({ - status: 200, - contentType: "application/json", - body: JSON.stringify([ - { - id: "pve-1", - type: "pve", - name: "pve-main", - host: "pve-main.local", - user: "root@pam", - verifySSL: true, - monitorVMs: true, - monitorContainers: true, - monitorStorage: true, - monitorBackups: true, - monitorPhysicalDisks: true, - status: "connected", - }, - { - id: "pbs-1", - type: "pbs", - name: "pbs-main", - host: "pbs-main.local", - user: "root@pam", - verifySSL: true, - monitorDatastores: true, - monitorSyncJobs: true, - monitorVerifyJobs: true, - monitorPruneJobs: true, - monitorGarbageJobs: true, - status: "connected", - }, - { - id: "pbs-2", - type: "pbs", - name: "pbs-vault", - host: "pbs-vault.local", - user: "root@pam", - verifySSL: true, - monitorDatastores: true, - monitorSyncJobs: true, - monitorVerifyJobs: true, - monitorPruneJobs: true, - monitorGarbageJobs: true, - status: "connected", - }, - { - id: "pmg-1", - type: "pmg", - name: "pmg-main", - host: "pmg-main.local", - user: "root@pam", - verifySSL: true, - monitorMailStats: true, - monitorQueues: true, - monitorQuarantine: true, - monitorDomainStats: true, - status: "connected", - }, - ]), - }); - }); - - await page.route("**/api/truenas/connections", async (route) => { - if (route.request().method() !== "GET") { - await route.continue(); - return; - } - - await route.fulfill({ - status: 200, - contentType: "application/json", - body: JSON.stringify([ - { - id: "truenas-1", - name: "Tower NAS", - host: "tower.local", - port: 443, - apiKey: "********", - useHttps: true, - insecureSkipVerify: false, - enabled: true, - }, - { - id: "truenas-2", - name: "Backup Vault", - host: "vault.local", - port: 443, - apiKey: "********", - useHttps: true, - insecureSkipVerify: false, - enabled: true, - }, - ]), - }); - }); - - await page.goto("/settings/infrastructure/operations", { - waitUntil: "domcontentloaded", - }); - await page.waitForURL(/\/settings\/infrastructure\/operations/, { - timeout: 15_000, - }); - - await expect( - page.getByRole("heading", { - level: 1, - name: "Infrastructure Operations", - }), - ).toBeVisible(); - await expect( - page.getByRole("heading", { level: 3, name: "Platform connections" }), - ).toBeVisible(); - await expect(page.getByTestId("platform-connections-pve")).toContainText( - "1", - ); - await expect(page.getByTestId("platform-connections-pbs")).toContainText( - "2", - ); - await expect(page.getByTestId("platform-connections-pmg")).toContainText( - "1", - ); - await expect( - page.getByTestId("platform-connections-truenas"), - ).toContainText("2"); - await expect( - page.getByTestId("platform-connections-truenas"), - ).toContainText("API-backed NAS connections"); - - fs.mkdirSync(path.dirname(OPERATIONS_SCREENSHOT_PATH), { recursive: true }); - await page.screenshot({ path: OPERATIONS_SCREENSHOT_PATH, fullPage: true }); - }); - - test("treats disabled TrueNAS as an explicit opt-out instead of a setup prerequisite", async ({ - page, - }) => { - await page.route("**/api/truenas/connections", async (route) => { - if (route.request().method() !== "GET") { - await route.continue(); - return; - } - - await route.fulfill({ - status: 404, - contentType: "application/json", - body: JSON.stringify({ - error: "truenas_disabled", - message: "TrueNAS integration has been explicitly disabled", - }), - }); - }); - - await page.goto("/settings/infrastructure/platforms/truenas", { - waitUntil: "domcontentloaded", - }); - await page.waitForURL(/\/settings\/infrastructure\/platforms\/truenas/, { - timeout: 15_000, - }); - - await expect( - page.getByText("TrueNAS integration is disabled"), - ).toBeVisible(); - await expect( - page.getByText("TrueNAS integration has been explicitly disabled"), - ).toBeVisible(); - await expect(page.getByText(/PULSE_ENABLE_TRUENAS=false/)).toBeVisible(); - await expect(page.getByText(/set it back to true/i)).toBeVisible(); - await expect( - page.getByRole("button", { name: "Add TrueNAS connection" }), - ).not.toBeVisible(); - await expect( - page.locator('[data-testid^=\"truenas-connection-\"]'), - ).toHaveCount(0); - }); -}); diff --git a/tests/integration/tests/22-vmware-connections-workspace.spec.ts b/tests/integration/tests/22-vmware-connections-workspace.spec.ts new file mode 100644 index 000000000..963e848c3 --- /dev/null +++ b/tests/integration/tests/22-vmware-connections-workspace.spec.ts @@ -0,0 +1,240 @@ +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { test as base, expect, type Page } from "@playwright/test"; +import { createAuthenticatedStorageState } from "./helpers"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +type WorkerFixtures = { + authStorageStatePath: string; +}; + +const test = base.extend<{}, WorkerFixtures>({ + storageState: async ({ authStorageStatePath }, use) => { + await use(authStorageStatePath); + }, + authStorageStatePath: [ + async ({ browser }, use, workerInfo) => { + const storageStatePath = path.resolve( + __dirname, + "..", + "..", + "tmp", + "playwright-auth", + `vmware-connections-workspace-${workerInfo.project.name}.json`, + ); + fs.mkdirSync(path.dirname(storageStatePath), { recursive: true }); + await createAuthenticatedStorageState(browser, storageStatePath); + try { + await use(storageStatePath); + } finally { + fs.rmSync(storageStatePath, { force: true }); + } + }, + { scope: "worker" }, + ], +}); + +const buildSafeVMwareAdmissionPreview = () => ({ + current_count: 1, + projected_count: 1, + additional_count: 0, + limit: 10, + would_exceed_limit: false, + effect: "attaches_existing", + current_systems: [], + projected_systems: [], + current_system: null, + projected_system: null, +}); + +const openAddVMwareDialog = async (page: Page) => { + await page.goto("/settings/infrastructure", { waitUntil: "domcontentloaded" }); + await expect( + page.getByRole("heading", { level: 2, name: "Connected systems" }), + ).toBeVisible(); + await page.getByRole("button", { name: "Add infrastructure" }).first().click(); + const picker = page.getByRole("dialog", { name: "Add infrastructure" }); + await expect(picker).toBeVisible(); + // The VMware tile sits behind the collapsed tail of the source catalog. + await picker.getByRole("button", { name: /Show \d+ more sources/ }).click(); + await picker.getByRole("button", { name: /VMware vCenter/ }).click(); + const dialog = page.getByRole("dialog", { name: "Add VMware vCenter" }); + await expect(dialog).toBeVisible(); + return dialog; +}; + +test.describe("VMware connections in the consolidated workspace", () => { + test.setTimeout(180_000); + + test("adds a vCenter connection with draft test and impact preview", async ({ + page, + }, testInfo) => { + test.skip( + testInfo.project.name.startsWith("mobile-"), + "Desktop-only connections workspace coverage", + ); + + let draftTestPayload: Record | null = null; + let createPayload: Record | null = null; + + await page.route("**/api/vmware/connections**", async (route) => { + const request = route.request(); + const method = request.method(); + const pathname = new URL(request.url()).pathname; + + if (pathname === "/api/vmware/connections" && method === "GET") { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify([]), + }); + return; + } + + if (pathname === "/api/vmware/connections/test" && method === "POST") { + draftTestPayload = JSON.parse(request.postData() || "{}"); + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ success: true }), + }); + return; + } + + if (pathname === "/api/vmware/connections/preview" && method === "POST") { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify(buildSafeVMwareAdmissionPreview()), + }); + return; + } + + if (pathname === "/api/vmware/connections" && method === "POST") { + createPayload = JSON.parse(request.postData() || "{}"); + await route.fulfill({ + status: 201, + contentType: "application/json", + body: JSON.stringify({ + id: "vc-1", + name: createPayload.name, + host: createPayload.host, + port: 443, + username: createPayload.username, + enabled: true, + }), + }); + return; + } + + await route.continue(); + }); + + const dialog = await openAddVMwareDialog(page); + + await dialog.getByRole("textbox", { name: "Name", exact: true }).fill("Lab VC"); + await dialog.getByRole("textbox", { name: "Host" }).fill("vcsa.lab.local"); + await dialog + .getByRole("textbox", { name: "Username" }) + .fill("administrator@vsphere.local"); + await dialog.getByRole("textbox", { name: "Password" }).fill("super-secret"); + + await dialog.getByRole("button", { name: "Test connection" }).click(); + await expect.poll(() => draftTestPayload).not.toBeNull(); + expect(draftTestPayload).toMatchObject({ + host: "vcsa.lab.local", + username: "administrator@vsphere.local", + password: "super-secret", + }); + + await dialog.getByRole("button", { name: "Preview impact" }).click(); + await expect( + dialog.getByText("This change keeps monitored-system count unchanged"), + ).toBeVisible(); + + await dialog.getByRole("button", { name: "Add connection" }).click(); + await expect.poll(() => createPayload).not.toBeNull(); + expect(createPayload).toMatchObject({ + name: "Lab VC", + host: "vcsa.lab.local", + username: "administrator@vsphere.local", + password: "super-secret", + }); + await expect(dialog).not.toBeVisible(); + }); + + test("surfaces structured draft test guidance for unsupported vCenter versions", async ({ + page, + }, testInfo) => { + test.skip( + testInfo.project.name.startsWith("mobile-"), + "Desktop-only connections workspace coverage", + ); + + let createCalls = 0; + + await page.route("**/api/vmware/connections**", async (route) => { + const request = route.request(); + const method = request.method(); + const pathname = new URL(request.url()).pathname; + + if (pathname === "/api/vmware/connections" && method === "GET") { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify([]), + }); + return; + } + + if (pathname === "/api/vmware/connections/test" && method === "POST") { + await route.fulfill({ + status: 400, + contentType: "application/json", + body: JSON.stringify({ + error: "Failed to connect to VMware vCenter", + code: "vmware_connection_failed", + status_code: 400, + details: { + category: "unsupported_version", + error: + "VMware vCenter 6.7 is below the supported VI JSON release floor", + }, + }), + }); + return; + } + + if (pathname === "/api/vmware/connections" && method === "POST") { + createCalls += 1; + await route.fulfill({ + status: 201, + contentType: "application/json", + body: JSON.stringify({}), + }); + return; + } + + await route.continue(); + }); + + const dialog = await openAddVMwareDialog(page); + + await dialog.getByRole("textbox", { name: "Host" }).fill("legacy.lab.local"); + await dialog + .getByRole("textbox", { name: "Username" }) + .fill("administrator@vsphere.local"); + await dialog.getByRole("textbox", { name: "Password" }).fill("super-secret"); + await dialog.getByRole("button", { name: "Test connection" }).click(); + + const feedback = page.getByTestId("vmware-connection-test-feedback"); + await expect(feedback).toBeVisible(); + await expect(feedback).toContainText("Unsupported vCenter version"); + await expect(feedback).toContainText( + "VMware vCenter 6.7 is below the supported VI JSON release floor", + ); + expect(createCalls).toBe(0); + }); +}); diff --git a/tests/integration/tests/22-vmware-settings-platform-connections.spec.ts b/tests/integration/tests/22-vmware-settings-platform-connections.spec.ts deleted file mode 100644 index e7a70eb45..000000000 --- a/tests/integration/tests/22-vmware-settings-platform-connections.spec.ts +++ /dev/null @@ -1,1347 +0,0 @@ -import fs from "node:fs"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; -import { test as base, expect } from "@playwright/test"; -import { createAuthenticatedStorageState } from "./helpers"; - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); - -type WorkerFixtures = { - authStorageStatePath: string; -}; - -const SCREENSHOT_PATH = path.resolve( - __dirname, - "..", - "..", - "tmp", - "vmware-settings-platform-connections.png", -); -const WORKFLOW_SCREENSHOT_PATH = path.resolve( - __dirname, - "..", - "..", - "tmp", - "vmware-settings-platform-workflow.png", -); - -const buildSafeVMwareAdmissionPreview = () => ({ - current_count: 1, - projected_count: 1, - additional_count: 0, - limit: 10, - would_exceed_limit: false, - effect: "attaches_existing", - current_systems: [], - projected_systems: [ - { - name: "esxi-01", - type: "host", - status: "online", - status_explanation: { summary: "", reasons: [] }, - latest_included_signal: { - name: "esxi-01", - type: "host", - source: "vmware", - at: new Date().toISOString(), - }, - source: "vmware", - explanation: { summary: "", reasons: [], surfaces: [] }, - }, - ], - current_system: null, - projected_system: null, -}); - -const test = base.extend<{}, WorkerFixtures>({ - storageState: async ({ authStorageStatePath }, use) => { - await use(authStorageStatePath); - }, - authStorageStatePath: [ - async ({ browser }, use, workerInfo) => { - const storageStatePath = path.resolve( - __dirname, - "..", - "..", - "tmp", - "playwright-auth", - `vmware-settings-platform-connections-${workerInfo.project.name}.json`, - ); - fs.mkdirSync(path.dirname(storageStatePath), { recursive: true }); - await createAuthenticatedStorageState(browser, storageStatePath); - try { - await use(storageStatePath); - } finally { - fs.rmSync(storageStatePath, { force: true }); - } - }, - { scope: "worker" }, - ], -}); - -test.describe("VMware platform connections settings", () => { - test.setTimeout(180_000); - - test("renders the platform-connections workspace with the VMware integration shell", async ({ - page, - }) => { - const healthyAt = new Date(Date.now() - 5 * 60_000).toISOString(); - const failingAt = new Date(Date.now() - 2 * 60_000).toISOString(); - - await page.route("**/api/vmware/connections", async (route) => { - if (route.request().method() !== "GET") { - await route.continue(); - return; - } - - await route.fulfill({ - status: 200, - contentType: "application/json", - body: JSON.stringify([ - { - id: "vmware-1", - name: "Primary vCenter", - host: "vcsa-primary.local", - port: 443, - username: "administrator@vsphere.local", - password: "********", - insecureSkipVerify: false, - enabled: true, - poll: { - lastSuccessAt: healthyAt, - }, - observed: { - collectedAt: healthyAt, - hosts: 6, - vms: 120, - datastores: 18, - viRelease: "8.0.3", - }, - }, - { - id: "vmware-2", - name: "Staging vCenter", - host: "vcsa-staging.local", - port: 443, - username: "operator@vsphere.local", - password: "********", - insecureSkipVerify: true, - enabled: true, - poll: { - lastAttemptAt: failingAt, - lastError: { - at: failingAt, - message: "authentication failed", - category: "auth", - }, - }, - }, - { - id: "vmware-3", - name: "Partial vCenter", - host: "vcsa-partial.local", - port: 443, - username: "readonly@vsphere.local", - password: "********", - insecureSkipVerify: false, - enabled: true, - poll: { - lastSuccessAt: healthyAt, - }, - observed: { - collectedAt: healthyAt, - hosts: 2, - vms: 18, - datastores: 4, - viRelease: "8.0.3", - degraded: true, - issueCount: 3, - issues: [ - { - stage: "signals", - category: "permission", - message: - "VMware permissions are insufficient for host overall status", - occurrences: 2, - }, - ], - }, - }, - ]), - }); - }); - - await page.goto("/settings/infrastructure/platforms/vmware", { - waitUntil: "domcontentloaded", - }); - await page.waitForURL(/\/settings\/infrastructure\/platforms\/vmware/, { - timeout: 15_000, - }); - - await expect( - page.getByRole("heading", { - level: 1, - name: "Infrastructure Operations", - }), - ).toBeVisible(); - await expect( - page.getByRole("tab", { name: "Platform connections" }), - ).toHaveAttribute("aria-selected", "true"); - await expect(page.getByRole("tab", { name: "VMware" })).toHaveAttribute( - "aria-selected", - "true", - ); - await expect(page.getByRole("tab", { name: "TrueNAS" })).toHaveAttribute( - "aria-selected", - "false", - ); - await expect(page.getByRole("tab", { name: "Proxmox" })).toHaveAttribute( - "aria-selected", - "false", - ); - - await expect( - page.getByText("VMware vSphere platform integration"), - ).toBeVisible(); - await expect( - page.getByRole("button", { name: "Add VMware connection" }), - ).toBeVisible(); - - const primaryCard = page.getByTestId("vmware-connection-vmware-1"); - await expect(primaryCard).toContainText("Primary vCenter"); - await expect(primaryCard).toContainText("Healthy"); - await expect(primaryCard).toContainText("6 hosts"); - await expect(primaryCard).toContainText("120 vms"); - await expect(primaryCard).toContainText("18 datastores"); - await expect(primaryCard).toContainText("VI JSON 8.0.3"); - - const failingCard = page.getByTestId("vmware-connection-vmware-2"); - await expect(failingCard).toContainText("Staging vCenter"); - await expect(failingCard).toContainText("Runtime failing"); - await expect(failingCard).toContainText("authentication failed"); - await expect(failingCard).toContainText("Authentication failed"); - await expect(failingCard).toContainText( - "Verify the username, password, and account scope in vCenter before retrying.", - ); - await expect(failingCard).toContainText("Skip TLS verification"); - - const partialCard = page.getByTestId("vmware-connection-vmware-3"); - await expect(partialCard).toContainText("Partial vCenter"); - await expect(partialCard).toContainText("Degraded"); - await expect(partialCard).toContainText("2 hosts"); - await expect(partialCard).toContainText("18 vms"); - await expect(partialCard).toContainText("4 datastores"); - await expect(partialCard).toContainText("3 degraded reads"); - await expect(partialCard).toContainText( - "VMware permissions are insufficient for host overall status", - ); - await expect(partialCard).toContainText("Permissions are insufficient"); - await expect(partialCard).toContainText( - "Grant the minimum VMware read privileges required for phase-1 inventory and health reads, then retry.", - ); - - fs.mkdirSync(path.dirname(SCREENSHOT_PATH), { recursive: true }); - await page.screenshot({ path: SCREENSHOT_PATH, fullPage: true }); - }); - - test("surfaces structured draft test guidance for unsupported vCenter versions", async ({ - page, - }) => { - let createCalls = 0; - - await page.route("**/api/vmware/connections**", async (route) => { - const request = route.request(); - const method = request.method(); - const pathname = new URL(request.url()).pathname; - - if (pathname === "/api/vmware/connections" && method === "GET") { - await route.fulfill({ - status: 200, - contentType: "application/json", - body: JSON.stringify([]), - }); - return; - } - - if (pathname === "/api/vmware/connections/test" && method === "POST") { - await route.fulfill({ - status: 400, - contentType: "application/json", - body: JSON.stringify({ - error: "Failed to connect to VMware vCenter", - code: "vmware_connection_failed", - status_code: 400, - details: { - category: "unsupported_version", - error: - "VMware vCenter 6.7 is below the supported VI JSON release floor", - }, - }), - }); - return; - } - - if (pathname === "/api/vmware/connections" && method === "POST") { - createCalls += 1; - await route.fulfill({ - status: 201, - contentType: "application/json", - body: JSON.stringify({}), - }); - return; - } - - await route.continue(); - }); - - await page.goto("/settings/infrastructure/platforms/vmware", { - waitUntil: "domcontentloaded", - }); - await page.waitForURL(/\/settings\/infrastructure\/platforms\/vmware/, { - timeout: 15_000, - }); - - await page.getByRole("button", { name: "Add VMware connection" }).click(); - await page.getByLabel("Host").fill("legacy.lab.local"); - await page.getByLabel("Username").fill("administrator@vsphere.local"); - await page.getByLabel("Password").fill("super-secret"); - await page.getByRole("button", { name: "Test connection" }).click(); - - const feedback = page.getByTestId("vmware-connection-test-feedback"); - await expect(feedback).toBeVisible(); - await expect(feedback).toContainText("Unsupported vCenter version"); - await expect(feedback).toContainText( - "VMware vCenter 6.7 is below the supported VI JSON release floor", - ); - await expect(feedback).toContainText( - "Use a supported vCenter release within the current VI JSON phase-1 floor, then retry this connection test.", - ); - await expect(page.getByRole("dialog")).toContainText( - "Configure the vCenter endpoint Pulse should validate for the VMware platform.", - ); - expect(createCalls).toBe(0); - }); - - test("surfaces shared draft onboarding guidance for auth, TLS, and network failures", async ({ - page, - }) => { - const scenarios = [ - { - category: "auth", - error: - "VMware authentication failed while creating the VI JSON API session", - expectedTitle: "Authentication failed", - expectedGuidance: - "Verify the username, password, and account scope in vCenter before retrying.", - }, - { - category: "tls", - error: - "VMware TLS validation failed during Automation API session bootstrap", - expectedTitle: "TLS validation failed", - expectedGuidance: - "Install a trusted certificate for vCenter, or enable Skip TLS verification only for controlled lab environments.", - }, - { - category: "network", - error: "VMware network error during VI JSON login", - expectedTitle: "Pulse could not reach vCenter", - expectedGuidance: - "Confirm DNS, reachability, port 443, and any firewall rules from the Pulse server to vCenter.", - }, - ]; - let draftFailureIndex = 0; - - await page.route("**/api/vmware/connections**", async (route) => { - const request = route.request(); - const method = request.method(); - const pathname = new URL(request.url()).pathname; - - if (pathname === "/api/vmware/connections" && method === "GET") { - await route.fulfill({ - status: 200, - contentType: "application/json", - body: JSON.stringify([]), - }); - return; - } - - if (pathname === "/api/vmware/connections/test" && method === "POST") { - const scenario = - scenarios[Math.min(draftFailureIndex, scenarios.length - 1)]; - draftFailureIndex += 1; - await route.fulfill({ - status: 400, - contentType: "application/json", - body: JSON.stringify({ - error: "Failed to connect to VMware vCenter", - code: "vmware_connection_failed", - status_code: 400, - details: { - category: scenario.category, - error: scenario.error, - }, - }), - }); - return; - } - - await route.continue(); - }); - - await page.goto("/settings/infrastructure/platforms/vmware", { - waitUntil: "domcontentloaded", - }); - await page.waitForURL(/\/settings\/infrastructure\/platforms\/vmware/, { - timeout: 15_000, - }); - - for (const scenario of scenarios) { - await page.getByRole("button", { name: "Add VMware connection" }).click(); - await page.getByLabel("Host").fill(`${scenario.category}.lab.local`); - await page.getByLabel("Username").fill("administrator@vsphere.local"); - await page.getByLabel("Password").fill("super-secret"); - await page.getByRole("button", { name: "Test connection" }).click(); - - const feedback = page.getByTestId("vmware-connection-test-feedback"); - await expect(feedback).toBeVisible(); - await expect(feedback).toContainText(scenario.expectedTitle); - await expect(feedback).toContainText(scenario.error); - await expect(feedback).toContainText(scenario.expectedGuidance); - - await page.getByRole("button", { name: "Cancel" }).click(); - await expect(page.getByRole("dialog")).not.toBeVisible(); - } - }); - - test("surfaces saved runtime guidance when a VMware retest fails", async ({ - page, - }) => { - const authError = - "VMware authentication failed while creating the VI JSON API session"; - const healthyAt = new Date(Date.now() - 5 * 60_000).toISOString(); - const failedAt = new Date(Date.now() - 60_000).toISOString(); - let connections: Record[] = [ - { - id: "conn-1", - name: "Primary vCenter", - host: "vcsa-primary.local", - port: 443, - username: "administrator@vsphere.local", - password: "********", - insecureSkipVerify: false, - enabled: true, - poll: { - lastSuccessAt: healthyAt, - }, - observed: { - collectedAt: healthyAt, - hosts: 6, - vms: 120, - datastores: 18, - viRelease: "8.0.3", - }, - }, - ]; - - await page.route("**/api/vmware/connections**", async (route) => { - const request = route.request(); - const method = request.method(); - const pathname = new URL(request.url()).pathname; - - if (pathname === "/api/vmware/connections" && method === "GET") { - await route.fulfill({ - status: 200, - contentType: "application/json", - body: JSON.stringify(connections), - }); - return; - } - - if ( - pathname === "/api/vmware/connections/conn-1/test" && - method === "POST" - ) { - connections = [ - { - ...connections[0], - poll: { - lastAttemptAt: failedAt, - lastSuccessAt: healthyAt, - lastError: { - at: failedAt, - category: "auth", - message: authError, - }, - }, - observed: { - collectedAt: healthyAt, - hosts: 6, - vms: 120, - datastores: 18, - viRelease: "8.0.3", - }, - }, - ]; - await route.fulfill({ - status: 400, - contentType: "application/json", - body: JSON.stringify({ - error: "Failed to connect to VMware vCenter", - code: "vmware_connection_failed", - status_code: 400, - details: { - category: "auth", - error: authError, - }, - }), - }); - return; - } - - await route.continue(); - }); - - await page.goto("/settings/infrastructure/platforms/vmware", { - waitUntil: "domcontentloaded", - }); - await page.waitForURL(/\/settings\/infrastructure\/platforms\/vmware/, { - timeout: 15_000, - }); - - const primaryCard = page.getByTestId("vmware-connection-conn-1"); - await expect(primaryCard).toContainText("Healthy"); - - await primaryCard.getByRole("button", { name: "Test" }).click(); - - await expect(primaryCard).toContainText("Runtime failing"); - await expect(primaryCard).toContainText("Authentication failed"); - await expect(primaryCard).toContainText(authError); - await expect(primaryCard).toContainText( - "Verify the username, password, and account scope in vCenter before retrying.", - ); - }); - - test("adds, edits, retests, and deletes VMware connections through the canonical settings workflow", async ({ - page, - }) => { - const validatedAt = new Date(Date.now() - 60_000).toISOString(); - let connections: Record[] = []; - let draftTestPayload: Record | null = null; - let createPayload: Record | null = null; - let updatePayload: Record | null = null; - let draftTestCalls = 0; - const savedTestRequests: Array<{ - path: string; - payload: Record | null; - }> = []; - const deletePaths: string[] = []; - - await page.route("**/api/vmware/connections**", async (route) => { - const request = route.request(); - const method = request.method(); - const pathname = new URL(request.url()).pathname; - - if (pathname === "/api/vmware/connections" && method === "GET") { - await route.fulfill({ - status: 200, - contentType: "application/json", - body: JSON.stringify(connections), - }); - return; - } - - if (pathname === "/api/vmware/connections/test" && method === "POST") { - draftTestCalls += 1; - draftTestPayload = JSON.parse(request.postData() || "{}"); - await route.fulfill({ - status: 200, - contentType: "application/json", - body: JSON.stringify({ success: true }), - }); - return; - } - - if (pathname === "/api/vmware/connections/preview" && method === "POST") { - await route.fulfill({ - status: 200, - contentType: "application/json", - body: JSON.stringify(buildSafeVMwareAdmissionPreview()), - }); - return; - } - - if (pathname === "/api/vmware/connections" && method === "POST") { - createPayload = JSON.parse(request.postData() || "{}"); - connections = [ - { - id: "conn-1", - name: createPayload.name, - host: createPayload.host, - port: createPayload.port, - username: createPayload.username, - password: "********", - insecureSkipVerify: createPayload.insecureSkipVerify, - enabled: createPayload.enabled, - poll: { - lastSuccessAt: validatedAt, - }, - observed: { - collectedAt: validatedAt, - hosts: 4, - vms: 48, - datastores: 8, - viRelease: "8.0.3", - }, - }, - ]; - await route.fulfill({ - status: 201, - contentType: "application/json", - body: JSON.stringify(connections[0]), - }); - return; - } - - if ( - pathname === "/api/vmware/connections/conn-1/test" && - method === "POST" - ) { - savedTestRequests.push({ - path: pathname, - payload: request.postData() - ? JSON.parse(request.postData() || "{}") - : null, - }); - await route.fulfill({ - status: 200, - contentType: "application/json", - body: JSON.stringify({ success: true }), - }); - return; - } - - if ( - pathname === "/api/vmware/connections/conn-1/preview" && - method === "POST" - ) { - await route.fulfill({ - status: 200, - contentType: "application/json", - body: JSON.stringify(buildSafeVMwareAdmissionPreview()), - }); - return; - } - - if (pathname === "/api/vmware/connections/conn-1" && method === "PUT") { - updatePayload = JSON.parse(request.postData() || "{}"); - connections = [ - { - id: "conn-1", - name: updatePayload.name, - host: updatePayload.host, - port: updatePayload.port, - username: updatePayload.username, - password: "********", - insecureSkipVerify: updatePayload.insecureSkipVerify, - enabled: updatePayload.enabled, - poll: { - lastSuccessAt: validatedAt, - }, - observed: { - collectedAt: validatedAt, - hosts: 4, - vms: 48, - datastores: 8, - viRelease: "8.0.3", - }, - }, - ]; - await route.fulfill({ - status: 200, - contentType: "application/json", - body: JSON.stringify(connections[0]), - }); - return; - } - - if ( - pathname === "/api/vmware/connections/conn-1" && - method === "DELETE" - ) { - deletePaths.push(pathname); - connections = []; - await route.fulfill({ - status: 200, - contentType: "application/json", - body: JSON.stringify({ success: true, id: "conn-1" }), - }); - return; - } - - await route.continue(); - }); - - await page.goto("/settings/infrastructure/platforms/vmware", { - waitUntil: "domcontentloaded", - }); - await page.waitForURL(/\/settings\/infrastructure\/platforms\/vmware/, { - timeout: 15_000, - }); - - await expect(page.getByText("No VMware connections yet")).toBeVisible(); - - await page.getByRole("button", { name: "Add VMware connection" }).click(); - const dialog = page.getByRole("dialog", { name: "Add VMware connection" }); - await expect(dialog).toBeVisible(); - - await dialog.getByPlaceholder("lab-vcenter").fill("Lab vCenter"); - await dialog.getByPlaceholder("vcsa.lab.local").fill("vcsa.lab.local"); - await dialog.getByPlaceholder("443").fill("443"); - await dialog - .getByPlaceholder("administrator@vsphere.local") - .fill("administrator@vsphere.local"); - await dialog.locator('input[type="password"]').first().fill("super-secret"); - - await dialog.getByRole("button", { name: "Test connection" }).click(); - await expect - .poll(() => draftTestPayload) - .toMatchObject({ - name: "Lab vCenter", - host: "vcsa.lab.local", - port: 443, - username: "administrator@vsphere.local", - password: "super-secret", - enabled: true, - insecureSkipVerify: false, - }); - - await expect( - dialog.getByText("Preview monitored-system impact before saving"), - ).toBeVisible(); - await expect( - dialog.getByRole("button", { name: "Add connection" }), - ).toBeDisabled(); - await dialog.getByRole("button", { name: "Preview impact" }).click(); - await expect(dialog.getByText(/Current usage 1 \/ 10/)).toBeVisible(); - await expect( - dialog.getByRole("button", { name: "Add connection" }), - ).toBeEnabled(); - await dialog.getByRole("button", { name: "Add connection" }).click(); - - const connectionCard = page.getByTestId("vmware-connection-conn-1"); - await expect(connectionCard).toBeVisible(); - await expect(connectionCard).toContainText("Lab vCenter"); - await expect(connectionCard).toContainText("Healthy"); - await expect - .poll(() => createPayload) - .toMatchObject({ - name: "Lab vCenter", - host: "vcsa.lab.local", - port: 443, - username: "administrator@vsphere.local", - password: "super-secret", - enabled: true, - insecureSkipVerify: false, - }); - - await connectionCard.getByRole("button", { name: "Edit" }).click(); - const editDialog = page.getByRole("dialog", { - name: "Edit VMware connection", - }); - await expect(editDialog).toBeVisible(); - await expect( - editDialog.getByPlaceholder("Saved password retained unless replaced"), - ).toBeVisible(); - - await editDialog.getByPlaceholder("lab-vcenter").fill("Lab vCenter Edited"); - await editDialog - .getByPlaceholder("vcsa.lab.local") - .fill("edited.lab.local"); - await editDialog.getByPlaceholder("443").fill("8443"); - await editDialog - .getByPlaceholder("administrator@vsphere.local") - .fill("operator@vsphere.local"); - await editDialog.getByLabel("Skip TLS verification").check(); - - await editDialog.getByRole("button", { name: "Test connection" }).click(); - await expect - .poll(() => savedTestRequests[0]) - .toMatchObject({ - path: "/api/vmware/connections/conn-1/test", - payload: { - name: "Lab vCenter Edited", - host: "edited.lab.local", - port: 8443, - username: "operator@vsphere.local", - password: "********", - enabled: true, - insecureSkipVerify: true, - }, - }); - await expect.poll(() => draftTestCalls).toBe(1); - - await expect( - editDialog.getByRole("button", { name: "Save connection" }), - ).toBeDisabled(); - await editDialog.getByRole("button", { name: "Preview impact" }).click(); - await expect(editDialog.getByText(/Current usage 1 \/ 10/)).toBeVisible(); - await expect( - editDialog.getByRole("button", { name: "Save connection" }), - ).toBeEnabled(); - await editDialog.getByRole("button", { name: "Save connection" }).click(); - await expect - .poll(() => updatePayload) - .toMatchObject({ - name: "Lab vCenter Edited", - host: "edited.lab.local", - port: 8443, - username: "operator@vsphere.local", - password: "********", - enabled: true, - insecureSkipVerify: true, - }); - await expect(connectionCard).toContainText("Lab vCenter Edited"); - - await connectionCard.getByRole("button", { name: "Test" }).click(); - await expect - .poll(() => savedTestRequests) - .toEqual([ - expect.objectContaining({ - path: "/api/vmware/connections/conn-1/test", - payload: expect.objectContaining({ - host: "edited.lab.local", - password: "********", - insecureSkipVerify: true, - }), - }), - { path: "/api/vmware/connections/conn-1/test", payload: null }, - ]); - await expect.poll(() => draftTestCalls).toBe(1); - - await connectionCard.getByRole("button", { name: "Delete" }).click(); - await page.getByRole("button", { name: "Delete connection" }).click(); - await expect - .poll(() => deletePaths) - .toEqual(["/api/vmware/connections/conn-1"]); - await expect(page.getByText("No VMware connections yet")).toBeVisible(); - - fs.mkdirSync(path.dirname(WORKFLOW_SCREENSHOT_PATH), { recursive: true }); - await page.screenshot({ path: WORKFLOW_SCREENSHOT_PATH, fullPage: true }); - }); - - test("previews canonical multi-system impact before creating a VMware connection", async ({ - page, - }) => { - let previewPayload: Record | null = null; - let createCalls = 0; - - await page.route("**/api/vmware/connections**", async (route) => { - const request = route.request(); - const method = request.method(); - const pathname = new URL(request.url()).pathname; - - if (pathname === "/api/vmware/connections" && method === "GET") { - await route.fulfill({ - status: 200, - contentType: "application/json", - body: JSON.stringify([]), - }); - return; - } - - if (pathname === "/api/vmware/connections/preview" && method === "POST") { - previewPayload = JSON.parse(request.postData() || "{}"); - await route.fulfill({ - status: 200, - contentType: "application/json", - body: JSON.stringify({ - current_count: 5, - projected_count: 7, - additional_count: 2, - limit: 6, - would_exceed_limit: true, - effect: "mixed_existing_and_new", - current_systems: [ - { - name: "esxi-01", - type: "agent", - status: "online", - status_explanation: { summary: "", reasons: [] }, - latest_included_signal: { - name: "esxi-01", - type: "agent", - source: "agent", - at: new Date(Date.now() - 60_000).toISOString(), - }, - source: "agent", - explanation: { summary: "", reasons: [], surfaces: [] }, - }, - ], - projected_systems: [ - { - name: "esxi-01", - type: "agent", - status: "online", - status_explanation: { summary: "", reasons: [] }, - latest_included_signal: { - name: "esxi-01", - type: "agent", - source: "vmware", - at: new Date().toISOString(), - }, - source: "vmware", - explanation: { summary: "", reasons: [], surfaces: [] }, - }, - { - name: "esxi-02", - type: "agent", - status: "online", - status_explanation: { summary: "", reasons: [] }, - latest_included_signal: { - name: "esxi-02", - type: "agent", - source: "vmware", - at: new Date().toISOString(), - }, - source: "vmware", - explanation: { summary: "", reasons: [], surfaces: [] }, - }, - ], - current_system: null, - projected_system: null, - }), - }); - return; - } - - if (pathname === "/api/vmware/connections" && method === "POST") { - createCalls += 1; - await route.fulfill({ - status: 201, - contentType: "application/json", - body: JSON.stringify({}), - }); - return; - } - - await route.continue(); - }); - - await page.goto("/settings/infrastructure/platforms/vmware", { - waitUntil: "domcontentloaded", - }); - await page.waitForURL(/\/settings\/infrastructure\/platforms\/vmware/, { - timeout: 15_000, - }); - - await page.getByRole("button", { name: "Add VMware connection" }).click(); - const dialog = page.getByRole("dialog", { name: "Add VMware connection" }); - await expect(dialog).toBeVisible(); - - await dialog.getByPlaceholder("lab-vcenter").fill("Lab VC"); - await dialog.getByPlaceholder("vcsa.lab.local").fill("vcsa.lab.local"); - await dialog - .getByPlaceholder("administrator@vsphere.local") - .fill("administrator@vsphere.local"); - await dialog.locator('input[type="password"]').first().fill("super-secret"); - - await dialog.getByRole("button", { name: "Preview impact" }).click(); - - await expect - .poll(() => previewPayload) - .toMatchObject({ - name: "Lab VC", - host: "vcsa.lab.local", - username: "administrator@vsphere.local", - password: "super-secret", - enabled: true, - }); - await expect( - dialog.getByText("This change exceeds your monitored-system limit"), - ).toBeVisible(); - await expect(dialog.getByText(/Current usage 5 \/ 6/)).toBeVisible(); - await expect(dialog.getByText("Current matched systems")).toBeVisible(); - await expect(dialog.getByText("esxi-01 (Host via Agent)")).toBeVisible(); - await expect(dialog.getByText("Projected systems")).toBeVisible(); - await expect(dialog.getByText("esxi-01 (Host via VMware)")).toBeVisible(); - await expect(dialog.getByText("esxi-02 (Host via VMware)")).toBeVisible(); - await expect( - dialog.getByRole("button", { name: "Add connection" }), - ).toBeDisabled(); - expect(createCalls).toBe(0); - }); - - test("shows freed monitored-system capacity when disabling a VMware connection", async ({ - page, - }) => { - const syncedAt = new Date(Date.now() - 60_000).toISOString(); - let previewPayload: Record | null = null; - - await page.route("**/api/vmware/connections**", async (route) => { - const request = route.request(); - const method = request.method(); - const pathname = new URL(request.url()).pathname; - - if (pathname === "/api/vmware/connections" && method === "GET") { - await route.fulfill({ - status: 200, - contentType: "application/json", - body: JSON.stringify([ - { - id: "conn-1", - name: "Lab VC", - host: "vcsa.lab.local", - port: 443, - username: "administrator@vsphere.local", - password: "********", - insecureSkipVerify: false, - enabled: true, - poll: { - lastSuccessAt: syncedAt, - }, - observed: { - collectedAt: syncedAt, - hosts: 4, - vms: 48, - datastores: 8, - viRelease: "8.0.3", - }, - }, - ]), - }); - return; - } - - if ( - pathname === "/api/vmware/connections/conn-1/preview" && - method === "POST" - ) { - previewPayload = JSON.parse(request.postData() || "{}"); - await route.fulfill({ - status: 200, - contentType: "application/json", - body: JSON.stringify({ - current_count: 4, - projected_count: 3, - additional_count: 0, - limit: 10, - would_exceed_limit: false, - effect: "removes_existing", - current_systems: [ - { - name: "esxi-01", - type: "host", - status: "online", - status_explanation: { summary: "", reasons: [] }, - latest_included_signal: { - name: "esxi-01", - type: "host", - source: "vmware", - at: new Date(Date.now() - 60_000).toISOString(), - }, - source: "vmware", - explanation: { summary: "", reasons: [], surfaces: [] }, - }, - ], - projected_systems: [], - current_system: null, - projected_system: null, - }), - }); - return; - } - - await route.continue(); - }); - - await page.goto("/settings/infrastructure/platforms/vmware", { - waitUntil: "domcontentloaded", - }); - await page.waitForURL(/\/settings\/infrastructure\/platforms\/vmware/, { - timeout: 15_000, - }); - - const connectionCard = page.getByTestId("vmware-connection-conn-1"); - await expect(connectionCard).toBeVisible(); - await connectionCard.getByRole("button", { name: "Edit" }).click(); - - const dialog = page.getByRole("dialog", { - name: "Edit VMware connection", - }); - await expect(dialog).toBeVisible(); - - await dialog.getByLabel("Enable this vCenter connection").uncheck(); - await expect( - dialog.getByRole("button", { name: "Save connection" }), - ).toBeDisabled(); - await dialog.getByRole("button", { name: "Preview impact" }).click(); - - await expect - .poll(() => previewPayload) - .toMatchObject({ - name: "Lab VC", - host: "vcsa.lab.local", - username: "administrator@vsphere.local", - password: "********", - enabled: false, - }); - await expect( - dialog.getByText("This change frees monitored-system capacity"), - ).toBeVisible(); - await expect( - dialog.getByText( - "Current usage 4 / 10. Saving this change would move usage to 3 / 10 (-1).", - ), - ).toBeVisible(); - await expect(dialog.getByText("Current matched systems")).toBeVisible(); - await expect(dialog.getByText("esxi-01 (Host via VMware)")).toBeVisible(); - await expect( - dialog.getByRole("button", { name: "Save connection" }), - ).toBeEnabled(); - }); - - test("reuses the canonical monitored-system explanation when a VMware save is denied", async ({ - page, - }) => { - let createPayload: Record | null = null; - - await page.route("**/api/vmware/connections**", async (route) => { - const request = route.request(); - const method = request.method(); - const pathname = new URL(request.url()).pathname; - - if (pathname === "/api/vmware/connections" && method === "GET") { - await route.fulfill({ - status: 200, - contentType: "application/json", - body: JSON.stringify([]), - }); - return; - } - - if (pathname === "/api/vmware/connections/preview" && method === "POST") { - await route.fulfill({ - status: 200, - contentType: "application/json", - body: JSON.stringify(buildSafeVMwareAdmissionPreview()), - }); - return; - } - - if (pathname === "/api/vmware/connections" && method === "POST") { - createPayload = JSON.parse(request.postData() || "{}"); - await route.fulfill({ - status: 402, - contentType: "application/json", - body: JSON.stringify({ - error: "license_required", - message: "Monitored-system capacity reached (7/6)", - feature: "max_monitored_systems", - monitored_system_preview: { - current_count: 5, - projected_count: 7, - additional_count: 2, - limit: 6, - would_exceed_limit: true, - effect: "mixed_existing_and_new", - current_systems: [ - { - name: "esxi-01", - type: "agent", - status: "online", - status_explanation: { summary: "", reasons: [] }, - latest_included_signal: { - name: "esxi-01", - type: "agent", - source: "agent", - at: new Date(Date.now() - 60_000).toISOString(), - }, - source: "agent", - explanation: { summary: "", reasons: [], surfaces: [] }, - }, - ], - projected_systems: [ - { - name: "esxi-01", - type: "agent", - status: "online", - status_explanation: { summary: "", reasons: [] }, - latest_included_signal: { - name: "esxi-01", - type: "agent", - source: "vmware", - at: new Date().toISOString(), - }, - source: "vmware", - explanation: { summary: "", reasons: [], surfaces: [] }, - }, - { - name: "esxi-02", - type: "agent", - status: "online", - status_explanation: { summary: "", reasons: [] }, - latest_included_signal: { - name: "esxi-02", - type: "agent", - source: "vmware", - at: new Date().toISOString(), - }, - source: "vmware", - explanation: { summary: "", reasons: [], surfaces: [] }, - }, - ], - current_system: null, - projected_system: null, - }, - }), - }); - return; - } - - await route.continue(); - }); - - await page.goto("/settings/infrastructure/platforms/vmware", { - waitUntil: "domcontentloaded", - }); - await page.waitForURL(/\/settings\/infrastructure\/platforms\/vmware/, { - timeout: 15_000, - }); - - await page.getByRole("button", { name: "Add VMware connection" }).click(); - const dialog = page.getByRole("dialog", { name: "Add VMware connection" }); - await expect(dialog).toBeVisible(); - - await dialog.getByPlaceholder("lab-vcenter").fill("Lab VC"); - await dialog.getByPlaceholder("vcsa.lab.local").fill("vcsa.lab.local"); - await dialog - .getByPlaceholder("administrator@vsphere.local") - .fill("administrator@vsphere.local"); - await dialog.locator('input[type="password"]').first().fill("super-secret"); - await expect( - dialog.getByRole("button", { name: "Add connection" }), - ).toBeDisabled(); - await dialog.getByRole("button", { name: "Preview impact" }).click(); - await expect(dialog.getByText(/Current usage 1 \/ 10/)).toBeVisible(); - await expect( - dialog.getByRole("button", { name: "Add connection" }), - ).toBeEnabled(); - await dialog.getByRole("button", { name: "Add connection" }).click(); - - await expect - .poll(() => createPayload) - .toMatchObject({ - name: "Lab VC", - host: "vcsa.lab.local", - username: "administrator@vsphere.local", - password: "super-secret", - enabled: true, - }); - await expect( - dialog.getByText("This change exceeds your monitored-system limit"), - ).toBeVisible(); - await expect(dialog.getByText(/Current usage 5 \/ 6/)).toBeVisible(); - await expect(dialog.getByText("Current matched systems")).toBeVisible(); - await expect(dialog.getByText("esxi-01 (Host via Agent)")).toBeVisible(); - await expect(dialog.getByText("Projected systems")).toBeVisible(); - await expect(dialog.getByText("esxi-01 (Host via VMware)")).toBeVisible(); - await expect(dialog.getByText("esxi-02 (Host via VMware)")).toBeVisible(); - await expect( - dialog.getByRole("button", { name: "Add connection" }), - ).toBeDisabled(); - }); - - test("blocks VMware save while monitored-system usage is rebuilding", async ({ - page, - }) => { - let previewPayload: Record | null = null; - let createCalls = 0; - - await page.route("**/api/vmware/connections**", async (route) => { - const request = route.request(); - const method = request.method(); - const pathname = new URL(request.url()).pathname; - - if (pathname === "/api/vmware/connections" && method === "GET") { - await route.fulfill({ - status: 200, - contentType: "application/json", - body: JSON.stringify([]), - }); - return; - } - - if (pathname === "/api/vmware/connections/preview" && method === "POST") { - previewPayload = JSON.parse(request.postData() || "{}"); - await route.fulfill({ - status: 503, - contentType: "application/json", - body: JSON.stringify({ - error: "Unable to verify monitored-system capacity right now", - code: "monitored_system_usage_unavailable", - status_code: 503, - details: { - reason: "supplemental_inventory_rebuild_pending", - }, - }), - }); - return; - } - - if (pathname === "/api/vmware/connections" && method === "POST") { - createCalls += 1; - await route.fulfill({ - status: 201, - contentType: "application/json", - body: JSON.stringify({}), - }); - return; - } - - await route.continue(); - }); - - await page.goto("/settings/infrastructure/platforms/vmware", { - waitUntil: "domcontentloaded", - }); - await page.waitForURL(/\/settings\/infrastructure\/platforms\/vmware/, { - timeout: 15_000, - }); - - await page.getByRole("button", { name: "Add VMware connection" }).click(); - const dialog = page.getByRole("dialog", { name: "Add VMware connection" }); - await expect(dialog).toBeVisible(); - - await dialog.getByPlaceholder("lab-vcenter").fill("Lab VC"); - await dialog.getByPlaceholder("vcsa.lab.local").fill("vcsa.lab.local"); - await dialog - .getByPlaceholder("administrator@vsphere.local") - .fill("administrator@vsphere.local"); - await dialog.locator('input[type="password"]').first().fill("super-secret"); - - await dialog.getByRole("button", { name: "Preview impact" }).click(); - - await expect - .poll(() => previewPayload) - .toMatchObject({ - name: "Lab VC", - host: "vcsa.lab.local", - username: "administrator@vsphere.local", - password: "super-secret", - enabled: true, - }); - await expect( - dialog.getByText("Monitored-system capacity is temporarily unavailable"), - ).toBeVisible(); - await expect( - dialog.getByText( - "Pulse has settled provider-owned inventory and is rebuilding the canonical monitored-system view, so this connection cannot be saved yet. Retry preview in a moment.", - ), - ).toBeVisible(); - await expect( - dialog.getByRole("button", { name: "Add connection" }), - ).toBeDisabled(); - expect(createCalls).toBe(0); - }); -}); diff --git a/tests/integration/tests/53-demo-mode-commercial-boundary.spec.ts b/tests/integration/tests/53-demo-mode-commercial-boundary.spec.ts index c5ae0041b..0573bbd87 100644 --- a/tests/integration/tests/53-demo-mode-commercial-boundary.spec.ts +++ b/tests/integration/tests/53-demo-mode-commercial-boundary.spec.ts @@ -318,7 +318,7 @@ base.describe('Demo mode commercial boundary', () => { await expect( page.getByText('Demo instance with mock data (read-only)', { exact: true }), ).toBeVisible(); - await expect(page.getByRole('heading', { level: 1, name: 'Infrastructure Operations' })).toBeVisible(); + await expect(page.getByRole('heading', { level: 1, name: 'Infrastructure' })).toBeVisible(); const settingsNavigation = page.locator('[aria-label="Settings navigation"]'); await expect(settingsNavigation).toBeVisible(); await expect(page.getByText('Default Organization', { exact: true })).toHaveCount(0); @@ -435,7 +435,7 @@ base.describe('Managed demo runtime commercial boundary', () => { await expect( page.getByText('Demo instance with mock data (read-only)', { exact: true }), ).toBeVisible(); - await expect(page.getByRole('heading', { level: 1, name: 'Infrastructure Operations' })).toBeVisible(); + await expect(page.getByRole('heading', { level: 1, name: 'Infrastructure' })).toBeVisible(); const settingsNavigation = page.locator('[aria-label="Settings navigation"]'); await expect(settingsNavigation).toBeVisible(); await expect(page.getByText('Default Organization', { exact: true })).toHaveCount(0);