test(site): add e2e tests for workspace proxies (#13009)

This commit is contained in:
Marcin Tojek 2024-04-19 14:45:52 +02:00 committed by GitHub
parent 3aa0d73811
commit 3d7740bd32
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 164 additions and 5 deletions

View File

@ -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;

View File

@ -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;

41
site/e2e/proxy.ts Normal file
View File

@ -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<ChildProcess> => {
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}`);
};

View File

@ -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

View File

@ -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`,
);
};

View File

@ -1270,6 +1270,13 @@ export const getWorkspaceProxies = async (): Promise<
return response.data;
};
export const createWorkspaceProxy = async (
b: TypesGen.CreateWorkspaceProxyRequest,
): Promise<TypesGen.UpdateWorkspaceProxyResponse> => {
const response = await axios.post(`/api/v2/workspaceproxies`, b);
return response.data;
};
export const getAppearance = async (): Promise<TypesGen.AppearanceConfig> => {
try {
const response = await axios.get(`/api/v2/appearance`);

View File

@ -41,6 +41,7 @@ export const LoginPageView: FC<LoginPageViewProps> = ({
css={{
maxWidth: "200px",
}}
className="application-logo"
/>
) : (
<CoderIcon fill="white" opacity={1} css={styles.icon} />

View File

@ -40,7 +40,7 @@ export const ProxyRow: FC<ProxyRowProps> = ({ proxy, latency }) => {
return (
<>
<TableRow key={proxy.name} data-testid={proxy.name}>
<TableCell>
<TableCell className="name">
<AvatarData
title={
proxy.display_name && proxy.display_name.length > 0
@ -60,8 +60,12 @@ export const ProxyRow: FC<ProxyRowProps> = ({ proxy, latency }) => {
/>
</TableCell>
<TableCell css={{ fontSize: 14 }}>{proxy.path_app_url}</TableCell>
<TableCell css={{ fontSize: 14 }}>{statusBadge}</TableCell>
<TableCell css={{ fontSize: 14 }} className="url">
{proxy.path_app_url}
</TableCell>
<TableCell css={{ fontSize: 14 }} className="status">
{statusBadge}
</TableCell>
<TableCell
css={{
fontSize: 14,