repos / starfx

a micro-mvc framework for react apps
git clone https://github.com/neurosnap/starfx.git

starfx / src / test
Eric Bower  ·  2025-06-06

fetch.test.ts

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