mirror of
https://github.com/oven-sh/bun
synced 2026-02-15 05:12:29 +00:00
test(undici): rm external http reqs from tests (#2459)
* 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
This commit is contained in:
168
test/http-test-server.ts
Normal file
168
test/http-test-server.ts
Normal file
@@ -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 };
|
||||
}
|
||||
@@ -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 {
|
||||
@@ -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",
|
||||
});
|
||||
@@ -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",
|
||||
});
|
||||
@@ -59,7 +78,7 @@ describe("undici", () => {
|
||||
}
|
||||
|
||||
try {
|
||||
await request("https://httpbin.org/head", {
|
||||
await request(`${hostUrl}/head`, {
|
||||
method: "HEAD",
|
||||
body: "Hello world",
|
||||
});
|
||||
@@ -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();
|
||||
@@ -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");
|
||||
@@ -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();
|
||||
@@ -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,
|
||||
// });
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"name": "test",
|
||||
"type": "module",
|
||||
"devDependencies": {},
|
||||
"dependencies": {
|
||||
"@swc/core": "^1.3.38",
|
||||
|
||||
Reference in New Issue
Block a user