Skip to content

Commit

Permalink
test(undici): rm external http reqs from tests (#2459)
Browse files Browse the repository at this point in the history
* test(undici): rm external http reqs from tests

* cleanup(http-test-server): remove finished TODOs

* test(undici): fix server type, remove type:module to fix typings in test dir

* test(undici): make the typings better

* test(undici): fix typo
  • Loading branch information
ThatOneBro authored Mar 23, 2023
1 parent 5fd406c commit 732c5e7
Show file tree
Hide file tree
Showing 3 changed files with 208 additions and 22 deletions.
168 changes: 168 additions & 0 deletions test/http-test-server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import { serve } from "bun";

// This is obviously incomplete but these are probably the most common status codes + the ones we need for testing
type ValidStatusCode = 200 | 201 | 400 | 404 | 405 | 500;

const defaultOpts = {
type: "json",
headers: {
"Content-Type": "application/json",
},
status: 200,
};

const defaultResponseBodies = {
200: "OK",
201: "Created",
400: "Bad Request",
404: "Not Found",
405: "Method Not Allowed",
500: "Internal Server Error",
} as Record<ValidStatusCode, string>;

function getDefaultJSONBody(request: Request) {
return {
url: request.url,
method: request.method,
};
}

function makeTestJsonResponse(
request: Request,
opts: ResponseInit & { type?: "plaintext" | "json" } = { status: 200, type: "json" },
body?: { [k: string | number]: any } | string,
): Response {
const defaultJSONBody = getDefaultJSONBody(request);

let type = opts.type || "json";
let resBody;
let headers;

// Setup headers

if (!opts.headers) headers = new Headers();

if (!(opts.headers instanceof Headers)) headers = new Headers(opts.headers);
else headers = opts.headers;

switch (type) {
case "json":
if (typeof body === "object" && body !== null) {
resBody = JSON.stringify({ ...defaultJSONBody, ...body }) as string;
} else if (typeof body === "string") {
resBody = JSON.stringify({ ...defaultJSONBody, data: body }) as string;
} else {
resBody = JSON.stringify(defaultJSONBody) as string;
}
// Check to set headers
headers.set("Content-Type", "application/json");
break;
case "plaintext":
if (typeof body === "object") {
if (body === null) {
resBody = "";
} else {
resBody = JSON.stringify(body);
}
}
// Check to set headers
headers.set("Content-Type", "text/plain");
default:
}

return new Response(resBody as string, {
...defaultOpts,
...opts,
headers: { ...defaultOpts.headers, ...headers },
});
}

export function createServer() {
const server = serve({
port: 0,
fetch: async req => {
const { pathname, search } = new URL(req.url);
const lowerPath = pathname.toLowerCase();

let response: Response;
switch (lowerPath.match(/\/\w+/)?.[0] || "") {
// START HTTP METHOD ROUTES
case "/get":
if (req.method.toUpperCase() !== "GET") {
response = makeTestJsonResponse(req, { status: 405 });
break;
}
if (search !== "") {
const params = new URLSearchParams(search);
const args = {} as Record<string, string | number>;
params.forEach((v, k) => {
if (!isNaN(parseInt(v))) {
args[k] = parseInt(v);
} else {
args[k] = v;
}
});
response = makeTestJsonResponse(req, { status: 200 }, { args });
break;
}
// Normal case
response = makeTestJsonResponse(req);
break;
case "/post":
if (req.method.toUpperCase() !== "POST") {
response = makeTestJsonResponse(req, { status: 405 });
break;
}
response = makeTestJsonResponse(req, { status: 201, type: "json" }, await req.text());
break;
case "/head":
if (req.method.toUpperCase() !== "HEAD") {
response = makeTestJsonResponse(req, { status: 405 });
break;
}
response = makeTestJsonResponse(req, { status: 200 });
break;

// END HTTP METHOD ROUTES

case "/status":
// Parse the status from URL path params: /status/200
const rawStatus = lowerPath.split("/").filter(Boolean)[1];
if (rawStatus) {
const status = parseInt(rawStatus);
if (!isNaN(status) && status > 100 && status < 599) {
response = makeTestJsonResponse(
req,
{ status },
{ data: defaultResponseBodies[(status || 200) as ValidStatusCode] },
);
break;
}
}
response = makeTestJsonResponse(req, { status: 400 }, { data: "Invalid status" });
break;
case "/delay":
const rawDelay = lowerPath.split("/").filter(Boolean)[1];
if (rawDelay) {
const delay = parseInt(rawDelay);
if (!isNaN(delay) && delay >= 0) {
await Bun.sleep(delay * 1000);
response = makeTestJsonResponse(req, { status: 200 }, { data: "Delayed" });
break;
}
}
response = makeTestJsonResponse(req, { status: 400 }, { data: "Invalid delay" });
break;
case "/headers":
response = makeTestJsonResponse(req, { status: 200 }, { headers: req.headers });
break;
default:
response = makeTestJsonResponse(req, { status: 404 });
}

return response;
},
});
const { port, stop } = server;
return { server, port, stop };
}
61 changes: 40 additions & 21 deletions test/js/first_party/undici/undici.test.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,36 @@
import { describe, it, expect } from "bun:test";
import { describe, it, expect, beforeAll, afterAll } from "bun:test";
import { request } from "undici";

import { createServer } from "../../../http-test-server";

describe("undici", () => {
let serverCtl: ReturnType<typeof createServer>;
let hostUrl: string;
let hostname = "localhost";
let port: number;
let host: string;

beforeAll(() => {
serverCtl = createServer();
port = serverCtl.port;
host = `${hostname}:${port}`;
hostUrl = `http://${host}`;
});

afterAll(() => {
serverCtl.stop();
});

describe("request", () => {
it("should make a GET request when passed a URL string", async () => {
const { body } = await request("https://httpbin.org/get");
const { body } = await request(`${hostUrl}/get`);
expect(body).toBeDefined();
const json = (await body.json()) as { url: string };
expect(json.url).toBe("https://httpbin.org/get");
expect(json.url).toBe(`${hostUrl}/get`);
});

it("should error when body has already been consumed", async () => {
const { body } = await request("https://httpbin.org/get");
const { body } = await request(`${hostUrl}/get`);
await body.json();
expect(body.bodyUsed).toBe(true);
try {
Expand All @@ -23,7 +42,7 @@ describe("undici", () => {
});

it("should make a POST request when provided a body and POST method", async () => {
const { body } = await request("https://httpbin.org/post", {
const { body } = await request(`${hostUrl}/post`, {
method: "POST",
body: "Hello world",
});
Expand All @@ -33,23 +52,23 @@ describe("undici", () => {
});

it("should accept a URL class object", async () => {
const { body } = await request(new URL("https://httpbin.org/get"));
const { body } = await request(new URL(`${hostUrl}/get`));
expect(body).toBeDefined();
const json = (await body.json()) as { url: string };
expect(json.url).toBe("https://httpbin.org/get");
expect(json.url).toBe(`${hostUrl}/get`);
});

// it("should accept an undici UrlObject", async () => {
// // @ts-ignore
// const { body } = await request({ protocol: "https:", hostname: "httpbin.org", path: "/get" });
// const { body } = await request({ protocol: "https:", hostname: host, path: "/get" });
// expect(body).toBeDefined();
// const json = (await body.json()) as { url: string };
// expect(json.url).toBe("https://httpbin.org/get");
// expect(json.url).toBe(`${hostUrl}/get`);
// });

it("should prevent body from being attached to GET or HEAD requests", async () => {
try {
await request("https://httpbin.org/get", {
await request(`${hostUrl}/get`, {
method: "GET",
body: "Hello world",
});
Expand All @@ -59,7 +78,7 @@ describe("undici", () => {
}

try {
await request("https://httpbin.org/head", {
await request(`${hostUrl}/head`, {
method: "HEAD",
body: "Hello world",
});
Expand All @@ -70,12 +89,12 @@ describe("undici", () => {
});

it("should allow a query string to be passed", async () => {
const { body } = await request("https://httpbin.org/get?foo=bar");
const { body } = await request(`${hostUrl}/get?foo=bar`);
expect(body).toBeDefined();
const json = (await body.json()) as { args: { foo: string } };
expect(json.args.foo).toBe("bar");

const { body: body2 } = await request("https://httpbin.org/get", {
const { body: body2 } = await request(`${hostUrl}/get`, {
query: { foo: "bar" },
});
expect(body2).toBeDefined();
Expand All @@ -85,14 +104,14 @@ describe("undici", () => {

it("should throw on HTTP 4xx or 5xx error when throwOnError is true", async () => {
try {
await request("https://httpbin.org/status/404", { throwOnError: true });
await request(`${hostUrl}/status/404`, { throwOnError: true });
throw new Error("Should have errored");
} catch (e) {
expect((e as Error).message).toBe("Request failed with status code 404");
}

try {
await request("https://httpbin.org/status/500", { throwOnError: true });
await request(`${hostUrl}/status/500`, { throwOnError: true });
throw new Error("Should have errored");
} catch (e) {
expect((e as Error).message).toBe("Request failed with status code 500");
Expand All @@ -102,8 +121,8 @@ describe("undici", () => {
it("should allow us to abort the request with a signal", async () => {
const controller = new AbortController();
try {
setTimeout(() => controller.abort(), 1000);
const req = await request("https://httpbin.org/delay/5", {
setTimeout(() => controller.abort(), 500);
const req = await request(`${hostUrl}/delay/5`, {
signal: controller.signal,
});
await req.body.json();
Expand All @@ -114,20 +133,20 @@ describe("undici", () => {
});

it("should properly append headers to the request", async () => {
const { body } = await request("https://httpbin.org/headers", {
const { body } = await request(`${hostUrl}/headers`, {
headers: {
"x-foo": "bar",
},
});
expect(body).toBeDefined();
const json = (await body.json()) as { headers: { "X-Foo": string } };
expect(json.headers["X-Foo"]).toBe("bar");
const json = (await body.json()) as { headers: { "x-foo": string } };
expect(json.headers["x-foo"]).toBe("bar");
});

// it("should allow the use of FormData", async () => {
// const form = new FormData();
// form.append("foo", "bar");
// const { body } = await request("https://httpbin.org/post", {
// const { body } = await request(`${hostUrl}/post`, {
// method: "POST",
// body: form,
// });
Expand Down
1 change: 0 additions & 1 deletion test/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
{
"name": "test",
"type": "module",
"devDependencies": {},
"dependencies": {
"@swc/core": "^1.3.38",
Expand Down

0 comments on commit 732c5e7

Please sign in to comment.