@shivaedev/test-story 0.8.0 → 0.9.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 +19 -0
- package/dist/server/forwardingProxy.d.ts +8 -0
- package/dist/server/forwardingProxy.d.ts.map +1 -0
- package/dist/server/forwardingProxy.js +32 -0
- package/dist/server/forwardingProxy.js.map +1 -0
- package/dist/server/internal/loopback.d.ts +4 -1
- package/dist/server/internal/loopback.d.ts.map +1 -1
- package/dist/server/internal/loopback.js +2 -2
- package/dist/server/internal/loopback.js.map +1 -1
- package/package.json +2 -2
- package/src/server/forwardingProxy.ts +39 -0
- package/src/server/internal/loopback.ts +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.9.0 - 2026-10-18
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `forwardingProxy(upstream, options?)` and `ForwardingProxyOptions` from `@shivaedev/test-story/forwardingProxy.ts` provide a scoped loopback proxy for host stories. It forwards requests and responses, appends the caller to `x-forwarded-for`, and applies the story's configured headers.
|
|
8
|
+
|
|
3
9
|
## 0.8.0 - 2026-10-18
|
|
4
10
|
|
|
5
11
|
### Added
|
package/README.md
CHANGED
|
@@ -233,6 +233,24 @@ app.it("a request continues its supplied trace", [], function* ({ request, reque
|
|
|
233
233
|
|
|
234
234
|
Detachment only removes the incoming parent: spans that application startup explicitly creates still follow native Effect semantics.
|
|
235
235
|
|
|
236
|
+
#### Forwarding requests to a host
|
|
237
|
+
|
|
238
|
+
Use `forwardingProxy` from `@shivaedev/test-story/forwardingProxy.ts` in the kit's scoped `create` or `after` hook after starting the host under test:
|
|
239
|
+
|
|
240
|
+
```ts
|
|
241
|
+
import { forwardingProxy } from "@shivaedev/test-story/forwardingProxy.ts";
|
|
242
|
+
|
|
243
|
+
const proxy = yield* forwardingProxy(upstream, {
|
|
244
|
+
headers: {
|
|
245
|
+
host: "internal.test",
|
|
246
|
+
"x-forwarded-host": "app.example",
|
|
247
|
+
"x-forwarded-proto": "https",
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Point story requests at `proxy.url`. The proxy listens on IPv4 and IPv6 loopback on the same allocated port, forwards the request's method, path, query, body and headers to `upstream`, and returns the host's status, headers and body. It appends the connecting caller's address to `x-forwarded-for`, preserving any existing chain. The optional `headers` map overrides incoming and generated headers, including `x-forwarded-for` when the story needs a fixed chain. The proxy returns 502 if it cannot connect to the host. Both listeners close with the story's Scope.
|
|
253
|
+
|
|
236
254
|
#### Reading OTLP exports
|
|
237
255
|
|
|
238
256
|
Use `otlpReceiver` from `@shivaedev/test-story/otlpReceiver.ts` in a kit's scoped `create` hook. It listens on an allocated loopback port and closes with the story scope. Point the application's real exporter at `${receiver.url}/v1/traces`, then flush the exporter before reading:
|
|
@@ -519,6 +537,7 @@ Import each module with the prefix `@shivaedev/test-story/`; there is no root en
|
|
|
519
537
|
| `genreTags.ts` | `genreTags(...kitNames)`, `GenreTag` |
|
|
520
538
|
| `domEnvironment.config.ts` | Default Vitest environment preserving Node host globals and happy-dom document transport |
|
|
521
539
|
| `fakeHttpService.ts` | `fakeHttpService(options)`, `FakeHttpRoute`, `FakeHttpRequest`, `FakeHttpReply`, `FakeHttpHold`, `FakeHttpServiceOptions` |
|
|
540
|
+
| `forwardingProxy.ts` | `forwardingProxy(upstream, options?)`, `ForwardingProxyOptions` |
|
|
522
541
|
| `otlpReceiver.ts` | `otlpReceiver(options?)`, `OtlpSpan`, `OtlpExport`, `OtlpReceiverOptions` |
|
|
523
542
|
|
|
524
543
|
### Install and limits
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
export interface ForwardingProxyOptions {
|
|
3
|
+
readonly headers?: Readonly<Record<string, string>>;
|
|
4
|
+
}
|
|
5
|
+
export declare const forwardingProxy: (upstream: string, options?: ForwardingProxyOptions | undefined) => Effect.Effect<{
|
|
6
|
+
url: string;
|
|
7
|
+
}, import("effect/Cause").UnknownError, import("effect/Scope").Scope>;
|
|
8
|
+
//# sourceMappingURL=forwardingProxy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"forwardingProxy.d.ts","sourceRoot":"","sources":["../../src/server/forwardingProxy.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAGhC,MAAM,WAAW,sBAAsB;IACtC,QAAQ,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CACpD;AAED,eAAO,MAAM,eAAe;;qEA8B1B,CAAC"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { createServer, request } from "node:http";
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
import { loopbackServer } from "#internal/loopback.ts";
|
|
4
|
+
export const forwardingProxy = Effect.fnUntraced(function* (upstream, options = {}) {
|
|
5
|
+
const forward = (incoming, response) => {
|
|
6
|
+
const outgoing = request(`${upstream}${incoming.url}`, {
|
|
7
|
+
headers: {
|
|
8
|
+
...incoming.headers,
|
|
9
|
+
"x-forwarded-for": [incoming.headers["x-forwarded-for"], incoming.socket.remoteAddress].filter(Boolean).join(", "),
|
|
10
|
+
...options.headers,
|
|
11
|
+
},
|
|
12
|
+
method: incoming.method,
|
|
13
|
+
}, (result) => {
|
|
14
|
+
response.writeHead(result.statusCode ?? 502, result.headers);
|
|
15
|
+
result.pipe(response);
|
|
16
|
+
});
|
|
17
|
+
outgoing.on("error", (error) => {
|
|
18
|
+
if (response.headersSent) {
|
|
19
|
+
response.destroy(error);
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
response.writeHead(502).end();
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
response.on("close", () => outgoing.destroy());
|
|
26
|
+
incoming.pipe(outgoing);
|
|
27
|
+
};
|
|
28
|
+
const url = yield* loopbackServer(createServer(forward));
|
|
29
|
+
yield* loopbackServer(createServer(forward), { host: "::1", port: Number(new URL(url).port) });
|
|
30
|
+
return { url };
|
|
31
|
+
});
|
|
32
|
+
//# sourceMappingURL=forwardingProxy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"forwardingProxy.js","sourceRoot":"","sources":["../../src/server/forwardingProxy.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAwB,OAAO,EAAE,MAAM,WAAW,CAAC;AACxE,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAMvD,MAAM,CAAC,MAAM,eAAe,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,QAAgB,EAAE,OAAO,GAA2B,EAAE;IACjH,MAAM,OAAO,GAAoB,CAAC,QAAQ,EAAE,QAAQ,EAAE,EAAE;QACvD,MAAM,QAAQ,GAAG,OAAO,CACvB,GAAG,QAAQ,GAAG,QAAQ,CAAC,GAAG,EAAE,EAC5B;YACC,OAAO,EAAE;gBACR,GAAG,QAAQ,CAAC,OAAO;gBACnB,iBAAiB,EAAE,CAAC,QAAQ,CAAC,OAAO,CAAC,iBAAiB,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;gBAClH,GAAG,OAAO,CAAC,OAAO;aAClB;YACD,MAAM,EAAE,QAAQ,CAAC,MAAM;SACvB,EACD,CAAC,MAAM,EAAE,EAAE;YACV,QAAQ,CAAC,SAAS,CAAC,MAAM,CAAC,UAAU,IAAI,GAAG,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC;YAC7D,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACvB,CAAC,CACD,CAAC;QACF,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE;YAC9B,IAAI,QAAQ,CAAC,WAAW,EAAE,CAAC;gBAC1B,QAAQ,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;YACzB,CAAC;iBAAM,CAAC;gBACP,QAAQ,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,CAAC;YAC/B,CAAC;QACF,CAAC,CAAC,CAAC;QACH,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC;QAC/C,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACzB,CAAC,CAAC;IACF,MAAM,GAAG,GAAG,KAAK,CAAC,CAAC,cAAc,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC;IACzD,KAAK,CAAC,CAAC,cAAc,CAAC,YAAY,CAAC,OAAO,CAAC,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC/F,OAAO,EAAE,GAAG,EAAE,CAAC;AAChB,CAAC,CAAC,CAAC"}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import type { Server } from "node:http";
|
|
2
2
|
import { Effect } from "effect";
|
|
3
|
-
export declare const loopbackServer: (server: Server<typeof import("http").IncomingMessage, typeof import("http").ServerResponse
|
|
3
|
+
export declare const loopbackServer: (server: Server<typeof import("http").IncomingMessage, typeof import("http").ServerResponse>, options?: {
|
|
4
|
+
readonly host?: string;
|
|
5
|
+
readonly port?: number;
|
|
6
|
+
} | undefined) => Effect.Effect<string, import("effect/Cause").UnknownError, import("effect/Scope").Scope>;
|
|
4
7
|
//# sourceMappingURL=loopback.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loopback.d.ts","sourceRoot":"","sources":["../../../src/server/internal/loopback.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AACxC,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,eAAO,MAAM,cAAc,
|
|
1
|
+
{"version":3,"file":"loopback.d.ts","sourceRoot":"","sources":["../../../src/server/internal/loopback.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AACxC,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,eAAO,MAAM,cAAc;oBAA2E,MAAM;oBAAkB,MAAM;0GA0BlI,CAAC"}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Effect } from "effect";
|
|
2
|
-
export const loopbackServer = Effect.fnUntraced(function* (server) {
|
|
2
|
+
export const loopbackServer = Effect.fnUntraced(function* (server, options = {}) {
|
|
3
3
|
yield* Effect.acquireRelease(Effect.tryPromise(() => new Promise((resolve, reject) => {
|
|
4
4
|
server.once("error", reject);
|
|
5
|
-
server.listen(0, "127.0.0.1", () => {
|
|
5
|
+
server.listen(options.port ?? 0, options.host ?? "127.0.0.1", () => {
|
|
6
6
|
server.off("error", reject);
|
|
7
7
|
resolve();
|
|
8
8
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loopback.js","sourceRoot":"","sources":["../../../src/server/internal/loopback.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,MAAM,CAAC,MAAM,cAAc,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,MAAc;
|
|
1
|
+
{"version":3,"file":"loopback.js","sourceRoot":"","sources":["../../../src/server/internal/loopback.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,MAAM,CAAC,MAAM,cAAc,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,MAAc,EAAE,OAAO,GAAuD,EAAE;IAC1I,KAAK,CAAC,CAAC,MAAM,CAAC,cAAc,CAC3B,MAAM,CAAC,UAAU,CAChB,GAAG,EAAE,CACJ,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QAC7B,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC,EAAE,OAAO,CAAC,IAAI,IAAI,WAAW,EAAE,GAAG,EAAE;YAClE,MAAM,CAAC,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;YAC5B,OAAO,EAAE,CAAC;QACX,CAAC,CAAC,CAAC;IACJ,CAAC,CAAC,CACH,EACD,GAAG,EAAE,CACJ,MAAM,CAAC,OAAO,CACb,GAAG,EAAE,CACJ,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;QAC7D,MAAM,CAAC,mBAAmB,EAAE,CAAC;IAC9B,CAAC,CAAC,CACH,CACF,CAAC;IACF,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,EAAE,CAAC;IACjC,IAAI,OAAO,KAAK,IAAI,IAAI,OAAO,OAAO,KAAK,QAAQ,EAAE,CAAC;QACrD,OAAO,KAAK,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,+GAA+G,CAAC,CAAC;IAC3I,CAAC;IACD,OAAO,oBAAoB,OAAO,CAAC,IAAI,EAAE,CAAC;AAC3C,CAAC,CAAC,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shivaedev/test-story",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.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.27.0",
|
|
50
50
|
"@types/node": "24.19.2",
|
|
51
51
|
"effect": "4.0.4",
|
|
52
52
|
"happy-dom": "20.14.6",
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { createServer, type RequestListener, request } from "node:http";
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
import { loopbackServer } from "#internal/loopback.ts";
|
|
4
|
+
|
|
5
|
+
export interface ForwardingProxyOptions {
|
|
6
|
+
readonly headers?: Readonly<Record<string, string>>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const forwardingProxy = Effect.fnUntraced(function* (upstream: string, options: ForwardingProxyOptions = {}) {
|
|
10
|
+
const forward: RequestListener = (incoming, response) => {
|
|
11
|
+
const outgoing = request(
|
|
12
|
+
`${upstream}${incoming.url}`,
|
|
13
|
+
{
|
|
14
|
+
headers: {
|
|
15
|
+
...incoming.headers,
|
|
16
|
+
"x-forwarded-for": [incoming.headers["x-forwarded-for"], incoming.socket.remoteAddress].filter(Boolean).join(", "),
|
|
17
|
+
...options.headers,
|
|
18
|
+
},
|
|
19
|
+
method: incoming.method,
|
|
20
|
+
},
|
|
21
|
+
(result) => {
|
|
22
|
+
response.writeHead(result.statusCode ?? 502, result.headers);
|
|
23
|
+
result.pipe(response);
|
|
24
|
+
},
|
|
25
|
+
);
|
|
26
|
+
outgoing.on("error", (error) => {
|
|
27
|
+
if (response.headersSent) {
|
|
28
|
+
response.destroy(error);
|
|
29
|
+
} else {
|
|
30
|
+
response.writeHead(502).end();
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
response.on("close", () => outgoing.destroy());
|
|
34
|
+
incoming.pipe(outgoing);
|
|
35
|
+
};
|
|
36
|
+
const url = yield* loopbackServer(createServer(forward));
|
|
37
|
+
yield* loopbackServer(createServer(forward), { host: "::1", port: Number(new URL(url).port) });
|
|
38
|
+
return { url };
|
|
39
|
+
});
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import type { Server } from "node:http";
|
|
2
2
|
import { Effect } from "effect";
|
|
3
3
|
|
|
4
|
-
export const loopbackServer = Effect.fnUntraced(function* (server: Server) {
|
|
4
|
+
export const loopbackServer = Effect.fnUntraced(function* (server: Server, options: { readonly host?: string; readonly port?: number } = {}) {
|
|
5
5
|
yield* Effect.acquireRelease(
|
|
6
6
|
Effect.tryPromise(
|
|
7
7
|
() =>
|
|
8
8
|
new Promise<void>((resolve, reject) => {
|
|
9
9
|
server.once("error", reject);
|
|
10
|
-
server.listen(0, "127.0.0.1", () => {
|
|
10
|
+
server.listen(options.port ?? 0, options.host ?? "127.0.0.1", () => {
|
|
11
11
|
server.off("error", reject);
|
|
12
12
|
resolve();
|
|
13
13
|
});
|