Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Apr 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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