Skip to content

Commit 5803f9e

Browse files
committed
Add httpbin-compatible /cookies endpoints
1 parent b27e5b4 commit 5803f9e

12 files changed

+178
-11
lines changed

package-lock.json

+10
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"dependencies": {
3333
"@httptoolkit/util": "^0.1.2",
3434
"acme-client": "^5.3.0",
35+
"cookie": "^1.0.2",
3536
"lodash": "^4.17.21",
3637
"node-forge": "^1.3.1",
3738
"parse-multipart-data": "^1.5.0",

src/endpoints/http-index.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ export type HttpHandler = (
55
req: http.IncomingMessage,
66
res: http.ServerResponse,
77
options: {
8-
path: string
8+
path: string;
9+
query: URLSearchParams;
910
}
1011
) => MaybePromise<void>;
1112

@@ -22,4 +23,5 @@ export * from './http/methods.js';
2223
export * from './http/headers.js';
2324
export * from './http/user-agent.js';
2425
export * from './http/robots.txt.js';
25-
export * from './http/delay.js';
26+
export * from './http/delay.js';
27+
export * from './http/cookies.js'

src/endpoints/http/cookies.ts

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import * as Cookie from 'cookie';
2+
3+
import { HttpEndpoint } from '../http-index.js';
4+
5+
export const getCookies: HttpEndpoint = {
6+
matchPath: (path) => path === '/cookies',
7+
handle: (req, res) => {
8+
const cookies = Cookie.parse(req.headers.cookie || '');
9+
res.writeHead(200, { 'Content-Type': 'application/json' });
10+
res.end(JSON.stringify({ cookies }, null, 2));
11+
}
12+
}
13+
14+
export const setCookies: HttpEndpoint = {
15+
matchPath: (path) =>
16+
path === '/cookies/set' ||
17+
(path.startsWith('/cookies/set/') && path.split('/').length === 5),
18+
handle: (req, res, { path, query }) => {
19+
const cookiesToSet: Array<[string, string]> = [];
20+
21+
const cookiePath = path.split('/').slice(3);
22+
23+
if (cookiePath.length) {
24+
cookiesToSet.push([cookiePath[0], cookiePath[1]]);
25+
} else if (query.size) {
26+
for (const key of new Set(query.keys())) {
27+
cookiesToSet.push([
28+
key,
29+
query.get(key)! // For duplicates, we use the first only
30+
]);
31+
}
32+
}
33+
34+
res.writeHead(302, [
35+
'Location', '/cookies',
36+
...cookiesToSet.map((([key, value]) => {
37+
return [
38+
'set-cookie',
39+
Cookie.serialize(key, value, {
40+
path: '/'
41+
})
42+
];
43+
})).flat()
44+
]).end();
45+
}
46+
}
47+
48+
export const deleteCookies: HttpEndpoint = {
49+
matchPath: (path) => path === '/cookies/delete',
50+
handle: (req, res, { query }) => {
51+
const cookieKeys = [...new Set(query.keys())];
52+
53+
res.writeHead(302, [
54+
'Location', '/cookies',
55+
...cookieKeys.map((key) => [
56+
'set-cookie',
57+
`${key}=; Expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=0; Path=/`
58+
]).flat()
59+
]).end();
60+
}
61+
}

src/http-handler.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,10 @@ export function createHttpHandler(options: {
7373

7474
if (matchingEndpoint) {
7575
console.log(`Request to ${path} matched endpoint ${matchingEndpoint.name}`);
76-
await matchingEndpoint.handle(req, res, { path });
76+
await matchingEndpoint.handle(req, res, {
77+
path,
78+
query: url.searchParams
79+
});
7780
} else {
7881
console.log(`Request to ${path} matched no endpoints`);
7982
res.writeHead(404);

test/anything.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as net from 'net';
22
import { expect } from 'chai';
33
import { DestroyableServer, makeDestroyable } from 'destroyable-server';
44

5-
import { createServer } from '../src/server';
5+
import { createServer } from '../src/server.js';
66

77
describe("Anything endpoint", () => {
88

test/cookie.spec.ts

+91
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import * as net from 'net';
2+
import { expect } from 'chai';
3+
import { DestroyableServer, makeDestroyable } from 'destroyable-server';
4+
5+
import * as Cookie from 'cookie';
6+
7+
import { createServer } from '../src/server.js';
8+
9+
describe("Cookie endpoints", () => {
10+
11+
let server: DestroyableServer;
12+
let serverPort: number;
13+
14+
beforeEach(async () => {
15+
server = makeDestroyable(await createServer());
16+
await new Promise<void>((resolve) => server.listen(resolve));
17+
serverPort = (server.address() as net.AddressInfo).port;
18+
});
19+
20+
afterEach(async () => {
21+
await server.destroy();
22+
});
23+
24+
it("returns cookies at /cookies", async () => {
25+
const cookieValue = 'test=value; another=cookie';
26+
const address = `http://localhost:${serverPort}/cookies`;
27+
const response = await fetch(address, {
28+
headers: {
29+
'Cookie': cookieValue
30+
}
31+
});
32+
33+
expect(response.status).to.equal(200);
34+
35+
const body = await response.json();
36+
expect(body.cookies).to.deep.equal(Cookie.parse(cookieValue));
37+
});
38+
39+
it("sets cookies using path parameters at /cookies/set/name/value", async () => {
40+
const address = `http://localhost:${serverPort}/cookies/set/test/value`;
41+
const response = await fetch(address, {
42+
redirect: 'manual' // Prevent auto-following redirects
43+
});
44+
45+
expect(response.status).to.equal(302);
46+
expect(response.headers.get('Location')).to.equal('/cookies');
47+
expect(response.headers.get('Set-Cookie')).to.equal('test=value; Path=/');
48+
});
49+
50+
it("sets cookies using query parameters at /cookies/set", async () => {
51+
const address = `http://localhost:${serverPort}/cookies/set?test=value&test=value2&another=cookie`;
52+
const response = await fetch(address, {
53+
redirect: 'manual' // Prevent auto-following redirects
54+
});
55+
56+
expect(response.status).to.equal(302);
57+
expect(response.headers.get('Location')).to.equal('/cookies');
58+
59+
const setCookies = response.headers.getSetCookie();
60+
const parsedCookies = setCookies.map((c) => Cookie.parse(c));
61+
expect(parsedCookies).to.deep.equal([
62+
{ test: 'value', Path: '/' },
63+
{ another: 'cookie', Path: '/' }
64+
]);
65+
});
66+
67+
it('deletes cookies at /cookies/delete?cookie', async () => {
68+
const address = `http://localhost:${serverPort}/cookies/delete?hello=world&test`;
69+
const response = await fetch(address, {
70+
redirect: 'manual' // Prevent auto-following redirects
71+
});
72+
73+
expect(response.status).to.equal(302);
74+
expect(response.headers.get('Location')).to.equal('/cookies');
75+
76+
expect(response.headers.getSetCookie()).to.deep.equal([
77+
[
78+
'hello=',
79+
'Expires=Thu, 01-Jan-1970 00:00:00 GMT',
80+
'Max-Age=0',
81+
'Path=/'
82+
].join('; '),
83+
[
84+
'test=',
85+
'Expires=Thu, 01-Jan-1970 00:00:00 GMT',
86+
'Max-Age=0',
87+
'Path=/'
88+
].join('; ')
89+
]);
90+
})
91+
});

test/echo.spec.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import * as net from 'net';
2-
import * as tls from 'tls';
32
import { expect } from 'chai';
43
import { DestroyableServer, makeDestroyable } from 'destroyable-server';
54

6-
import { createServer } from '../src/server';
5+
import { createServer } from '../src/server.js';
76

87
describe("Echo endpoint", () => {
98

test/https.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import * as streamConsumers from 'stream/consumers';
66
import { expect } from 'chai';
77
import { DestroyableServer, makeDestroyable } from 'destroyable-server';
88

9-
import { createServer } from '../src/server';
9+
import { createServer } from '../src/server.js';
1010

1111
describe("HTTPS requests", () => {
1212

test/methods.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as net from 'net';
22
import { expect } from 'chai';
33
import { DestroyableServer, makeDestroyable } from 'destroyable-server';
44

5-
import { createServer } from '../src/server';
5+
import { createServer } from '../src/server.js';
66

77
describe("Method endpoints", () => {
88

test/status.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as net from 'net';
22
import { expect } from 'chai';
33
import { DestroyableServer, makeDestroyable } from 'destroyable-server';
44

5-
import { createServer } from '../src/server';
5+
import { createServer } from '../src/server.js';
66

77
describe("Status endpoint", () => {
88

tsconfig.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
"compilerOptions": {
33
"noEmit": true,
44
"target": "ES2022",
5-
"module": "node16",
6-
"moduleResolution": "node16",
5+
"module": "nodenext",
6+
"moduleResolution": "nodenext",
77
"esModuleInterop": true,
88
"allowSyntheticDefaultImports": true,
99
"sourceMap": true,

0 commit comments

Comments
 (0)