mirror of
https://github.com/coder/coder.git
synced 2026-10-16 14:48:56 +08:00
> 🤖 This PR was written by Coder Agents on behalf of Jake Howell. Stack: 1. #26575 `fix(site/e2e): close mock external-auth servers in teardown` 2. #26793 `fix(site/e2e): accept 404 from external auth reset hook` 3. #26795 `fix(site/src): refresh provider state after device-flow exchange` 4. #26798 `fix(site/e2e): reset both providers in external auth hook` ← this PR 5. #26648 `chore(site/e2e): re-enable externalAuth suite` Surfaced by stress-testing the leaf state of the stack locally with `--repeat-each=N`. The suite passes on iter 1, then deterministically fails the device test on every subsequent iteration: | iter | device | web | workspace | |---|---|---|---| | 1 | ✓ 2.8s | ✓ 2.5s | ✓ 6.2s | | 2 | **✘ 30.1s** | ✓ 2.6s | ✓ 6.2s | | 3 | **✘ 30.1s** | ✓ ~2.5s | ✓ ~6s | | … | … | … | … | `resetExternalAuthKey` in `site/e2e/hooks.ts` only deletes the link for `gitAuth.webProvider`, never `gitAuth.deviceProvider`. So: ```ts path: `/api/v2/external-auth/${gitAuth.webProvider}?coder_session_token=${sessionCookie?.value}`, ``` Iter 1 of the device test creates a link for the `device` provider against the test user. The cleanup hook between tests resets `web` but not `device`. On iter 2 the page hits `/external-auth/device`, `externalAuthProviderQuery.data.authenticated` is already `true`, the page renders the authorized state immediately, and the test's `sentPending.wait()` hangs forever because the mock token endpoint is never polled. The whole test times out at 30s. Fix: reset every provider the suite exercises. Refactored the per-link delete into a Promise-returning helper so errors actually propagate; the previous `throw` inside the response and `error` callbacks crashed the worker rather than rejecting the call. Doesn't affect CI (single run per PR, fresh `--ephemeral` coderd), but it makes the suite robust to local stress testing and to anyone re-running it in the same coderd process. Filed before the un-skip lands so the un-skipped suite is repeatable from day one. Local verification: `pnpm playwright:test e2e/tests/externalAuth.spec.ts --repeat-each=20 --workers=1` → 61/61 passing in 4m48s. Same command before this fix → 1 pass, then deterministic device-test timeouts. Refs https://linear.app/codercom/issue/DEVEX-413 <details> <summary>Why a separate PR</summary> This is the fourth distinct root cause that piled up while the suite was skipped, and it's orthogonal to the other three. #26575 fixes the listener leak, #26793 fixes the 404 hook contract drift, #26795 fixes the dropped invalidation, and this PR fixes the per-test cleanup so the suite is actually repeatable. #26648 then just flips `.skip`. </details>
97 lines
2.8 KiB
TypeScript
97 lines
2.8 KiB
TypeScript
import type { BrowserContext, Page } from "@playwright/test";
|
|
import { coderPort, gitAuth } from "./constants";
|
|
|
|
export const beforeCoderTest = (page: Page) => {
|
|
page.on("console", (msg) => {
|
|
const location = msg.location();
|
|
// Filters out a bunch of junk warnings the browser produces.
|
|
if (!location.url) {
|
|
return;
|
|
}
|
|
// Filters out the gigantic CODER logo we print on every page load, as well
|
|
// as some other noise.
|
|
if (msg.type() === "info") {
|
|
return;
|
|
}
|
|
console.info(`[console][${msg.type()}] ${msg.text()}`);
|
|
});
|
|
|
|
page.on("response", async (response) => {
|
|
// Don't log responses for static assets.
|
|
if (!isApiCall(response.url())) {
|
|
return;
|
|
}
|
|
// Don't log successful responses. Those are almost always less interesting.
|
|
if (response.ok()) {
|
|
return;
|
|
}
|
|
|
|
let responseText: string;
|
|
try {
|
|
responseText = await response.text();
|
|
responseText = responseText.replaceAll("\n", "");
|
|
} catch {
|
|
responseText = "<n/a>";
|
|
}
|
|
|
|
console.info(
|
|
`[response] url=${response.url()} status=${response.status()} body=${responseText}`,
|
|
);
|
|
});
|
|
|
|
page.on("popup", async (popup) => {
|
|
console.info(`[popup] url=${popup.url()}`);
|
|
});
|
|
|
|
page.on("pageerror", async (error) => {
|
|
console.error("[pageerror]", error);
|
|
});
|
|
|
|
page.on("crash", async (page) => {
|
|
console.error("[crash]", page.url());
|
|
});
|
|
};
|
|
|
|
export const resetExternalAuthKey = async (context: BrowserContext) => {
|
|
// Find the session token so we can destroy the external auth links between
|
|
// tests, to ensure valid authentication happens each time.
|
|
const cookies = await context.cookies();
|
|
const sessionCookie = cookies.find((c) => c.name === "coder_session_token");
|
|
|
|
// Reset every provider the suite exercises so repeated runs against the same
|
|
// coderd instance get a clean slate; otherwise the device-flow link from a
|
|
// previous iteration trips the test on its next pass.
|
|
const providers = [gitAuth.webProvider, gitAuth.deviceProvider];
|
|
await Promise.all(
|
|
providers.map((provider) =>
|
|
deleteExternalAuthLink(provider, sessionCookie?.value),
|
|
),
|
|
);
|
|
};
|
|
|
|
const deleteExternalAuthLink = async (
|
|
provider: string,
|
|
sessionToken: string | undefined,
|
|
): Promise<void> => {
|
|
const response = await fetch(
|
|
`http://127.0.0.1:${coderPort}/api/v2/external-auth/${provider}?coder_session_token=${sessionToken}`,
|
|
{ method: "DELETE" },
|
|
);
|
|
|
|
// 200 = link deleted; 404 = no link existed for this provider.
|
|
if (response.status !== 200 && response.status !== 404) {
|
|
const data = await response.text();
|
|
console.error("failed to delete external auth link", data);
|
|
throw new Error(
|
|
`failed to delete external auth link: HTTP response ${response.status}`,
|
|
);
|
|
}
|
|
};
|
|
|
|
const isApiCall = (urlString: string): boolean => {
|
|
const url = new URL(urlString);
|
|
const apiPath = "/api/v2";
|
|
|
|
return url.pathname.startsWith(apiPath);
|
|
};
|