Eric Bower
·
05 Mar 24
fetch.test.ts
1import { describe, expect, install, it, mock } from "../test.ts";
2import {
3 createSchema,
4 createStore,
5 slice,
6 waitForLoader,
7 waitForLoaders,
8} from "../store/mod.ts";
9import { ApiCtx, createApi, mdw, takeEvery } from "../mod.ts";
10
11install();
12
13const baseUrl = "https://starfx.com";
14const mockUser = { id: "1", email: "test@starfx.com" };
15
16const testStore = () => {
17 const [schema, initialState] = createSchema({
18 loaders: slice.loaders(),
19 cache: slice.table({ empty: {} }),
20 });
21 const store = createStore({ initialState });
22 return { schema, store };
23};
24
25const getTestData = (ctx: ApiCtx) => {
26 return { request: { ...ctx.req() }, json: { ...ctx.json } };
27};
28
29const tests = describe("mdw.fetch()");
30
31it(
32 tests,
33 "should be able to fetch a resource and save automatically",
34 async () => {
35 mock(`GET@/users`, () => {
36 return new Response(JSON.stringify(mockUser));
37 });
38
39 const { store, schema } = testStore();
40 const api = createApi();
41 api.use(mdw.api({ schema }));
42 api.use(api.routes());
43 api.use(mdw.headers);
44 api.use(mdw.fetch({ baseUrl }));
45
46 const actual: any[] = [];
47 const fetchUsers = api.get(
48 "/users",
49 { supervisor: takeEvery },
50 function* (ctx, next) {
51 ctx.cache = true;
52 yield* next();
53
54 actual.push(ctx.request);
55 actual.push(ctx.json);
56 },
57 );
58
59 store.run(api.bootup);
60
61 const action = fetchUsers();
62 store.dispatch(action);
63
64 await store.run(waitForLoader(schema.loaders, action));
65
66 const state = store.getState();
67 expect(state.cache[action.payload.key]).toEqual(mockUser);
68 expect(actual).toEqual([{
69 url: `${baseUrl}/users`,
70 method: "GET",
71 headers: {
72 "Content-Type": "application/json",
73 },
74 }, { ok: true, value: mockUser }]);
75 },
76);
77
78it(
79 tests,
80 "should be able to fetch a resource and parse as text instead of json",
81 async () => {
82 mock(`GET@/users`, () => {
83 return new Response("this is some text");
84 });
85
86 const { store, schema } = testStore();
87 const api = createApi();
88 api.use(mdw.api({ schema }));
89 api.use(api.routes());
90 api.use(mdw.fetch({ baseUrl }));
91
92 let actual = null;
93 const fetchUsers = api.get(
94 "/users",
95 { supervisor: takeEvery },
96 function* (ctx, next) {
97 ctx.cache = true;
98 ctx.bodyType = "text";
99 yield* next();
100 actual = ctx.json;
101 },
102 );
103
104 store.run(api.bootup);
105
106 const action = fetchUsers();
107 store.dispatch(action);
108
109 await store.run(waitForLoader(schema.loaders, action));
110
111 const data = "this is some text";
112 expect(actual).toEqual({ ok: true, value: data });
113 },
114);
115
116it(tests, "error handling", async () => {
117 const errMsg = { message: "something happened" };
118 mock(`GET@/users`, () => {
119 return new Response(JSON.stringify(errMsg), { status: 500 });
120 });
121
122 const { schema, store } = testStore();
123 const api = createApi();
124 api.use(mdw.api({ schema }));
125 api.use(api.routes());
126 api.use(mdw.fetch({ baseUrl }));
127
128 let actual = null;
129 const fetchUsers = api.get(
130 "/users",
131 { supervisor: takeEvery },
132 function* (ctx, next) {
133 ctx.cache = true;
134 yield* next();
135
136 actual = ctx.json;
137 },
138 );
139
140 store.run(api.bootup);
141
142 const action = fetchUsers();
143 store.dispatch(action);
144
145 await store.run(waitForLoader(schema.loaders, action));
146
147 const state = store.getState();
148 expect(state.cache[action.payload.key]).toEqual(errMsg);
149 expect(actual).toEqual({ ok: false, error: errMsg });
150});
151
152it(tests, "status 204", async () => {
153 mock(`GET@/users`, () => {
154 return new Response(null, { status: 204 });
155 });
156
157 const { schema, store } = testStore();
158 const api = createApi();
159 api.use(mdw.api({ schema }));
160 api.use(api.routes());
161 api.use(function* (ctx, next) {
162 const url = ctx.req().url;
163 ctx.request = ctx.req({ url: `${baseUrl}${url}` });
164 yield* next();
165 });
166 api.use(mdw.fetch());
167
168 let actual = null;
169 const fetchUsers = api.get(
170 "/users",
171 { supervisor: takeEvery },
172 function* (ctx, next) {
173 ctx.cache = true;
174 yield* next();
175 actual = ctx.json;
176 },
177 );
178
179 store.run(api.bootup);
180
181 const action = fetchUsers();
182 store.dispatch(action);
183
184 await store.run(waitForLoader(schema.loaders, action));
185
186 const state = store.getState();
187 expect(state.cache[action.payload.key]).toEqual({});
188 expect(actual).toEqual({ ok: true, value: {} });
189});
190
191it(tests, "malformed json", async () => {
192 mock(`GET@/users`, () => {
193 return new Response("not json", { status: 200 });
194 });
195
196 const { schema, store } = testStore();
197 const api = createApi();
198 api.use(mdw.api({ schema }));
199 api.use(api.routes());
200 api.use(function* (ctx, next) {
201 const url = ctx.req().url;
202 ctx.request = ctx.req({ url: `${baseUrl}${url}` });
203 yield* next();
204 });
205 api.use(mdw.fetch());
206
207 let actual = null;
208 const fetchUsers = api.get(
209 "/users",
210 { supervisor: takeEvery },
211 function* (ctx, next) {
212 ctx.cache = true;
213 yield* next();
214
215 actual = ctx.json;
216 },
217 );
218
219 store.run(api.bootup);
220 const action = fetchUsers();
221 store.dispatch(action);
222
223 await store.run(waitForLoader(schema.loaders, action));
224
225 const data = {
226 message: "Unexpected token 'o', \"not json\" is not valid JSON",
227 };
228 expect(actual).toEqual({
229 ok: false,
230 error: data,
231 });
232});
233
234it(tests, "POST", async () => {
235 mock(`POST@/users`, () => {
236 return new Response(JSON.stringify(mockUser));
237 });
238
239 const { schema, store } = testStore();
240 const api = createApi();
241 api.use(mdw.api({ schema }));
242 api.use(api.routes());
243 api.use(mdw.headers);
244 api.use(mdw.fetch({ baseUrl }));
245
246 const fetchUsers = api.post(
247 "/users",
248 { supervisor: takeEvery },
249 function* (ctx, next) {
250 ctx.cache = true;
251 ctx.request = ctx.req({
252 body: JSON.stringify(mockUser),
253 });
254 yield* next();
255
256 ctx.loader = { meta: getTestData(ctx) };
257 },
258 );
259
260 store.run(api.bootup);
261 const action = fetchUsers();
262 store.dispatch(action);
263
264 const loader = await store.run(waitForLoader(schema.loaders, action));
265 if (!loader.ok) {
266 throw loader.error;
267 }
268
269 expect(loader.value.meta.request).toEqual({
270 url: `${baseUrl}/users`,
271 headers: {
272 "Content-Type": "application/json",
273 },
274 method: "POST",
275 body: JSON.stringify(mockUser),
276 });
277
278 expect(loader.value.meta.json).toEqual({
279 ok: true,
280 value: mockUser,
281 });
282});
283
284it(tests, "POST multiple endpoints with same uri", async () => {
285 mock(`POST@/users/1/something`, () => {
286 return new Response(JSON.stringify(mockUser));
287 });
288
289 const { store, schema } = testStore();
290 const api = createApi();
291 api.use(mdw.api({ schema }));
292 api.use(api.routes());
293 api.use(mdw.headers);
294 api.use(mdw.fetch({ baseUrl }));
295
296 const fetchUsers = api.post<{ id: string }>(
297 "/users/:id/something",
298 { supervisor: takeEvery },
299 function* (ctx, next) {
300 ctx.cache = true;
301 ctx.request = ctx.req({ body: JSON.stringify(mockUser) });
302 yield* next();
303
304 ctx.loader = { meta: getTestData(ctx) };
305 },
306 );
307
308 const fetchUsersSecond = api.post<{ id: string }>(
309 ["/users/:id/something", "next"],
310 { supervisor: takeEvery },
311 function* (ctx, next) {
312 ctx.cache = true;
313 ctx.request = ctx.req({ body: JSON.stringify(mockUser) });
314 yield* next();
315 ctx.loader = { meta: getTestData(ctx) };
316 },
317 );
318
319 store.run(api.bootup);
320
321 const action1 = fetchUsers({ id: "1" });
322 const action2 = fetchUsersSecond({ id: "1" });
323 store.dispatch(action1);
324 store.dispatch(action2);
325
326 const results = await store.run(
327 waitForLoaders(schema.loaders, [action1, action2]),
328 );
329 if (!results.ok) {
330 throw results.error;
331 }
332 const result1 = results.value[0];
333 if (!result1.ok) {
334 throw result1.error;
335 }
336 const result2 = results.value[1];
337 if (!result2.ok) {
338 throw result2.error;
339 }
340
341 expect(result1.value.meta.request).toEqual({
342 url: `${baseUrl}/users/1/something`,
343 headers: {
344 "Content-Type": "application/json",
345 },
346 method: "POST",
347 body: JSON.stringify(mockUser),
348 });
349
350 expect(result1.value.meta.json).toEqual({
351 ok: true,
352 value: mockUser,
353 });
354
355 expect(result2.value.meta.request).toEqual({
356 url: `${baseUrl}/users/1/something`,
357 headers: {
358 "Content-Type": "application/json",
359 },
360 method: "POST",
361 body: JSON.stringify(mockUser),
362 });
363
364 expect(result2.value.meta.json).toEqual({
365 ok: true,
366 value: mockUser,
367 });
368});
369
370it(
371 tests,
372 "slug in url but payload has empty string for slug value",
373 () => {
374 const { store, schema } = testStore();
375 const api = createApi();
376 api.use(mdw.api({ schema }));
377 api.use(api.routes());
378 api.use(mdw.fetch({ baseUrl }));
379 let actual = "";
380
381 const fetchUsers = api.post<{ id: string }>(
382 "/users/:id",
383 { supervisor: takeEvery },
384 function* (ctx, next) {
385 ctx.cache = true;
386 ctx.request = ctx.req({ body: JSON.stringify(mockUser) });
387
388 yield* next();
389 if (!ctx.json.ok) {
390 actual = ctx.json.error;
391 }
392 },
393 );
394
395 store.run(api.bootup);
396 const action = fetchUsers({ id: "" });
397 store.dispatch(action);
398
399 const data =
400 "found :id in endpoint name (/users/:id [POST]) but payload has falsy value ()";
401 expect(actual).toEqual(data);
402 },
403);
404
405it(
406 tests,
407 "with success - should keep retrying fetch request",
408 async () => {
409 let counter = 0;
410 mock(`GET@/users`, () => {
411 counter += 1;
412 if (counter > 4) {
413 return new Response(JSON.stringify(mockUser));
414 }
415 return new Response(JSON.stringify({ message: "error" }), {
416 status: 400,
417 });
418 });
419
420 const { schema, store } = testStore();
421 const api = createApi();
422 api.use(mdw.api({ schema }));
423 api.use(api.routes());
424 api.use(mdw.fetch({ baseUrl }));
425
426 let actual = null;
427 const fetchUsers = api.get("/users", { supervisor: takeEvery }, [
428 function* (ctx, next) {
429 ctx.cache = true;
430 yield* next();
431
432 if (!ctx.json.ok) {
433 return;
434 }
435
436 actual = ctx.json;
437 },
438 mdw.fetchRetry((n) => (n > 4 ? -1 : 10)),
439 ]);
440
441 store.run(api.bootup);
442
443 const action = fetchUsers();
444 store.dispatch(action);
445
446 const loader = await store.run(waitForLoader(schema.loaders, action));
447 if (!loader.ok) {
448 throw loader.error;
449 }
450
451 const state = store.getState();
452 expect(state.cache[action.payload.key]).toEqual(mockUser);
453 expect(actual).toEqual({ ok: true, value: mockUser });
454 },
455);
456
457it(
458 tests,
459 "fetch retry - with failure - should keep retrying and then quit",
460 async () => {
461 mock(`GET@/users`, () => {
462 return new Response(JSON.stringify({ message: "error" }), {
463 status: 400,
464 });
465 });
466
467 const { schema, store } = testStore();
468 let actual = null;
469 const api = createApi();
470 api.use(mdw.api({ schema }));
471 api.use(api.routes());
472 api.use(mdw.fetch({ baseUrl }));
473
474 const fetchUsers = api.get("/users", { supervisor: takeEvery }, [
475 function* (ctx, next) {
476 ctx.cache = true;
477 yield* next();
478 actual = ctx.json;
479 },
480 mdw.fetchRetry((n) => (n > 2 ? -1 : 10)),
481 ]);
482
483 store.run(api.bootup);
484 const action = fetchUsers();
485 store.dispatch(action);
486
487 const loader = await store.run(waitForLoader(schema.loaders, action));
488 if (!loader.ok) {
489 throw loader.error;
490 }
491 const data = { message: "error" };
492 expect(actual).toEqual({ ok: false, error: data });
493 },
494);
495
496it(
497 tests,
498 "should *not* make http request and instead simply mock response",
499 async () => {
500 const { schema, store } = testStore();
501 let actual = null;
502 const api = createApi();
503 api.use(mdw.api({ schema }));
504 api.use(api.routes());
505 api.use(mdw.fetch({ baseUrl }));
506
507 const fetchUsers = api.get("/users", { supervisor: takeEvery }, [
508 function* (ctx, next) {
509 yield* next();
510 actual = ctx.json;
511 },
512 mdw.response(new Response(JSON.stringify(mockUser))),
513 ]);
514
515 store.run(api.bootup);
516 store.dispatch(fetchUsers());
517
518 const loader = await store.run(waitForLoader(schema.loaders, fetchUsers));
519 if (!loader.ok) {
520 throw loader.error;
521 }
522 expect(actual).toEqual({ ok: true, value: mockUser });
523 },
524);
525
526it(tests, "should use dynamic mdw to mock response", async () => {
527 const { schema, store } = testStore();
528 let actual = null;
529 const api = createApi();
530 api.use(mdw.api({ schema }));
531 api.use(api.routes());
532 api.use(mdw.fetch({ baseUrl }));
533
534 const fetchUsers = api.get("/users", { supervisor: takeEvery }, [
535 function* (ctx, next) {
536 yield* next();
537 actual = ctx.json;
538 },
539 mdw.response(new Response(JSON.stringify(mockUser))),
540 ]);
541
542 store.run(api.bootup);
543
544 // override default response with dynamic mdw
545 const dynamicUser = { id: "2", email: "dynamic@starfx.com" };
546 fetchUsers.use(mdw.response(new Response(JSON.stringify(dynamicUser))));
547 store.dispatch(fetchUsers());
548 let loader = await store.run(waitForLoader(schema.loaders, fetchUsers));
549 if (!loader.ok) {
550 throw loader.error;
551 }
552 expect(actual).toEqual({ ok: true, value: dynamicUser });
553
554 // reset dynamic mdw and try again
555 api.reset();
556 store.dispatch(fetchUsers());
557 loader = await store.run(waitForLoader(schema.loaders, fetchUsers));
558 if (!loader.ok) {
559 throw loader.error;
560 }
561 expect(actual).toEqual({ ok: true, value: mockUser });
562});