opinionated-machine 6.19.1 → 6.20.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/README.md CHANGED
@@ -1511,6 +1511,39 @@ it('streams chat completions', async () => {
1511
1511
  })
1512
1512
  ```
1513
1513
 
1514
+ #### Asserting documented error responses with `bodyForStatus`
1515
+
1516
+ 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:
1517
+
1518
+ ```ts
1519
+ import { buildSseContract } from '@lokalise/api-contracts'
1520
+ import { z } from 'zod'
1521
+ import { injectSSE } from 'opinionated-machine'
1522
+
1523
+ const streamContract = buildSseContract({
1524
+ method: 'get',
1525
+ pathResolver: () => '/api/stream',
1526
+ requestQuerySchema: z.object({}),
1527
+ requestHeaderSchema: z.object({}),
1528
+ responseBodySchemasByStatusCode: {
1529
+ 401: z.object({ message: z.string() }),
1530
+ 404: z.object({ resourceId: z.string() }),
1531
+ },
1532
+ serverSentEventSchemas: { message: z.object({ text: z.string() }) },
1533
+ })
1534
+
1535
+ it('returns the documented 401 body when unauthenticated', async () => {
1536
+ const { bodyForStatus } = injectSSE(app, streamContract, {})
1537
+
1538
+ // `body` is typed as `{ message: string }` — the 401 schema.
1539
+ // TS rejects status codes the contract doesn't declare, e.g. bodyForStatus(500).
1540
+ const body = await bodyForStatus(401)
1541
+ expect(body.message).toBe('Unauthorized')
1542
+ })
1543
+ ```
1544
+
1545
+ `bodyForStatus(status)` awaits the response, asserts the actual status matches, JSON-parses the body, and runs it through the Zod schema declared for that status. It throws — with the offending status and a truncated body snippet — if the status doesn't match, the contract declares no schema for that status, the body isn't valid JSON, or Zod parsing fails. The raw `closed` promise is still exposed for callers that want to read `body: string` directly.
1546
+
1514
1547
  ### SSESessionSpy API
1515
1548
 
1516
1549
  The `connectionSpy` is available when `isTestMode: true` is passed to `asSSEControllerClass`:
@@ -1,7 +1,19 @@
1
- import type { SSEContractDefinition } from '@lokalise/api-contracts';
1
+ import type { HttpStatusCode, SSEContractDefinition } from '@lokalise/api-contracts';
2
2
  import type { z } from 'zod';
3
3
  import type { AnyFastifyInstance } from './AnyFastifyInstance.ts';
4
- import type { InjectPayloadSSEOptions, InjectSSEOptions, InjectSSEResult } from './sseTestTypes.ts';
4
+ import type { InjectPayloadSSEOptions, InjectSSEOptions, InjectSSEResult, SSEResponse } from './sseTestTypes.ts';
5
+ /**
6
+ * Build a `bodyForStatus` accessor bound to one inject call. The closure
7
+ * captures the contract's schemas map so the resulting helper knows which
8
+ * schemas to parse against; at the type level the caller is constrained to
9
+ * status codes the contract actually declares.
10
+ *
11
+ * @internal Exported only for unit testing — not part of the public API
12
+ * (the testing barrel re-exports `injectSSE`/`injectPayloadSSE` by name).
13
+ */
14
+ export declare function bindBodyForStatus<Schemas extends Partial<Record<HttpStatusCode, z.ZodTypeAny>> | undefined>(contract: {
15
+ responseBodySchemasByStatusCode?: Schemas;
16
+ }, closed: Promise<SSEResponse>): InjectSSEResult<Schemas>['bodyForStatus'];
5
17
  /**
6
18
  * Inject a GET SSE request using a contract definition.
7
19
  *
@@ -21,7 +33,7 @@ import type { InjectPayloadSSEOptions, InjectSSEOptions, InjectSSEResult } from
21
33
  * const events = parseSSEEvents(result.body)
22
34
  * ```
23
35
  */
24
- export declare function injectSSE<Contract extends SSEContractDefinition<'get', z.ZodTypeAny, z.ZodTypeAny, z.ZodTypeAny, undefined, Record<string, z.ZodTypeAny>>>(app: AnyFastifyInstance, contract: Contract, options?: InjectSSEOptions<Contract>): InjectSSEResult;
36
+ export declare function injectSSE<Contract extends SSEContractDefinition<'get', z.ZodTypeAny, z.ZodTypeAny, z.ZodTypeAny, undefined, Record<string, z.ZodTypeAny>, Partial<Record<HttpStatusCode, z.ZodTypeAny>> | undefined>>(app: AnyFastifyInstance, contract: Contract, options?: InjectSSEOptions<Contract>): InjectSSEResult<Contract['responseBodySchemasByStatusCode']>;
25
37
  /**
26
38
  * Inject a POST/PUT/PATCH SSE request using a contract definition.
27
39
  *
@@ -49,4 +61,4 @@ export declare function injectSSE<Contract extends SSEContractDefinition<'get',
49
61
  * )
50
62
  * ```
51
63
  */
52
- export declare function injectPayloadSSE<Contract extends SSEContractDefinition<'post' | 'put' | 'patch', z.ZodTypeAny, z.ZodTypeAny, z.ZodTypeAny, z.ZodTypeAny, Record<string, z.ZodTypeAny>>>(app: AnyFastifyInstance, contract: Contract, options: InjectPayloadSSEOptions<Contract>): InjectSSEResult;
64
+ export declare function injectPayloadSSE<Contract extends SSEContractDefinition<'post' | 'put' | 'patch', z.ZodTypeAny, z.ZodTypeAny, z.ZodTypeAny, z.ZodTypeAny, Record<string, z.ZodTypeAny>, Partial<Record<HttpStatusCode, z.ZodTypeAny>> | undefined>>(app: AnyFastifyInstance, contract: Contract, options: InjectPayloadSSEOptions<Contract>): InjectSSEResult<Contract['responseBodySchemasByStatusCode']>;
@@ -1,3 +1,54 @@
1
+ /** Truncate a long body string for error messages. */
2
+ const BODY_TRUNCATE_LIMIT = 500;
3
+ const truncateBody = (body) => {
4
+ if (body.length <= BODY_TRUNCATE_LIMIT) {
5
+ return body;
6
+ }
7
+ // Step back one unit if the cut would split a surrogate pair, so the
8
+ // snippet never ends in a lone (invalid) surrogate.
9
+ const lastCode = body.charCodeAt(BODY_TRUNCATE_LIMIT - 1);
10
+ const end = lastCode >= 0xd800 && lastCode <= 0xdbff ? BODY_TRUNCATE_LIMIT - 1 : BODY_TRUNCATE_LIMIT;
11
+ return `${body.slice(0, end)}…`;
12
+ };
13
+ /**
14
+ * Build a `bodyForStatus` accessor bound to one inject call. The closure
15
+ * captures the contract's schemas map so the resulting helper knows which
16
+ * schemas to parse against; at the type level the caller is constrained to
17
+ * status codes the contract actually declares.
18
+ *
19
+ * @internal Exported only for unit testing — not part of the public API
20
+ * (the testing barrel re-exports `injectSSE`/`injectPayloadSSE` by name).
21
+ */
22
+ export function bindBodyForStatus(contract, closed) {
23
+ // A generic arrow function can't be assigned directly to the generic
24
+ // method signature, so the whole closure is cast once. Keep this
25
+ // implementation in sync with `InjectSSEResult['bodyForStatus']`.
26
+ return (async (statusCode) => {
27
+ const res = await closed;
28
+ const expected = statusCode;
29
+ if (res.statusCode !== expected) {
30
+ throw new Error(`bodyForStatus(${expected}) — actual status ${res.statusCode}, body: ${truncateBody(res.body)}`);
31
+ }
32
+ // Widen the generic schemas map to a concrete type so it can be indexed.
33
+ const schemas = contract.responseBodySchemasByStatusCode;
34
+ const schema = schemas?.[expected];
35
+ if (!schema) {
36
+ throw new Error(`bodyForStatus(${expected}) — no response body schema declared for status ${expected} in contract.responseBodySchemasByStatusCode`);
37
+ }
38
+ let parsedJson;
39
+ try {
40
+ parsedJson = JSON.parse(res.body);
41
+ }
42
+ catch (err) {
43
+ throw new Error(`bodyForStatus(${expected}) — body is not valid JSON: ${err.message}; body: ${truncateBody(res.body)}`);
44
+ }
45
+ const parsed = schema.safeParse(parsedJson);
46
+ if (!parsed.success) {
47
+ throw new Error(`bodyForStatus(${expected}) — body does not match the declared schema: ${parsed.error.message}; body: ${truncateBody(res.body)}`);
48
+ }
49
+ return parsed.data;
50
+ });
51
+ }
1
52
  /**
2
53
  * Build query string from query params object.
3
54
  * @internal
@@ -62,7 +113,7 @@ export function injectSSE(app, contract, options) {
62
113
  headers: res.headers,
63
114
  body: res.body,
64
115
  }));
65
- return { closed };
116
+ return { closed, bodyForStatus: bindBodyForStatus(contract, closed) };
66
117
  }
67
118
  /**
68
119
  * Inject a POST/PUT/PATCH SSE request using a contract definition.
@@ -109,6 +160,6 @@ export function injectPayloadSSE(app, contract, options) {
109
160
  headers: res.headers,
110
161
  body: res.body,
111
162
  }));
112
- return { closed };
163
+ return { closed, bodyForStatus: bindBodyForStatus(contract, closed) };
113
164
  }
114
165
  //# sourceMappingURL=sseInjectHelpers.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"sseInjectHelpers.js","sourceRoot":"","sources":["../../../lib/testing/sseInjectHelpers.ts"],"names":[],"mappings":"AAaA;;;GAGG;AACH,SAAS,gBAAgB,CAAC,KAA8B;IACtD,MAAM,YAAY,GAAG,IAAI,eAAe,EAAE,CAAA;IAC1C,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACjD,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;YAC1C,YAAY,CAAC,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAA;QACzC,CAAC;IACH,CAAC;IACD,OAAO,YAAY,CAAC,QAAQ,EAAE,CAAA;AAChC,CAAC;AAED;;;GAGG;AACH,SAAS,QAAQ,CACf,QAAkB,EAClB,MAA+B,EAC/B,KAA+B;IAE/B,IAAI,GAAG,GAAG,QAAQ,CAAC,YAAY,CAAC,MAAM,IAAI,EAAE,CAAC,CAAA;IAE7C,iCAAiC;IACjC,IAAI,KAAK,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC3C,MAAM,WAAW,GAAG,gBAAgB,CAAC,KAAK,CAAC,CAAA;QAC3C,IAAI,WAAW,EAAE,CAAC;YAChB,GAAG,GAAG,GAAG,GAAG,IAAI,WAAW,EAAE,CAAA;QAC/B,CAAC;IACH,CAAC;IAED,OAAO,GAAG,CAAA;AACZ,CAAC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,SAAS,CAUvB,GAAuB,EACvB,QAAkB,EAClB,OAAoC;IAEpC,MAAM,GAAG,GAAG,QAAQ,CAClB,QAAQ,EACR,OAAO,EAAE,MAA4C,EACrD,OAAO,EAAE,KAA4C,CACtD,CAAA;IAED,mEAAmE;IACnE,MAAM,MAAM,GAAG,GAAG;SACf,MAAM,CAAC;QACN,MAAM,EAAE,KAAK;QACb,GAAG;QACH,OAAO,EAAE;YACP,MAAM,EAAE,mBAAmB;YAC3B,GAAI,OAAO,EAAE,OAA8C;SAC5D;KACF,CAAC;SACD,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;QACd,UAAU,EAAE,GAAG,CAAC,UAAU;QAC1B,OAAO,EAAE,GAAG,CAAC,OAAwD;QACrE,IAAI,EAAE,GAAG,CAAC,IAAI;KACf,CAAC,CAAC,CAAA;IAEL,OAAO,EAAE,MAAM,EAAE,CAAA;AACnB,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,UAAU,gBAAgB,CAU9B,GAAuB,EACvB,QAAkB,EAClB,OAA0C;IAE1C,MAAM,GAAG,GAAG,QAAQ,CAClB,QAAQ,EACR,OAAO,CAAC,MAA4C,EACpD,OAAO,CAAC,KAA4C,CACrD,CAAA;IAED,MAAM,MAAM,GAAG,GAAG;SACf,MAAM,CAAC;QACN,MAAM,EAAE,QAAQ,CAAC,MAAM;QACvB,GAAG;QACH,OAAO,EAAE;YACP,MAAM,EAAE,mBAAmB;YAC3B,cAAc,EAAE,kBAAkB;YAClC,GAAI,OAAO,CAAC,OAA8C;SAC3D;QACD,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC;KACtC,CAAC;SACD,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;QACd,UAAU,EAAE,GAAG,CAAC,UAAU;QAC1B,OAAO,EAAE,GAAG,CAAC,OAAwD;QACrE,IAAI,EAAE,GAAG,CAAC,IAAI;KACf,CAAC,CAAC,CAAA;IAEL,OAAO,EAAE,MAAM,EAAE,CAAA;AACnB,CAAC"}
1
+ {"version":3,"file":"sseInjectHelpers.js","sourceRoot":"","sources":["../../../lib/testing/sseInjectHelpers.ts"],"names":[],"mappings":"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"}
@@ -1,8 +1,16 @@
1
- import type { AnySSEContractDefinition } from '@lokalise/api-contracts';
1
+ import type { AnySSEContractDefinition, HttpStatusCode } from '@lokalise/api-contracts';
2
2
  import type { z } from 'zod';
3
3
  import type { ParsedSSEEvent } from '../sse/sseParser.ts';
4
4
  /** Safely infer the output type of an optional Zod schema property. */
5
5
  type InferOptionalSchema<T, Fallback = unknown> = NonNullable<T> extends z.ZodTypeAny ? z.infer<NonNullable<T>> : Fallback;
6
+ /**
7
+ * Status codes that the given schemas-map declares.
8
+ * Resolves to `never` when the map is `undefined`, so `bodyForStatus` is
9
+ * uncallable for contracts that declare no response body schemas at all.
10
+ */
11
+ export type DeclaredResponseStatus<Schemas extends Partial<Record<HttpStatusCode, z.ZodTypeAny>> | undefined> = Schemas extends Partial<Record<HttpStatusCode, z.ZodTypeAny>> ? keyof Schemas & HttpStatusCode : never;
12
+ /** Type of the parsed response body for a declared status. */
13
+ export type DeclaredResponseBody<Schemas extends Partial<Record<HttpStatusCode, z.ZodTypeAny>> | undefined, Status extends DeclaredResponseStatus<Schemas>> = Schemas extends Partial<Record<HttpStatusCode, z.ZodTypeAny>> ? Status extends keyof Schemas ? Schemas[Status] extends z.ZodTypeAny ? z.infer<Schemas[Status]> : never : never : never;
6
14
  /**
7
15
  * Represents an active SSE test connection (inject-based).
8
16
  *
@@ -82,12 +90,45 @@ export type SSEResponse = {
82
90
  * Note: Fastify's inject() waits for the full response, so these helpers
83
91
  * work best for streaming that completes (OpenAI-style). For long-lived
84
92
  * SSE connections, use `SSEHttpClient` with a real HTTP server instead.
93
+ *
94
+ * When the contract declares `responseBodySchemasByStatusCode`, the result
95
+ * exposes `bodyForStatus(status)` — a typed accessor that parses the response
96
+ * body against the contract's schema for that status. TS rejects status codes
97
+ * the contract doesn't declare.
85
98
  */
86
- export type InjectSSEResult = {
99
+ export type InjectSSEResult<Schemas extends Partial<Record<HttpStatusCode, z.ZodTypeAny>> | undefined = undefined> = {
87
100
  /**
88
101
  * Resolves when the response completes with the full SSE body.
89
102
  * Parse the body with `parseSSEEvents()` to get individual events.
90
103
  */
91
104
  closed: Promise<SSEResponse>;
105
+ /**
106
+ * Awaits the response, asserts the status code matches, parses the body
107
+ * against the contract's schema for that status, and returns the parsed
108
+ * object. Useful for asserting on documented error response shapes.
109
+ *
110
+ * Intended for non-streaming responses emitted via `sse.respond(status, body)`
111
+ * before streaming starts (auth failures, validation errors, not-found).
112
+ * Calling it for a status served by an actual SSE stream fails at the
113
+ * JSON-parse step, since a stream body is `text/event-stream`, not JSON.
114
+ *
115
+ * Throws (with the offending status and a truncated body snippet) if:
116
+ * - the actual status code doesn't match the expected one;
117
+ * - the contract declares no schema for that status;
118
+ * - the body isn't valid JSON;
119
+ * - the body doesn't match the declared Zod schema.
120
+ *
121
+ * At the type level, `statusCode` is constrained to the keys of the
122
+ * contract's `responseBodySchemasByStatusCode`. Contracts without any
123
+ * declared schemas can't call this method (`statusCode: never`).
124
+ *
125
+ * @example
126
+ * ```typescript
127
+ * const { bodyForStatus } = injectSSE(app, contract, { headers })
128
+ * const error = await bodyForStatus(401) // typed as z.infer<401-schema>
129
+ * expect(error.message).toBe('Unauthorized')
130
+ * ```
131
+ */
132
+ bodyForStatus<Status extends DeclaredResponseStatus<Schemas>>(statusCode: Status): Promise<DeclaredResponseBody<Schemas, Status>>;
92
133
  };
93
134
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opinionated-machine",
3
- "version": "6.19.1",
3
+ "version": "6.20.0",
4
4
  "description": "Very opinionated DI framework for fastify, built on top of awilix ",
5
5
  "type": "module",
6
6
  "license": "MIT",