Skip to content

Commit 3d7740b

Browse files
authored
test(site): add e2e tests for workspace proxies (coder#13009)
1 parent 3aa0d73 commit 3d7740b

File tree

8 files changed

+164
-5
lines changed

8 files changed

+164
-5
lines changed

site/e2e/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export const coderPort = process.env.CODER_E2E_PORT
77
? Number(process.env.CODER_E2E_PORT)
88
: 3111;
99
export const prometheusPort = 2114;
10+
export const workspaceProxyPort = 3112;
1011

1112
// Use alternate ports in case we're running in a Coder Workspace.
1213
export const agentPProfPort = 6061;

site/e2e/helpers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -391,7 +391,7 @@ export const stopAgent = async (cp: ChildProcess, goRun: boolean = true) => {
391391
await waitUntilUrlIsNotResponding("http://localhost:" + prometheusPort);
392392
};
393393

394-
const waitUntilUrlIsNotResponding = async (url: string) => {
394+
export const waitUntilUrlIsNotResponding = async (url: string) => {
395395
const maxRetries = 30;
396396
const retryIntervalMs = 1000;
397397
let retries = 0;

site/e2e/proxy.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { spawn, type ChildProcess, exec } from "child_process";
2+
import { coderMain, coderPort, workspaceProxyPort } from "./constants";
3+
import { waitUntilUrlIsNotResponding } from "./helpers";
4+
5+
export const startWorkspaceProxy = async (
6+
token: string,
7+
): Promise<ChildProcess> => {
8+
const cp = spawn("go", ["run", coderMain, "wsproxy", "server"], {
9+
env: {
10+
...process.env,
11+
CODER_PRIMARY_ACCESS_URL: `http://127.0.0.1:${coderPort}`,
12+
CODER_PROXY_SESSION_TOKEN: token,
13+
CODER_HTTP_ADDRESS: `localhost:${workspaceProxyPort}`,
14+
},
15+
});
16+
cp.stdout.on("data", (data: Buffer) => {
17+
// eslint-disable-next-line no-console -- Log wsproxy activity
18+
console.log(
19+
`[wsproxy] [stdout] [onData] ${data.toString().replace(/\n$/g, "")}`,
20+
);
21+
});
22+
cp.stderr.on("data", (data: Buffer) => {
23+
// eslint-disable-next-line no-console -- Log wsproxy activity
24+
console.log(
25+
`[wsproxy] [stderr] [onData] ${data.toString().replace(/\n$/g, "")}`,
26+
);
27+
});
28+
return cp;
29+
};
30+
31+
export const stopWorkspaceProxy = async (
32+
cp: ChildProcess,
33+
goRun: boolean = true,
34+
) => {
35+
exec(goRun ? `pkill -P ${cp.pid}` : `kill ${cp.pid}`, (error) => {
36+
if (error) {
37+
throw new Error(`exec error: ${JSON.stringify(error)}`);
38+
}
39+
});
40+
await waitUntilUrlIsNotResponding(`http://127.0.0.1:${workspaceProxyPort}`);
41+
};

site/e2e/tests/deployment/appearance.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ test("set application logo", async ({ page }) => {
5252
await incognitoPage.goto("/", { waitUntil: "domcontentloaded" });
5353

5454
// Verify banner
55-
const logo = incognitoPage.locator("img");
55+
const logo = incognitoPage.locator("img.application-logo");
5656
await expect(logo).toHaveAttribute("src", imageLink);
5757

5858
// Shut down browser
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { test, expect, type Page } from "@playwright/test";
2+
import { createWorkspaceProxy } from "api/api";
3+
import { setupApiCalls } from "../../api";
4+
import { coderPort, workspaceProxyPort } from "../../constants";
5+
import { randomName, requiresEnterpriseLicense } from "../../helpers";
6+
import { startWorkspaceProxy, stopWorkspaceProxy } from "../../proxy";
7+
8+
test("default proxy is online", async ({ page }) => {
9+
requiresEnterpriseLicense();
10+
await setupApiCalls(page);
11+
12+
await page.goto("/deployment/workspace-proxies", {
13+
waitUntil: "domcontentloaded",
14+
});
15+
16+
// Verify if the default proxy is healthy
17+
const workspaceProxyPrimary = page.locator(
18+
`table.MuiTable-root tr[data-testid="primary"]`,
19+
);
20+
21+
const workspaceProxyName = workspaceProxyPrimary.locator("td.name span");
22+
const workspaceProxyURL = workspaceProxyPrimary.locator("td.url");
23+
const workspaceProxyStatus = workspaceProxyPrimary.locator("td.status span");
24+
25+
await expect(workspaceProxyName).toHaveText("Default");
26+
await expect(workspaceProxyURL).toHaveText("http://localhost:" + coderPort);
27+
await expect(workspaceProxyStatus).toHaveText("Healthy");
28+
});
29+
30+
test("custom proxy is online", async ({ page }) => {
31+
requiresEnterpriseLicense();
32+
await setupApiCalls(page);
33+
34+
const proxyName = randomName();
35+
36+
// Register workspace proxy
37+
const proxyResponse = await createWorkspaceProxy({
38+
name: proxyName,
39+
display_name: "",
40+
icon: "/emojis/1f1e7-1f1f7.png",
41+
});
42+
expect(proxyResponse.proxy_token).toBeDefined();
43+
44+
// Start "wsproxy server"
45+
const proxyServer = await startWorkspaceProxy(proxyResponse.proxy_token);
46+
await waitUntilWorkspaceProxyIsHealthy(page, proxyName);
47+
48+
// Verify if custom proxy is healthy
49+
await page.goto("/deployment/workspace-proxies", {
50+
waitUntil: "domcontentloaded",
51+
});
52+
53+
const workspaceProxy = page.locator(`table.MuiTable-root tr`, {
54+
hasText: proxyName,
55+
});
56+
57+
const workspaceProxyName = workspaceProxy.locator("td.name span");
58+
const workspaceProxyURL = workspaceProxy.locator("td.url");
59+
const workspaceProxyStatus = workspaceProxy.locator("td.status span");
60+
61+
await expect(workspaceProxyName).toHaveText(proxyName);
62+
await expect(workspaceProxyURL).toHaveText(
63+
`http://127.0.0.1:${workspaceProxyPort}`,
64+
);
65+
await expect(workspaceProxyStatus).toHaveText("Healthy");
66+
67+
// Tear down the proxy
68+
await stopWorkspaceProxy(proxyServer);
69+
});
70+
71+
const waitUntilWorkspaceProxyIsHealthy = async (
72+
page: Page,
73+
proxyName: string,
74+
) => {
75+
await page.goto("/deployment/workspace-proxies", {
76+
waitUntil: "domcontentloaded",
77+
});
78+
79+
const maxRetries = 30;
80+
const retryIntervalMs = 1000;
81+
let retries = 0;
82+
while (retries < maxRetries) {
83+
await page.reload();
84+
85+
const workspaceProxy = page.locator(`table.MuiTable-root tr`, {
86+
hasText: proxyName,
87+
});
88+
const workspaceProxyStatus = workspaceProxy.locator("td.status span");
89+
90+
try {
91+
await expect(workspaceProxyStatus).toHaveText("Healthy", {
92+
timeout: 1_000,
93+
});
94+
return; // healthy!
95+
} catch {
96+
retries++;
97+
await new Promise((resolve) => setTimeout(resolve, retryIntervalMs));
98+
}
99+
}
100+
throw new Error(
101+
`Workspace proxy "${proxyName}" is unhealthy after ${
102+
maxRetries * retryIntervalMs
103+
}ms`,
104+
);
105+
};

site/src/api/api.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1270,6 +1270,13 @@ export const getWorkspaceProxies = async (): Promise<
12701270
return response.data;
12711271
};
12721272

1273+
export const createWorkspaceProxy = async (
1274+
b: TypesGen.CreateWorkspaceProxyRequest,
1275+
): Promise<TypesGen.UpdateWorkspaceProxyResponse> => {
1276+
const response = await axios.post(`/api/v2/workspaceproxies`, b);
1277+
return response.data;
1278+
};
1279+
12731280
export const getAppearance = async (): Promise<TypesGen.AppearanceConfig> => {
12741281
try {
12751282
const response = await axios.get(`/api/v2/appearance`);

site/src/pages/LoginPage/LoginPageView.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export const LoginPageView: FC<LoginPageViewProps> = ({
4141
css={{
4242
maxWidth: "200px",
4343
}}
44+
className="application-logo"
4445
/>
4546
) : (
4647
<CoderIcon fill="white" opacity={1} css={styles.icon} />

site/src/pages/UserSettingsPage/WorkspaceProxyPage/WorkspaceProxyRow.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export const ProxyRow: FC<ProxyRowProps> = ({ proxy, latency }) => {
4040
return (
4141
<>
4242
<TableRow key={proxy.name} data-testid={proxy.name}>
43-
<TableCell>
43+
<TableCell className="name">
4444
<AvatarData
4545
title={
4646
proxy.display_name && proxy.display_name.length > 0
@@ -60,8 +60,12 @@ export const ProxyRow: FC<ProxyRowProps> = ({ proxy, latency }) => {
6060
/>
6161
</TableCell>
6262

63-
<TableCell css={{ fontSize: 14 }}>{proxy.path_app_url}</TableCell>
64-
<TableCell css={{ fontSize: 14 }}>{statusBadge}</TableCell>
63+
<TableCell css={{ fontSize: 14 }} className="url">
64+
{proxy.path_app_url}
65+
</TableCell>
66+
<TableCell css={{ fontSize: 14 }} className="status">
67+
{statusBadge}
68+
</TableCell>
6569
<TableCell
6670
css={{
6771
fontSize: 14,

0 commit comments

Comments
 (0)