Skip to content

Commit

Permalink
test(site): add e2e tests for workspace proxies (#13009)
Browse files Browse the repository at this point in the history
  • Loading branch information
mtojek committed Apr 19, 2024
1 parent 3aa0d73 commit 3d7740b
Show file tree
Hide file tree
Showing 8 changed files with 164 additions and 5 deletions.
1 change: 1 addition & 0 deletions site/e2e/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion site/e2e/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
41 changes: 41 additions & 0 deletions site/e2e/proxy.ts
Original file line number Diff line number Diff line change
@@ -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}`);
};
2 changes: 1 addition & 1 deletion site/e2e/tests/deployment/appearance.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
105 changes: 105 additions & 0 deletions site/e2e/tests/deployment/workspaceProxies.spec.ts
Original file line number Diff line number Diff line change
@@ -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`,
);
};
7 changes: 7 additions & 0 deletions site/src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`);
Expand Down
1 change: 1 addition & 0 deletions site/src/pages/LoginPage/LoginPageView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export const LoginPageView: FC<LoginPageViewProps> = ({
css={{
maxWidth: "200px",
}}
className="application-logo"
/>
) : (
<CoderIcon fill="white" opacity={1} css={styles.icon} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down

0 comments on commit 3d7740b

Please sign in to comment.