repos / starfx

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

starfx / test
Eric Bower · 05 Mar 24

mdw.test.ts

  1import { assertLike, asserts, describe, expect, it } from "../test.ts";
  2import {
  3  createSchema,
  4  createStore,
  5  slice,
  6  updateStore,
  7  waitForLoader,
  8} from "../store/mod.ts";
  9import {
 10  createApi,
 11  createKey,
 12  mdw,
 13  put,
 14  safe,
 15  takeEvery,
 16  takeLatest,
 17  waitFor,
 18} from "../mod.ts";
 19import type { ApiCtx, Next, ThunkCtx } from "../mod.ts";
 20
 21interface User {
 22  id: string;
 23  name: string;
 24  email: string;
 25}
 26
 27const emptyUser: User = { id: "", name: "", email: "" };
 28const mockUser: User = { id: "1", name: "test", email: "test@test.com" };
 29const mockUser2: User = { id: "2", name: "two", email: "two@test.com" };
 30
 31// deno-lint-ignore no-explicit-any
 32const jsonBlob = (data: any) => {
 33  return JSON.stringify(data);
 34};
 35
 36const testStore = () => {
 37  const [schema, initialState] = createSchema({
 38    users: slice.table<User>({ empty: emptyUser }),
 39    loaders: slice.loaders(),
 40    cache: slice.table({ empty: {} }),
 41  });
 42  const store = createStore({ initialState });
 43  return { schema, store };
 44};
 45
 46const tests = describe("middleware");
 47
 48it(tests, "basic", () => {
 49  const { store, schema } = testStore();
 50  const query = createApi<ApiCtx>();
 51  query.use(mdw.api({ schema }));
 52  query.use(query.routes());
 53  query.use(function* fetchApi(ctx, next) {
 54    if (`${ctx.req().url}`.startsWith("/users/")) {
 55      ctx.json = { ok: true, value: mockUser2 };
 56      yield* next();
 57      return;
 58    }
 59    const data = {
 60      users: [mockUser],
 61    };
 62    ctx.json = { ok: true, value: data };
 63    yield* next();
 64  });
 65
 66  const fetchUsers = query.create(
 67    `/users`,
 68    { supervisor: takeEvery },
 69    function* processUsers(ctx: ApiCtx<unknown, { users: User[] }>, next) {
 70      yield* next();
 71      if (!ctx.json.ok) return;
 72      const { users } = ctx.json.value;
 73
 74      yield* updateStore((state) => {
 75        users.forEach((u) => {
 76          state.users[u.id] = u;
 77        });
 78      });
 79    },
 80  );
 81
 82  const fetchUser = query.create<{ id: string }>(
 83    `/users/:id`,
 84    {
 85      supervisor: takeLatest,
 86    },
 87    function* processUser(ctx, next) {
 88      ctx.request = ctx.req({ method: "POST" });
 89      yield* next();
 90      if (!ctx.json.ok) return;
 91      const curUser = ctx.json.value;
 92      yield* updateStore((state) => {
 93        state.users[curUser.id] = curUser;
 94      });
 95    },
 96  );
 97
 98  store.run(query.bootup);
 99
100  store.dispatch(fetchUsers());
101  expect(store.getState().users).toEqual(
102    { [mockUser.id]: mockUser },
103  );
104  store.dispatch(fetchUser({ id: "2" }));
105  expect(store.getState().users).toEqual({
106    [mockUser.id]: mockUser,
107    [mockUser2.id]: mockUser2,
108  });
109});
110
111it(tests, "with loader", () => {
112  const { schema, store } = testStore();
113  const api = createApi<ApiCtx>();
114  api.use(mdw.api({ schema }));
115  api.use(api.routes());
116  api.use(function* fetchApi(ctx, next) {
117    ctx.response = new Response(jsonBlob(mockUser), { status: 200 });
118    const data = { users: [mockUser] };
119    ctx.json = { ok: true, value: data };
120    yield* next();
121  });
122
123  const fetchUsers = api.create(
124    `/users`,
125    { supervisor: takeEvery },
126    function* processUsers(ctx: ApiCtx<unknown, { users: User[] }>, next) {
127      yield* next();
128      if (!ctx.json.ok) return;
129
130      const { value } = ctx.json;
131
132      yield* updateStore((state) => {
133        value.users.forEach((u) => {
134          state.users[u.id] = u;
135        });
136      });
137    },
138  );
139
140  store.run(api.bootup);
141
142  store.dispatch(fetchUsers());
143  assertLike(store.getState(), {
144    users: { [mockUser.id]: mockUser },
145    loaders: {
146      "/users": {
147        status: "success",
148      },
149    },
150  });
151});
152
153it(tests, "with item loader", () => {
154  const { store, schema } = testStore();
155  const api = createApi<ApiCtx>();
156  api.use(mdw.api({ schema }));
157  api.use(api.routes());
158  api.use(function* fetchApi(ctx, next) {
159    ctx.response = new Response(jsonBlob(mockUser), { status: 200 });
160    const data = { users: [mockUser] };
161    ctx.json = { ok: true, value: data };
162    yield* next();
163  });
164
165  const fetchUser = api.create<{ id: string }>(
166    `/users/:id`,
167    { supervisor: takeEvery },
168    function* processUsers(ctx: ApiCtx<unknown, { users: User[] }>, next) {
169      yield* next();
170      if (!ctx.json.ok) return;
171
172      const { value } = ctx.json;
173      yield* updateStore((state) => {
174        value.users.forEach((u) => {
175          state.users[u.id] = u;
176        });
177      });
178    },
179  );
180
181  store.run(api.bootup);
182
183  const action = fetchUser({ id: mockUser.id });
184  store.dispatch(action);
185  assertLike(store.getState(), {
186    users: { [mockUser.id]: mockUser },
187    loaders: {
188      "/users/:id": {
189        status: "success",
190      },
191      [action.payload.key]: {
192        status: "success",
193      },
194    },
195  });
196});
197
198it(tests, "with POST", () => {
199  const { store, schema } = testStore();
200  const query = createApi();
201  query.use(mdw.queryCtx);
202  query.use(mdw.api({ schema }));
203  query.use(query.routes());
204  query.use(function* fetchApi(ctx, next) {
205    const request = ctx.req();
206    asserts.assertEquals(request, {
207      url: "/users",
208      headers: {},
209      method: "POST",
210      body: JSON.stringify({ email: "test@test.com" }),
211    });
212
213    const data = {
214      users: [mockUser],
215    };
216    ctx.response = new Response(jsonBlob(data), { status: 200 });
217    yield* next();
218  });
219
220  const createUser = query.create<{ email: string }>(
221    `/users [POST]`,
222    { supervisor: takeEvery },
223    function* processUsers(
224      ctx: ApiCtx<{ email: string }, { users: User[] }>,
225      next,
226    ) {
227      ctx.request = ctx.req({
228        method: "POST",
229        body: JSON.stringify({ email: ctx.payload.email }),
230      });
231
232      yield* next();
233
234      if (!ctx.json.ok) return;
235
236      const { users } = ctx.json.value;
237      yield* updateStore((state) => {
238        users.forEach((u) => {
239          state.users[u.id] = u;
240        });
241      });
242    },
243  );
244
245  store.run(query.bootup);
246  store.dispatch(createUser({ email: mockUser.email }));
247});
248
249it(tests, "simpleCache", () => {
250  const { store, schema } = testStore();
251  const api = createApi<ApiCtx>();
252  api.use(mdw.api({ schema }));
253  api.use(api.routes());
254  api.use(function* fetchApi(ctx, next) {
255    const data = { users: [mockUser] };
256    ctx.response = new Response(jsonBlob(data));
257    ctx.json = { ok: true, value: data };
258    yield* next();
259  });
260
261  const fetchUsers = api.get("/users", { supervisor: takeEvery }, api.cache());
262  store.run(api.bootup);
263
264  const action = fetchUsers();
265  store.dispatch(action);
266  assertLike(store.getState(), {
267    data: {
268      [action.payload.key]: { users: [mockUser] },
269    },
270    loaders: {
271      [`${fetchUsers}`]: {
272        status: "success",
273      },
274    },
275  });
276});
277
278it(tests, "overriding default loader behavior", () => {
279  const { store, schema } = testStore();
280  const api = createApi<ApiCtx>();
281  api.use(mdw.api({ schema }));
282  api.use(api.routes());
283  api.use(function* fetchApi(ctx, next) {
284    const data = { users: [mockUser] };
285    ctx.response = new Response(jsonBlob(data));
286    ctx.json = { ok: true, value: data };
287    yield* next();
288  });
289
290  const fetchUsers = api.create(
291    `/users`,
292    { supervisor: takeEvery },
293    function* (ctx: ApiCtx<unknown, { users: User[] }>, next) {
294      yield* next();
295
296      if (!ctx.json.ok) {
297        return;
298      }
299      const { value } = ctx.json;
300      ctx.loader = { message: "yes", meta: { wow: true } };
301      yield* updateStore((state) => {
302        value.users.forEach((u) => {
303          state.users[u.id] = u;
304        });
305      });
306    },
307  );
308
309  store.run(api.bootup);
310
311  store.dispatch(fetchUsers());
312  assertLike(store.getState(), {
313    users: { [mockUser.id]: mockUser },
314    loaders: {
315      [`${fetchUsers}`]: {
316        status: "success",
317        message: "yes",
318        meta: { wow: true },
319      },
320    },
321  });
322});
323
324it(tests, "mdw.api() - error handler", () => {
325  let err = false;
326  console.error = (msg: string) => {
327    if (err) return;
328    asserts.assertEquals(
329      msg,
330      "Error: something happened.  Check the endpoint [/users]",
331    );
332    err = true;
333  };
334
335  const { schema, store } = testStore();
336  const query = createApi<ApiCtx>();
337  query.use(mdw.api({ schema }));
338  query.use(query.routes());
339  query.use(function* () {
340    throw new Error("something happened");
341  });
342
343  const fetchUsers = query.create(`/users`, { supervisor: takeEvery });
344
345  store.run(query.bootup);
346  store.dispatch(fetchUsers());
347});
348
349it(tests, "createApi with own key", async () => {
350  const { schema, store } = testStore();
351  const query = createApi();
352  query.use(mdw.api({ schema }));
353  query.use(query.routes());
354  query.use(mdw.customKey);
355  query.use(function* fetchApi(ctx, next) {
356    const data = {
357      users: [{ ...mockUser, ...ctx.action.payload.options }],
358    };
359    ctx.response = new Response(jsonBlob(data), { status: 200 });
360    yield* next();
361  });
362
363  const theTestKey = `some-custom-key-${Math.ceil(Math.random() * 1000)}`;
364
365  const createUserCustomKey = query.post<{ email: string }>(
366    `/users`,
367    { supervisor: takeEvery },
368    function* processUsers(ctx: ApiCtx, next) {
369      ctx.cache = true;
370      ctx.key = theTestKey; // or some calculated key //
371      yield* next();
372      const buff = yield* safe(() => {
373        if (!ctx.response) throw new Error("no response");
374        return ctx.response.arrayBuffer();
375      });
376      if (!buff.ok) {
377        throw buff.error;
378      }
379
380      const result = new TextDecoder("utf-8").decode(buff.value);
381      const { users } = JSON.parse(result);
382      if (!users) return;
383      const curUsers = (users as User[]).reduce<Record<string, User>>(
384        (acc, u) => {
385          acc[u.id] = u;
386          return acc;
387        },
388        {},
389      );
390      ctx.response = new Response();
391      ctx.json = {
392        ok: true,
393        value: curUsers,
394      };
395    },
396  );
397  const newUEmail = mockUser.email + ".org";
398
399  store.run(query.bootup);
400
401  store.dispatch(createUserCustomKey({ email: newUEmail }));
402
403  await store.run(waitForLoader(schema.loaders, createUserCustomKey));
404
405  const expectedKey = theTestKey
406    ? `/users [POST]|${theTestKey}`
407    : createKey("/users [POST]", { email: newUEmail });
408
409  const s = store.getState();
410  asserts.assertEquals(schema.cache.selectById(s, { id: expectedKey }), {
411    "1": { id: "1", name: "test", email: newUEmail },
412  });
413
414  asserts.assert(
415    expectedKey.split("|")[1] === theTestKey,
416    "the keypart should match the input",
417  );
418});
419
420it(tests, "createApi with custom key but no payload", async () => {
421  const { store, schema } = testStore();
422  const query = createApi();
423  query.use(mdw.api({ schema }));
424  query.use(query.routes());
425  query.use(mdw.customKey);
426  query.use(function* fetchApi(ctx, next) {
427    const data = {
428      users: [mockUser],
429    };
430    ctx.response = new Response(jsonBlob(data), { status: 200 });
431    yield* next();
432  });
433
434  const theTestKey = `some-custom-key-${Math.ceil(Math.random() * 1000)}`;
435
436  const getUsers = query.get(
437    `/users`,
438    { supervisor: takeEvery },
439    function* processUsers(ctx: ApiCtx, next) {
440      ctx.cache = true;
441      ctx.key = theTestKey; // or some calculated key //
442      yield* next();
443      const buff = yield* safe(() => {
444        if (!ctx.response) throw new Error("no response");
445        return ctx.response?.arrayBuffer();
446      });
447      if (!buff.ok) {
448        throw buff.error;
449      }
450
451      const result = new TextDecoder("utf-8").decode(buff.value);
452      const { users } = JSON.parse(result);
453      if (!users) return;
454      const curUsers = (users as User[]).reduce<Record<string, User>>(
455        (acc, u) => {
456          acc[u.id] = u;
457          return acc;
458        },
459        {},
460      );
461      ctx.response = new Response();
462      ctx.json = {
463        ok: true,
464        value: curUsers,
465      };
466    },
467  );
468
469  store.run(query.bootup);
470  store.dispatch(getUsers());
471
472  await store.run(waitForLoader(schema.loaders, getUsers));
473
474  const expectedKey = theTestKey
475    ? `/users [GET]|${theTestKey}`
476    : createKey("/users [GET]", null);
477
478  const s = store.getState();
479  asserts.assertEquals(schema.cache.selectById(s, { id: expectedKey }), {
480    "1": mockUser,
481  });
482
483  asserts.assert(
484    expectedKey.split("|")[1] === theTestKey,
485    "the keypart should match the input",
486  );
487});
488
489it(tests, "errorHandler", () => {
490  let a = 0;
491  const query = createApi<ApiCtx>();
492  query.use(function* errorHandler<Ctx extends ThunkCtx = ThunkCtx>(
493    ctx: Ctx,
494    next: Next,
495  ) {
496    try {
497      a = 1;
498      yield* next();
499      a = 2;
500    } catch (err) {
501      console.error(
502        `Error: ${err.message}.  Check the endpoint [${ctx.name}]`,
503        ctx,
504      );
505    }
506  });
507  query.use(mdw.queryCtx);
508  query.use(query.routes());
509  query.use(function* fetchApi(ctx, next) {
510    if (`${ctx.req().url}`.startsWith("/users/")) {
511      ctx.json = { ok: true, value: mockUser2 };
512      yield* next();
513      return;
514    }
515    const data = {
516      users: [mockUser],
517    };
518    ctx.json = { ok: true, value: data };
519    yield* next();
520  });
521
522  const fetchUsers = query.create(
523    `/users`,
524    { supervisor: takeEvery },
525    function* processUsers(_: ApiCtx<unknown, { users: User[] }>, next) {
526      // throw new Error("some error");
527      yield* next();
528    },
529  );
530
531  const store = createStore({
532    initialState: {
533      users: {},
534    },
535  });
536  store.run(query.bootup);
537  store.dispatch(fetchUsers());
538  expect(store.getState()).toEqual({
539    users: {},
540  });
541  expect(a).toEqual(2);
542});
543
544it(tests, "stub predicate", async () => {
545  let actual: { ok: boolean } = { ok: false };
546  const { store, schema } = testStore();
547  const api = createApi();
548  api.use(function* (ctx, next) {
549    ctx.stub = true;
550    yield* next();
551  });
552
553  api.use(mdw.api({ schema }));
554  api.use(api.routes());
555  api.use(mdw.fetch({ baseUrl: "http://nowhere.com" }));
556
557  const stub = mdw.predicate((ctx) => ctx.stub === true);
558
559  const fetchUsers = api.get("/users", { supervisor: takeEvery }, [
560    function* (ctx, next) {
561      yield* next();
562      actual = ctx.json;
563      yield* put({ type: "DONE" });
564    },
565    stub(function* (ctx, next) {
566      ctx.response = new Response(JSON.stringify({ frodo: "shire" }));
567      yield* next();
568    }),
569  ]);
570
571  store.run(api.bootup);
572  store.dispatch(fetchUsers());
573
574  await store.run(waitFor(() => actual.ok));
575
576  expect(actual).toEqual({
577    ok: true,
578    value: { frodo: "shire" },
579  });
580});