mirror of https://github.com/coder/coder.git
test(site): add e2e tests for workspace proxies (#13009)
This commit is contained in:
parent
3aa0d73811
commit
3d7740bd32
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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}`);
|
||||
};
|
|
@ -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
|
||||
|
|
|
@ -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`,
|
||||
);
|
||||
};
|
|
@ -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`);
|
||||
|
|
|
@ -41,6 +41,7 @@ export const LoginPageView: FC<LoginPageViewProps> = ({
|
|||
css={{
|
||||
maxWidth: "200px",
|
||||
}}
|
||||
className="application-logo"
|
||||
/>
|
||||
) : (
|
||||
<CoderIcon fill="white" opacity={1} css={styles.icon} />
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue