@pureq/pureq 1.1.3 → 1.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -1
- package/dist/src/middleware/dedupe.d.ts.map +1 -1
- package/dist/src/middleware/dedupe.js +11 -2
- package/dist/src/middleware/dedupe.js.map +1 -1
- package/dist/src/utils/crypto.d.ts.map +1 -1
- package/dist/src/utils/crypto.js +2 -1
- package/dist/src/utils/crypto.js.map +1 -1
- package/dist/tests/auth-refresh.test.d.ts +2 -0
- package/dist/tests/auth-refresh.test.d.ts.map +1 -0
- package/dist/tests/auth-refresh.test.js +75 -0
- package/dist/tests/auth-refresh.test.js.map +1 -0
- package/dist/tests/chaos-heavy.test.d.ts +2 -0
- package/dist/tests/chaos-heavy.test.d.ts.map +1 -0
- package/dist/tests/chaos-heavy.test.js +164 -0
- package/dist/tests/chaos-heavy.test.js.map +1 -0
- package/dist/tests/crypto-utils.test.d.ts +2 -0
- package/dist/tests/crypto-utils.test.d.ts.map +1 -0
- package/dist/tests/crypto-utils.test.js +34 -0
- package/dist/tests/crypto-utils.test.js.map +1 -0
- package/dist/tests/fallback-validation-stale-policy.test.d.ts +2 -0
- package/dist/tests/fallback-validation-stale-policy.test.d.ts.map +1 -0
- package/dist/tests/fallback-validation-stale-policy.test.js +138 -0
- package/dist/tests/fallback-validation-stale-policy.test.js.map +1 -0
- package/dist/tests/invariant-fuzz.test.d.ts +2 -0
- package/dist/tests/invariant-fuzz.test.d.ts.map +1 -0
- package/dist/tests/invariant-fuzz.test.js +149 -0
- package/dist/tests/invariant-fuzz.test.js.map +1 -0
- package/dist/tests/resilience-shock.test.d.ts +2 -0
- package/dist/tests/resilience-shock.test.d.ts.map +1 -0
- package/dist/tests/resilience-shock.test.js +114 -0
- package/dist/tests/resilience-shock.test.js.map +1 -0
- package/dist/tests/storage-adapters.test.d.ts +2 -0
- package/dist/tests/storage-adapters.test.d.ts.map +1 -0
- package/dist/tests/storage-adapters.test.js +101 -0
- package/dist/tests/storage-adapters.test.js.map +1 -0
- package/dist/tests/traffic-load.test.d.ts +2 -0
- package/dist/tests/traffic-load.test.d.ts.map +1 -0
- package/dist/tests/traffic-load.test.js +91 -0
- package/dist/tests/traffic-load.test.js.map +1 -0
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -1121,7 +1121,12 @@ pureq takes a defense-in-depth approach to transport layer security:
|
|
|
1121
1121
|
```bash
|
|
1122
1122
|
npm run typecheck # type checking
|
|
1123
1123
|
npm test # all tests
|
|
1124
|
-
npm run test:
|
|
1124
|
+
npm run test:unit # lightweight unit tests (heavy load tests excluded)
|
|
1125
|
+
npm run test:stress # baseline stress test
|
|
1126
|
+
npm run test:heavy # heavy load + shock + chaos + invariant-fuzz tests (nightly-recommended)
|
|
1127
|
+
npm run test:nightly # alias of test:heavy
|
|
1128
|
+
npm run test:ci # lightweight CI: unit + integration + contract + stress + typecheck
|
|
1129
|
+
npm run test:ci:full # CI + heavy tests
|
|
1125
1130
|
npm run test:browser # browser runtime smoke test
|
|
1126
1131
|
npm run test:edge # edge runtime smoke test
|
|
1127
1132
|
npm run build # production build
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dedupe.d.ts","sourceRoot":"","sources":["../../../src/middleware/dedupe.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAe,aAAa,EAAE,MAAM,eAAe,CAAC;AAI5E,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,aAAa,CAAC,QAAQ,CAAC,EAAE,CAAC;IACtD,QAAQ,CAAC,cAAc,CAAC,EAAE,OAAO,CAAC;IAClC,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC;IAC/B,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,QAAQ,CAAC,aAAa,CAAC,KAAK,MAAM,CAAC;CAChE;AAkBD;;;GAGG;AACH,wBAAgB,MAAM,CAAC,OAAO,GAAE,aAAkB,GAAG,UAAU,
|
|
1
|
+
{"version":3,"file":"dedupe.d.ts","sourceRoot":"","sources":["../../../src/middleware/dedupe.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAe,aAAa,EAAE,MAAM,eAAe,CAAC;AAI5E,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,aAAa,CAAC,QAAQ,CAAC,EAAE,CAAC;IACtD,QAAQ,CAAC,cAAc,CAAC,EAAE,OAAO,CAAC;IAClC,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC;IAC/B,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,QAAQ,CAAC,aAAa,CAAC,KAAK,MAAM,CAAC;CAChE;AAkBD;;;GAGG;AACH,wBAAgB,MAAM,CAAC,OAAO,GAAE,aAAkB,GAAG,UAAU,CAuC9D"}
|
|
@@ -23,8 +23,17 @@ export function dedupe(options = {}) {
|
|
|
23
23
|
if (!methods.has(req.method)) {
|
|
24
24
|
return next(req);
|
|
25
25
|
}
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
let key;
|
|
27
|
+
try {
|
|
28
|
+
key =
|
|
29
|
+
options.keyBuilder?.(req) ??
|
|
30
|
+
defaultKeyBuilder(req, options.includeHeaders ?? false, options.includeBody ?? false);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// If signature generation fails (e.g., circular body),
|
|
34
|
+
// safely bypass dedupe and execute request normally.
|
|
35
|
+
return next(req);
|
|
36
|
+
}
|
|
28
37
|
const existing = inflight.get(key);
|
|
29
38
|
if (existing) {
|
|
30
39
|
const sharedResponse = await existing;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dedupe.js","sourceRoot":"","sources":["../../../src/middleware/dedupe.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AASlE,uEAAuE;AAEvE,SAAS,iBAAiB,CAAC,GAA4B,EAAE,cAAuB,EAAE,WAAoB;IACpG,MAAM,WAAW,GAAG,cAAc,CAAC,CAAC,CAAC,eAAe,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IACvE,MAAM,QAAQ,GAAG,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAErE,OAAO;QACL,GAAG,CAAC,MAAM;QACV,GAAG,CAAC,GAAG;QACP,eAAe,CAAC,GAAG,CAAC,MAAM,CAAC;QAC3B,WAAW,CAAC,GAAG,CAAC,KAAK,CAAC;QACtB,WAAW;QACX,QAAQ;KACT,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACd,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,MAAM,CAAC,UAAyB,EAAE;IAChD,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,OAAO,IAAI,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC;IAC5D,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAiC,CAAC;IAE1D,OAAO,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;QACzB,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;YAC7B,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC;QACnB,CAAC;QAED,
|
|
1
|
+
{"version":3,"file":"dedupe.js","sourceRoot":"","sources":["../../../src/middleware/dedupe.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AASlE,uEAAuE;AAEvE,SAAS,iBAAiB,CAAC,GAA4B,EAAE,cAAuB,EAAE,WAAoB;IACpG,MAAM,WAAW,GAAG,cAAc,CAAC,CAAC,CAAC,eAAe,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IACvE,MAAM,QAAQ,GAAG,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAErE,OAAO;QACL,GAAG,CAAC,MAAM;QACV,GAAG,CAAC,GAAG;QACP,eAAe,CAAC,GAAG,CAAC,MAAM,CAAC;QAC3B,WAAW,CAAC,GAAG,CAAC,KAAK,CAAC;QACtB,WAAW;QACX,QAAQ;KACT,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACd,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,MAAM,CAAC,UAAyB,EAAE;IAChD,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,OAAO,IAAI,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC;IAC5D,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAiC,CAAC;IAE1D,OAAO,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;QACzB,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;YAC7B,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC;QACnB,CAAC;QAED,IAAI,GAAW,CAAC;QAChB,IAAI,CAAC;YACH,GAAG;gBACD,OAAO,CAAC,UAAU,EAAE,CAAC,GAAG,CAAC;oBACzB,iBAAiB,CAAC,GAAG,EAAE,OAAO,CAAC,cAAc,IAAI,KAAK,EAAE,OAAO,CAAC,WAAW,IAAI,KAAK,CAAC,CAAC;QAC1F,CAAC;QAAC,MAAM,CAAC;YACP,uDAAuD;YACvD,qDAAqD;YACrD,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC;QACnB,CAAC;QAED,MAAM,QAAQ,GAAG,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACnC,IAAI,QAAQ,EAAE,CAAC;YACb,MAAM,cAAc,GAAG,MAAM,QAAQ,CAAC;YACtC,OAAO,cAAc,CAAC,KAAK,EAAE,CAAC;QAChC,CAAC;QAED,MAAM,OAAO,GAAG,CAAC,KAAK,IAAI,EAAE;YAC1B,IAAI,CAAC;gBACH,OAAO,MAAM,IAAI,CAAC,GAAG,CAAC,CAAC;YACzB,CAAC;oBAAS,CAAC;gBACT,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACvB,CAAC;QACH,CAAC,CAAC,EAAE,CAAC;QAEL,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;QAE3B,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC;QAC/B,OAAO,QAAQ,CAAC,KAAK,EAAE,CAAC;IAC1B,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../../../src/utils/crypto.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,
|
|
1
|
+
{"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../../../src/utils/crypto.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAgBxD;AAcD;;;GAGG;AACH,wBAAsB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,CAe3E;AAED;;GAEG;AACH,wBAAsB,OAAO,CAAC,aAAa,EAAE,MAAM,EAAE,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,CAyBpF"}
|
package/dist/src/utils/crypto.js
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
export function generateSecureId(prefix) {
|
|
8
8
|
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
9
|
-
|
|
9
|
+
const value = crypto.randomUUID();
|
|
10
|
+
return prefix ? `${prefix}-${value}` : value;
|
|
10
11
|
}
|
|
11
12
|
if (typeof crypto !== "undefined" && typeof crypto.getRandomValues === "function") {
|
|
12
13
|
const bytes = new Uint8Array(16);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"crypto.js","sourceRoot":"","sources":["../../../src/utils/crypto.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAAC,MAAe;IAC9C,IAAI,OAAO,MAAM,KAAK,WAAW,IAAI,OAAO,MAAM,CAAC,UAAU,KAAK,UAAU,EAAE,CAAC;QAC7E,
|
|
1
|
+
{"version":3,"file":"crypto.js","sourceRoot":"","sources":["../../../src/utils/crypto.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-refresh.test.d.ts","sourceRoot":"","sources":["../../tests/auth-refresh.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { authRefresh } from "../src/middleware/authRefresh";
|
|
3
|
+
import { HttpResponse } from "../src/response/response";
|
|
4
|
+
describe("authRefresh middleware", () => {
|
|
5
|
+
it("refreshes token and retries once on 401", async () => {
|
|
6
|
+
const refresh = vi.fn(async () => "token-2");
|
|
7
|
+
const middleware = authRefresh({ refresh });
|
|
8
|
+
const seenAuth = [];
|
|
9
|
+
const req = {
|
|
10
|
+
method: "GET",
|
|
11
|
+
url: "/profile",
|
|
12
|
+
headers: { Authorization: "Bearer token-1" },
|
|
13
|
+
};
|
|
14
|
+
const res = await middleware(req, async (nextReq) => {
|
|
15
|
+
seenAuth.push(nextReq.headers?.Authorization ?? "");
|
|
16
|
+
if (seenAuth.length === 1) {
|
|
17
|
+
return new HttpResponse(new Response("unauthorized", { status: 401 }));
|
|
18
|
+
}
|
|
19
|
+
return new HttpResponse(new Response("ok", { status: 200 }));
|
|
20
|
+
});
|
|
21
|
+
expect(res.status).toBe(200);
|
|
22
|
+
expect(refresh).toHaveBeenCalledTimes(1);
|
|
23
|
+
expect(seenAuth).toEqual(["Bearer token-1", "Bearer token-2"]);
|
|
24
|
+
});
|
|
25
|
+
it("shares a single refresh call across concurrent 401s", async () => {
|
|
26
|
+
let refreshCount = 0;
|
|
27
|
+
const middleware = authRefresh({
|
|
28
|
+
refresh: async () => {
|
|
29
|
+
refreshCount += 1;
|
|
30
|
+
return "shared-token";
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
const reqA = { method: "GET", url: "/a" };
|
|
34
|
+
const reqB = { method: "GET", url: "/b" };
|
|
35
|
+
const next = async (req) => {
|
|
36
|
+
if (req.headers?.Authorization === "Bearer shared-token") {
|
|
37
|
+
return new HttpResponse(new Response("ok", { status: 200 }));
|
|
38
|
+
}
|
|
39
|
+
return new HttpResponse(new Response("unauthorized", { status: 401 }));
|
|
40
|
+
};
|
|
41
|
+
const [resA, resB] = await Promise.all([
|
|
42
|
+
middleware(reqA, next),
|
|
43
|
+
middleware(reqB, next),
|
|
44
|
+
]);
|
|
45
|
+
expect(resA.status).toBe(200);
|
|
46
|
+
expect(resB.status).toBe(200);
|
|
47
|
+
expect(refreshCount).toBe(1);
|
|
48
|
+
});
|
|
49
|
+
it("throws wrapped error when refresh fails", async () => {
|
|
50
|
+
const rootError = new Error("refresh down");
|
|
51
|
+
const middleware = authRefresh({
|
|
52
|
+
refresh: async () => {
|
|
53
|
+
throw rootError;
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
await expect(middleware({ method: "GET", url: "/x" }, async () => {
|
|
57
|
+
return new HttpResponse(new Response("unauthorized", { status: 401 }));
|
|
58
|
+
})).rejects.toMatchObject({
|
|
59
|
+
message: "pureq: token refresh failed",
|
|
60
|
+
code: "PUREQ_AUTH_REFRESH_FAILED",
|
|
61
|
+
kind: "auth-error",
|
|
62
|
+
cause: rootError,
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
it("does not refresh when maxAttempts is zero", async () => {
|
|
66
|
+
const refresh = vi.fn(async () => "unused");
|
|
67
|
+
const middleware = authRefresh({ refresh, maxAttempts: 0 });
|
|
68
|
+
const res = await middleware({ method: "GET", url: "/x" }, async () => {
|
|
69
|
+
return new HttpResponse(new Response("unauthorized", { status: 401 }));
|
|
70
|
+
});
|
|
71
|
+
expect(res.status).toBe(401);
|
|
72
|
+
expect(refresh).not.toHaveBeenCalled();
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
//# sourceMappingURL=auth-refresh.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-refresh.test.js","sourceRoot":"","sources":["../../tests/auth-refresh.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chaos-heavy.test.d.ts","sourceRoot":"","sources":["../../tests/chaos-heavy.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { createCircuitBreaker, PureqCircuitOpenError } from "../src/middleware/circuitBreaker";
|
|
3
|
+
import { concurrencyLimit } from "../src/middleware/concurrencyLimit";
|
|
4
|
+
import { dedupe } from "../src/middleware/dedupe";
|
|
5
|
+
import { retry } from "../src/middleware/retry";
|
|
6
|
+
import { compose } from "../src/middleware/compose";
|
|
7
|
+
import { execute } from "../src/executor/execute";
|
|
8
|
+
import { HttpResponse } from "../src/response/response";
|
|
9
|
+
import { INTERNAL_MIDDLEWARES } from "../src/types/internal";
|
|
10
|
+
function sleep(ms) {
|
|
11
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
12
|
+
}
|
|
13
|
+
function makePrng(seed = 0x12345678) {
|
|
14
|
+
let value = seed >>> 0;
|
|
15
|
+
return () => {
|
|
16
|
+
value = (value * 1664525 + 1013904223) >>> 0;
|
|
17
|
+
return value / 0x100000000;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
vi.useRealTimers();
|
|
22
|
+
vi.restoreAllMocks();
|
|
23
|
+
});
|
|
24
|
+
describe("chaos/heavy: half-open stampede", () => {
|
|
25
|
+
it("allows only one half-open probe under concurrent burst", async () => {
|
|
26
|
+
vi.useFakeTimers();
|
|
27
|
+
vi.setSystemTime(new Date("2026-01-01T00:00:00.000Z"));
|
|
28
|
+
const breaker = createCircuitBreaker({
|
|
29
|
+
failureThreshold: 1,
|
|
30
|
+
successThreshold: 1,
|
|
31
|
+
cooldownMs: 1000,
|
|
32
|
+
keyBuilder: () => "dep:critical",
|
|
33
|
+
});
|
|
34
|
+
const req = { method: "GET", url: "/critical" };
|
|
35
|
+
await expect(breaker.middleware(req, async () => {
|
|
36
|
+
throw new Error("boom");
|
|
37
|
+
})).rejects.toThrow("boom");
|
|
38
|
+
vi.setSystemTime(new Date("2026-01-01T00:00:01.100Z"));
|
|
39
|
+
let nextCalls = 0;
|
|
40
|
+
let releaseProbe;
|
|
41
|
+
const probePromise = breaker.middleware(req, async () => {
|
|
42
|
+
nextCalls += 1;
|
|
43
|
+
await new Promise((resolve) => {
|
|
44
|
+
releaseProbe = resolve;
|
|
45
|
+
});
|
|
46
|
+
return new HttpResponse(new Response("ok", { status: 200 }));
|
|
47
|
+
});
|
|
48
|
+
const followers = await Promise.allSettled(Array.from({ length: 40 }, () => breaker.middleware(req, async () => {
|
|
49
|
+
nextCalls += 1;
|
|
50
|
+
return new HttpResponse(new Response("unexpected", { status: 200 }));
|
|
51
|
+
})));
|
|
52
|
+
const openErrors = followers.filter((entry) => entry.status === "rejected" &&
|
|
53
|
+
entry.reason instanceof PureqCircuitOpenError).length;
|
|
54
|
+
releaseProbe?.();
|
|
55
|
+
const probe = await probePromise;
|
|
56
|
+
expect(probe.status).toBe(200);
|
|
57
|
+
expect(nextCalls).toBe(1);
|
|
58
|
+
expect(openErrors).toBe(40);
|
|
59
|
+
vi.useRealTimers();
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
describe("chaos/heavy: mixed failure traffic", () => {
|
|
63
|
+
it("settles large mixed traffic without deadlock and respects concurrency ceiling", async () => {
|
|
64
|
+
const prng = makePrng(42);
|
|
65
|
+
let inFlight = 0;
|
|
66
|
+
let maxInFlight = 0;
|
|
67
|
+
const base = async (_req) => {
|
|
68
|
+
inFlight += 1;
|
|
69
|
+
maxInFlight = Math.max(maxInFlight, inFlight);
|
|
70
|
+
try {
|
|
71
|
+
await sleep(Math.floor(prng() * 4));
|
|
72
|
+
const r = prng();
|
|
73
|
+
if (r < 0.12) {
|
|
74
|
+
throw new TypeError("network chaos");
|
|
75
|
+
}
|
|
76
|
+
if (r < 0.32) {
|
|
77
|
+
return new HttpResponse(new Response("temporary", { status: 503 }));
|
|
78
|
+
}
|
|
79
|
+
return new HttpResponse(new Response("ok", { status: 200 }));
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
inFlight -= 1;
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
const pipeline = compose([
|
|
86
|
+
concurrencyLimit({ maxConcurrent: 20 }),
|
|
87
|
+
dedupe(),
|
|
88
|
+
retry({
|
|
89
|
+
maxRetries: 2,
|
|
90
|
+
delay: 0,
|
|
91
|
+
backoff: false,
|
|
92
|
+
retryOnStatus: [503],
|
|
93
|
+
retryOnNetworkError: true,
|
|
94
|
+
}),
|
|
95
|
+
], base);
|
|
96
|
+
const total = 320;
|
|
97
|
+
const tasks = Array.from({ length: total }, (_, i) => pipeline({
|
|
98
|
+
method: "GET",
|
|
99
|
+
url: `https://example.com/hot/${i % 80}`,
|
|
100
|
+
[INTERNAL_MIDDLEWARES]: [],
|
|
101
|
+
}).then(() => ({ ok: true }), () => ({ ok: false })));
|
|
102
|
+
const startedAt = Date.now();
|
|
103
|
+
const settled = await Promise.all(tasks);
|
|
104
|
+
const durationMs = Date.now() - startedAt;
|
|
105
|
+
const success = settled.filter((x) => x.ok).length;
|
|
106
|
+
const failure = settled.length - success;
|
|
107
|
+
console.info("[pureq][chaos-metrics]", {
|
|
108
|
+
scenario: "mixed-failure-traffic",
|
|
109
|
+
total,
|
|
110
|
+
success,
|
|
111
|
+
failure,
|
|
112
|
+
maxInFlight,
|
|
113
|
+
durationMs,
|
|
114
|
+
throughputRps: Number((total / Math.max(durationMs / 1000, 0.001)).toFixed(2)),
|
|
115
|
+
});
|
|
116
|
+
expect(settled).toHaveLength(total);
|
|
117
|
+
expect(maxInFlight).toBeLessThanOrEqual(20);
|
|
118
|
+
expect(success).toBeGreaterThan(0);
|
|
119
|
+
expect(failure).toBeGreaterThan(0);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
describe("chaos/heavy: hostile input", () => {
|
|
123
|
+
it("handles hostile query keys without prototype pollution", async () => {
|
|
124
|
+
const query = Object.create(null);
|
|
125
|
+
query["__proto__"] = "x";
|
|
126
|
+
query["constructor"] = "y";
|
|
127
|
+
query["prototype"] = "z";
|
|
128
|
+
query["normal"] = "ok";
|
|
129
|
+
let capturedUrl = "";
|
|
130
|
+
await execute({
|
|
131
|
+
method: "GET",
|
|
132
|
+
url: "/hostile",
|
|
133
|
+
query,
|
|
134
|
+
}, {
|
|
135
|
+
adapter: async (url) => {
|
|
136
|
+
capturedUrl = url;
|
|
137
|
+
return new Response("ok", { status: 200 });
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
expect(capturedUrl.includes("__proto__=x")).toBe(true);
|
|
141
|
+
expect(capturedUrl.includes("constructor=y")).toBe(true);
|
|
142
|
+
expect(capturedUrl.includes("prototype=z")).toBe(true);
|
|
143
|
+
expect({}.polluted).toBeUndefined();
|
|
144
|
+
});
|
|
145
|
+
it("bypasses dedupe safely for circular request body", async () => {
|
|
146
|
+
const middleware = dedupe({ includeBody: true, methods: ["POST"] });
|
|
147
|
+
const circular = { a: 1 };
|
|
148
|
+
circular.self = circular;
|
|
149
|
+
let upstreamCalls = 0;
|
|
150
|
+
const send = () => middleware({
|
|
151
|
+
method: "POST",
|
|
152
|
+
url: "/circular",
|
|
153
|
+
body: circular,
|
|
154
|
+
}, async () => {
|
|
155
|
+
upstreamCalls += 1;
|
|
156
|
+
return new HttpResponse(new Response("ok", { status: 200 }));
|
|
157
|
+
});
|
|
158
|
+
const [r1, r2] = await Promise.all([send(), send()]);
|
|
159
|
+
expect(r1.status).toBe(200);
|
|
160
|
+
expect(r2.status).toBe(200);
|
|
161
|
+
expect(upstreamCalls).toBe(2);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
//# sourceMappingURL=chaos-heavy.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chaos-heavy.test.js","sourceRoot":"","sources":["../../tests/chaos-heavy.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crypto-utils.test.d.ts","sourceRoot":"","sources":["../../tests/crypto-utils.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { decrypt, encrypt, generateSecureId } from "../src/utils/crypto";
|
|
3
|
+
describe("crypto utils", () => {
|
|
4
|
+
afterEach(() => {
|
|
5
|
+
vi.restoreAllMocks();
|
|
6
|
+
});
|
|
7
|
+
it("applies prefix even when randomUUID is available", () => {
|
|
8
|
+
const spy = vi
|
|
9
|
+
.spyOn(globalThis.crypto, "randomUUID")
|
|
10
|
+
.mockReturnValue("123e4567-e89b-12d3-a456-426614174000");
|
|
11
|
+
const id = generateSecureId("pureq");
|
|
12
|
+
expect(id).toBe("pureq-123e4567-e89b-12d3-a456-426614174000");
|
|
13
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
14
|
+
});
|
|
15
|
+
it("round-trips encrypt/decrypt with AES-GCM key", async () => {
|
|
16
|
+
const key = await crypto.subtle.generateKey({ name: "AES-GCM", length: 256 }, true, [
|
|
17
|
+
"encrypt",
|
|
18
|
+
"decrypt",
|
|
19
|
+
]);
|
|
20
|
+
const input = JSON.stringify({ userId: "u1", roles: ["admin"] });
|
|
21
|
+
const encrypted = await encrypt(input, key);
|
|
22
|
+
const decrypted = await decrypt(encrypted, key);
|
|
23
|
+
expect(encrypted.includes(":")).toBe(true);
|
|
24
|
+
expect(decrypted).toBe(input);
|
|
25
|
+
});
|
|
26
|
+
it("throws helpful error for invalid encrypted format", async () => {
|
|
27
|
+
const key = await crypto.subtle.generateKey({ name: "AES-GCM", length: 256 }, true, [
|
|
28
|
+
"encrypt",
|
|
29
|
+
"decrypt",
|
|
30
|
+
]);
|
|
31
|
+
await expect(decrypt("not-valid", key)).rejects.toThrow("pureq: invalid encrypted data format");
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
//# sourceMappingURL=crypto-utils.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crypto-utils.test.js","sourceRoot":"","sources":["../../tests/crypto-utils.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAC7D,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAEzE,QAAQ,CAAC,cAAc,EAAE,GAAG,EAAE;IAC5B,SAAS,CAAC,GAAG,EAAE;QACb,EAAE,CAAC,eAAe,EAAE,CAAC;IACvB,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,kDAAkD,EAAE,GAAG,EAAE;QAC1D,MAAM,GAAG,GAAG,EAAE;aACX,KAAK,CAAC,UAAU,CAAC,MAAM,EAAE,YAAY,CAAC;aACtC,eAAe,CAAC,sCAAsC,CAAC,CAAC;QAE3D,MAAM,EAAE,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAC;QAErC,MAAM,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,4CAA4C,CAAC,CAAC;QAC9D,MAAM,CAAC,GAAG,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,CAAC;IACvC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,8CAA8C,EAAE,KAAK,IAAI,EAAE;QAC5D,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,EAAE,IAAI,EAAE;YAClF,SAAS;YACT,SAAS;SACV,CAAC,CAAC;QACH,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QAEjE,MAAM,SAAS,GAAG,MAAM,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;QAC5C,MAAM,SAAS,GAAG,MAAM,OAAO,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC;QAEhD,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC3C,MAAM,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAChC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,mDAAmD,EAAE,KAAK,IAAI,EAAE;QACjE,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,EAAE,IAAI,EAAE;YAClF,SAAS;YACT,SAAS;SACV,CAAC,CAAC;QAEH,MAAM,MAAM,CAAC,OAAO,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,sCAAsC,CAAC,CAAC;IAClG,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fallback-validation-stale-policy.test.d.ts","sourceRoot":"","sources":["../../tests/fallback-validation-stale-policy.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { fallback } from "../src/middleware/fallback";
|
|
3
|
+
import { validation } from "../src/middleware/validation";
|
|
4
|
+
import { resolveStalePolicy } from "../src/middleware/stalePolicy";
|
|
5
|
+
import { HttpResponse } from "../src/response/response";
|
|
6
|
+
import { getPolicyTrace } from "../src/utils/policyTrace";
|
|
7
|
+
describe("fallback middleware", () => {
|
|
8
|
+
it("returns fallback value on thrown error", async () => {
|
|
9
|
+
const mw = fallback({
|
|
10
|
+
value: new HttpResponse(new Response("fallback", { status: 200 })),
|
|
11
|
+
});
|
|
12
|
+
const req = { method: "GET", url: "/unstable" };
|
|
13
|
+
const res = await mw(req, async () => {
|
|
14
|
+
throw new Error("boom");
|
|
15
|
+
});
|
|
16
|
+
expect(res.status).toBe(200);
|
|
17
|
+
expect(await res.text()).toBe("fallback");
|
|
18
|
+
expect(getPolicyTrace(req)).toHaveLength(1);
|
|
19
|
+
});
|
|
20
|
+
it("can fallback on non-ok response when when() returns true", async () => {
|
|
21
|
+
const mw = fallback({
|
|
22
|
+
when: (trigger) => trigger.type === "response" && trigger.response.status === 503,
|
|
23
|
+
value: ({ type }) => new HttpResponse(new Response(`from-${type}`, { status: 200 })),
|
|
24
|
+
});
|
|
25
|
+
const res = await mw({ method: "GET", url: "/service" }, async () => {
|
|
26
|
+
return new HttpResponse(new Response("down", { status: 503 }));
|
|
27
|
+
});
|
|
28
|
+
expect(res.status).toBe(200);
|
|
29
|
+
expect(await res.text()).toBe("from-response");
|
|
30
|
+
});
|
|
31
|
+
it("does not fallback on non-ok response by default", async () => {
|
|
32
|
+
const mw = fallback({
|
|
33
|
+
value: new HttpResponse(new Response("fallback", { status: 200 })),
|
|
34
|
+
});
|
|
35
|
+
const res = await mw({ method: "GET", url: "/service" }, async () => {
|
|
36
|
+
return new HttpResponse(new Response("bad", { status: 500 }));
|
|
37
|
+
});
|
|
38
|
+
expect(res.status).toBe(500);
|
|
39
|
+
});
|
|
40
|
+
it("rethrows error when when() returns false", async () => {
|
|
41
|
+
const mw = fallback({
|
|
42
|
+
when: () => false,
|
|
43
|
+
value: new HttpResponse(new Response("fallback", { status: 200 })),
|
|
44
|
+
});
|
|
45
|
+
await expect(mw({ method: "GET", url: "/x" }, async () => {
|
|
46
|
+
throw new Error("boom");
|
|
47
|
+
})).rejects.toThrow("boom");
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
describe("validation middleware", () => {
|
|
51
|
+
it("passes when validate succeeds", async () => {
|
|
52
|
+
const validate = vi.fn(async (data) => {
|
|
53
|
+
expect(data).toEqual({ ok: true });
|
|
54
|
+
return true;
|
|
55
|
+
});
|
|
56
|
+
const mw = validation({ validate });
|
|
57
|
+
const res = await mw({ method: "GET", url: "/x" }, async () => {
|
|
58
|
+
return new HttpResponse(new Response(JSON.stringify({ ok: true }), {
|
|
59
|
+
status: 200,
|
|
60
|
+
headers: { "content-type": "application/json" },
|
|
61
|
+
}));
|
|
62
|
+
});
|
|
63
|
+
expect(res.status).toBe(200);
|
|
64
|
+
expect(validate).toHaveBeenCalledTimes(1);
|
|
65
|
+
});
|
|
66
|
+
it("skips validation for non-ok responses", async () => {
|
|
67
|
+
const validate = vi.fn(() => true);
|
|
68
|
+
const mw = validation({ validate });
|
|
69
|
+
const res = await mw({ method: "GET", url: "/x" }, async () => {
|
|
70
|
+
return new HttpResponse(new Response("bad", { status: 500 }));
|
|
71
|
+
});
|
|
72
|
+
expect(res.status).toBe(500);
|
|
73
|
+
expect(validate).not.toHaveBeenCalled();
|
|
74
|
+
});
|
|
75
|
+
it("throws wrapped validation error with custom message factory", async () => {
|
|
76
|
+
const mw = validation({
|
|
77
|
+
validate: () => false,
|
|
78
|
+
message: (data) => `invalid:${JSON.stringify(data)}`,
|
|
79
|
+
});
|
|
80
|
+
await expect(mw({ method: "GET", url: "/x" }, async () => {
|
|
81
|
+
return new HttpResponse(new Response(JSON.stringify({ id: "u1" }), {
|
|
82
|
+
status: 200,
|
|
83
|
+
headers: { "content-type": "application/json" },
|
|
84
|
+
}));
|
|
85
|
+
})).rejects.toMatchObject({
|
|
86
|
+
message: "invalid:{\"id\":\"u1\"}",
|
|
87
|
+
code: "PUREQ_VALIDATION_ERROR",
|
|
88
|
+
kind: "validation-error",
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
it("returns response in silent mode when validation fails", async () => {
|
|
92
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => { });
|
|
93
|
+
const mw = validation({
|
|
94
|
+
validate: () => null,
|
|
95
|
+
silent: true,
|
|
96
|
+
});
|
|
97
|
+
const res = await mw({ method: "GET", url: "/x" }, async () => {
|
|
98
|
+
return new HttpResponse(new Response(JSON.stringify({ id: "u1" }), {
|
|
99
|
+
status: 200,
|
|
100
|
+
headers: { "content-type": "application/json" },
|
|
101
|
+
}));
|
|
102
|
+
});
|
|
103
|
+
expect(res.status).toBe(200);
|
|
104
|
+
expect(warnSpy).toHaveBeenCalled();
|
|
105
|
+
warnSpy.mockRestore();
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
describe("resolveStalePolicy", () => {
|
|
109
|
+
it("marks fresh and stale-if-error within bounds", () => {
|
|
110
|
+
const result = resolveStalePolicy({
|
|
111
|
+
storedAt: 100,
|
|
112
|
+
now: 150,
|
|
113
|
+
ttlMs: 60,
|
|
114
|
+
staleIfErrorMs: 20,
|
|
115
|
+
});
|
|
116
|
+
expect(result.ageMs).toBe(50);
|
|
117
|
+
expect(result.isFresh).toBe(true);
|
|
118
|
+
expect(result.canServeStaleOnError).toBe(true);
|
|
119
|
+
});
|
|
120
|
+
it("clamps negative age and expires stale-if-error after window", () => {
|
|
121
|
+
const futureStored = resolveStalePolicy({
|
|
122
|
+
storedAt: 200,
|
|
123
|
+
now: 100,
|
|
124
|
+
ttlMs: 10,
|
|
125
|
+
staleIfErrorMs: 5,
|
|
126
|
+
});
|
|
127
|
+
expect(futureStored.ageMs).toBe(0);
|
|
128
|
+
const expired = resolveStalePolicy({
|
|
129
|
+
storedAt: 100,
|
|
130
|
+
now: 130,
|
|
131
|
+
ttlMs: 10,
|
|
132
|
+
staleIfErrorMs: 5,
|
|
133
|
+
});
|
|
134
|
+
expect(expired.isFresh).toBe(false);
|
|
135
|
+
expect(expired.canServeStaleOnError).toBe(false);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
//# sourceMappingURL=fallback-validation-stale-policy.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fallback-validation-stale-policy.test.js","sourceRoot":"","sources":["../../tests/fallback-validation-stale-policy.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"invariant-fuzz.test.d.ts","sourceRoot":"","sources":["../../tests/invariant-fuzz.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { execute } from "../src/executor/execute";
|
|
3
|
+
import { compose } from "../src/middleware/compose";
|
|
4
|
+
import { dedupe } from "../src/middleware/dedupe";
|
|
5
|
+
import { retry } from "../src/middleware/retry";
|
|
6
|
+
import { concurrencyLimit } from "../src/middleware/concurrencyLimit";
|
|
7
|
+
import { stableKeyValues, stableQuery } from "../src/utils/stableKey";
|
|
8
|
+
import { HttpResponse } from "../src/response/response";
|
|
9
|
+
import { INTERNAL_MIDDLEWARES } from "../src/types/internal";
|
|
10
|
+
function makePrng(seed = 0xdecafbad) {
|
|
11
|
+
let value = seed >>> 0;
|
|
12
|
+
return () => {
|
|
13
|
+
value = (value * 1664525 + 1013904223) >>> 0;
|
|
14
|
+
return value / 0x100000000;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function pick(prng, chars, min, max) {
|
|
18
|
+
const len = min + Math.floor(prng() * (max - min + 1));
|
|
19
|
+
let out = "";
|
|
20
|
+
for (let i = 0; i < len; i++) {
|
|
21
|
+
const idx = Math.floor(prng() * chars.length);
|
|
22
|
+
out += chars[idx] ?? "x";
|
|
23
|
+
}
|
|
24
|
+
return out;
|
|
25
|
+
}
|
|
26
|
+
function sleep(ms) {
|
|
27
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
28
|
+
}
|
|
29
|
+
describe("invariant/fuzz: URL and query construction", () => {
|
|
30
|
+
it("preserves parameter/query information under randomized inputs", async () => {
|
|
31
|
+
const prng = makePrng(7);
|
|
32
|
+
const chars = "abcdefghijklmnopqrstuvwxyz0123456789-_ .~";
|
|
33
|
+
for (let i = 0; i < 160; i++) {
|
|
34
|
+
const p1 = pick(prng, chars, 1, 8);
|
|
35
|
+
const p2 = pick(prng, chars, 1, 8);
|
|
36
|
+
const k1 = `k${Math.floor(prng() * 10)}`;
|
|
37
|
+
const k2 = `m${Math.floor(prng() * 10)}`;
|
|
38
|
+
const v1 = pick(prng, chars, 1, 6);
|
|
39
|
+
const arrCount = 1 + Math.floor(prng() * 4);
|
|
40
|
+
const v2 = Array.from({ length: arrCount }, () => pick(prng, chars, 1, 6));
|
|
41
|
+
let capturedUrl = "";
|
|
42
|
+
await execute({
|
|
43
|
+
method: "GET",
|
|
44
|
+
url: "/f/:p1/:p2",
|
|
45
|
+
params: { p1, p2 },
|
|
46
|
+
query: {
|
|
47
|
+
[k1]: v1,
|
|
48
|
+
[k2]: v2,
|
|
49
|
+
},
|
|
50
|
+
}, {
|
|
51
|
+
adapter: async (url) => {
|
|
52
|
+
capturedUrl = url;
|
|
53
|
+
return new Response("ok", { status: 200 });
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
const parsed = new URL(capturedUrl, "http://localhost");
|
|
57
|
+
const pathParts = parsed.pathname.split("/").slice(-2).map(decodeURIComponent);
|
|
58
|
+
expect(pathParts).toEqual([p1, p2]);
|
|
59
|
+
expect(parsed.searchParams.get(k1)).toBe(v1);
|
|
60
|
+
expect(parsed.searchParams.getAll(k2).sort()).toEqual([...v2].sort());
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
describe("invariant/fuzz: mixed fault traffic convergence", () => {
|
|
65
|
+
it("always settles randomized fault traffic without violating concurrency ceiling", async () => {
|
|
66
|
+
const prng = makePrng(11);
|
|
67
|
+
let inFlight = 0;
|
|
68
|
+
let maxInFlight = 0;
|
|
69
|
+
const executor = async (req) => {
|
|
70
|
+
inFlight += 1;
|
|
71
|
+
maxInFlight = Math.max(maxInFlight, inFlight);
|
|
72
|
+
try {
|
|
73
|
+
await sleep(Math.floor(prng() * 3));
|
|
74
|
+
if (req.signal?.aborted) {
|
|
75
|
+
throw req.signal.reason ?? new DOMException("Aborted", "AbortError");
|
|
76
|
+
}
|
|
77
|
+
const r = prng();
|
|
78
|
+
if (r < 0.1) {
|
|
79
|
+
throw new TypeError("chaos-network");
|
|
80
|
+
}
|
|
81
|
+
if (r < 0.3) {
|
|
82
|
+
return new HttpResponse(new Response("retry", { status: 503 }));
|
|
83
|
+
}
|
|
84
|
+
return new HttpResponse(new Response("ok", { status: 200 }));
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
inFlight -= 1;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
const run = compose([
|
|
91
|
+
concurrencyLimit({ maxConcurrent: 12 }),
|
|
92
|
+
dedupe(),
|
|
93
|
+
retry({
|
|
94
|
+
maxRetries: 1,
|
|
95
|
+
delay: 0,
|
|
96
|
+
backoff: false,
|
|
97
|
+
retryOnStatus: [503],
|
|
98
|
+
retryOnNetworkError: true,
|
|
99
|
+
}),
|
|
100
|
+
], executor);
|
|
101
|
+
const total = 260;
|
|
102
|
+
const tasks = Array.from({ length: total }, (_, i) => {
|
|
103
|
+
const ac = new AbortController();
|
|
104
|
+
const shouldAbort = prng() < 0.15;
|
|
105
|
+
if (shouldAbort) {
|
|
106
|
+
ac.abort(new DOMException("Aborted", "AbortError"));
|
|
107
|
+
}
|
|
108
|
+
return run({
|
|
109
|
+
method: "GET",
|
|
110
|
+
url: `https://fuzz.local/r/${i % 50}`,
|
|
111
|
+
signal: ac.signal,
|
|
112
|
+
[INTERNAL_MIDDLEWARES]: [],
|
|
113
|
+
}).then(() => ({ ok: true }), () => ({ ok: false }));
|
|
114
|
+
});
|
|
115
|
+
const settled = await Promise.all(tasks);
|
|
116
|
+
const success = settled.filter((x) => x.ok).length;
|
|
117
|
+
const failure = settled.length - success;
|
|
118
|
+
console.info("[pureq][fuzz-metrics]", {
|
|
119
|
+
scenario: "mixed-fault-convergence",
|
|
120
|
+
total,
|
|
121
|
+
success,
|
|
122
|
+
failure,
|
|
123
|
+
maxInFlight,
|
|
124
|
+
});
|
|
125
|
+
expect(settled).toHaveLength(total);
|
|
126
|
+
expect(maxInFlight).toBeLessThanOrEqual(12);
|
|
127
|
+
expect(success).toBeGreaterThan(0);
|
|
128
|
+
expect(failure).toBeGreaterThan(0);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
describe("invariant/fuzz: stable key generation", () => {
|
|
132
|
+
it("is insensitive to insertion order for equivalent maps", () => {
|
|
133
|
+
const leftHeaders = { b: "2", a: "1", c: "3" };
|
|
134
|
+
const rightHeaders = { c: "3", a: "1", b: "2" };
|
|
135
|
+
expect(stableKeyValues(leftHeaders)).toBe(stableKeyValues(rightHeaders));
|
|
136
|
+
const leftQuery = {
|
|
137
|
+
z: [3, 1, 2],
|
|
138
|
+
a: "x",
|
|
139
|
+
m: true,
|
|
140
|
+
};
|
|
141
|
+
const rightQuery = {
|
|
142
|
+
m: true,
|
|
143
|
+
a: "x",
|
|
144
|
+
z: [3, 1, 2],
|
|
145
|
+
};
|
|
146
|
+
expect(stableQuery(leftQuery)).toBe(stableQuery(rightQuery));
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
//# sourceMappingURL=invariant-fuzz.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"invariant-fuzz.test.js","sourceRoot":"","sources":["../../tests/invariant-fuzz.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resilience-shock.test.d.ts","sourceRoot":"","sources":["../../tests/resilience-shock.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { createOfflineQueue } from "../src/middleware/offlineQueue";
|
|
3
|
+
import { concurrencyLimit } from "../src/middleware/concurrencyLimit";
|
|
4
|
+
import { execute } from "../src/executor/execute";
|
|
5
|
+
import { HttpResponse } from "../src/response/response";
|
|
6
|
+
function sleep(ms) {
|
|
7
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
8
|
+
}
|
|
9
|
+
describe("resilience/shock: abort and queue storms", () => {
|
|
10
|
+
it("remains healthy after queued abort storm", async () => {
|
|
11
|
+
const startedAt = Date.now();
|
|
12
|
+
const middleware = concurrencyLimit({ maxConcurrent: 1, maxQueue: 400 });
|
|
13
|
+
const first = middleware({ method: "GET", url: "/gate" }, async () => {
|
|
14
|
+
await sleep(25);
|
|
15
|
+
return new HttpResponse(new Response("first", { status: 200 }));
|
|
16
|
+
});
|
|
17
|
+
const controllers = Array.from({ length: 80 }, () => new AbortController());
|
|
18
|
+
const abortedTasks = controllers.map((controller, i) => middleware({ method: "GET", url: `/queued/${i}`, signal: controller.signal }, async () => {
|
|
19
|
+
return new HttpResponse(new Response("never", { status: 200 }));
|
|
20
|
+
}).then(() => ({ ok: true }), (error) => ({ ok: false, error })));
|
|
21
|
+
for (const c of controllers) {
|
|
22
|
+
c.abort(new DOMException("Aborted", "AbortError"));
|
|
23
|
+
}
|
|
24
|
+
await first;
|
|
25
|
+
const settled = await Promise.all(abortedTasks);
|
|
26
|
+
const rejectedCount = settled.filter((x) => x.ok === false).length;
|
|
27
|
+
console.info("[pureq][shock-metrics]", {
|
|
28
|
+
scenario: "abort-storm",
|
|
29
|
+
aborted: controllers.length,
|
|
30
|
+
rejectedCount,
|
|
31
|
+
durationMs: Date.now() - startedAt,
|
|
32
|
+
});
|
|
33
|
+
expect(rejectedCount).toBe(80);
|
|
34
|
+
const healthy = await middleware({ method: "GET", url: "/healthy" }, async () => {
|
|
35
|
+
return new HttpResponse(new Response("ok", { status: 200 }));
|
|
36
|
+
});
|
|
37
|
+
expect(healthy.status).toBe(200);
|
|
38
|
+
});
|
|
39
|
+
it("flushes large offline queue with partial replay failures", async () => {
|
|
40
|
+
const startedAt = Date.now();
|
|
41
|
+
const queue = createOfflineQueue({
|
|
42
|
+
isOffline: () => true,
|
|
43
|
+
maxQueueSize: 500,
|
|
44
|
+
});
|
|
45
|
+
const total = 140;
|
|
46
|
+
for (let i = 0; i < total; i++) {
|
|
47
|
+
await queue.middleware({
|
|
48
|
+
method: "POST",
|
|
49
|
+
url: `/events/${i}`,
|
|
50
|
+
priority: i % 7,
|
|
51
|
+
body: { i },
|
|
52
|
+
}, async () => new HttpResponse(new Response("live", { status: 200 })));
|
|
53
|
+
}
|
|
54
|
+
const flushed = await queue.flush(async (req) => {
|
|
55
|
+
const id = Number(req.url.split("/").pop());
|
|
56
|
+
await sleep(1);
|
|
57
|
+
if (id % 10 === 0) {
|
|
58
|
+
throw new Error("replay failed");
|
|
59
|
+
}
|
|
60
|
+
return new HttpResponse(new Response("replayed", { status: 201 }));
|
|
61
|
+
}, { concurrency: 16 });
|
|
62
|
+
const snapshot = await queue.snapshot();
|
|
63
|
+
console.info("[pureq][shock-metrics]", {
|
|
64
|
+
scenario: "offline-flush-burst",
|
|
65
|
+
queued: total,
|
|
66
|
+
flushed: flushed.length,
|
|
67
|
+
remaining: snapshot.size,
|
|
68
|
+
replayFailureRate: Number(((snapshot.size / total) * 100).toFixed(2)),
|
|
69
|
+
durationMs: Date.now() - startedAt,
|
|
70
|
+
});
|
|
71
|
+
expect(flushed.length).toBe(126);
|
|
72
|
+
expect(snapshot.size).toBe(14);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
describe("aggressive input robustness", () => {
|
|
76
|
+
it("fails fast on unresolved path placeholders instead of issuing request", async () => {
|
|
77
|
+
const startedAt = Date.now();
|
|
78
|
+
await expect(execute({
|
|
79
|
+
method: "GET",
|
|
80
|
+
url: "https://example.com/users/:id/orders/:orderId",
|
|
81
|
+
params: { id: "u1" },
|
|
82
|
+
}, {
|
|
83
|
+
adapter: async () => new Response("unexpected", { status: 200 }),
|
|
84
|
+
})).rejects.toThrow("pureq: unresolved path parameters");
|
|
85
|
+
console.info("[pureq][aggressive-metrics]", {
|
|
86
|
+
scenario: "unresolved-placeholder-fast-fail",
|
|
87
|
+
durationMs: Date.now() - startedAt,
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
it("handles very large query arrays without crashing", async () => {
|
|
91
|
+
const startedAt = Date.now();
|
|
92
|
+
const values = Array.from({ length: 1000 }, (_, i) => i);
|
|
93
|
+
let capturedUrl = "";
|
|
94
|
+
await execute({
|
|
95
|
+
method: "GET",
|
|
96
|
+
url: "/search",
|
|
97
|
+
query: { tag: values },
|
|
98
|
+
}, {
|
|
99
|
+
adapter: async (url) => {
|
|
100
|
+
capturedUrl = url;
|
|
101
|
+
return new Response("ok", { status: 200 });
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
console.info("[pureq][aggressive-metrics]", {
|
|
105
|
+
scenario: "large-query-array",
|
|
106
|
+
queryItems: values.length,
|
|
107
|
+
finalUrlLength: capturedUrl.length,
|
|
108
|
+
durationMs: Date.now() - startedAt,
|
|
109
|
+
});
|
|
110
|
+
expect(capturedUrl.includes("search?")).toBe(true);
|
|
111
|
+
expect(capturedUrl.includes("tag=999")).toBe(true);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
//# sourceMappingURL=resilience-shock.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resilience-shock.test.js","sourceRoot":"","sources":["../../tests/resilience-shock.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storage-adapters.test.d.ts","sourceRoot":"","sources":["../../tests/storage-adapters.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { mkdtemp, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { EncryptedQueueStorageAdapter } from "../src/adapters/storage/encryptedStorage";
|
|
6
|
+
import { IndexedDBQueueStorageAdapter } from "../src/adapters/storage/indexedDBAdapter";
|
|
7
|
+
import { FileSystemQueueStorageAdapter } from "../src/node/fsAdapter";
|
|
8
|
+
function createMemoryStorage(initial = []) {
|
|
9
|
+
let items = [...initial];
|
|
10
|
+
return {
|
|
11
|
+
async push(item) {
|
|
12
|
+
items = [...items.filter((x) => x.id !== item.id), item];
|
|
13
|
+
},
|
|
14
|
+
async getAll() {
|
|
15
|
+
return items;
|
|
16
|
+
},
|
|
17
|
+
async remove(id) {
|
|
18
|
+
items = items.filter((x) => x.id !== id);
|
|
19
|
+
},
|
|
20
|
+
async clear() {
|
|
21
|
+
items = [];
|
|
22
|
+
},
|
|
23
|
+
async size() {
|
|
24
|
+
return items.length;
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
describe("EncryptedQueueStorageAdapter", () => {
|
|
29
|
+
it("encrypts on push and decrypts on getAll", async () => {
|
|
30
|
+
const key = await crypto.subtle.generateKey({ name: "AES-GCM", length: 256 }, true, [
|
|
31
|
+
"encrypt",
|
|
32
|
+
"decrypt",
|
|
33
|
+
]);
|
|
34
|
+
const inner = createMemoryStorage();
|
|
35
|
+
const adapter = new EncryptedQueueStorageAdapter(inner, key);
|
|
36
|
+
await adapter.push({
|
|
37
|
+
id: 1,
|
|
38
|
+
queuedAt: 100,
|
|
39
|
+
req: { method: "POST", url: "/items", body: { id: "a" } },
|
|
40
|
+
});
|
|
41
|
+
const raw = await inner.getAll();
|
|
42
|
+
expect(typeof raw[0].req).toBe("string");
|
|
43
|
+
const all = await adapter.getAll();
|
|
44
|
+
expect(all).toHaveLength(1);
|
|
45
|
+
expect(all[0]?.req).toMatchObject({ method: "POST", url: "/items" });
|
|
46
|
+
});
|
|
47
|
+
it("throws aggregate error when one or more entries fail decryption", async () => {
|
|
48
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => { });
|
|
49
|
+
const key = await crypto.subtle.generateKey({ name: "AES-GCM", length: 256 }, true, [
|
|
50
|
+
"encrypt",
|
|
51
|
+
"decrypt",
|
|
52
|
+
]);
|
|
53
|
+
const inner = createMemoryStorage([
|
|
54
|
+
{
|
|
55
|
+
id: 1,
|
|
56
|
+
queuedAt: 1,
|
|
57
|
+
req: "invalid:ciphertext",
|
|
58
|
+
},
|
|
59
|
+
]);
|
|
60
|
+
const adapter = new EncryptedQueueStorageAdapter(inner, key);
|
|
61
|
+
await expect(adapter.getAll()).rejects.toMatchObject({
|
|
62
|
+
code: "PUREQ_DECRYPTION_FAILURE",
|
|
63
|
+
});
|
|
64
|
+
expect(warnSpy).toHaveBeenCalledTimes(1);
|
|
65
|
+
warnSpy.mockRestore();
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
describe("IndexedDBQueueStorageAdapter", () => {
|
|
69
|
+
it("rejects when IndexedDB is unavailable", async () => {
|
|
70
|
+
const original = globalThis.indexedDB;
|
|
71
|
+
globalThis.indexedDB = undefined;
|
|
72
|
+
const adapter = new IndexedDBQueueStorageAdapter("pureq-test-db");
|
|
73
|
+
await expect(adapter.size()).rejects.toThrow("pureq: IndexedDB is not available in this environment");
|
|
74
|
+
globalThis.indexedDB = original;
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
describe("FileSystemQueueStorageAdapter", () => {
|
|
78
|
+
it("stores, loads, removes, and clears queued requests", async () => {
|
|
79
|
+
const dir = await mkdtemp(join(tmpdir(), "pureq-fs-"));
|
|
80
|
+
const adapter = new FileSystemQueueStorageAdapter(dir);
|
|
81
|
+
await adapter.push({ id: 10, queuedAt: 1, req: { method: "POST", url: "/a" } });
|
|
82
|
+
await adapter.push({ id: 11, queuedAt: 2, req: { method: "DELETE", url: "/b" } });
|
|
83
|
+
expect(await adapter.size()).toBe(2);
|
|
84
|
+
const all = await adapter.getAll();
|
|
85
|
+
expect(all.map((x) => x.id).sort()).toEqual([10, 11]);
|
|
86
|
+
await adapter.remove(10);
|
|
87
|
+
expect(await adapter.size()).toBe(1);
|
|
88
|
+
await adapter.clear();
|
|
89
|
+
expect(await adapter.size()).toBe(0);
|
|
90
|
+
});
|
|
91
|
+
it("skips malformed json files", async () => {
|
|
92
|
+
const dir = await mkdtemp(join(tmpdir(), "pureq-fs-"));
|
|
93
|
+
const adapter = new FileSystemQueueStorageAdapter(dir);
|
|
94
|
+
await writeFile(join(dir, "broken.json"), "{not-json", "utf8");
|
|
95
|
+
await adapter.push({ id: 22, queuedAt: 2, req: { method: "POST", url: "/ok" } });
|
|
96
|
+
const all = await adapter.getAll();
|
|
97
|
+
expect(all).toHaveLength(1);
|
|
98
|
+
expect(all[0]?.id).toBe(22);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
//# sourceMappingURL=storage-adapters.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storage-adapters.test.js","sourceRoot":"","sources":["../../tests/storage-adapters.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"traffic-load.test.d.ts","sourceRoot":"","sources":["../../tests/traffic-load.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { concurrencyLimit } from "../src/middleware/concurrencyLimit";
|
|
3
|
+
import { dedupe } from "../src/middleware/dedupe";
|
|
4
|
+
import { createCircuitBreaker, PureqCircuitOpenError } from "../src/middleware/circuitBreaker";
|
|
5
|
+
import { HttpResponse } from "../src/response/response";
|
|
6
|
+
function sleep(ms) {
|
|
7
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
8
|
+
}
|
|
9
|
+
describe("traffic/load: high concurrency", () => {
|
|
10
|
+
it("enforces maxConcurrent under burst traffic", async () => {
|
|
11
|
+
const startedAt = Date.now();
|
|
12
|
+
const maxConcurrent = 8;
|
|
13
|
+
const middleware = concurrencyLimit({ maxConcurrent });
|
|
14
|
+
let inFlight = 0;
|
|
15
|
+
let maxSeen = 0;
|
|
16
|
+
const total = 240;
|
|
17
|
+
const tasks = Array.from({ length: total }, (_, i) => middleware({ method: "GET", url: `/burst/${i}` }, async () => {
|
|
18
|
+
inFlight += 1;
|
|
19
|
+
maxSeen = Math.max(maxSeen, inFlight);
|
|
20
|
+
await sleep(3);
|
|
21
|
+
inFlight -= 1;
|
|
22
|
+
return new HttpResponse(new Response("ok", { status: 200 }));
|
|
23
|
+
}));
|
|
24
|
+
const responses = await Promise.all(tasks);
|
|
25
|
+
const durationMs = Date.now() - startedAt;
|
|
26
|
+
console.info("[pureq][traffic-metrics]", {
|
|
27
|
+
scenario: "concurrency-burst",
|
|
28
|
+
total,
|
|
29
|
+
maxConcurrent,
|
|
30
|
+
maxSeen,
|
|
31
|
+
success: responses.length,
|
|
32
|
+
durationMs,
|
|
33
|
+
throughputRps: Number((responses.length / Math.max(durationMs / 1000, 0.001)).toFixed(2)),
|
|
34
|
+
});
|
|
35
|
+
expect(responses).toHaveLength(total);
|
|
36
|
+
expect(responses.every((res) => res.status === 200)).toBe(true);
|
|
37
|
+
expect(maxSeen).toBeLessThanOrEqual(maxConcurrent);
|
|
38
|
+
});
|
|
39
|
+
it("collapses same-signature request storms via dedupe", async () => {
|
|
40
|
+
const startedAt = Date.now();
|
|
41
|
+
const middleware = dedupe();
|
|
42
|
+
let upstreamCalls = 0;
|
|
43
|
+
const req = { method: "GET", url: "https://example.com/hot" };
|
|
44
|
+
const total = 120;
|
|
45
|
+
const calls = Array.from({ length: total }, () => middleware(req, async () => {
|
|
46
|
+
upstreamCalls += 1;
|
|
47
|
+
await sleep(5);
|
|
48
|
+
return new HttpResponse(new Response("shared", { status: 200 }));
|
|
49
|
+
}));
|
|
50
|
+
const responses = await Promise.all(calls);
|
|
51
|
+
const texts = await Promise.all(responses.map((r) => r.text()));
|
|
52
|
+
const durationMs = Date.now() - startedAt;
|
|
53
|
+
console.info("[pureq][traffic-metrics]", {
|
|
54
|
+
scenario: "dedupe-storm",
|
|
55
|
+
total,
|
|
56
|
+
upstreamCalls,
|
|
57
|
+
dedupeRate: Number((((total - upstreamCalls) / total) * 100).toFixed(2)),
|
|
58
|
+
durationMs,
|
|
59
|
+
});
|
|
60
|
+
expect(upstreamCalls).toBe(1);
|
|
61
|
+
expect(texts.every((t) => t === "shared")).toBe(true);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
describe("traffic/load: failure burst handling", () => {
|
|
65
|
+
it("opens circuit quickly after repeated failures and short-circuits subsequent traffic", async () => {
|
|
66
|
+
const startedAt = Date.now();
|
|
67
|
+
const controller = createCircuitBreaker({
|
|
68
|
+
failureThreshold: 3,
|
|
69
|
+
cooldownMs: 60000,
|
|
70
|
+
keyBuilder: () => "api:orders",
|
|
71
|
+
});
|
|
72
|
+
const req = { method: "GET", url: "/orders" };
|
|
73
|
+
for (let i = 0; i < 3; i++) {
|
|
74
|
+
await expect(controller.middleware(req, async () => {
|
|
75
|
+
throw new Error("upstream down");
|
|
76
|
+
})).rejects.toThrow("upstream down");
|
|
77
|
+
}
|
|
78
|
+
await expect(controller.middleware(req, async () => {
|
|
79
|
+
return new HttpResponse(new Response("should-not-run", { status: 200 }));
|
|
80
|
+
})).rejects.toBeInstanceOf(PureqCircuitOpenError);
|
|
81
|
+
const snapshot = await controller.snapshot();
|
|
82
|
+
console.info("[pureq][traffic-metrics]", {
|
|
83
|
+
scenario: "circuit-open-burst",
|
|
84
|
+
failuresToOpen: 3,
|
|
85
|
+
openCircuits: snapshot.summary.open,
|
|
86
|
+
durationMs: Date.now() - startedAt,
|
|
87
|
+
});
|
|
88
|
+
expect(snapshot.summary.open).toBe(1);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
//# sourceMappingURL=traffic-load.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"traffic-load.test.js","sourceRoot":"","sources":["../../tests/traffic-load.test.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pureq/pureq",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.4",
|
|
4
4
|
"description": "Functional, immutable, and type-safe HTTP client layer with middleware composition.",
|
|
5
5
|
"main": "./dist/src/index.js",
|
|
6
6
|
"types": "./dist/src/index.d.ts",
|
|
@@ -30,11 +30,14 @@
|
|
|
30
30
|
"test:edge": "esbuild scripts/edge-smoke-entry.ts --bundle --format=iife --platform=browser --outfile=.tmp/edge-smoke.bundle.js && node ./scripts/run-edge-smoke.mjs",
|
|
31
31
|
"test": "vitest run",
|
|
32
32
|
"test:watch": "vitest",
|
|
33
|
-
"test:unit": "vitest run tests --exclude tests/*.integration.test.ts --exclude tests/*.contract.test.ts --exclude tests/*.stress.test.ts",
|
|
33
|
+
"test:unit": "vitest run tests --exclude tests/*.integration.test.ts --exclude tests/*.contract.test.ts --exclude tests/*.stress.test.ts --exclude tests/traffic-load.test.ts --exclude tests/resilience-shock.test.ts --exclude tests/chaos-heavy.test.ts --exclude tests/invariant-fuzz.test.ts",
|
|
34
34
|
"test:integration": "vitest run tests/client.integration.test.ts",
|
|
35
35
|
"test:contract": "vitest run tests/public-api.contract.test.ts",
|
|
36
36
|
"test:stress": "vitest run tests/retry.stress.test.ts",
|
|
37
|
-
"test:
|
|
37
|
+
"test:heavy": "vitest run tests/traffic-load.test.ts tests/resilience-shock.test.ts tests/retry.stress.test.ts tests/chaos-heavy.test.ts tests/invariant-fuzz.test.ts",
|
|
38
|
+
"test:nightly": "npm run test:heavy",
|
|
39
|
+
"test:ci": "npm run test:unit && npm run test:integration && npm run test:contract && npm run test:stress && npm run typecheck",
|
|
40
|
+
"test:ci:full": "npm run test:ci && npm run test:heavy"
|
|
38
41
|
},
|
|
39
42
|
"keywords": [
|
|
40
43
|
"http",
|