@shivaedev/test-story 0.11.1 → 0.12.0
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/CHANGELOG.md +6 -0
- package/README.md +22 -2
- package/dist/server/fakeHttpService.d.ts +11 -0
- package/dist/server/fakeHttpService.d.ts.map +1 -1
- package/dist/server/fakeHttpService.js +8 -1
- package/dist/server/fakeHttpService.js.map +1 -1
- package/dist/server/internal/fakeHttpControls.d.ts +11 -5
- package/dist/server/internal/fakeHttpControls.d.ts.map +1 -1
- package/dist/server/internal/fakeHttpControls.js +56 -34
- package/dist/server/internal/fakeHttpControls.js.map +1 -1
- package/dist/server/internal/fakeHttpGates.d.ts +10 -0
- package/dist/server/internal/fakeHttpGates.d.ts.map +1 -0
- package/dist/server/internal/fakeHttpGates.js +38 -0
- package/dist/server/internal/fakeHttpGates.js.map +1 -0
- package/dist/server/internal/fakeHttpReceive.d.ts.map +1 -1
- package/dist/server/internal/fakeHttpReceive.js +12 -13
- package/dist/server/internal/fakeHttpReceive.js.map +1 -1
- package/dist/server/internal/fakeHttpSend.d.ts +8 -0
- package/dist/server/internal/fakeHttpSend.d.ts.map +1 -0
- package/dist/server/internal/fakeHttpSend.js +51 -0
- package/dist/server/internal/fakeHttpSend.js.map +1 -0
- package/package.json +2 -2
- package/src/server/fakeHttpService.ts +12 -1
- package/src/server/internal/fakeHttpControls.ts +63 -35
- package/src/server/internal/fakeHttpGates.ts +39 -0
- package/src/server/internal/fakeHttpReceive.ts +12 -14
- package/src/server/internal/fakeHttpSend.ts +55 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.12.0 - 2026-10-18
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Fake HTTP services expose `stall`, `replyOnce`, `releaseOnArrival` and `fault` controls for body stalls, raw byte and iterable chunk replies, automatic hold release on route arrival, and faults injected into arrived requests or replies in flight. `FakeHttpReply.chunks` accepts string or byte iterables, and the exported `FakeHttpFault` describes status, pre-header drop and mid-body reset faults. Controls validate route names, narrate through the story and clean up with its scope.
|
|
8
|
+
|
|
3
9
|
## 0.11.1 - 2026-10-18
|
|
4
10
|
|
|
5
11
|
### Changed
|
package/README.md
CHANGED
|
@@ -270,7 +270,7 @@ For timeout stories, return `undefined` from the `status` callback to accept an
|
|
|
270
270
|
|
|
271
271
|
#### Faking an outbound HTTP API
|
|
272
272
|
|
|
273
|
-
Use `fakeHttpService` from `@shivaedev/test-story/fakeHttpService.ts` in a kit's scoped `create` hook. Like the OTLP receiver, it listens on an allocated loopback port and closes with the scope. Routes have names for controls, a method (default `GET`), and a path whose `:segments` become decoded `request.params`. A reply receives the request and returns a JSON `body
|
|
273
|
+
Use `fakeHttpService` from `@shivaedev/test-story/fakeHttpService.ts` in a kit's scoped `create` hook. Like the OTLP receiver, it listens on an allocated loopback port and closes with the scope. Routes have names for controls, a method (default `GET`), and a path whose `:segments` become decoded `request.params`. A reply receives the request and returns a JSON `body` (or a `Uint8Array`/Buffer sent as raw bytes), optional `status` (default 200), and optional response `headers`.
|
|
274
274
|
|
|
275
275
|
```ts
|
|
276
276
|
import { fakeHttpService } from "@shivaedev/test-story/fakeHttpService.ts";
|
|
@@ -300,6 +300,26 @@ const user = yield* Fiber.join(pending);
|
|
|
300
300
|
|
|
301
301
|
`application.loadUser` is the application's real client call. `failOnce` affects the next authorized matching request to that named route, then the route answers normally. `hold` pauses that route until `release`; `waiting` completes when a request reaches the hold, so a story needs no polling to know when to release it. Other routes still answer. Effects that wait for HTTP should use a live-clock kit.
|
|
302
302
|
|
|
303
|
+
Use the controls through kit verbs after acquiring the service. `replyOnce(route, reply)` scripts the next authorized reply, including raw bytes or `chunks`, a synchronous or asynchronous iterable of strings and byte arrays. Set `headers["content-type"]` to the story's media type; the default is JSON. Chunks are written in iteration order as they become available, so an async iterable can wait between pieces.
|
|
304
|
+
|
|
305
|
+
```ts
|
|
306
|
+
yield* api.replyOnce("download", {
|
|
307
|
+
body: Uint8Array.from([0, 255, 128]),
|
|
308
|
+
headers: { "content-type": "application/octet-stream" },
|
|
309
|
+
});
|
|
310
|
+
const stalled = yield* api.stall("download");
|
|
311
|
+
const pending = yield* Effect.forkChild(application.download());
|
|
312
|
+
yield* stalled.waiting;
|
|
313
|
+
yield* stalled.release;
|
|
314
|
+
const bytes = yield* Fiber.join(pending);
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
`stall(route)` sends the reply's status and headers, then pauses its body. Its `waiting` effect completes at that pause. Its `release` lets the body continue; a client that gives up also stops waiting on the server. Like a hold, a stall remains installed for matching requests until released. Holds pause before headers. `releaseOnArrival(heldRoute, arrivingRoute)` releases the current hold when an authorized request reaches the named route; script it before that arrival. Rejected requests do not trigger release.
|
|
318
|
+
|
|
319
|
+
`fault(route, fault)` acts on an arrived request in the required phase. `{ kind: "status", reply: { status: 503, body: { error: "unavailable" } } }` replaces a held reply before headers. `{ kind: "drop" }` drops a connection before headers. `{ kind: "reset" }` terminates a reply after headers, including after body chunks have arrived. Wait for the hold, stall or client observation before injecting the fault. A fault fails with guidance when no request is in the required phase; it does not queue a failure for a future call. `failOnce` continues to queue a status reply for the next request.
|
|
320
|
+
|
|
321
|
+
All controls validate route names and narrate through the service's story sink. Closing the scope closes active connections and releases holds, stalls and arrival scripts.
|
|
322
|
+
|
|
303
323
|
A `pages` route accepts a one-based `?page=` query and returns `{ items, next }`, with `next: null` on the last page. No query means page 1; a page past the end returns an empty list; a non-positive or non-integer page gets 400. `api.requests` reads a snapshot of the request log, including rejected and held requests, with `method`, `path` (including query), raw `body` Buffer, `headers`, `params` and `query`.
|
|
304
324
|
|
|
305
325
|
### 2. Once per feature: traits and verbs
|
|
@@ -577,7 +597,7 @@ Import each module with the prefix `@shivaedev/test-story/`; there is no root en
|
|
|
577
597
|
| `domNavigate.ts` | `domNavigate(url?)` |
|
|
578
598
|
| `genreTags.ts` | `genreTags(...kitNames)`, `GenreTag` |
|
|
579
599
|
| `domEnvironment.config.ts` | Default Vitest environment preserving Node host globals and happy-dom document transport |
|
|
580
|
-
| `fakeHttpService.ts` | `fakeHttpService(options)`, `FakeHttpRoute`, `FakeHttpRequest`, `FakeHttpReply`, `FakeHttpHold`, `FakeHttpServiceOptions` |
|
|
600
|
+
| `fakeHttpService.ts` | `fakeHttpService(options)`, `FakeHttpRoute`, `FakeHttpRequest`, `FakeHttpReply`, `FakeHttpFault`, `FakeHttpHold`, `FakeHttpServiceOptions` |
|
|
581
601
|
| `forwardingProxy.ts` | `forwardingProxy(upstream, options?)`, `ForwardingProxyOptions` |
|
|
582
602
|
| `otlpReceiver.ts` | `otlpReceiver(options?)`, `OtlpSpan`, `OtlpExport`, `OtlpReceiverOptions` |
|
|
583
603
|
|
|
@@ -10,9 +10,16 @@ export interface FakeHttpRequest {
|
|
|
10
10
|
}
|
|
11
11
|
export interface FakeHttpReply {
|
|
12
12
|
readonly body?: unknown;
|
|
13
|
+
readonly chunks?: Iterable<string | Uint8Array> | AsyncIterable<string | Uint8Array>;
|
|
13
14
|
readonly headers?: Readonly<Record<string, string>>;
|
|
14
15
|
readonly status?: number;
|
|
15
16
|
}
|
|
17
|
+
export type FakeHttpFault = {
|
|
18
|
+
readonly kind: "status";
|
|
19
|
+
readonly reply: FakeHttpReply;
|
|
20
|
+
} | {
|
|
21
|
+
readonly kind: "drop" | "reset";
|
|
22
|
+
};
|
|
16
23
|
export type FakeHttpRoute = {
|
|
17
24
|
readonly method?: string;
|
|
18
25
|
readonly path: string;
|
|
@@ -34,9 +41,13 @@ export interface FakeHttpHold {
|
|
|
34
41
|
}
|
|
35
42
|
export declare const fakeHttpService: (options: FakeHttpServiceOptions) => Effect.Effect<{
|
|
36
43
|
failOnce: (route: string, reply: FakeHttpReply) => Effect.Effect<void, never, never>;
|
|
44
|
+
fault: (route: string, fault: FakeHttpFault) => Effect.Effect<void, never, never>;
|
|
37
45
|
hold: (route: string) => Effect.Effect<FakeHttpHold, never, never>;
|
|
38
46
|
narrate: (sink: (line: string) => void) => void;
|
|
47
|
+
releaseOnArrival: (route: string, arrival: string) => Effect.Effect<void, never, never>;
|
|
48
|
+
replyOnce: (route: string, reply: FakeHttpReply) => Effect.Effect<void, never, never>;
|
|
39
49
|
readonly requests: readonly FakeHttpRequest[];
|
|
50
|
+
stall: (route: string) => Effect.Effect<FakeHttpHold, never, never>;
|
|
40
51
|
url: string;
|
|
41
52
|
}, import("effect/Cause").UnknownError, import("effect/Scope").Scope>;
|
|
42
53
|
//# sourceMappingURL=fakeHttpService.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fakeHttpService.d.ts","sourceRoot":"","sources":["../../src/server/fakeHttpService.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,mBAAmB,EAAE,MAAM,WAAW,CAAC;AACnE,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAKhC,MAAM,WAAW,eAAe;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,mBAAmB,CAAC;IACtC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAClD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,eAAe,CAAC;CAChC;AAED,MAAM,WAAW,aAAa;IAC7B,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACpD,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,MAAM,aAAa,GAAG;IAC3B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACtB,GAAG,CACD;IAAE,QAAQ,CAAC,KAAK,EAAE,CAAC,OAAO,EAAE,eAAe,KAAK,aAAa,CAAC;IAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAA;CAAE,GACvF;IAAE,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC,SAAS,OAAO,EAAE,CAAC,EAAE,CAAC;IAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAA;CAAE,CAC7E,CAAC;AAEF,MAAM,WAAW,sBAAsB;IACtC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,eAAe,KAAK,OAAO,CAAC;IACvD,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC,CAAC;IACzD,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACvC;AAED,MAAM,WAAW,YAAY;IAC5B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IACtC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;CACtC;AAED,eAAO,MAAM,eAAe;sBAgBR,MAAM,SAAS,aAAa;
|
|
1
|
+
{"version":3,"file":"fakeHttpService.d.ts","sourceRoot":"","sources":["../../src/server/fakeHttpService.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,mBAAmB,EAAE,MAAM,WAAW,CAAC;AACnE,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAKhC,MAAM,WAAW,eAAe;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,mBAAmB,CAAC;IACtC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAClD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,eAAe,CAAC;CAChC;AAED,MAAM,WAAW,aAAa;IAC7B,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,MAAM,CAAC,EAAE,QAAQ,CAAC,MAAM,GAAG,UAAU,CAAC,GAAG,aAAa,CAAC,MAAM,GAAG,UAAU,CAAC,CAAC;IACrF,QAAQ,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACpD,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,MAAM,aAAa,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAA;CAAE,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAA;CAAE,CAAC;AAE7H,MAAM,MAAM,aAAa,GAAG;IAC3B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACtB,GAAG,CACD;IAAE,QAAQ,CAAC,KAAK,EAAE,CAAC,OAAO,EAAE,eAAe,KAAK,aAAa,CAAC;IAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAA;CAAE,GACvF;IAAE,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC,SAAS,OAAO,EAAE,CAAC,EAAE,CAAC;IAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAA;CAAE,CAC7E,CAAC;AAEF,MAAM,WAAW,sBAAsB;IACtC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,eAAe,KAAK,OAAO,CAAC;IACvD,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC,CAAC;IACzD,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACvC;AAED,MAAM,WAAW,YAAY;IAC5B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IACtC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;CACtC;AAED,eAAO,MAAM,eAAe;sBAgBR,MAAM,SAAS,aAAa;mBAC/B,MAAM,SAAS,aAAa;kBAC7B,MAAM;oBACJ,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI;8BAGZ,MAAM,WAAW,MAAM;uBAK9B,MAAM,SAAS,aAAa;uBAC/B,SAAS,eAAe,EAAE;mBAG3B,MAAM;;qEAGrB,CAAC"}
|
|
@@ -19,14 +19,21 @@ export const fakeHttpService = Effect.fnUntraced(function* (options) {
|
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
21
|
return {
|
|
22
|
-
failOnce: (route, reply) => Effect.sync(() => checkRoute(route)).pipe(Effect.andThen(controls.
|
|
22
|
+
failOnce: (route, reply) => Effect.sync(() => checkRoute(route)).pipe(Effect.andThen(controls.queue(route, reply))),
|
|
23
|
+
fault: (route, fault) => Effect.sync(() => checkRoute(route)).pipe(Effect.andThen(controls.fault(route, fault))),
|
|
23
24
|
hold: (route) => Effect.sync(() => checkRoute(route)).pipe(Effect.andThen(controls.hold(route))),
|
|
24
25
|
narrate: (sink) => {
|
|
25
26
|
narrate = sink;
|
|
26
27
|
},
|
|
28
|
+
releaseOnArrival: (route, arrival) => Effect.sync(() => {
|
|
29
|
+
checkRoute(route);
|
|
30
|
+
checkRoute(arrival);
|
|
31
|
+
}).pipe(Effect.andThen(controls.releaseOnArrival(route, arrival))),
|
|
32
|
+
replyOnce: (route, reply) => Effect.sync(() => checkRoute(route)).pipe(Effect.andThen(controls.queue(route, reply))),
|
|
27
33
|
get requests() {
|
|
28
34
|
return requests.slice();
|
|
29
35
|
},
|
|
36
|
+
stall: (route) => Effect.sync(() => checkRoute(route)).pipe(Effect.andThen(controls.stall(route))),
|
|
30
37
|
url,
|
|
31
38
|
};
|
|
32
39
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fakeHttpService.js","sourceRoot":"","sources":["../../src/server/fakeHttpService.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAA4B,MAAM,WAAW,CAAC;AACnE,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AACjE,OAAO,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAC/D,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"fakeHttpService.js","sourceRoot":"","sources":["../../src/server/fakeHttpService.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,10 +1,16 @@
|
|
|
1
|
+
import type { ServerResponse } from "node:http";
|
|
1
2
|
import { Effect } from "effect";
|
|
2
|
-
import type {
|
|
3
|
+
import type { FakeHttpFault, FakeHttpReply } from "#fakeHttpService.ts";
|
|
3
4
|
export declare function fakeHttpControls(tell: (line: string) => void): {
|
|
5
|
+
arrive: (route: string, response: ServerResponse) => void;
|
|
4
6
|
close: () => void;
|
|
5
|
-
|
|
6
|
-
hold: (route: string) => Effect.Effect<FakeHttpHold>;
|
|
7
|
-
pause: (route: string) => Promise<
|
|
8
|
-
|
|
7
|
+
fault: (route: string, fault: FakeHttpFault) => Effect.Effect<void, never, never>;
|
|
8
|
+
hold: (route: string) => Effect.Effect<import("#fakeHttpService.ts").FakeHttpHold, never, never>;
|
|
9
|
+
pause: (route: string, response: ServerResponse) => Promise<boolean>;
|
|
10
|
+
queue: (route: string, reply: FakeHttpReply) => Effect.Effect<void, never, never>;
|
|
11
|
+
releaseOnArrival: (route: string, arrival: string) => Effect.Effect<void, never, never>;
|
|
12
|
+
stall: (route: string) => Effect.Effect<import("#fakeHttpService.ts").FakeHttpHold, never, never>;
|
|
13
|
+
stallBody: (route: string) => (() => Promise<void>) | undefined;
|
|
14
|
+
takeReply: (route: string) => FakeHttpReply | undefined;
|
|
9
15
|
};
|
|
10
16
|
//# sourceMappingURL=fakeHttpControls.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fakeHttpControls.d.ts","sourceRoot":"","sources":["../../../src/server/internal/fakeHttpControls.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"fakeHttpControls.d.ts","sourceRoot":"","sources":["../../../src/server/internal/fakeHttpControls.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAChD,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAIxE,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI;IAO3D,MAAM,UAAU,MAAM,YAAY,cAAc;IAQhD,KAAK;IASL,KAAK,UAAU,MAAM,SAAS,aAAa;IAkB3C,IAAI,UAAU,MAAM;IACpB,KAAK,UAAgB,MAAM,YAAY,cAAc;IAIrD,KAAK,UAAU,MAAM,SAAS,aAAa;IAO3C,gBAAgB,UAAU,MAAM,WAAW,MAAM;IAWjD,KAAK,UAAU,MAAM;IACrB,SAAS,UAAU,MAAM;IAIzB,SAAS,UAAU,MAAM;EAE1B"}
|
|
@@ -1,48 +1,70 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { fakeHttpGates } from "#internal/fakeHttpGates.ts";
|
|
3
|
+
import { fakeHttpConnection, fakeHttpSend } from "#internal/fakeHttpSend.ts";
|
|
2
4
|
export function fakeHttpControls(tell) {
|
|
3
|
-
const
|
|
4
|
-
const holds =
|
|
5
|
+
const replies = new Map();
|
|
6
|
+
const holds = fakeHttpGates(tell, "hold");
|
|
7
|
+
const stalls = fakeHttpGates(tell, "stall");
|
|
8
|
+
const arrivals = new Map();
|
|
9
|
+
const active = new Map();
|
|
5
10
|
return {
|
|
11
|
+
arrive: (route, response) => {
|
|
12
|
+
active.set(response, route);
|
|
13
|
+
response.once("close", () => active.delete(response));
|
|
14
|
+
for (const release of arrivals.get(route) ?? []) {
|
|
15
|
+
release();
|
|
16
|
+
}
|
|
17
|
+
arrivals.delete(route);
|
|
18
|
+
},
|
|
6
19
|
close: () => {
|
|
7
|
-
for (const
|
|
8
|
-
|
|
20
|
+
for (const response of active.keys()) {
|
|
21
|
+
response.destroy();
|
|
22
|
+
}
|
|
23
|
+
holds.close();
|
|
24
|
+
stalls.close();
|
|
25
|
+
arrivals.clear();
|
|
26
|
+
replies.clear();
|
|
27
|
+
},
|
|
28
|
+
fault: (route, fault) => Effect.promise(async () => {
|
|
29
|
+
const response = [...active].find(([candidate, name]) => name === route && !(candidate.destroyed || candidate.writableEnded) && candidate.headersSent === (fault.kind === "reset"))?.[0];
|
|
30
|
+
if (!response) {
|
|
31
|
+
throw new Error(`Fake HTTP route "${route}" has no request ready for a ${fault.kind} fault.\nhelp: Wait for a hold before a status or drop fault, or for response headers before a reset fault.`);
|
|
9
32
|
}
|
|
33
|
+
tell(`fake HTTP ${route} fault ${fault.kind}`);
|
|
34
|
+
if (fault.kind === "status") {
|
|
35
|
+
await fakeHttpSend(response, fault.reply);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
response.destroy();
|
|
39
|
+
}
|
|
40
|
+
}),
|
|
41
|
+
hold: (route) => Effect.sync(() => holds.make(route)),
|
|
42
|
+
pause: async (route, response) => {
|
|
43
|
+
await fakeHttpConnection(response).pause(holds.get(route)?.enter());
|
|
44
|
+
return !(response.destroyed || response.writableEnded || response.headersSent);
|
|
10
45
|
},
|
|
11
|
-
|
|
12
|
-
const queue =
|
|
46
|
+
queue: (route, reply) => Effect.sync(() => {
|
|
47
|
+
const queue = replies.get(route) ?? [];
|
|
13
48
|
queue.push(reply);
|
|
14
|
-
|
|
49
|
+
replies.set(route, queue);
|
|
15
50
|
tell(`fake HTTP ${route} will answer once with ${reply.status ?? 200}`);
|
|
16
51
|
}),
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
52
|
+
releaseOnArrival: (route, arrival) => Effect.sync(() => {
|
|
53
|
+
const hold = holds.get(route);
|
|
54
|
+
if (!hold) {
|
|
55
|
+
throw new Error(`Fake HTTP route "${route}" has no hold.\nhelp: Hold the route before scripting its release on arrival.`);
|
|
20
56
|
}
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
tell(`fake HTTP ${route} has a waiting request`);
|
|
26
|
-
Deferred.doneUnsafe(waiting, Effect.void);
|
|
27
|
-
return released.promise;
|
|
28
|
-
},
|
|
29
|
-
release: () => {
|
|
30
|
-
if (holds.get(route) !== hold) {
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
holds.delete(route);
|
|
34
|
-
released.resolve();
|
|
35
|
-
tell(`fake HTTP ${route} released`);
|
|
36
|
-
},
|
|
37
|
-
};
|
|
38
|
-
holds.set(route, hold);
|
|
39
|
-
tell(`fake HTTP ${route} held`);
|
|
40
|
-
return { release: Effect.sync(hold.release), waiting: Deferred.await(waiting) };
|
|
57
|
+
const releases = arrivals.get(arrival) ?? new Set();
|
|
58
|
+
releases.add(hold.release);
|
|
59
|
+
arrivals.set(arrival, releases);
|
|
60
|
+
tell(`fake HTTP ${route} will release when ${arrival} arrives`);
|
|
41
61
|
}),
|
|
42
|
-
|
|
43
|
-
|
|
62
|
+
stall: (route) => Effect.sync(() => stalls.make(route)),
|
|
63
|
+
stallBody: (route) => {
|
|
64
|
+
const stall = stalls.get(route);
|
|
65
|
+
return stall ? () => stall.enter() : undefined;
|
|
44
66
|
},
|
|
45
|
-
|
|
67
|
+
takeReply: (route) => replies.get(route)?.shift(),
|
|
46
68
|
};
|
|
47
69
|
}
|
|
48
70
|
//# sourceMappingURL=fakeHttpControls.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fakeHttpControls.js","sourceRoot":"","sources":["../../../src/server/internal/fakeHttpControls.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"fakeHttpControls.js","sourceRoot":"","sources":["../../../src/server/internal/fakeHttpControls.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { FakeHttpHold } from "#fakeHttpService.ts";
|
|
2
|
+
export declare function fakeHttpGates(tell: (line: string) => void, kind: "hold" | "stall"): {
|
|
3
|
+
close: () => void;
|
|
4
|
+
get: (route: string) => {
|
|
5
|
+
enter: () => Promise<void>;
|
|
6
|
+
release: () => void;
|
|
7
|
+
} | undefined;
|
|
8
|
+
make: (route: string) => FakeHttpHold;
|
|
9
|
+
};
|
|
10
|
+
//# sourceMappingURL=fakeHttpGates.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fakeHttpGates.d.ts","sourceRoot":"","sources":["../../../src/server/internal/fakeHttpGates.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAExD,wBAAgB,aAAa,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO;IAGhF,KAAK;IAKL,GAAG,UAAU,MAAM;eAPmB,MAAM,OAAO,CAAC,IAAI,CAAC;iBAAW,MAAM,IAAI;;IAQ9E,IAAI,UAAU,MAAM,KAAG,YAAY;EA0BpC"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Deferred, Effect } from "effect";
|
|
2
|
+
export function fakeHttpGates(tell, kind) {
|
|
3
|
+
const gates = new Map();
|
|
4
|
+
return {
|
|
5
|
+
close: () => {
|
|
6
|
+
for (const gate of gates.values()) {
|
|
7
|
+
gate.release();
|
|
8
|
+
}
|
|
9
|
+
},
|
|
10
|
+
get: (route) => gates.get(route),
|
|
11
|
+
make: (route) => {
|
|
12
|
+
if (gates.has(route)) {
|
|
13
|
+
throw new Error(`Fake HTTP route "${route}" already has a ${kind}.\nhelp: Release its current ${kind} before scripting another.`);
|
|
14
|
+
}
|
|
15
|
+
const waiting = Deferred.makeUnsafe();
|
|
16
|
+
const released = Promise.withResolvers();
|
|
17
|
+
const gate = {
|
|
18
|
+
enter: () => {
|
|
19
|
+
tell(kind === "hold" ? `fake HTTP ${route} has a waiting request` : `fake HTTP ${route} body stalled`);
|
|
20
|
+
Deferred.doneUnsafe(waiting, Effect.void);
|
|
21
|
+
return released.promise;
|
|
22
|
+
},
|
|
23
|
+
release: () => {
|
|
24
|
+
if (gates.get(route) !== gate) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
gates.delete(route);
|
|
28
|
+
released.resolve();
|
|
29
|
+
tell(kind === "hold" ? `fake HTTP ${route} released` : `fake HTTP ${route} body released`);
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
gates.set(route, gate);
|
|
33
|
+
tell(kind === "hold" ? `fake HTTP ${route} held` : `fake HTTP ${route} body will stall`);
|
|
34
|
+
return { release: Effect.sync(gate.release), waiting: Deferred.await(waiting) };
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=fakeHttpGates.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fakeHttpGates.js","sourceRoot":"","sources":["../../../src/server/internal/fakeHttpGates.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAG1C,MAAM,UAAU,aAAa,CAAC,IAA4B,EAAE,IAAsB;IACjF,MAAM,KAAK,GAAG,IAAI,GAAG,EAA+D,CAAC;IACrF,OAAO;QACN,KAAK,EAAE,GAAG,EAAE;YACX,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC;gBACnC,IAAI,CAAC,OAAO,EAAE,CAAC;YAChB,CAAC;QACF,CAAC;QACD,GAAG,EAAE,CAAC,KAAa,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC;QACxC,IAAI,EAAE,CAAC,KAAa,EAAgB,EAAE;YACrC,IAAI,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;gBACtB,MAAM,IAAI,KAAK,CAAC,oBAAoB,KAAK,mBAAmB,IAAI,gCAAgC,IAAI,4BAA4B,CAAC,CAAC;YACnI,CAAC;YACD,MAAM,OAAO,GAAG,QAAQ,CAAC,UAAU,EAAQ,CAAC;YAC5C,MAAM,QAAQ,GAAG,OAAO,CAAC,aAAa,EAAQ,CAAC;YAC/C,MAAM,IAAI,GAAG;gBACZ,KAAK,EAAE,GAAG,EAAE;oBACX,IAAI,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,aAAa,KAAK,wBAAwB,CAAC,CAAC,CAAC,aAAa,KAAK,eAAe,CAAC,CAAC;oBACvG,QAAQ,CAAC,UAAU,CAAC,OAAO,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC;oBAC1C,OAAO,QAAQ,CAAC,OAAO,CAAC;gBACzB,CAAC;gBACD,OAAO,EAAE,GAAG,EAAE;oBACb,IAAI,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,IAAI,EAAE,CAAC;wBAC/B,OAAO;oBACR,CAAC;oBACD,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;oBACpB,QAAQ,CAAC,OAAO,EAAE,CAAC;oBACnB,IAAI,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,aAAa,KAAK,WAAW,CAAC,CAAC,CAAC,aAAa,KAAK,gBAAgB,CAAC,CAAC;gBAC5F,CAAC;aACD,CAAC;YACF,KAAK,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;YACvB,IAAI,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,aAAa,KAAK,OAAO,CAAC,CAAC,CAAC,aAAa,KAAK,kBAAkB,CAAC,CAAC;YACzF,OAAO,EAAE,OAAO,EAAE,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC;QACjF,CAAC;KACD,CAAC;AACH,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fakeHttpReceive.d.ts","sourceRoot":"","sources":["../../../src/server/internal/fakeHttpReceive.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AACjE,OAAO,KAAK,EAAiB,eAAe,EAAiB,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AACjH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;
|
|
1
|
+
{"version":3,"file":"fakeHttpReceive.d.ts","sourceRoot":"","sources":["../../../src/server/internal/fakeHttpReceive.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AACjE,OAAO,KAAK,EAAiB,eAAe,EAAiB,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AACjH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AA8CtE,wBAAsB,eAAe,CACpC,QAAQ,EAAE,eAAe,EACzB,QAAQ,EAAE,cAAc,EACxB,OAAO,EAAE,sBAAsB,EAC/B,QAAQ,EAAE,eAAe,EAAE,EAC3B,QAAQ,EAAE,UAAU,CAAC,OAAO,gBAAgB,CAAC,EAC7C,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,iBAuD5B"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { fakeHttpSend } from "#internal/fakeHttpSend.ts";
|
|
1
2
|
function match(route, method, pathname) {
|
|
2
3
|
if ((route.method ?? "GET").toUpperCase() !== method) {
|
|
3
4
|
return;
|
|
@@ -40,11 +41,6 @@ function pageReply(route, request) {
|
|
|
40
41
|
}
|
|
41
42
|
return { body: { items: route.pages[page - 1] ?? [], next: page < route.pages.length ? page + 1 : null } };
|
|
42
43
|
}
|
|
43
|
-
function send(response, reply) {
|
|
44
|
-
const body = reply.body === undefined ? undefined : JSON.stringify(reply.body);
|
|
45
|
-
response.writeHead(reply.status ?? 200, { "content-type": "application/json", ...reply.headers });
|
|
46
|
-
response.end(body);
|
|
47
|
-
}
|
|
48
44
|
export async function fakeHttpReceive(incoming, response, options, requests, controls, tell) {
|
|
49
45
|
try {
|
|
50
46
|
const path = incoming.url ?? "/";
|
|
@@ -71,31 +67,34 @@ export async function fakeHttpReceive(incoming, response, options, requests, con
|
|
|
71
67
|
tell(`fake HTTP received ${method} ${path}`);
|
|
72
68
|
if (!decodeParams(params)) {
|
|
73
69
|
tell(`fake HTTP rejected ${method} ${path} with 400`);
|
|
74
|
-
|
|
70
|
+
await fakeHttpSend(response, { status: 400 });
|
|
75
71
|
return;
|
|
76
72
|
}
|
|
77
73
|
if (options.guard && !options.guard(request)) {
|
|
78
74
|
tell(`fake HTTP rejected ${method} ${path} with 401`);
|
|
79
|
-
|
|
75
|
+
await fakeHttpSend(response, { status: 401 });
|
|
80
76
|
return;
|
|
81
77
|
}
|
|
82
78
|
if (!found) {
|
|
83
79
|
tell(`fake HTTP answered ${method} ${path} with 404`);
|
|
84
|
-
|
|
80
|
+
await fakeHttpSend(response, { status: 404 });
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
controls.arrive(found.name, response);
|
|
84
|
+
if (!(await controls.pause(found.name, response))) {
|
|
85
85
|
return;
|
|
86
86
|
}
|
|
87
|
-
|
|
88
|
-
const reply = controls.takeFailure(found.name) ?? pageReply(found.route, request);
|
|
87
|
+
const reply = controls.takeReply(found.name) ?? pageReply(found.route, request);
|
|
89
88
|
tell(`fake HTTP answered ${method} ${path} with ${reply.status ?? 200}`);
|
|
90
|
-
|
|
89
|
+
await fakeHttpSend(response, reply, controls.stallBody(found.name));
|
|
91
90
|
}
|
|
92
91
|
catch (error) {
|
|
93
92
|
tell(`fake HTTP could not answer: ${String(error)}`);
|
|
94
93
|
if (response.headersSent) {
|
|
95
|
-
response.
|
|
94
|
+
response.destroy();
|
|
96
95
|
}
|
|
97
96
|
else {
|
|
98
|
-
|
|
97
|
+
await fakeHttpSend(response, { status: 500 });
|
|
99
98
|
}
|
|
100
99
|
}
|
|
101
100
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fakeHttpReceive.js","sourceRoot":"","sources":["../../../src/server/internal/fakeHttpReceive.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"fakeHttpReceive.js","sourceRoot":"","sources":["../../../src/server/internal/fakeHttpReceive.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ServerResponse } from "node:http";
|
|
2
|
+
import type { FakeHttpReply } from "#fakeHttpService.ts";
|
|
3
|
+
export declare function fakeHttpConnection(response: ServerResponse): {
|
|
4
|
+
closed: Promise<undefined>;
|
|
5
|
+
pause: (pending: Promise<void> | undefined) => Promise<void>;
|
|
6
|
+
};
|
|
7
|
+
export declare function fakeHttpSend(response: ServerResponse, reply: FakeHttpReply, pause?: () => Promise<void>): Promise<void>;
|
|
8
|
+
//# sourceMappingURL=fakeHttpSend.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fakeHttpSend.d.ts","sourceRoot":"","sources":["../../../src/server/internal/fakeHttpSend.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAChD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,cAAc;;qBAIlC,OAAO,CAAC,IAAI,CAAC,GAAG,SAAS;EAMjD;AAwBD,wBAAsB,YAAY,CAAC,QAAQ,EAAE,cAAc,EAAE,KAAK,EAAE,aAAa,EAAE,KAAK,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,iBAiB7G"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export function fakeHttpConnection(response) {
|
|
2
|
+
const closed = new Promise((resolve) => response.once("close", () => resolve(undefined)));
|
|
3
|
+
return {
|
|
4
|
+
closed,
|
|
5
|
+
pause: async (pending) => {
|
|
6
|
+
if (pending !== undefined) {
|
|
7
|
+
await Promise.race([pending, closed]);
|
|
8
|
+
}
|
|
9
|
+
},
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
async function sendChunks(response, chunks, closed) {
|
|
13
|
+
const iterator = Symbol.asyncIterator in chunks ? chunks[Symbol.asyncIterator]() : chunks[Symbol.iterator]();
|
|
14
|
+
try {
|
|
15
|
+
while (!(response.destroyed || response.writableEnded)) {
|
|
16
|
+
const next = await Promise.race([iterator.next(), closed]);
|
|
17
|
+
if (!next || next.done) {
|
|
18
|
+
break;
|
|
19
|
+
}
|
|
20
|
+
await new Promise((resolve, reject) => response.write(next.value, (error) => (error ? reject(error) : resolve())));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
finally {
|
|
24
|
+
void Promise.resolve(iterator.return?.()).catch(() => response.destroy());
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function replyBody(body) {
|
|
28
|
+
if (body instanceof Uint8Array || body === undefined) {
|
|
29
|
+
return body;
|
|
30
|
+
}
|
|
31
|
+
return JSON.stringify(body);
|
|
32
|
+
}
|
|
33
|
+
export async function fakeHttpSend(response, reply, pause) {
|
|
34
|
+
if (response.destroyed || response.writableEnded) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const connection = fakeHttpConnection(response);
|
|
38
|
+
const body = replyBody(reply.body);
|
|
39
|
+
response.writeHead(reply.status ?? 200, { "content-type": "application/json", ...reply.headers });
|
|
40
|
+
if (pause) {
|
|
41
|
+
response.flushHeaders();
|
|
42
|
+
await connection.pause(pause());
|
|
43
|
+
}
|
|
44
|
+
if (reply.chunks) {
|
|
45
|
+
await sendChunks(response, reply.chunks, connection.closed);
|
|
46
|
+
}
|
|
47
|
+
if (!(response.destroyed || response.writableEnded)) {
|
|
48
|
+
response.end(body);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=fakeHttpSend.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fakeHttpSend.js","sourceRoot":"","sources":["../../../src/server/internal/fakeHttpSend.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shivaedev/test-story",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Tests that read like stories over the real engine: a story kit per engine with its own it, traits per feature, and a failure that prints the story, where it stopped and the engine's state",
|
|
5
5
|
"homepage": "https://github.com/ShivaeDev/platform/tree/main/packages/test-story#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@effect/vitest": "4.0.4",
|
|
49
|
-
"@shivaedev/quality": "0.
|
|
49
|
+
"@shivaedev/quality": "0.28.0",
|
|
50
50
|
"@types/node": "24.19.2",
|
|
51
51
|
"effect": "4.0.4",
|
|
52
52
|
"happy-dom": "20.14.6",
|
|
@@ -15,10 +15,13 @@ export interface FakeHttpRequest {
|
|
|
15
15
|
|
|
16
16
|
export interface FakeHttpReply {
|
|
17
17
|
readonly body?: unknown;
|
|
18
|
+
readonly chunks?: Iterable<string | Uint8Array> | AsyncIterable<string | Uint8Array>;
|
|
18
19
|
readonly headers?: Readonly<Record<string, string>>;
|
|
19
20
|
readonly status?: number;
|
|
20
21
|
}
|
|
21
22
|
|
|
23
|
+
export type FakeHttpFault = { readonly kind: "status"; readonly reply: FakeHttpReply } | { readonly kind: "drop" | "reset" };
|
|
24
|
+
|
|
22
25
|
export type FakeHttpRoute = {
|
|
23
26
|
readonly method?: string;
|
|
24
27
|
readonly path: string;
|
|
@@ -54,14 +57,22 @@ export const fakeHttpService = Effect.fnUntraced(function* (options: FakeHttpSer
|
|
|
54
57
|
}
|
|
55
58
|
}
|
|
56
59
|
return {
|
|
57
|
-
failOnce: (route: string, reply: FakeHttpReply) => Effect.sync(() => checkRoute(route)).pipe(Effect.andThen(controls.
|
|
60
|
+
failOnce: (route: string, reply: FakeHttpReply) => Effect.sync(() => checkRoute(route)).pipe(Effect.andThen(controls.queue(route, reply))),
|
|
61
|
+
fault: (route: string, fault: FakeHttpFault) => Effect.sync(() => checkRoute(route)).pipe(Effect.andThen(controls.fault(route, fault))),
|
|
58
62
|
hold: (route: string) => Effect.sync(() => checkRoute(route)).pipe(Effect.andThen(controls.hold(route))),
|
|
59
63
|
narrate: (sink: (line: string) => void) => {
|
|
60
64
|
narrate = sink;
|
|
61
65
|
},
|
|
66
|
+
releaseOnArrival: (route: string, arrival: string) =>
|
|
67
|
+
Effect.sync(() => {
|
|
68
|
+
checkRoute(route);
|
|
69
|
+
checkRoute(arrival);
|
|
70
|
+
}).pipe(Effect.andThen(controls.releaseOnArrival(route, arrival))),
|
|
71
|
+
replyOnce: (route: string, reply: FakeHttpReply) => Effect.sync(() => checkRoute(route)).pipe(Effect.andThen(controls.queue(route, reply))),
|
|
62
72
|
get requests(): readonly FakeHttpRequest[] {
|
|
63
73
|
return requests.slice();
|
|
64
74
|
},
|
|
75
|
+
stall: (route: string) => Effect.sync(() => checkRoute(route)).pipe(Effect.andThen(controls.stall(route))),
|
|
65
76
|
url,
|
|
66
77
|
};
|
|
67
78
|
});
|
|
@@ -1,51 +1,79 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
1
|
+
import type { ServerResponse } from "node:http";
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
import type { FakeHttpFault, FakeHttpReply } from "#fakeHttpService.ts";
|
|
4
|
+
import { fakeHttpGates } from "#internal/fakeHttpGates.ts";
|
|
5
|
+
import { fakeHttpConnection, fakeHttpSend } from "#internal/fakeHttpSend.ts";
|
|
3
6
|
|
|
4
7
|
export function fakeHttpControls(tell: (line: string) => void) {
|
|
5
|
-
const
|
|
6
|
-
const holds =
|
|
8
|
+
const replies = new Map<string, FakeHttpReply[]>();
|
|
9
|
+
const holds = fakeHttpGates(tell, "hold");
|
|
10
|
+
const stalls = fakeHttpGates(tell, "stall");
|
|
11
|
+
const arrivals = new Map<string, Set<() => void>>();
|
|
12
|
+
const active = new Map<ServerResponse, string>();
|
|
7
13
|
return {
|
|
14
|
+
arrive: (route: string, response: ServerResponse) => {
|
|
15
|
+
active.set(response, route);
|
|
16
|
+
response.once("close", () => active.delete(response));
|
|
17
|
+
for (const release of arrivals.get(route) ?? []) {
|
|
18
|
+
release();
|
|
19
|
+
}
|
|
20
|
+
arrivals.delete(route);
|
|
21
|
+
},
|
|
8
22
|
close: () => {
|
|
9
|
-
for (const
|
|
10
|
-
|
|
23
|
+
for (const response of active.keys()) {
|
|
24
|
+
response.destroy();
|
|
11
25
|
}
|
|
26
|
+
holds.close();
|
|
27
|
+
stalls.close();
|
|
28
|
+
arrivals.clear();
|
|
29
|
+
replies.clear();
|
|
30
|
+
},
|
|
31
|
+
fault: (route: string, fault: FakeHttpFault) =>
|
|
32
|
+
Effect.promise(async () => {
|
|
33
|
+
const response = [...active].find(
|
|
34
|
+
([candidate, name]) =>
|
|
35
|
+
name === route && !(candidate.destroyed || candidate.writableEnded) && candidate.headersSent === (fault.kind === "reset"),
|
|
36
|
+
)?.[0];
|
|
37
|
+
if (!response) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
`Fake HTTP route "${route}" has no request ready for a ${fault.kind} fault.\nhelp: Wait for a hold before a status or drop fault, or for response headers before a reset fault.`,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
tell(`fake HTTP ${route} fault ${fault.kind}`);
|
|
43
|
+
if (fault.kind === "status") {
|
|
44
|
+
await fakeHttpSend(response, fault.reply);
|
|
45
|
+
} else {
|
|
46
|
+
response.destroy();
|
|
47
|
+
}
|
|
48
|
+
}),
|
|
49
|
+
hold: (route: string) => Effect.sync(() => holds.make(route)),
|
|
50
|
+
pause: async (route: string, response: ServerResponse) => {
|
|
51
|
+
await fakeHttpConnection(response).pause(holds.get(route)?.enter());
|
|
52
|
+
return !(response.destroyed || response.writableEnded || response.headersSent);
|
|
12
53
|
},
|
|
13
|
-
|
|
54
|
+
queue: (route: string, reply: FakeHttpReply) =>
|
|
14
55
|
Effect.sync(() => {
|
|
15
|
-
const queue =
|
|
56
|
+
const queue = replies.get(route) ?? [];
|
|
16
57
|
queue.push(reply);
|
|
17
|
-
|
|
58
|
+
replies.set(route, queue);
|
|
18
59
|
tell(`fake HTTP ${route} will answer once with ${reply.status ?? 200}`);
|
|
19
60
|
}),
|
|
20
|
-
|
|
61
|
+
releaseOnArrival: (route: string, arrival: string) =>
|
|
21
62
|
Effect.sync(() => {
|
|
22
|
-
|
|
23
|
-
|
|
63
|
+
const hold = holds.get(route);
|
|
64
|
+
if (!hold) {
|
|
65
|
+
throw new Error(`Fake HTTP route "${route}" has no hold.\nhelp: Hold the route before scripting its release on arrival.`);
|
|
24
66
|
}
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
tell(`fake HTTP ${route} has a waiting request`);
|
|
30
|
-
Deferred.doneUnsafe(waiting, Effect.void);
|
|
31
|
-
return released.promise;
|
|
32
|
-
},
|
|
33
|
-
release: () => {
|
|
34
|
-
if (holds.get(route) !== hold) {
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
holds.delete(route);
|
|
38
|
-
released.resolve();
|
|
39
|
-
tell(`fake HTTP ${route} released`);
|
|
40
|
-
},
|
|
41
|
-
};
|
|
42
|
-
holds.set(route, hold);
|
|
43
|
-
tell(`fake HTTP ${route} held`);
|
|
44
|
-
return { release: Effect.sync(hold.release), waiting: Deferred.await(waiting) };
|
|
67
|
+
const releases = arrivals.get(arrival) ?? new Set<() => void>();
|
|
68
|
+
releases.add(hold.release);
|
|
69
|
+
arrivals.set(arrival, releases);
|
|
70
|
+
tell(`fake HTTP ${route} will release when ${arrival} arrives`);
|
|
45
71
|
}),
|
|
46
|
-
|
|
47
|
-
|
|
72
|
+
stall: (route: string) => Effect.sync(() => stalls.make(route)),
|
|
73
|
+
stallBody: (route: string) => {
|
|
74
|
+
const stall = stalls.get(route);
|
|
75
|
+
return stall ? () => stall.enter() : undefined;
|
|
48
76
|
},
|
|
49
|
-
|
|
77
|
+
takeReply: (route: string) => replies.get(route)?.shift(),
|
|
50
78
|
};
|
|
51
79
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Deferred, Effect } from "effect";
|
|
2
|
+
import type { FakeHttpHold } from "#fakeHttpService.ts";
|
|
3
|
+
|
|
4
|
+
export function fakeHttpGates(tell: (line: string) => void, kind: "hold" | "stall") {
|
|
5
|
+
const gates = new Map<string, { enter: () => Promise<void>; release: () => void }>();
|
|
6
|
+
return {
|
|
7
|
+
close: () => {
|
|
8
|
+
for (const gate of gates.values()) {
|
|
9
|
+
gate.release();
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
get: (route: string) => gates.get(route),
|
|
13
|
+
make: (route: string): FakeHttpHold => {
|
|
14
|
+
if (gates.has(route)) {
|
|
15
|
+
throw new Error(`Fake HTTP route "${route}" already has a ${kind}.\nhelp: Release its current ${kind} before scripting another.`);
|
|
16
|
+
}
|
|
17
|
+
const waiting = Deferred.makeUnsafe<void>();
|
|
18
|
+
const released = Promise.withResolvers<void>();
|
|
19
|
+
const gate = {
|
|
20
|
+
enter: () => {
|
|
21
|
+
tell(kind === "hold" ? `fake HTTP ${route} has a waiting request` : `fake HTTP ${route} body stalled`);
|
|
22
|
+
Deferred.doneUnsafe(waiting, Effect.void);
|
|
23
|
+
return released.promise;
|
|
24
|
+
},
|
|
25
|
+
release: () => {
|
|
26
|
+
if (gates.get(route) !== gate) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
gates.delete(route);
|
|
30
|
+
released.resolve();
|
|
31
|
+
tell(kind === "hold" ? `fake HTTP ${route} released` : `fake HTTP ${route} body released`);
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
gates.set(route, gate);
|
|
35
|
+
tell(kind === "hold" ? `fake HTTP ${route} held` : `fake HTTP ${route} body will stall`);
|
|
36
|
+
return { release: Effect.sync(gate.release), waiting: Deferred.await(waiting) };
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
2
|
import type { FakeHttpReply, FakeHttpRequest, FakeHttpRoute, FakeHttpServiceOptions } from "#fakeHttpService.ts";
|
|
3
3
|
import type { fakeHttpControls } from "#internal/fakeHttpControls.ts";
|
|
4
|
+
import { fakeHttpSend } from "#internal/fakeHttpSend.ts";
|
|
4
5
|
|
|
5
6
|
function match(route: FakeHttpRoute, method: string, pathname: string) {
|
|
6
7
|
if ((route.method ?? "GET").toUpperCase() !== method) {
|
|
@@ -45,12 +46,6 @@ function pageReply(route: FakeHttpRoute, request: FakeHttpRequest): FakeHttpRepl
|
|
|
45
46
|
return { body: { items: route.pages[page - 1] ?? [], next: page < route.pages.length ? page + 1 : null } };
|
|
46
47
|
}
|
|
47
48
|
|
|
48
|
-
function send(response: ServerResponse, reply: FakeHttpReply) {
|
|
49
|
-
const body = reply.body === undefined ? undefined : JSON.stringify(reply.body);
|
|
50
|
-
response.writeHead(reply.status ?? 200, { "content-type": "application/json", ...reply.headers });
|
|
51
|
-
response.end(body);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
49
|
export async function fakeHttpReceive(
|
|
55
50
|
incoming: IncomingMessage,
|
|
56
51
|
response: ServerResponse,
|
|
@@ -84,29 +79,32 @@ export async function fakeHttpReceive(
|
|
|
84
79
|
tell(`fake HTTP received ${method} ${path}`);
|
|
85
80
|
if (!decodeParams(params)) {
|
|
86
81
|
tell(`fake HTTP rejected ${method} ${path} with 400`);
|
|
87
|
-
|
|
82
|
+
await fakeHttpSend(response, { status: 400 });
|
|
88
83
|
return;
|
|
89
84
|
}
|
|
90
85
|
if (options.guard && !options.guard(request)) {
|
|
91
86
|
tell(`fake HTTP rejected ${method} ${path} with 401`);
|
|
92
|
-
|
|
87
|
+
await fakeHttpSend(response, { status: 401 });
|
|
93
88
|
return;
|
|
94
89
|
}
|
|
95
90
|
if (!found) {
|
|
96
91
|
tell(`fake HTTP answered ${method} ${path} with 404`);
|
|
97
|
-
|
|
92
|
+
await fakeHttpSend(response, { status: 404 });
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
controls.arrive(found.name, response);
|
|
96
|
+
if (!(await controls.pause(found.name, response))) {
|
|
98
97
|
return;
|
|
99
98
|
}
|
|
100
|
-
|
|
101
|
-
const reply = controls.takeFailure(found.name) ?? pageReply(found.route, request);
|
|
99
|
+
const reply = controls.takeReply(found.name) ?? pageReply(found.route, request);
|
|
102
100
|
tell(`fake HTTP answered ${method} ${path} with ${reply.status ?? 200}`);
|
|
103
|
-
|
|
101
|
+
await fakeHttpSend(response, reply, controls.stallBody(found.name));
|
|
104
102
|
} catch (error) {
|
|
105
103
|
tell(`fake HTTP could not answer: ${String(error)}`);
|
|
106
104
|
if (response.headersSent) {
|
|
107
|
-
response.
|
|
105
|
+
response.destroy();
|
|
108
106
|
} else {
|
|
109
|
-
|
|
107
|
+
await fakeHttpSend(response, { status: 500 });
|
|
110
108
|
}
|
|
111
109
|
}
|
|
112
110
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { ServerResponse } from "node:http";
|
|
2
|
+
import type { FakeHttpReply } from "#fakeHttpService.ts";
|
|
3
|
+
|
|
4
|
+
export function fakeHttpConnection(response: ServerResponse) {
|
|
5
|
+
const closed = new Promise<undefined>((resolve) => response.once("close", () => resolve(undefined)));
|
|
6
|
+
return {
|
|
7
|
+
closed,
|
|
8
|
+
pause: async (pending: Promise<void> | undefined) => {
|
|
9
|
+
if (pending !== undefined) {
|
|
10
|
+
await Promise.race([pending, closed]);
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function sendChunks(response: ServerResponse, chunks: NonNullable<FakeHttpReply["chunks"]>, closed: Promise<undefined>) {
|
|
17
|
+
const iterator = Symbol.asyncIterator in chunks ? chunks[Symbol.asyncIterator]() : chunks[Symbol.iterator]();
|
|
18
|
+
try {
|
|
19
|
+
while (!(response.destroyed || response.writableEnded)) {
|
|
20
|
+
const next = await Promise.race([iterator.next(), closed]);
|
|
21
|
+
if (!next || next.done) {
|
|
22
|
+
break;
|
|
23
|
+
}
|
|
24
|
+
await new Promise<void>((resolve, reject) => response.write(next.value, (error) => (error ? reject(error) : resolve())));
|
|
25
|
+
}
|
|
26
|
+
} finally {
|
|
27
|
+
void Promise.resolve(iterator.return?.()).catch(() => response.destroy());
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function replyBody(body: unknown) {
|
|
32
|
+
if (body instanceof Uint8Array || body === undefined) {
|
|
33
|
+
return body;
|
|
34
|
+
}
|
|
35
|
+
return JSON.stringify(body);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function fakeHttpSend(response: ServerResponse, reply: FakeHttpReply, pause?: () => Promise<void>) {
|
|
39
|
+
if (response.destroyed || response.writableEnded) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const connection = fakeHttpConnection(response);
|
|
43
|
+
const body = replyBody(reply.body);
|
|
44
|
+
response.writeHead(reply.status ?? 200, { "content-type": "application/json", ...reply.headers });
|
|
45
|
+
if (pause) {
|
|
46
|
+
response.flushHeaders();
|
|
47
|
+
await connection.pause(pause());
|
|
48
|
+
}
|
|
49
|
+
if (reply.chunks) {
|
|
50
|
+
await sendChunks(response, reply.chunks, connection.closed);
|
|
51
|
+
}
|
|
52
|
+
if (!(response.destroyed || response.writableEnded)) {
|
|
53
|
+
response.end(body);
|
|
54
|
+
}
|
|
55
|
+
}
|