Skip to content
Draft
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
35 changes: 29 additions & 6 deletions site/e2e/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { type ChildProcess, exec, spawn } from "node:child_process";
import { randomUUID } from "node:crypto";
import type { Server } from "node:http";
import net from "node:net";
import path from "node:path";
import { Duplex } from "node:stream";
Expand Down Expand Up @@ -865,17 +866,39 @@ export class Awaiter {
}
}

export const createServer = async (
port: number,
): Promise<ReturnType<typeof express>> => {
type MockServer = {
app: ReturnType<typeof express>;
/**
* close stops the server and force-closes any lingering keep-alive
* connections so the port is released promptly. Callers must invoke
* this in their teardown (e.g. test.afterAll, try/finally) to avoid
* leaking the listener into the next test run, which historically
* caused EADDRINUSE flakes in this suite.
*/
close: () => Promise<void>;
};

export const createServer = async (port: number): Promise<MockServer> => {
await waitForPort(port); // Wait until the port is available

const e = express();
const app = express();
// We need to specify the local IP address as the web server
// tends to fail with IPv6 related error:
// listen EADDRINUSE: address already in use :::50516
await new Promise<void>((r) => e.listen(port, "0.0.0.0", r));
return e;
const server = await new Promise<Server>((resolve) => {
const s = app.listen(port, "0.0.0.0", () => resolve(s));
});

return {
app,
close: () =>
new Promise<void>((resolve, reject) => {
// closeAllConnections (Node >= 18.2 / 16.17) forces lingering
// keep-alives to terminate so server.close() is bounded.
server.closeAllConnections?.();
server.close((err) => (err ? reject(err) : resolve()));
}),
};
};

async function waitForPort(
Expand Down
85 changes: 49 additions & 36 deletions site/e2e/tests/externalAuth.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,11 @@ import { beforeCoderTest, resetExternalAuthKey } from "../hooks";

test.describe
.skip("externalAuth", () => {
let closeWebServer: (() => Promise<void>) | undefined;

test.beforeAll(async ({ baseURL }) => {
const srv = await createServer(gitAuth.webPort);
const { app: srv, close } = await createServer(gitAuth.webPort);
closeWebServer = close;

// The GitHub validate endpoint returns the currently authenticated user!
srv.use(gitAuth.validatePath, (_req, res) => {
Expand All @@ -34,6 +37,10 @@ test.describe
});
});

test.afterAll(async () => {
await closeWebServer?.();
});

test.beforeEach(async ({ context, page }) => {
beforeCoderTest(page);
await login(page);
Expand All @@ -51,43 +58,49 @@ test.describe
};

// Start a server to mock the GitHub API.
const srv = await createServer(gitAuth.devicePort);
srv.use(gitAuth.validatePath, (_req, res) => {
res.write(JSON.stringify(ghUser));
res.end();
});
srv.use(gitAuth.codePath, (_req, res) => {
res.write(JSON.stringify(device));
res.end();
});
srv.use(gitAuth.installationsPath, (_req, res) => {
res.write(JSON.stringify(ghInstall));
res.end();
});
const { app: srv, close: closeServer } = await createServer(
gitAuth.devicePort,
);
try {
srv.use(gitAuth.validatePath, (_req, res) => {
res.write(JSON.stringify(ghUser));
res.end();
});
srv.use(gitAuth.codePath, (_req, res) => {
res.write(JSON.stringify(device));
res.end();
});
srv.use(gitAuth.installationsPath, (_req, res) => {
res.write(JSON.stringify(ghInstall));
res.end();
});

const token = {
access_token: "",
error: "authorization_pending",
error_description: "",
};
// First we send a result from the API that the token hasn't been
// authorized yet to ensure the UI reacts properly.
const sentPending = new Awaiter();
srv.use(gitAuth.tokenPath, (_req, res) => {
res.write(JSON.stringify(token));
res.end();
sentPending.done();
});
const token = {
access_token: "",
error: "authorization_pending",
error_description: "",
};
// First we send a result from the API that the token hasn't been
// authorized yet to ensure the UI reacts properly.
const sentPending = new Awaiter();
srv.use(gitAuth.tokenPath, (_req, res) => {
res.write(JSON.stringify(token));
res.end();
sentPending.done();
});

await page.goto(`/external-auth/${gitAuth.deviceProvider}`, {
waitUntil: "domcontentloaded",
});
await page.getByText(device.user_code).isVisible();
await sentPending.wait();
// Update the token to be valid and ensure the UI updates!
token.error = "";
token.access_token = "hello-world";
await page.waitForSelector("text=1 organization authorized");
await page.goto(`/external-auth/${gitAuth.deviceProvider}`, {
waitUntil: "domcontentloaded",
});
await page.getByText(device.user_code).isVisible();
await sentPending.wait();
// Update the token to be valid and ensure the UI updates!
token.error = "";
token.access_token = "hello-world";
await page.waitForSelector("text=1 organization authorized");
} finally {
await closeServer();
}
});

test("external auth web", async ({ page }) => {
Expand Down
Loading