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)
|
? Number(process.env.CODER_E2E_PORT)
|
||||||
: 3111;
|
: 3111;
|
||||||
export const prometheusPort = 2114;
|
export const prometheusPort = 2114;
|
||||||
|
export const workspaceProxyPort = 3112;
|
||||||
|
|
||||||
// Use alternate ports in case we're running in a Coder Workspace.
|
// Use alternate ports in case we're running in a Coder Workspace.
|
||||||
export const agentPProfPort = 6061;
|
export const agentPProfPort = 6061;
|
||||||
|
|
|
@ -391,7 +391,7 @@ export const stopAgent = async (cp: ChildProcess, goRun: boolean = true) => {
|
||||||
await waitUntilUrlIsNotResponding("http://localhost:" + prometheusPort);
|
await waitUntilUrlIsNotResponding("http://localhost:" + prometheusPort);
|
||||||
};
|
};
|
||||||
|
|
||||||
const waitUntilUrlIsNotResponding = async (url: string) => {
|
export const waitUntilUrlIsNotResponding = async (url: string) => {
|
||||||
const maxRetries = 30;
|
const maxRetries = 30;
|
||||||
const retryIntervalMs = 1000;
|
const retryIntervalMs = 1000;
|
||||||
let retries = 0;
|
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" });
|
await incognitoPage.goto("/", { waitUntil: "domcontentloaded" });
|
||||||
|
|
||||||
// Verify banner
|
// Verify banner
|
||||||
const logo = incognitoPage.locator("img");
|
const logo = incognitoPage.locator("img.application-logo");
|
||||||
await expect(logo).toHaveAttribute("src", imageLink);
|
await expect(logo).toHaveAttribute("src", imageLink);
|
||||||
|
|
||||||
// Shut down browser
|
// 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;
|
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> => {
|
export const getAppearance = async (): Promise<TypesGen.AppearanceConfig> => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(`/api/v2/appearance`);
|
const response = await axios.get(`/api/v2/appearance`);
|
||||||
|
|
|
@ -41,6 +41,7 @@ export const LoginPageView: FC<LoginPageViewProps> = ({
|
||||||
css={{
|
css={{
|
||||||
maxWidth: "200px",
|
maxWidth: "200px",
|
||||||
}}
|
}}
|
||||||
|
className="application-logo"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<CoderIcon fill="white" opacity={1} css={styles.icon} />
|
<CoderIcon fill="white" opacity={1} css={styles.icon} />
|
||||||
|
|
|
@ -40,7 +40,7 @@ export const ProxyRow: FC<ProxyRowProps> = ({ proxy, latency }) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TableRow key={proxy.name} data-testid={proxy.name}>
|
<TableRow key={proxy.name} data-testid={proxy.name}>
|
||||||
<TableCell>
|
<TableCell className="name">
|
||||||
<AvatarData
|
<AvatarData
|
||||||
title={
|
title={
|
||||||
proxy.display_name && proxy.display_name.length > 0
|
proxy.display_name && proxy.display_name.length > 0
|
||||||
|
@ -60,8 +60,12 @@ export const ProxyRow: FC<ProxyRowProps> = ({ proxy, latency }) => {
|
||||||
/>
|
/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
<TableCell css={{ fontSize: 14 }}>{proxy.path_app_url}</TableCell>
|
<TableCell css={{ fontSize: 14 }} className="url">
|
||||||
<TableCell css={{ fontSize: 14 }}>{statusBadge}</TableCell>
|
{proxy.path_app_url}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell css={{ fontSize: 14 }} className="status">
|
||||||
|
{statusBadge}
|
||||||
|
</TableCell>
|
||||||
<TableCell
|
<TableCell
|
||||||
css={{
|
css={{
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
|
|
Loading…
Reference in New Issue