opinionated-machine 10.2.0 → 10.4.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 CHANGED
@@ -1,5 +1,18 @@
1
1
  # opinionated-machine
2
2
 
3
+ ## 10.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 8a388e7: Expose the response body on `SSEInjectConnection`: `getBody()` returns the raw body string and `json<T>()` parses it as JSON, mirroring Fastify's inject response. This lets tests using the untyped `SSEInjectClient` assert on JSON error bodies that an SSE route sends before streaming starts (auth failures, validation errors, unavailable integrations), which previously were unreachable.
8
+
9
+ ## 10.3.0
10
+
11
+ ### Minor Changes
12
+
13
+ - 197d746: Support non-GET requests in `SSEHttpClient.connect()` via new `method` and `body` connect options, so POST/PUT/PATCH SSE endpoints can be tested over real HTTP instead of only through `SSEInjectClient`. `method` is accepted in either case, matching the lowercase spelling used by route contracts. Bodies `fetch()` sends natively (strings, `URLSearchParams`, `FormData`, `Blob`, `ArrayBuffer`, typed arrays, `ReadableStream`) are passed through untouched, everything else is JSON-stringified, and `content-type: application/json` is only defaulted when it does not overwrite an encoding `fetch()` describes itself. The response body is now locked lazily, so `client.response.json()` still works for endpoints that answered with a regular HTTP response (e.g. an error raised before `sse.start()`), and a bodiless response is reported when events are consumed rather than from `connect()`. `awaitServerConnection` now matches the request method as well as the URL, so a path served by both a GET and a POST route resolves the right session; `SpiedSSESession` accordingly requires `request.method`, which every Fastify-backed session already carries.
14
+ - 197d746: Type `SSEConnectOptions.method` as the new exported `SSEInjectMethod`, derived from Fastify's own inject options instead of a hand-listed `'GET' | 'POST' | 'PUT' | 'PATCH'` union. `SSEInjectClient.connectWithBody()` now accepts every method `inject()` accepts (including `DELETE`, `HEAD`, `OPTIONS` and the lowercase spellings), and consumers can import the union instead of redeclaring it.
15
+
3
16
  ## 10.2.0
4
17
 
5
18
  ### Minor Changes
package/README.md CHANGED
@@ -1579,6 +1579,8 @@ it('streams chat completions', async () => {
1579
1579
  })
1580
1580
  ```
1581
1581
 
1582
+ If the route answers with an error status before streaming starts, the response carries a JSON body instead of events - read it with `conn.getBody()` or `conn.json()` (see [SSEInjectClient](#sseinjectclient)).
1583
+
1582
1584
  #### Asserting documented error responses with `bodyForStatus`
1583
1585
 
1584
1586
  When a contract declares `responseBodySchemasByStatusCode` for non-2xx responses (the shape the handler emits via `sse.respond(status, body)` before streaming starts), `injectSSE` / `injectPayloadSSE` expose a typed `bodyForStatus(status)` accessor:
@@ -2251,12 +2253,14 @@ The library provides utilities for testing SSE endpoints.
2251
2253
  | **Event delivery** | All events returned at once (after handler closes) | Events arrive incrementally |
2252
2254
  | **Connection lifecycle** | Handler must close for request to complete | Can stay open indefinitely |
2253
2255
  | **Server requirement** | No `listen()` needed | Requires a listening server (`SSETestServer.start(app)` or manual `app.listen()`) |
2254
- | **Best for** | `autoClose` SSE (OpenAI-style, batch exports) | `keepAlive` SSE (notifications, live feeds, rooms) |
2256
+ | **Request body** | `injectPayloadSSE` / `connectWithBody` | `method` + `body` connect options |
2257
+ | **Assertions before the handler finishes** | Not possible (response is buffered) | `client.response` is available as soon as headers arrive |
2258
+ | **Best for** | `autoClose` SSE (OpenAI-style, batch exports) | `keepAlive` SSE (notifications, live feeds, rooms), streams whose headers must be asserted mid-handler |
2255
2259
  | **Dual-mode sync** | Use `app.inject()` with `accept: 'application/json'` | Same |
2256
2260
 
2257
2261
  #### SSEHttpClient
2258
2262
 
2259
- For testing `keepAlive` SSE connections using real HTTP. Requires a listening server — use `SSETestServer.start(app)` to start your app on a random port:
2263
+ For testing `keepAlive` SSE connections using real HTTP, and for any assertion that has to happen on the wire while the handler is still running. Supports `GET`, `POST`, `PUT` and `PATCH`. Requires a listening server — use `SSETestServer.start(app)` to start your app on a random port:
2260
2264
 
2261
2265
  ```ts
2262
2266
  import { SSEHttpClient } from 'opinionated-machine'
@@ -2368,6 +2372,65 @@ expect(client.response.headers.get('content-type')).toContain('text/event-stream
2368
2372
  client.close()
2369
2373
  ```
2370
2374
 
2375
+ **POST/PUT/PATCH endpoints**
2376
+
2377
+ `connect()` also issues non-GET requests, so SSE endpoints that take a request body can be tested over real HTTP. Pass `method` and `body`. The method is accepted in either case, so the lowercase spelling your contracts already use (`method: 'post'`) works as-is:
2378
+
2379
+ ```ts
2380
+ const client = await SSEHttpClient.connect(server.baseUrl, '/api/chat/completions', {
2381
+ method: 'POST',
2382
+ body: { message: 'Hello', stream: true },
2383
+ })
2384
+
2385
+ const events = await client.collectEvents((event) => event.event === 'done')
2386
+ ```
2387
+
2388
+ Bodies `fetch()` can send natively — strings, `URLSearchParams`, `FormData`, `Blob`/`File`, `ArrayBuffer`, typed arrays (`Buffer`, `Uint8Array`, …) and `ReadableStream` — are passed through untouched; anything else is JSON-stringified. `content-type: application/json` is defaulted for JSON-stringified and string bodies (so a raw string stays verbatim, which is handy for asserting on malformed payloads), unless you provide your own content type. Payloads that describe their own encoding — `URLSearchParams`, `FormData`, `Blob` — keep the content type `fetch()` gives them:
2389
+
2390
+ ```ts
2391
+ // Sent as application/x-www-form-urlencoded, not JSON-stringified into `{}`
2392
+ const client = await SSEHttpClient.connect(server.baseUrl, '/api/chat/completions', {
2393
+ method: 'post',
2394
+ body: new URLSearchParams({ message: 'Hello' }),
2395
+ })
2396
+ ```
2397
+
2398
+ A body without a non-GET `method` throws — `fetch()` cannot attach one to a GET request.
2399
+
2400
+ **Asserting on the wire before the handler finishes**
2401
+
2402
+ `connect()` resolves as soon as HTTP headers arrive, and `client.response` is populated at that point. That is what makes the "open the stream before the slow work starts" behaviour testable: assert on status and headers while the handler is still awaiting its slow call, then let it proceed.
2403
+
2404
+ ```ts
2405
+ // Handler calls sse.start() and only then makes its slow LLM call
2406
+ const client = await SSEHttpClient.connect(server.baseUrl, '/api/chat/completions', {
2407
+ method: 'POST',
2408
+ body: { message: 'Hello', stream: true },
2409
+ })
2410
+
2411
+ // Already on the wire while the LLM call is still in flight
2412
+ expect(client.response.status).toBe(200)
2413
+ expect(client.response.headers.get('content-type')).toContain('text/event-stream')
2414
+
2415
+ releaseSlowCall()
2416
+ const events = await client.collectEvents((event) => event.event === 'done')
2417
+ ```
2418
+
2419
+ The mirror case works too: a failure raised *before* `sse.start()` reaches the client as the JSON status the contract declares, not as a terminal `error` event. The response body is only locked once you start consuming events, so it can still be read as JSON:
2420
+
2421
+ ```ts
2422
+ const client = await SSEHttpClient.connect(server.baseUrl, '/api/chat/completions', {
2423
+ method: 'POST',
2424
+ body: { message: 'Hello', stream: true },
2425
+ })
2426
+
2427
+ expect(client.response.status).toBe(503)
2428
+ expect(client.response.headers.get('content-type')).not.toContain('text/event-stream')
2429
+ expect(await client.response.json()).toEqual({ message: 'Upstream unavailable' })
2430
+ ```
2431
+
2432
+ Read that body *before* `close()`: closing aborts the request, so a body read after it (or from a `finally { client.close() }` block that runs first) rejects with an `AbortError`.
2433
+
2371
2434
  #### SSEInjectClient
2372
2435
 
2373
2436
  For testing `autoClose` SSE streams (like OpenAI completions). Uses Fastify's `inject()` - no `app.listen()` needed:
@@ -2388,12 +2451,39 @@ const conn = await client.connectWithBody(
2388
2451
  { model: 'gpt-4', messages: [...], stream: true },
2389
2452
  )
2390
2453
 
2454
+ // Any other method inject() accepts works too - the option is typed as
2455
+ // `SSEInjectMethod`, exported so you never have to redeclare that union
2456
+ const conn = await client.connectWithBody(
2457
+ '/api/exports/42',
2458
+ { reason: 'cleanup' },
2459
+ { method: 'DELETE' },
2460
+ )
2461
+
2391
2462
  // All events are available immediately (inject waits for handler to complete)
2392
2463
  expect(conn.getStatusCode()).toBe(200)
2393
2464
  const events = conn.getReceivedEvents()
2394
2465
  const chunks = events.filter(e => e.event === 'chunk')
2395
2466
  ```
2396
2467
 
2468
+ When the route answers with a status code *before* streaming starts (auth failure,
2469
+ validation error, integration unavailable), it sends a JSON body rather than events.
2470
+ `getBody()` returns that body raw and `json()` parses it, mirroring Fastify's own
2471
+ inject response:
2472
+
2473
+ ```ts
2474
+ const conn = await client.connect('/api/export/progress')
2475
+
2476
+ expect(conn.getStatusCode()).toBe(503)
2477
+ expect(conn.json<{ errorCode: string }>()).toMatchObject({
2478
+ errorCode: 'INTEGRATION_NOT_AVAILABLE',
2479
+ })
2480
+ ```
2481
+
2482
+ `json()` throws if the body is empty or isn't valid JSON, so it only makes sense for
2483
+ these pre-stream responses - a `text/event-stream` body is not JSON. For contract-typed
2484
+ tests, `injectSSE`/`injectPayloadSSE`/`injectApiSSE` offer `bodyForStatus(status)`, which
2485
+ also validates the body against the contract's schema for that status.
2486
+
2397
2487
  #### Contract-Aware Inject Helpers
2398
2488
 
2399
2489
  For typed testing with SSE contracts:
@@ -4,12 +4,16 @@ import type { SSESession } from './AbstractSSEController.ts';
4
4
  *
5
5
  * Both the `SSESession` produced by this package's own SSE routes and the one
6
6
  * produced by `@lokalise/fastify-api-contracts` (used by `buildApiRoute`)
7
- * satisfy it, which lets a single spy be attached to either route style.
7
+ * satisfy it, which lets a single spy be attached to either route style. Both
8
+ * carry a Fastify request, so `method` is always there to match a waiter
9
+ * against - a path served by both a GET and a POST route needs it to resolve
10
+ * the right session.
8
11
  */
9
12
  export type SpiedSSESession = {
10
13
  id: string;
11
14
  request: {
12
15
  url: string;
16
+ method: string;
13
17
  };
14
18
  };
15
19
  export type SSESessionEvent<TSession extends SpiedSSESession = SSESession> = {
@@ -1 +1 @@
1
- {"version":3,"file":"SSESessionSpy.js","sourceRoot":"","sources":["../../../lib/sse/SSESessionSpy.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"SSESessionSpy.js","sourceRoot":"","sources":["../../../lib/sse/SSESessionSpy.ts"],"names":[],"mappings":"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"}
@@ -1,8 +1,8 @@
1
1
  export { injectApiSSE } from './apiSseInjectHelpers.js';
2
2
  export type { ApiDeclaredResponseBody, ApiDeclaredResponseStatus, ApiSSEEvent, ApiSSEEventReader, InjectApiSSEParams, InjectApiSSEResult, } from './apiSseTestTypes.js';
3
- export { type HasSessionSpy, SSEHttpClient, type SSEHttpConnectOptions, type SSEHttpConnectResult, type SSEHttpConnectWithSessionSpyOptions, type SSEHttpConnectWithSpyOptions, } from './sseHttpClient.js';
3
+ export { type HasSessionSpy, SSEHttpClient, type SSEHttpConnectOptions, type SSEHttpConnectResult, type SSEHttpConnectWithSessionSpyOptions, type SSEHttpConnectWithSpyOptions, type SSEHttpMethod, } from './sseHttpClient.js';
4
4
  export { SSEInjectClient, SSEInjectConnection } from './sseInjectClient.js';
5
5
  export { injectPayloadSSE, injectSSE } from './sseInjectHelpers.js';
6
6
  export { type CreateSSESessionSpyResult, createSSESessionSpy, type SSESessionSpyHooks, type SSESessionSpyRouteOptions, } from './sseSessionSpyFactory.js';
7
7
  export { SSETestServer } from './sseTestServer.js';
8
- export type { InjectPayloadSSEOptions, InjectSSEOptions, InjectSSEResult, SSEConnectOptions, SSEResponse, SSETestConnection, } from './sseTestTypes.js';
8
+ export type { InjectPayloadSSEOptions, InjectSSEOptions, InjectSSEResult, SSEConnectOptions, SSEInjectMethod, SSEResponse, SSETestConnection, } from './sseTestTypes.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../lib/testing/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AASvD,OAAO,EAEL,aAAa,GAKd,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAA;AAC3E,OAAO,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAA;AACnE,OAAO,EAEL,mBAAmB,GAGpB,MAAM,2BAA2B,CAAA;AAClC,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../lib/testing/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AASvD,OAAO,EAEL,aAAa,GAMd,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAA;AAC3E,OAAO,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAA;AACnE,OAAO,EAEL,mBAAmB,GAGpB,MAAM,2BAA2B,CAAA;AAClC,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA"}
@@ -7,6 +7,16 @@ import { type ParsedSSEEvent } from '../sse/sseParser.ts';
7
7
  export type HasSessionSpy = {
8
8
  connectionSpy: SSESessionSpy;
9
9
  };
10
+ /** Canonical, on-the-wire spelling of a supported method. */
11
+ type NormalizedSSEHttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH';
12
+ /**
13
+ * HTTP methods supported when connecting to an SSE endpoint.
14
+ *
15
+ * Both spellings are accepted and normalized internally, so the lowercase form
16
+ * used by route contracts (`buildContract({ method: 'post' })`) can be handed
17
+ * over as-is.
18
+ */
19
+ export type SSEHttpMethod = NormalizedSSEHttpMethod | Lowercase<NormalizedSSEHttpMethod>;
10
20
  /**
11
21
  * Options for connecting to an SSE endpoint via HTTP.
12
22
  */
@@ -15,6 +25,24 @@ export type SSEHttpConnectOptions = {
15
25
  query?: Record<string, string | undefined>;
16
26
  /** Additional headers to send with the request */
17
27
  headers?: Record<string, string>;
28
+ /** HTTP method to use, upper- or lowercase (default: 'GET') */
29
+ method?: SSEHttpMethod;
30
+ /**
31
+ * Request body.
32
+ *
33
+ * Values `fetch()` can send natively - strings, `URLSearchParams`, `FormData`,
34
+ * `Blob`/`File`, `ArrayBuffer`, typed arrays (`Buffer`, `Uint8Array`, ...) and
35
+ * `ReadableStream` - are passed through untouched. Anything else is
36
+ * JSON-stringified.
37
+ *
38
+ * `content-type: application/json` is defaulted for JSON-stringified and string
39
+ * bodies, unless `headers` already provides a content type. Bodies that describe
40
+ * their own encoding (`URLSearchParams`, `FormData`, `Blob`) are left for
41
+ * `fetch()` to label, so their content type is never overwritten with a wrong one.
42
+ *
43
+ * Requires a non-GET `method`.
44
+ */
45
+ body?: unknown;
18
46
  };
19
47
  /**
20
48
  * Options for connecting with automatic server-side connection waiting,
@@ -70,6 +98,13 @@ export type SSEHttpConnectResult<TSession extends SpiedSSESession = SSESession>
70
98
  * - **Long-lived connections** that stay open indefinitely
71
99
  * - **Real-time notifications** where events arrive over time
72
100
  * - **Push-based streaming** where the client waits for server-initiated events
101
+ * - **Assertions on the wire while the handler is still running** - `connect()`
102
+ * resolves as soon as headers arrive, so `response.status` / `response.headers`
103
+ * can be checked before the handler produces its first event
104
+ *
105
+ * GET, POST, PUT and PATCH are all supported (see `method` / `body` in
106
+ * {@link SSEHttpConnectOptions}), so POST SSE endpoints that take a request
107
+ * body can be tested over real HTTP too.
73
108
  *
74
109
  * **When to use SSEHttpClient vs SSEInjectClient:**
75
110
  *
@@ -113,14 +148,33 @@ export type SSEHttpConnectResult<TSession extends SpiedSSESession = SSESession>
113
148
  * ```
114
149
  */
115
150
  export declare class SSEHttpClient {
116
- /** The fetch Response object. Available immediately after connect() returns. */
151
+ /**
152
+ * The fetch Response object. Available immediately after connect() returns,
153
+ * before any event is consumed, so status and headers can be asserted while
154
+ * the handler is still running.
155
+ *
156
+ * The response body is only locked once events are first consumed, so
157
+ * `response.json()` / `response.text()` still work for endpoints that
158
+ * answered with a regular HTTP response instead of a stream (for example an
159
+ * error raised before `sse.start()`). Read that body *before* calling
160
+ * `close()` - `close()` aborts the request, which rejects any pending or
161
+ * subsequent body read with an `AbortError`.
162
+ */
117
163
  readonly response: Response;
118
164
  private readonly abortController;
119
- private readonly reader;
165
+ private readonly responseBody;
166
+ private streamReader;
120
167
  private readonly decoder;
121
168
  private buffer;
122
169
  private closed;
123
170
  private constructor();
171
+ /**
172
+ * Lazily acquire the stream reader, locking the response body on first use.
173
+ *
174
+ * A missing body is reported here rather than from the constructor, so a
175
+ * bodiless response (e.g. `204`) can still be inspected via `response`.
176
+ */
177
+ private get reader();
124
178
  /**
125
179
  * Connect to an SSE endpoint.
126
180
  *
@@ -130,7 +184,7 @@ export declare class SSEHttpClient {
130
184
  *
131
185
  * @param baseUrl - Base URL of the server (e.g., 'http://localhost:3000')
132
186
  * @param path - SSE endpoint path (e.g., '/api/notifications')
133
- * @param options - Connection options (query params, headers)
187
+ * @param options - Connection options (method, body, query params, headers)
134
188
  * @returns Connected SSE client ready to receive events
135
189
  *
136
190
  * @example
@@ -142,6 +196,16 @@ export declare class SSEHttpClient {
142
196
  * { query: { userId: '123' }, headers: { authorization: 'Bearer token' } }
143
197
  * )
144
198
  *
199
+ * // POST with a JSON body (content-type defaults to application/json)
200
+ * const client = await SSEHttpClient.connect(
201
+ * 'http://localhost:3000',
202
+ * '/api/chat/completions',
203
+ * { method: 'POST', body: { message: 'Hello', stream: true } }
204
+ * )
205
+ * // Headers are on the wire before the handler finished its slow work
206
+ * expect(client.response.status).toBe(200)
207
+ * expect(client.response.headers.get('content-type')).toContain('text/event-stream')
208
+ *
145
209
  * // With awaitServerConnection (waits for server-side registration)
146
210
  * const { client, serverConnection } = await SSEHttpClient.connect(
147
211
  * 'http://localhost:3000',
@@ -230,6 +294,11 @@ export declare class SSEHttpClient {
230
294
  *
231
295
  * This aborts the underlying fetch request. Call this when done
232
296
  * consuming events to clean up resources.
297
+ *
298
+ * Aborting also discards any unread response body, so for a non-stream
299
+ * response (an error raised before `sse.start()`, say) read
300
+ * `response.json()` / `response.text()` before calling this.
233
301
  */
234
302
  close(): void;
235
303
  }
304
+ export {};
@@ -1,5 +1,54 @@
1
1
  import { stringify } from 'fast-querystring';
2
2
  import { parseSSEBuffer } from "../sse/sseParser.js";
3
+ /**
4
+ * Whether `fetch()` can send this value as-is. Everything else is JSON-stringified,
5
+ * so binary and form payloads are not silently mangled into `{}` or `{"0":1}`.
6
+ */
7
+ function isFetchBody(body) {
8
+ return (typeof body === 'string' ||
9
+ body instanceof URLSearchParams ||
10
+ body instanceof FormData ||
11
+ body instanceof Blob ||
12
+ body instanceof ArrayBuffer ||
13
+ ArrayBuffer.isView(body) ||
14
+ body instanceof ReadableStream);
15
+ }
16
+ /**
17
+ * Whether the body carries its own content type, which `fetch()` sets correctly
18
+ * (form encoding, multipart boundary, blob type) and we must not overwrite.
19
+ */
20
+ function describesOwnContentType(body) {
21
+ return body instanceof URLSearchParams || body instanceof FormData || body instanceof Blob;
22
+ }
23
+ function hasContentTypeHeader(headers) {
24
+ return Object.keys(headers).some((header) => header.toLowerCase() === 'content-type');
25
+ }
26
+ /**
27
+ * Resolve the value handed to `fetch()` as the request body, defaulting the
28
+ * content type (into `headers`) whenever we are the ones choosing the encoding.
29
+ */
30
+ function buildRequestBody(body, method, headers) {
31
+ if (body === undefined) {
32
+ return undefined;
33
+ }
34
+ if (method === 'GET') {
35
+ throw new Error("SSEHttpClient.connect(): a request body requires a non-GET method. Pass e.g. { method: 'POST', body }.");
36
+ }
37
+ if (isFetchBody(body)) {
38
+ if (!describesOwnContentType(body) && !hasContentTypeHeader(headers)) {
39
+ headers['content-type'] = 'application/json';
40
+ }
41
+ return body;
42
+ }
43
+ const serializedBody = JSON.stringify(body);
44
+ if (serializedBody === undefined) {
45
+ throw new Error(`SSEHttpClient.connect(): a body of type ${typeof body} cannot be serialized to JSON. Pass a JSON-serializable value, a string, or a body fetch() accepts natively.`);
46
+ }
47
+ if (!hasContentTypeHeader(headers)) {
48
+ headers['content-type'] = 'application/json';
49
+ }
50
+ return serializedBody;
51
+ }
3
52
  /**
4
53
  * SSE client for testing long-lived connections using real HTTP.
5
54
  *
@@ -10,6 +59,13 @@ import { parseSSEBuffer } from "../sse/sseParser.js";
10
59
  * - **Long-lived connections** that stay open indefinitely
11
60
  * - **Real-time notifications** where events arrive over time
12
61
  * - **Push-based streaming** where the client waits for server-initiated events
62
+ * - **Assertions on the wire while the handler is still running** - `connect()`
63
+ * resolves as soon as headers arrive, so `response.status` / `response.headers`
64
+ * can be checked before the handler produces its first event
65
+ *
66
+ * GET, POST, PUT and PATCH are all supported (see `method` / `body` in
67
+ * {@link SSEHttpConnectOptions}), so POST SSE endpoints that take a request
68
+ * body can be tested over real HTTP too.
13
69
  *
14
70
  * **When to use SSEHttpClient vs SSEInjectClient:**
15
71
  *
@@ -53,20 +109,44 @@ import { parseSSEBuffer } from "../sse/sseParser.js";
53
109
  * ```
54
110
  */
55
111
  export class SSEHttpClient {
56
- /** The fetch Response object. Available immediately after connect() returns. */
112
+ /**
113
+ * The fetch Response object. Available immediately after connect() returns,
114
+ * before any event is consumed, so status and headers can be asserted while
115
+ * the handler is still running.
116
+ *
117
+ * The response body is only locked once events are first consumed, so
118
+ * `response.json()` / `response.text()` still work for endpoints that
119
+ * answered with a regular HTTP response instead of a stream (for example an
120
+ * error raised before `sse.start()`). Read that body *before* calling
121
+ * `close()` - `close()` aborts the request, which rejects any pending or
122
+ * subsequent body read with an `AbortError`.
123
+ */
57
124
  response;
58
125
  abortController;
59
- reader;
126
+ responseBody;
127
+ streamReader;
60
128
  decoder = new TextDecoder();
61
129
  buffer = '';
62
130
  closed = false;
63
131
  constructor(response, abortController) {
64
132
  this.response = response;
65
133
  this.abortController = abortController;
66
- if (!response.body) {
67
- throw new Error('SSE response has no body');
134
+ this.responseBody = response.body;
135
+ }
136
+ /**
137
+ * Lazily acquire the stream reader, locking the response body on first use.
138
+ *
139
+ * A missing body is reported here rather than from the constructor, so a
140
+ * bodiless response (e.g. `204`) can still be inspected via `response`.
141
+ */
142
+ get reader() {
143
+ if (!this.streamReader) {
144
+ if (!this.responseBody) {
145
+ throw new Error(`SSE response has no body to stream (status ${this.response.status}). Inspect \`client.response\` instead of consuming events.`);
146
+ }
147
+ this.streamReader = this.responseBody.getReader();
68
148
  }
69
- this.reader = response.body.getReader();
149
+ return this.streamReader;
70
150
  }
71
151
  static async connect(baseUrl, path, options) {
72
152
  // Build path with query string
@@ -77,30 +157,34 @@ export class SSEHttpClient {
77
157
  pathWithQuery = `${path}?${queryString}`;
78
158
  }
79
159
  }
160
+ // Normalize once - contracts spell methods in lowercase, the wire and Node use uppercase
161
+ const method = (options?.method ?? 'GET').toUpperCase();
162
+ const headers = {
163
+ Accept: 'text/event-stream',
164
+ ...options?.headers,
165
+ };
166
+ const body = buildRequestBody(options?.body, method, headers);
80
167
  // Connect - fetch() returns when headers are received
81
168
  const abortController = new AbortController();
82
- const response = await fetch(`${baseUrl}${pathWithQuery}`, {
83
- headers: {
84
- Accept: 'text/event-stream',
85
- ...options?.headers,
86
- },
169
+ const requestInit = {
170
+ method,
171
+ headers,
172
+ body,
87
173
  signal: abortController.signal,
88
- });
89
- let client;
90
- try {
91
- client = new SSEHttpClient(response, abortController);
92
- }
93
- catch (error) {
94
- // Nothing owns the response yet, so abort it here rather than leaking it.
95
- abortController.abort();
96
- throw error;
174
+ };
175
+ if (body instanceof ReadableStream) {
176
+ // A streamed request body has to declare half-duplex explicitly, or fetch() rejects it
177
+ ;
178
+ requestInit.duplex = 'half';
97
179
  }
180
+ const response = await fetch(`${baseUrl}${pathWithQuery}`, requestInit);
181
+ const client = new SSEHttpClient(response, abortController);
98
182
  // If awaitServerConnection is specified, wait for server-side registration
99
183
  if (options && 'awaitServerConnection' in options && options.awaitServerConnection) {
100
184
  const awaitOptions = options.awaitServerConnection;
101
185
  const waitOptions = {
102
186
  timeout: awaitOptions.timeout ?? 5000,
103
- predicate: (conn) => conn.request.url === pathWithQuery,
187
+ predicate: (conn) => conn.request.url === pathWithQuery && conn.request.method.toUpperCase() === method,
104
188
  };
105
189
  try {
106
190
  // Both branches call the same method; the spy is invariant in its session
@@ -272,11 +356,17 @@ export class SSEHttpClient {
272
356
  *
273
357
  * This aborts the underlying fetch request. Call this when done
274
358
  * consuming events to clean up resources.
359
+ *
360
+ * Aborting also discards any unread response body, so for a non-stream
361
+ * response (an error raised before `sse.start()`, say) read
362
+ * `response.json()` / `response.text()` before calling this.
275
363
  */
276
364
  close() {
277
365
  this.closed = true;
278
- // Cancel the reader first to prevent unhandled rejections from pending reads
279
- this.reader.cancel().catch(() => {
366
+ // Cancel the reader first to prevent unhandled rejections from pending reads.
367
+ // Only touch it if events were actually consumed - acquiring it here would
368
+ // lock a body the caller may still want to read as JSON/text.
369
+ this.streamReader?.cancel().catch(() => {
280
370
  // Expected: may already be closed or errored
281
371
  });
282
372
  this.abortController.abort();
@@ -1 +1 @@
1
- {"version":3,"file":"sseHttpClient.js","sourceRoot":"","sources":["../../../lib/testing/sseHttpClient.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAG5C,OAAO,EAAuB,cAAc,EAAE,MAAM,qBAAqB,CAAA;AAiEzE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AACH,MAAM,OAAO,aAAa;IACxB,gFAAgF;IACvE,QAAQ,CAAU;IACV,eAAe,CAAiB;IAChC,MAAM,CAAyC;IAC/C,OAAO,GAAG,IAAI,WAAW,EAAE,CAAA;IACpC,MAAM,GAAG,EAAE,CAAA;IACX,MAAM,GAAG,KAAK,CAAA;IAEtB,YAAoB,QAAkB,EAAE,eAAgC;QACtE,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAA;QACxB,IAAI,CAAC,eAAe,GAAG,eAAe,CAAA;QACtC,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;YACnB,MAAM,IAAI,KAAK,CAAC,0BAA0B,CAAC,CAAA;QAC7C,CAAC;QACD,IAAI,CAAC,MAAM,GAAG,QAAQ,CAAC,IAAI,CAAC,SAAS,EAAE,CAAA;IACzC,CAAC;IA0DD,MAAM,CAAC,KAAK,CAAC,OAAO,CAClB,OAAe,EACf,IAAY,EACZ,OAGwD;QAExD,+BAA+B;QAC/B,IAAI,aAAa,GAAG,IAAI,CAAA;QACxB,IAAI,OAAO,EAAE,KAAK,EAAE,CAAC;YACnB,MAAM,WAAW,GAAG,SAAS,CAAC,OAAO,CAAC,KAAK,CAAC,CAAA;YAC5C,IAAI,WAAW,EAAE,CAAC;gBAChB,aAAa,GAAG,GAAG,IAAI,IAAI,WAAW,EAAE,CAAA;YAC1C,CAAC;QACH,CAAC;QAED,sDAAsD;QACtD,MAAM,eAAe,GAAG,IAAI,eAAe,EAAE,CAAA;QAC7C,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,OAAO,GAAG,aAAa,EAAE,EAAE;YACzD,OAAO,EAAE;gBACP,MAAM,EAAE,mBAAmB;gBAC3B,GAAG,OAAO,EAAE,OAAO;aACpB;YACD,MAAM,EAAE,eAAe,CAAC,MAAM;SAC/B,CAAC,CAAA;QAEF,IAAI,MAAqB,CAAA;QACzB,IAAI,CAAC;YACH,MAAM,GAAG,IAAI,aAAa,CAAC,QAAQ,EAAE,eAAe,CAAC,CAAA;QACvD,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,0EAA0E;YAC1E,eAAe,CAAC,KAAK,EAAE,CAAA;YACvB,MAAM,KAAK,CAAA;QACb,CAAC;QAED,2EAA2E;QAC3E,IAAI,OAAO,IAAI,uBAAuB,IAAI,OAAO,IAAI,OAAO,CAAC,qBAAqB,EAAE,CAAC;YACnF,MAAM,YAAY,GAAG,OAAO,CAAC,qBAAqB,CAAA;YAClD,MAAM,WAAW,GAAG;gBAClB,OAAO,EAAE,YAAY,CAAC,OAAO,IAAI,IAAI;gBACrC,SAAS,EAAE,CAAC,IAAqB,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,KAAK,aAAa;aACzE,CAAA;YACD,IAAI,CAAC;gBACH,0EAA0E;gBAC1E,wDAAwD;gBACxD,MAAM,gBAAgB,GACpB,KAAK,IAAI,YAAY;oBACnB,CAAC,CAAC,MAAM,YAAY,CAAC,GAAG,CAAC,iBAAiB,CAAC,WAAW,CAAC;oBACvD,CAAC,CAAC,MAAM,YAAY,CAAC,UAAU,CAAC,aAAa,CAAC,iBAAiB,CAAC,WAAW,CAAC,CAAA;gBAChF,OAAO,EAAE,MAAM,EAAE,gBAAgB,EAAE,CAAA;YACrC,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,yEAAyE;gBACzE,0EAA0E;gBAC1E,yEAAyE;gBACzE,gCAAgC;gBAChC,MAAM,CAAC,KAAK,EAAE,CAAA;gBACd,MAAM,KAAK,CAAA;YACb,CAAC;QACH,CAAC;QAED,OAAO,MAAM,CAAA;IACf,CAAC;IAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA8BG;IACH,KAAK,CAAC,CAAC,MAAM,CAAC,MAAoB;QAChC,OAAO,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;YACpB,IAAI,MAAM,EAAE,OAAO,EAAE,CAAC;gBACpB,OAAM;YACR,CAAC;YAED,MAAM,UAAU,GAAG,MAAM,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,CAAA;YACnD,IAAI,UAAU,KAAK,SAAS,EAAE,CAAC;gBAC7B,OAAM;YACR,CAAC;YAED,IAAI,UAAU,CAAC,IAAI,EAAE,CAAC;gBACpB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAA;gBAClB,MAAK;YACP,CAAC;YAED,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAA;YACtE,MAAM,WAAW,GAAG,cAAc,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;YAC/C,IAAI,CAAC,MAAM,GAAG,WAAW,CAAC,SAAS,CAAA;YAEnC,KAAK,MAAM,KAAK,IAAI,WAAW,CAAC,MAAM,EAAE,CAAC;gBACvC,IAAI,MAAM,EAAE,OAAO,EAAE,CAAC;oBACpB,OAAM;gBACR,CAAC;gBACD,MAAM,KAAK,CAAA;YACb,CAAC;QACH,CAAC;IACH,CAAC;IAED;;;OAGG;IACK,KAAK,CAAC,aAAa,CAAC,MAAoB;QAC9C,MAAM,WAAW,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAA;QAEtC,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,OAAO,WAAW,CAAA;QACpB,CAAC;QAED,IAAI,WAAW,GAAG,KAAK,CAAA;QAEvB,MAAM,YAAY,GAAG,IAAI,OAAO,CAAY,CAAC,OAAO,EAAE,EAAE;YACtD,MAAM,OAAO,GAAG,GAAG,EAAE;gBACnB,IAAI,CAAC,WAAW,EAAE,CAAC;oBACjB,OAAO,CAAC,SAAS,CAAC,CAAA;gBACpB,CAAC;YACH,CAAC,CAAA;YACD,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;gBACnB,OAAO,EAAE,CAAA;YACX,CAAC;iBAAM,CAAC;gBACN,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAA;YAC3D,CAAC;QACH,CAAC,CAAC,CAAA;QAEF,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE,YAAY,CAAC,CAAC,CAAA;QAC9D,WAAW,GAAG,IAAI,CAAA;QAElB,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;YACzB,qDAAqD;YACrD,WAAW,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAA;QAC7B,CAAC;QAED,OAAO,MAAM,CAAA;IACf,CAAC;IAED;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,KAAK,CAAC,aAAa,CACjB,gBAA+D,EAC/D,OAAO,GAAG,IAAI;QAEd,MAAM,SAAS,GAAqB,EAAE,CAAA;QACtC,MAAM,OAAO,GAAG,OAAO,gBAAgB,KAAK,QAAQ,CAAA;QACpD,MAAM,eAAe,GAAG,IAAI,eAAe,EAAE,CAAA;QAC7C,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC,eAAe,CAAC,MAAM,CAAC,CAAA;QACpD,IAAI,QAAQ,GAAG,KAAK,CAAA;QAEpB,MAAM,SAAS,GAAG,UAAU,CAAC,GAAG,EAAE;YAChC,QAAQ,GAAG,IAAI,CAAA;YACf,eAAe,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,kCAAkC,SAAS,CAAC,MAAM,GAAG,CAAC,CAAC,CAAA;QACzF,CAAC,EAAE,OAAO,CAAC,CAAA;QAEX,IAAI,CAAC;YACH,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,QAAQ,EAAE,CAAC;gBACnC,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;gBAErB,IAAI,OAAO,IAAI,SAAS,CAAC,MAAM,IAAI,gBAAgB,EAAE,CAAC;oBACpD,MAAK;gBACP,CAAC;gBACD,IAAI,CAAC,OAAO,IAAI,gBAAgB,CAAC,KAAK,CAAC,EAAE,CAAC;oBACxC,MAAK;gBACP,CAAC;YACH,CAAC;YAED,2EAA2E;YAC3E,IAAI,QAAQ,EAAE,CAAC;gBACb,MAAM,eAAe,CAAC,MAAM,CAAC,MAAM,CAAA;YACrC,CAAC;QACH,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,iDAAiD;YACjD,IAAI,QAAQ,IAAI,eAAe,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;gBAC/C,MAAM,eAAe,CAAC,MAAM,CAAC,MAAM,CAAA;YACrC,CAAC;YACD,MAAM,GAAG,CAAA;QACX,CAAC;gBAAS,CAAC;YACT,YAAY,CAAC,SAAS,CAAC,CAAA;YACvB,eAAe,CAAC,KAAK,EAAE,CAAA,CAAC,0CAA0C;QACpE,CAAC;QAED,OAAO,SAAS,CAAA;IAClB,CAAC;IAED;;;;;OAKG;IACH,KAAK;QACH,IAAI,CAAC,MAAM,GAAG,IAAI,CAAA;QAClB,6EAA6E;QAC7E,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE;YAC9B,6CAA6C;QAC/C,CAAC,CAAC,CAAA;QACF,IAAI,CAAC,eAAe,CAAC,KAAK,EAAE,CAAA;IAC9B,CAAC;CACF"}
1
+ {"version":3,"file":"sseHttpClient.js","sourceRoot":"","sources":["../../../lib/testing/sseHttpClient.ts"],"names":[],"mappings":"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"}
@@ -54,6 +54,23 @@ export declare class SSEInjectConnection implements SSETestConnection {
54
54
  * Get the response headers.
55
55
  */
56
56
  getHeaders(): Record<string, string | string[] | undefined>;
57
+ /**
58
+ * Get the raw response body as a string.
59
+ *
60
+ * For a streaming response this is the raw `text/event-stream` payload the
61
+ * events were parsed from. For a route that answered with a status code
62
+ * before streaming started (auth failure, validation error, integration
63
+ * unavailable) it is that non-streaming body - typically JSON.
64
+ */
65
+ getBody(): string;
66
+ /**
67
+ * Parse the raw response body as JSON, mirroring Fastify's inject `json()`.
68
+ *
69
+ * @throws if the body is empty or not valid JSON. A `text/event-stream` body
70
+ * is not JSON, so this only makes sense for responses emitted before
71
+ * streaming started.
72
+ */
73
+ json<T = unknown>(): T;
57
74
  }
58
75
  /**
59
76
  * SSE client using Fastify's inject() for testing SSE endpoints.
@@ -1,4 +1,5 @@
1
1
  import { parseSSEEvents } from "../sse/sseParser.js";
2
+ import { truncateBody } from "./sseInjectShared.js";
2
3
  /**
3
4
  * SSE connection object returned by SSEInjectClient.
4
5
  *
@@ -80,6 +81,36 @@ export class SSEInjectConnection {
80
81
  getHeaders() {
81
82
  return this.response.headers;
82
83
  }
84
+ /**
85
+ * Get the raw response body as a string.
86
+ *
87
+ * For a streaming response this is the raw `text/event-stream` payload the
88
+ * events were parsed from. For a route that answered with a status code
89
+ * before streaming started (auth failure, validation error, integration
90
+ * unavailable) it is that non-streaming body - typically JSON.
91
+ */
92
+ getBody() {
93
+ return this.response.body;
94
+ }
95
+ /**
96
+ * Parse the raw response body as JSON, mirroring Fastify's inject `json()`.
97
+ *
98
+ * @throws if the body is empty or not valid JSON. A `text/event-stream` body
99
+ * is not JSON, so this only makes sense for responses emitted before
100
+ * streaming started.
101
+ */
102
+ json() {
103
+ const { body } = this.response;
104
+ if (!body) {
105
+ throw new Error(`json() — response body is empty (status ${this.response.statusCode})`);
106
+ }
107
+ try {
108
+ return JSON.parse(body);
109
+ }
110
+ catch (err) {
111
+ throw new Error(`json() — body is not valid JSON: ${err.message}; body: ${truncateBody(body)}`);
112
+ }
113
+ }
83
114
  }
84
115
  /**
85
116
  * SSE client using Fastify's inject() for testing SSE endpoints.
@@ -1 +1 @@
1
- {"version":3,"file":"sseInjectClient.js","sourceRoot":"","sources":["../../../lib/testing/sseInjectClient.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"sseInjectClient.js","sourceRoot":"","sources":["../../../lib/testing/sseInjectClient.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,16 @@
1
1
  import type { AnySSEContractDefinition, HttpStatusCode } from '@lokalise/api-contracts';
2
+ import type { InjectOptions } from 'fastify';
2
3
  import type { z } from 'zod';
3
4
  import type { ParsedSSEEvent } from '../sse/sseParser.ts';
5
+ /**
6
+ * HTTP methods Fastify's `inject()` accepts, in either case.
7
+ *
8
+ * Derived from Fastify's own inject options instead of being hand-listed, so it
9
+ * tracks whatever the installed Fastify supports and never needs to be redefined
10
+ * downstream. Note that Fastify's `HTTPMethods` is wider than this - it also
11
+ * covers `SEARCH`, `QUERY` and the WebDAV verbs, which `inject()` does not take.
12
+ */
13
+ export type SSEInjectMethod = NonNullable<InjectOptions['method']>;
4
14
  /** Safely infer the output type of an optional Zod schema property. */
5
15
  type InferOptionalSchema<T, Fallback = unknown> = NonNullable<T> extends z.ZodTypeAny ? z.infer<NonNullable<T>> : Fallback;
6
16
  /**
@@ -50,13 +60,42 @@ export interface SSETestConnection {
50
60
  * Get response headers.
51
61
  */
52
62
  getHeaders(): Record<string, string | string[] | undefined>;
63
+ /**
64
+ * Get the raw response body as a string.
65
+ *
66
+ * For a successful SSE response this is the raw `text/event-stream` payload
67
+ * (already parsed into events, available via `getReceivedEvents()`). It is
68
+ * most useful when the route answered with a status code before streaming
69
+ * started - an auth failure, a validation error, an unavailable integration -
70
+ * and responded with a JSON body instead of events.
71
+ */
72
+ getBody(): string;
73
+ /**
74
+ * Parse the raw response body as JSON.
75
+ *
76
+ * Mirrors Fastify's own inject response `json()`. Intended for non-streaming
77
+ * responses emitted before streaming starts; calling it on an actual SSE
78
+ * stream body throws, since `text/event-stream` is not JSON.
79
+ *
80
+ * @throws if the body is empty or not valid JSON (the message includes a
81
+ * truncated body snippet).
82
+ *
83
+ * @example
84
+ * ```typescript
85
+ * const conn = await client.connect('/api/stream')
86
+ * expect(conn.getStatusCode()).toBe(503)
87
+ * expect(conn.json()).toMatchObject({ errorCode: 'INTEGRATION_NOT_AVAILABLE' })
88
+ * ```
89
+ */
90
+ json<T = unknown>(): T;
53
91
  }
54
92
  /**
55
93
  * Options for establishing an SSE connection.
56
94
  */
57
95
  export type SSEConnectOptions = {
58
96
  headers?: Record<string, string>;
59
- method?: 'GET' | 'POST' | 'PUT' | 'PATCH';
97
+ /** Any method `inject()` accepts (default: `'POST'` for `connectWithBody`) */
98
+ method?: SSEInjectMethod;
60
99
  body?: unknown;
61
100
  };
62
101
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opinionated-machine",
3
- "version": "10.2.0",
3
+ "version": "10.4.0",
4
4
  "description": "Very opinionated DI framework for fastify, built on top of awilix ",
5
5
  "type": "module",
6
6
  "license": "MIT",