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});