repos / starfx

supercharged async flow control library.
git clone https://github.com/neurosnap/starfx.git

starfx / test
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});