|
| 1 | +import { describe, expect, it } from "vitest" |
| 2 | +import { createMisina } from "../src/index.ts" |
| 3 | + |
| 4 | +declare module "../src/types.ts" { |
| 5 | + interface MisinaState { |
| 6 | + counter?: number |
| 7 | + token?: string |
| 8 | + } |
| 9 | +} |
| 10 | + |
| 11 | +describe("state — session-scoped mutable shared object", () => { |
| 12 | + it("hooks see the same state reference across calls", async () => { |
| 13 | + const driver = { |
| 14 | + name: "p", |
| 15 | + request: async () => new Response("{}", { headers: { "content-type": "application/json" } }), |
| 16 | + } |
| 17 | + const m = createMisina({ |
| 18 | + driver, |
| 19 | + retry: 0, |
| 20 | + state: { counter: 0 }, |
| 21 | + hooks: { |
| 22 | + beforeRequest: (ctx) => { |
| 23 | + if (ctx.options.state.counter != null) { |
| 24 | + ctx.options.state.counter++ |
| 25 | + } |
| 26 | + }, |
| 27 | + }, |
| 28 | + }) |
| 29 | + |
| 30 | + await m.get("https://api.test/") |
| 31 | + await m.get("https://api.test/") |
| 32 | + await m.get("https://api.test/") |
| 33 | + |
| 34 | + // The state object is mutated by every call — read back via a hook |
| 35 | + // by triggering a final call and inspecting. |
| 36 | + let observed: number | undefined |
| 37 | + await m.get("https://api.test/", { |
| 38 | + hooks: { |
| 39 | + afterResponse: (ctx) => { |
| 40 | + observed = ctx.options.state.counter |
| 41 | + }, |
| 42 | + }, |
| 43 | + }) |
| 44 | + expect(observed).toBe(4) |
| 45 | + }) |
| 46 | + |
| 47 | + it("token rotation in hooks affects subsequent calls", async () => { |
| 48 | + let captured: (string | null)[] = [] |
| 49 | + const driver = { |
| 50 | + name: "p", |
| 51 | + request: async (req: Request) => { |
| 52 | + captured.push(req.headers.get("authorization")) |
| 53 | + return new Response("{}", { headers: { "content-type": "application/json" } }) |
| 54 | + }, |
| 55 | + } |
| 56 | + const m = createMisina({ |
| 57 | + driver, |
| 58 | + retry: 0, |
| 59 | + state: { token: "v1" }, |
| 60 | + hooks: { |
| 61 | + beforeRequest: (ctx) => { |
| 62 | + const headers = new Headers(ctx.request.headers) |
| 63 | + if (ctx.options.state.token) |
| 64 | + headers.set("authorization", `Bearer ${ctx.options.state.token}`) |
| 65 | + return new Request(ctx.request, { headers }) |
| 66 | + }, |
| 67 | + }, |
| 68 | + }) |
| 69 | + |
| 70 | + await m.get("https://api.test/") |
| 71 | + expect(captured[0]).toBe("Bearer v1") |
| 72 | + |
| 73 | + // Mutate state externally — next call uses the new token. |
| 74 | + await m.get("https://api.test/", { |
| 75 | + hooks: { |
| 76 | + init: (opts) => { |
| 77 | + opts.state.token = "v2" |
| 78 | + }, |
| 79 | + }, |
| 80 | + }) |
| 81 | + expect(captured[1]).toBe("Bearer v2") |
| 82 | + }) |
| 83 | + |
| 84 | + it("default empty {} when no state supplied", async () => { |
| 85 | + let captured: object | undefined |
| 86 | + const driver = { |
| 87 | + name: "p", |
| 88 | + request: async () => new Response("{}", { headers: { "content-type": "application/json" } }), |
| 89 | + } |
| 90 | + const m = createMisina({ |
| 91 | + driver, |
| 92 | + retry: 0, |
| 93 | + hooks: { |
| 94 | + beforeRequest: (ctx) => { |
| 95 | + captured = ctx.options.state |
| 96 | + }, |
| 97 | + }, |
| 98 | + }) |
| 99 | + |
| 100 | + await m.get("https://api.test/") |
| 101 | + expect(captured).toEqual({}) |
| 102 | + }) |
| 103 | + |
| 104 | + it(".extend() does NOT inherit parent state — child gets its own", async () => { |
| 105 | + const driver = { |
| 106 | + name: "p", |
| 107 | + request: async () => new Response("{}", { headers: { "content-type": "application/json" } }), |
| 108 | + } |
| 109 | + const parent = createMisina({ |
| 110 | + driver, |
| 111 | + retry: 0, |
| 112 | + state: { counter: 100 }, |
| 113 | + }) |
| 114 | + const child = parent.extend({ state: { counter: 0 } }) |
| 115 | + |
| 116 | + let parentObserved: number | undefined |
| 117 | + let childObserved: number | undefined |
| 118 | + await parent.get("https://api.test/", { |
| 119 | + hooks: { |
| 120 | + beforeRequest: (ctx) => { |
| 121 | + ctx.options.state.counter = (ctx.options.state.counter ?? 0) + 1 |
| 122 | + parentObserved = ctx.options.state.counter |
| 123 | + }, |
| 124 | + }, |
| 125 | + }) |
| 126 | + await child.get("https://api.test/", { |
| 127 | + hooks: { |
| 128 | + beforeRequest: (ctx) => { |
| 129 | + ctx.options.state.counter = (ctx.options.state.counter ?? 0) + 1 |
| 130 | + childObserved = ctx.options.state.counter |
| 131 | + }, |
| 132 | + }, |
| 133 | + }) |
| 134 | + |
| 135 | + expect(parentObserved).toBe(101) |
| 136 | + expect(childObserved).toBe(1) // child's state is fresh |
| 137 | + }) |
| 138 | + |
| 139 | + it("state is the same reference across multiple calls on one instance", async () => { |
| 140 | + const driver = { |
| 141 | + name: "p", |
| 142 | + request: async () => new Response("{}", { headers: { "content-type": "application/json" } }), |
| 143 | + } |
| 144 | + const captured: object[] = [] |
| 145 | + const m = createMisina({ |
| 146 | + driver, |
| 147 | + retry: 0, |
| 148 | + state: { counter: 0 }, |
| 149 | + hooks: { |
| 150 | + beforeRequest: (ctx) => { |
| 151 | + captured.push(ctx.options.state) |
| 152 | + }, |
| 153 | + }, |
| 154 | + }) |
| 155 | + |
| 156 | + await m.get("https://api.test/") |
| 157 | + await m.get("https://api.test/") |
| 158 | + expect(captured[0]).toBe(captured[1]) // same reference |
| 159 | + }) |
| 160 | +}) |
0 commit comments