From 3d7740bd322db24b2a17c19f7311fbe78574a07a Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Fri, 19 Apr 2024 14:45:52 +0200 Subject: [PATCH] test(site): add e2e tests for workspace proxies (#13009) --- site/e2e/constants.ts | 1 + site/e2e/helpers.ts | 2 +- site/e2e/proxy.ts | 41 +++++++ site/e2e/tests/deployment/appearance.spec.ts | 2 +- .../tests/deployment/workspaceProxies.spec.ts | 105 ++++++++++++++++++ site/src/api/api.ts | 7 ++ site/src/pages/LoginPage/LoginPageView.tsx | 1 + .../WorkspaceProxyPage/WorkspaceProxyRow.tsx | 10 +- 8 files changed, 164 insertions(+), 5 deletions(-) create mode 100644 site/e2e/proxy.ts create mode 100644 site/e2e/tests/deployment/workspaceProxies.spec.ts diff --git a/site/e2e/constants.ts b/site/e2e/constants.ts index 6998968977..3e1283e549 100644 --- a/site/e2e/constants.ts +++ b/site/e2e/constants.ts @@ -7,6 +7,7 @@ export const coderPort = process.env.CODER_E2E_PORT ? Number(process.env.CODER_E2E_PORT) : 3111; export const prometheusPort = 2114; +export const workspaceProxyPort = 3112; // Use alternate ports in case we're running in a Coder Workspace. export const agentPProfPort = 6061; diff --git a/site/e2e/helpers.ts b/site/e2e/helpers.ts index ca3a507007..26e416cad7 100644 --- a/site/e2e/helpers.ts +++ b/site/e2e/helpers.ts @@ -391,7 +391,7 @@ export const stopAgent = async (cp: ChildProcess, goRun: boolean = true) => { await waitUntilUrlIsNotResponding("http://localhost:" + prometheusPort); }; -const waitUntilUrlIsNotResponding = async (url: string) => { +export const waitUntilUrlIsNotResponding = async (url: string) => { const maxRetries = 30; const retryIntervalMs = 1000; let retries = 0; diff --git a/site/e2e/proxy.ts b/site/e2e/proxy.ts new file mode 100644 index 0000000000..620fcf0a96 --- /dev/null +++ b/site/e2e/proxy.ts @@ -0,0 +1,41 @@ +import { spawn, type ChildProcess, exec } from "child_process"; +import { coderMain, coderPort, workspaceProxyPort } from "./constants"; +import { waitUntilUrlIsNotResponding } from "./helpers"; + +export const startWorkspaceProxy = async ( + token: string, +): Promise => { + const cp = spawn("go", ["run", coderMain, "wsproxy", "server"], { + env: { + ...process.env, + CODER_PRIMARY_ACCESS_URL: `http://127.0.0.1:${coderPort}`, + CODER_PROXY_SESSION_TOKEN: token, + CODER_HTTP_ADDRESS: `localhost:${workspaceProxyPort}`, + }, + }); + cp.stdout.on("data", (data: Buffer) => { + // eslint-disable-next-line no-console -- Log wsproxy activity + console.log( + `[wsproxy] [stdout] [onData] ${data.toString().replace(/\n$/g, "")}`, + ); + }); + cp.stderr.on("data", (data: Buffer) => { + // eslint-disable-next-line no-console -- Log wsproxy activity + console.log( + `[wsproxy] [stderr] [onData] ${data.toString().replace(/\n$/g, "")}`, + ); + }); + return cp; +}; + +export const stopWorkspaceProxy = async ( + cp: ChildProcess, + goRun: boolean = true, +) => { + exec(goRun ? `pkill -P ${cp.pid}` : `kill ${cp.pid}`, (error) => { + if (error) { + throw new Error(`exec error: ${JSON.stringify(error)}`); + } + }); + await waitUntilUrlIsNotResponding(`http://127.0.0.1:${workspaceProxyPort}`); +}; diff --git a/site/e2e/tests/deployment/appearance.spec.ts b/site/e2e/tests/deployment/appearance.spec.ts index 0fec1a7d75..14aeafe750 100644 --- a/site/e2e/tests/deployment/appearance.spec.ts +++ b/site/e2e/tests/deployment/appearance.spec.ts @@ -52,7 +52,7 @@ test("set application logo", async ({ page }) => { await incognitoPage.goto("/", { waitUntil: "domcontentloaded" }); // Verify banner - const logo = incognitoPage.locator("img"); + const logo = incognitoPage.locator("img.application-logo"); await expect(logo).toHaveAttribute("src", imageLink); // Shut down browser diff --git a/site/e2e/tests/deployment/workspaceProxies.spec.ts b/site/e2e/tests/deployment/workspaceProxies.spec.ts new file mode 100644 index 0000000000..5f67bda7d7 --- /dev/null +++ b/site/e2e/tests/deployment/workspaceProxies.spec.ts @@ -0,0 +1,105 @@ +import { test, expect, type Page } from "@playwright/test"; +import { createWorkspaceProxy } from "api/api"; +import { setupApiCalls } from "../../api"; +import { coderPort, workspaceProxyPort } from "../../constants"; +import { randomName, requiresEnterpriseLicense } from "../../helpers"; +import { startWorkspaceProxy, stopWorkspaceProxy } from "../../proxy"; + +test("default proxy is online", async ({ page }) => { + requiresEnterpriseLicense(); + await setupApiCalls(page); + + await page.goto("/deployment/workspace-proxies", { + waitUntil: "domcontentloaded", + }); + + // Verify if the default proxy is healthy + const workspaceProxyPrimary = page.locator( + `table.MuiTable-root tr[data-testid="primary"]`, + ); + + const workspaceProxyName = workspaceProxyPrimary.locator("td.name span"); + const workspaceProxyURL = workspaceProxyPrimary.locator("td.url"); + const workspaceProxyStatus = workspaceProxyPrimary.locator("td.status span"); + + await expect(workspaceProxyName).toHaveText("Default"); + await expect(workspaceProxyURL).toHaveText("http://localhost:" + coderPort); + await expect(workspaceProxyStatus).toHaveText("Healthy"); +}); + +test("custom proxy is online", async ({ page }) => { + requiresEnterpriseLicense(); + await setupApiCalls(page); + + const proxyName = randomName(); + + // Register workspace proxy + const proxyResponse = await createWorkspaceProxy({ + name: proxyName, + display_name: "", + icon: "/emojis/1f1e7-1f1f7.png", + }); + expect(proxyResponse.proxy_token).toBeDefined(); + + // Start "wsproxy server" + const proxyServer = await startWorkspaceProxy(proxyResponse.proxy_token); + await waitUntilWorkspaceProxyIsHealthy(page, proxyName); + + // Verify if custom proxy is healthy + await page.goto("/deployment/workspace-proxies", { + waitUntil: "domcontentloaded", + }); + + const workspaceProxy = page.locator(`table.MuiTable-root tr`, { + hasText: proxyName, + }); + + const workspaceProxyName = workspaceProxy.locator("td.name span"); + const workspaceProxyURL = workspaceProxy.locator("td.url"); + const workspaceProxyStatus = workspaceProxy.locator("td.status span"); + + await expect(workspaceProxyName).toHaveText(proxyName); + await expect(workspaceProxyURL).toHaveText( + `http://127.0.0.1:${workspaceProxyPort}`, + ); + await expect(workspaceProxyStatus).toHaveText("Healthy"); + + // Tear down the proxy + await stopWorkspaceProxy(proxyServer); +}); + +const waitUntilWorkspaceProxyIsHealthy = async ( + page: Page, + proxyName: string, +) => { + await page.goto("/deployment/workspace-proxies", { + waitUntil: "domcontentloaded", + }); + + const maxRetries = 30; + const retryIntervalMs = 1000; + let retries = 0; + while (retries < maxRetries) { + await page.reload(); + + const workspaceProxy = page.locator(`table.MuiTable-root tr`, { + hasText: proxyName, + }); + const workspaceProxyStatus = workspaceProxy.locator("td.status span"); + + try { + await expect(workspaceProxyStatus).toHaveText("Healthy", { + timeout: 1_000, + }); + return; // healthy! + } catch { + retries++; + await new Promise((resolve) => setTimeout(resolve, retryIntervalMs)); + } + } + throw new Error( + `Workspace proxy "${proxyName}" is unhealthy after ${ + maxRetries * retryIntervalMs + }ms`, + ); +}; diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 12c2a63b2c..a243123a31 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -1270,6 +1270,13 @@ export const getWorkspaceProxies = async (): Promise< return response.data; }; +export const createWorkspaceProxy = async ( + b: TypesGen.CreateWorkspaceProxyRequest, +): Promise => { + const response = await axios.post(`/api/v2/workspaceproxies`, b); + return response.data; +}; + export const getAppearance = async (): Promise => { try { const response = await axios.get(`/api/v2/appearance`); diff --git a/site/src/pages/LoginPage/LoginPageView.tsx b/site/src/pages/LoginPage/LoginPageView.tsx index b3039e5ad9..e62e8be0d8 100644 --- a/site/src/pages/LoginPage/LoginPageView.tsx +++ b/site/src/pages/LoginPage/LoginPageView.tsx @@ -41,6 +41,7 @@ export const LoginPageView: FC = ({ css={{ maxWidth: "200px", }} + className="application-logo" /> ) : ( diff --git a/site/src/pages/UserSettingsPage/WorkspaceProxyPage/WorkspaceProxyRow.tsx b/site/src/pages/UserSettingsPage/WorkspaceProxyPage/WorkspaceProxyRow.tsx index fbd76ba2be..e0807cf758 100644 --- a/site/src/pages/UserSettingsPage/WorkspaceProxyPage/WorkspaceProxyRow.tsx +++ b/site/src/pages/UserSettingsPage/WorkspaceProxyPage/WorkspaceProxyRow.tsx @@ -40,7 +40,7 @@ export const ProxyRow: FC = ({ proxy, latency }) => { return ( <> - + 0 @@ -60,8 +60,12 @@ export const ProxyRow: FC = ({ proxy, latency }) => { /> - {proxy.path_app_url} - {statusBadge} + + {proxy.path_app_url} + + + {statusBadge} +