-
Notifications
You must be signed in to change notification settings - Fork 2
/
mod.ts
148 lines (124 loc) · 3.91 KB
/
mod.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
import { MatchHandler, router, Routes } from "https://crux.land/[email protected]";
export type { MatchHandler };
class UnhandledRouteError extends Error {
routes: Routes;
request: Request;
constructor(init: { request: Request; routes: Routes }) {
const { request, routes } = init;
const method = request.method;
const reqPath = new URL(request.url).pathname;
const routesNumber = Object.entries(routes).length;
const routePlural = routesNumber === 1
? "route has a handler"
: "routes have handlers";
// deno-fmt-ignore
super(`${method} ${reqPath} (${routesNumber} ${routePlural})`);
this.name = this.constructor.name;
if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor);
}
this.routes = routes;
this.request = request;
}
}
export interface MockFetch {
fetch: typeof globalThis.fetch;
mock: (route: string, handler: MatchHandler) => void;
remove: (route: string) => void;
reset: () => void;
}
/**
* Get a set of functions that do not share any state with the globals.
*
* The returned object can be destructured.
*
* ```
* const { fetch, mock, remove, reset } = sandbox()
* ```
*/
export function sandbox(): MockFetch {
const routeStore = new Map<string, MatchHandler>();
async function fetch(
input: string | Request | URL,
init?: RequestInit,
): Promise<Response> {
// Request constructor won't take a URL, so we need to normalize it first.
if (input instanceof URL) input = input.toString();
const req = new Request(input, init);
const routes = Object.fromEntries(routeStore.entries());
// The router needs to be constructed every time because the routes map is
// very likely to change between fetches.
return await router(
routes,
// If an unhandled route is fetched, throw an error.
(request) => {
throw new UnhandledRouteError({ request, routes });
},
// Errors thrown by a handler, including the unknown route handler, will
// return a 500 Internal Server Error. That's the right behaviour in most
// cases, but we actually *want* that to throw.
(_, error) => {
throw error;
},
)(req);
}
function mock(route: string, handler: MatchHandler) {
routeStore.set(route, handler);
}
function remove(route: string) {
routeStore.delete(route);
}
function reset() {
routeStore.clear();
}
return {
reset,
mock,
remove,
fetch,
};
}
const globalMockFetch = sandbox();
/** This is the function that replaces `fetch` when you call `install()`. */
export const mockedFetch = globalMockFetch.fetch;
/**
* Mock a new route, or override an existing handler.
*
* The route uses URLPattern syntax, with the additional extension of
* (optional) method routing by prefixing with the method,
* eg. `"POST@/user/:id"`.
*
* The handler function may be asynchronous.
*
* ```
* mock("GET@/users/:id", async (_req, params) => {
* const id = parseInt(params["id"]);
* const data = await magicallyGetMyUserData(id);
* return new Response(JSON.stringify(data));
* })
* ```
*/
export const mock = globalMockFetch.mock;
/** Remove an existing route handler. */
export const remove = globalMockFetch.remove;
/** Remove all existing route handlers. */
export const reset = globalMockFetch.reset;
// Store the original fetch so it can be restored later
const originalFetch = globalThis.fetch;
// The functions below are `const` for consistency.
/**
* Replace `globalThis.fetch` with `mockedFetch` (or another function that
* matches the `fetch` signature)
*
* To restore the original `globalThis.fetch`, call `uninstall()`.
*/
export const install = (replacement?: typeof fetch) => {
globalThis.fetch = replacement ?? mockedFetch;
};
/**
* Restore `globalThis.fetch` to what it was before this library was imported.
*/
export const uninstall = () => {
globalThis.fetch = originalFetch;
reset();
};