japa-openapi-assertions 0.1.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 ADDED
@@ -0,0 +1,240 @@
1
+ # @drew-daniels/japa-openapi-assertions
2
+
3
+ OpenAPI 3.1 assertions plugin for [Japa](https://japa.dev) test framework. Validate your API responses against OpenAPI specifications with full OpenAPI 3.1 support.
4
+
5
+ ## Why This Fork?
6
+
7
+ This package is a fork of [`@japa/openapi-assertions`](https://japa.dev/docs/plugins/openapi-assertions) created to add **OpenAPI 3.1 support**. The original package uses [`api-contract-validator`](https://github.com/PayU/api-contract-validator) under the hood, which only supports OpenAPI 3.0.
8
+
9
+ OpenAPI 3.1 introduced significant changes that break compatibility with OpenAPI 3.0 tooling. This fork replaces the underlying validation engine with [AJV](https://ajv.js.org/) configured for [JSON Schema Draft 2020-12](https://json-schema.org/draft/2020-12/json-schema-core.html), providing native support for OpenAPI 3.1 specifications.
10
+
11
+ ## OpenAPI 3.0 vs 3.1: Key Differences
12
+
13
+ OpenAPI 3.1 aligns fully with JSON Schema Draft 2020-12, which introduced several breaking changes from the modified JSON Schema subset used in OpenAPI 3.0:
14
+
15
+ ### Nullable Types
16
+
17
+ The `nullable` keyword was removed. Use JSON Schema type arrays instead:
18
+
19
+ ```yaml
20
+ # OpenAPI 3.0
21
+ type: string
22
+ nullable: true
23
+
24
+ # OpenAPI 3.1
25
+ type: ["string", "null"]
26
+ ```
27
+
28
+ ### Exclusive Min/Max
29
+
30
+ Boolean modifiers changed to numeric values:
31
+
32
+ ```yaml
33
+ # OpenAPI 3.0
34
+ minimum: 0
35
+ exclusiveMinimum: true
36
+
37
+ # OpenAPI 3.1
38
+ exclusiveMinimum: 0
39
+ ```
40
+
41
+ ### $ref Behavior
42
+
43
+ OpenAPI 3.1 allows sibling keywords alongside `$ref`, eliminating the need for `allOf` workarounds:
44
+
45
+ ```yaml
46
+ # OpenAPI 3.0 (workaround required)
47
+ allOf:
48
+ - $ref: '#/components/schemas/Pet'
49
+ - description: A pet with extra info
50
+
51
+ # OpenAPI 3.1 (direct usage)
52
+ $ref: '#/components/schemas/Pet'
53
+ description: A pet with extra info
54
+ ```
55
+
56
+ ### Examples
57
+
58
+ The `example` keyword is deprecated in favor of `examples` (array format):
59
+
60
+ ```yaml
61
+ # OpenAPI 3.0
62
+ example: "Fluffy"
63
+
64
+ # OpenAPI 3.1
65
+ examples:
66
+ - "Fluffy"
67
+ - "Buddy"
68
+ ```
69
+
70
+ For a complete list of changes, see the [OpenAPI Initiative migration guide](https://www.openapis.org/blog/2021/02/16/migrating-from-openapi-3-0-to-3-1-0).
71
+
72
+ ## Backward Compatibility
73
+
74
+ **This package is designed for OpenAPI 3.1 specifications.** It may work with some OpenAPI 3.0 specs, but compatibility is not guaranteed due to the fundamental differences in how schemas are validated:
75
+
76
+ | Feature | OpenAPI 3.0 Spec | This Package |
77
+ |---------|------------------|--------------|
78
+ | `nullable: true` | Supported in 3.0 | Not supported - use `type: ["string", "null"]` |
79
+ | `type: ["string", "null"]` | Not valid in 3.0 | Fully supported |
80
+ | `exclusiveMinimum: true` | Supported in 3.0 | Not supported - use numeric value |
81
+ | `$ref` with siblings | Not valid in 3.0 | Fully supported |
82
+
83
+ If you need OpenAPI 3.0 support, use the original [`@japa/openapi-assertions`](https://www.npmjs.com/package/@japa/openapi-assertions) package.
84
+
85
+ ## Installation
86
+
87
+ ```bash
88
+ npm install @drew-daniels/japa-openapi-assertions
89
+ ```
90
+
91
+ ### Peer Dependencies
92
+
93
+ This plugin requires:
94
+ - `@japa/assert` ^4.0.0
95
+ - `@japa/runner` ^3.0.0 || ^4.0.0 || ^5.0.0
96
+
97
+ ```bash
98
+ npm install @japa/assert @japa/runner
99
+ ```
100
+
101
+ ## Setup
102
+
103
+ Configure the plugin in your Japa configuration file:
104
+
105
+ ```typescript
106
+ // tests/bootstrap.ts
107
+ import { assert } from '@japa/assert'
108
+ import { apiClient } from '@japa/api-client'
109
+ import { openapi } from '@drew-daniels/japa-openapi-assertions'
110
+
111
+ export const plugins = [
112
+ assert(),
113
+ openapi({
114
+ schemas: [new URL('../openapi.json', import.meta.url)],
115
+ reportCoverage: true,
116
+ }),
117
+ apiClient(),
118
+ ]
119
+ ```
120
+
121
+ ## Usage
122
+
123
+ Use the `isValidApiResponse` assertion method to validate responses against your OpenAPI specification:
124
+
125
+ ```typescript
126
+ import { test } from '@japa/runner'
127
+
128
+ test.group('Pets API', () => {
129
+ test('list all pets', async ({ client, assert }) => {
130
+ const response = await client.get('/pets')
131
+
132
+ response.assertStatus(200)
133
+ assert.isValidApiResponse(response)
134
+ })
135
+
136
+ test('get a single pet', async ({ client, assert }) => {
137
+ const response = await client.get('/pets/123')
138
+
139
+ response.assertStatus(200)
140
+ assert.isValidApiResponse(response)
141
+ })
142
+
143
+ test('create a pet', async ({ client, assert }) => {
144
+ const response = await client
145
+ .post('/pets')
146
+ .json({ name: 'Fluffy', tag: 'cat' })
147
+
148
+ response.assertStatus(201)
149
+ assert.isValidApiResponse(response)
150
+ })
151
+ })
152
+ ```
153
+
154
+ ### Validation Behavior
155
+
156
+ The plugin validates:
157
+
158
+ - **Path matching**: Ensures the request path exists in your OpenAPI spec (supports parameterized paths like `/pets/{petId}`)
159
+ - **HTTP method**: Validates the method is defined for the matched path
160
+ - **Status code**: Checks that a response schema exists for the returned status
161
+ - **Response body**: Validates the response body against the JSON schema defined in your spec
162
+
163
+ If validation fails, a detailed `AssertionError` is thrown with information about what didn't match.
164
+
165
+ ## Configuration Options
166
+
167
+ | Option | Type | Description |
168
+ |--------|------|-------------|
169
+ | `schemas` | `(string \| URL)[]` | **Required.** Paths to OpenAPI specification files (JSON format) |
170
+ | `reportCoverage` | `boolean` | Print coverage report to console on process exit |
171
+ | `exportCoverage` | `boolean` | Export coverage data to `coverage.json` on process exit |
172
+
173
+ ### Coverage Reporting
174
+
175
+ Enable coverage tracking to see which API endpoints have been tested:
176
+
177
+ ```typescript
178
+ openapi({
179
+ schemas: ['./openapi.json'],
180
+ reportCoverage: true,
181
+ exportCoverage: true,
182
+ })
183
+ ```
184
+
185
+ The coverage report shows:
186
+ - Total endpoints defined in your spec
187
+ - Number of endpoints tested
188
+ - List of untested endpoints
189
+ - Coverage percentage
190
+
191
+ ## Supported HTTP Clients
192
+
193
+ The plugin automatically detects and parses responses from popular HTTP client libraries:
194
+
195
+ | Library | Support |
196
+ |---------|---------|
197
+ | [@japa/api-client](https://japa.dev/docs/plugins/api-client) | Full support |
198
+ | [Axios](https://axios-http.com/) | Full support |
199
+ | [Supertest](https://github.com/ladjs/supertest) | Full support |
200
+
201
+ ### Response Format Detection
202
+
203
+ The plugin inspects the response object to determine which format to use:
204
+
205
+ - **Japa api-client**: Detected by `request` and `statusCode` properties
206
+ - **Axios**: Detected by `data`, `config`, and `status` properties
207
+ - **Supertest**: Detected by `body`, `req`, and `statusCode` properties
208
+
209
+ ## Requirements
210
+
211
+ - Node.js >= 20.6.0
212
+ - OpenAPI 3.1.x specification in JSON format
213
+
214
+ ## Technical Details
215
+
216
+ This package uses:
217
+ - [AJV](https://ajv.js.org/) with JSON Schema 2020-12 support for schema validation
218
+ - [ajv-formats](https://github.com/ajv-validator/ajv-formats) for format validation (email, uri, date-time, etc.)
219
+ - [@seriousme/openapi-schema-validator](https://github.com/seriousme/openapi-schema-validator) for OpenAPI document validation
220
+
221
+ All `$ref` pointers are resolved locally before validation, supporting references like `$ref: '#/components/schemas/Pet'`.
222
+
223
+ ## License
224
+
225
+ MIT
226
+
227
+ ## Contributing
228
+
229
+ Issues and pull requests are welcome at [github.com/drew-daniels/japa-openapi-assertions](https://github.com/drew-daniels/japa-openapi-assertions).
230
+
231
+ ## Credits
232
+
233
+ This package is a fork of the original [@japa/openapi-assertions](https://japa.dev/docs/plugins/openapi-assertions) by the Japa team.
234
+
235
+ ## Further Reading
236
+
237
+ - [OpenAPI 3.1 Migration Guide](https://www.openapis.org/blog/2021/02/16/migrating-from-openapi-3-0-to-3-1-0)
238
+ - [Upgrading from OpenAPI 3.0 to 3.1](https://learn.openapis.org/upgrading/v3.0-to-v3.1.html)
239
+ - [JSON Schema Draft 2020-12](https://json-schema.org/draft/2020-12/json-schema-core.html)
240
+ - [Japa Testing Framework](https://japa.dev/)
@@ -0,0 +1,53 @@
1
+ import type { CoverageEntry } from './types.js';
2
+ interface CoverageRecord {
3
+ route: string;
4
+ method: string;
5
+ status: string;
6
+ }
7
+ /**
8
+ * Tracks API endpoint coverage during test runs.
9
+ */
10
+ export declare class CoverageTracker {
11
+ private allEndpoints;
12
+ private covered;
13
+ private reportOnExit;
14
+ private exportOnExit;
15
+ private exitHandlerRegistered;
16
+ /**
17
+ * Register all endpoints from the OpenAPI spec for coverage tracking.
18
+ */
19
+ registerEndpoints(entries: CoverageEntry[]): void;
20
+ /**
21
+ * Record that an endpoint was covered during testing.
22
+ */
23
+ recordCoverage(route: string, method: string, status: string): void;
24
+ /**
25
+ * Get list of uncovered endpoints.
26
+ */
27
+ getUncovered(): CoverageRecord[];
28
+ /**
29
+ * Get coverage statistics.
30
+ */
31
+ getStats(): {
32
+ total: number;
33
+ covered: number;
34
+ percentage: number;
35
+ };
36
+ /**
37
+ * Enable reporting on process exit.
38
+ */
39
+ enableReporting(report: boolean, exportFile: boolean): void;
40
+ /**
41
+ * Print coverage report to console.
42
+ */
43
+ report(): void;
44
+ /**
45
+ * Export coverage data to JSON file.
46
+ */
47
+ export(filePath?: string): void;
48
+ private makeKey;
49
+ private registerExitHandler;
50
+ }
51
+ export declare const coverageTracker: CoverageTracker;
52
+ export {};
53
+ //# sourceMappingURL=coverage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"coverage.d.ts","sourceRoot":"","sources":["../src/coverage.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAE/C,UAAU,cAAc;IACtB,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;CACf;AAED;;GAEG;AACH,qBAAa,eAAe;IAC1B,OAAO,CAAC,YAAY,CAAuB;IAC3C,OAAO,CAAC,OAAO,CAAyB;IACxC,OAAO,CAAC,YAAY,CAAiB;IACrC,OAAO,CAAC,YAAY,CAAiB;IACrC,OAAO,CAAC,qBAAqB,CAAiB;IAE9C;;OAEG;IACH,iBAAiB,CAAC,OAAO,EAAE,aAAa,EAAE,GAAG,IAAI;IAYjD;;OAEG;IACH,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IAKnE;;OAEG;IACH,YAAY,IAAI,cAAc,EAAE;IAMhC;;OAEG;IACH,QAAQ,IAAI;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE;IAQlE;;OAEG;IACH,eAAe,CAAC,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE,OAAO,GAAG,IAAI;IAS3D;;OAEG;IACH,MAAM,IAAI,IAAI;IAuCd;;OAEG;IACH,MAAM,CAAC,QAAQ,GAAE,MAAwB,GAAG,IAAI;IAYhD,OAAO,CAAC,OAAO;IAIf,OAAO,CAAC,mBAAmB;CAY5B;AAGD,eAAO,MAAM,eAAe,iBAAwB,CAAA"}
@@ -0,0 +1,123 @@
1
+ import { writeFileSync } from 'node:fs';
2
+ import chalk from 'chalk';
3
+ /**
4
+ * Tracks API endpoint coverage during test runs.
5
+ */
6
+ export class CoverageTracker {
7
+ allEndpoints = [];
8
+ covered = new Set();
9
+ reportOnExit = false;
10
+ exportOnExit = false;
11
+ exitHandlerRegistered = false;
12
+ /**
13
+ * Register all endpoints from the OpenAPI spec for coverage tracking.
14
+ */
15
+ registerEndpoints(entries) {
16
+ for (const entry of entries) {
17
+ for (const status of entry.statuses) {
18
+ this.allEndpoints.push({
19
+ route: entry.route,
20
+ method: entry.method,
21
+ status,
22
+ });
23
+ }
24
+ }
25
+ }
26
+ /**
27
+ * Record that an endpoint was covered during testing.
28
+ */
29
+ recordCoverage(route, method, status) {
30
+ const key = this.makeKey(route, method, status);
31
+ this.covered.add(key);
32
+ }
33
+ /**
34
+ * Get list of uncovered endpoints.
35
+ */
36
+ getUncovered() {
37
+ return this.allEndpoints.filter((e) => !this.covered.has(this.makeKey(e.route, e.method, e.status)));
38
+ }
39
+ /**
40
+ * Get coverage statistics.
41
+ */
42
+ getStats() {
43
+ const total = this.allEndpoints.length;
44
+ const covered = this.covered.size;
45
+ const percentage = total > 0 ? Math.round((covered / total) * 100) : 100;
46
+ return { total, covered, percentage };
47
+ }
48
+ /**
49
+ * Enable reporting on process exit.
50
+ */
51
+ enableReporting(report, exportFile) {
52
+ this.reportOnExit = report;
53
+ this.exportOnExit = exportFile;
54
+ if ((report || exportFile) && !this.exitHandlerRegistered) {
55
+ this.registerExitHandler();
56
+ }
57
+ }
58
+ /**
59
+ * Print coverage report to console.
60
+ */
61
+ report() {
62
+ const uncovered = this.getUncovered();
63
+ const stats = this.getStats();
64
+ console.log('');
65
+ console.log(chalk.bold('API Coverage Report'));
66
+ console.log(chalk.dim('─'.repeat(50)));
67
+ if (uncovered.length === 0) {
68
+ console.log(chalk.green('✓ All endpoints covered!'));
69
+ }
70
+ else {
71
+ console.log(chalk.yellow(`Uncovered endpoints (${uncovered.length}):`));
72
+ console.log('');
73
+ // Group by route for cleaner output
74
+ const byRoute = new Map();
75
+ for (const endpoint of uncovered) {
76
+ const existing = byRoute.get(endpoint.route) || [];
77
+ existing.push(endpoint);
78
+ byRoute.set(endpoint.route, existing);
79
+ }
80
+ for (const [route, endpoints] of byRoute) {
81
+ console.log(chalk.dim(` ${route}`));
82
+ for (const ep of endpoints) {
83
+ console.log(` ${chalk.cyan(ep.method.padEnd(7))} ${chalk.dim(ep.status)}`);
84
+ }
85
+ }
86
+ }
87
+ console.log('');
88
+ console.log(chalk.dim('─'.repeat(50)));
89
+ console.log(`Coverage: ${stats.covered}/${stats.total} endpoints `
90
+ + `(${chalk.bold(stats.percentage + '%')})`);
91
+ console.log('');
92
+ }
93
+ /**
94
+ * Export coverage data to JSON file.
95
+ */
96
+ export(filePath = 'coverage.json') {
97
+ const uncovered = this.getUncovered();
98
+ const data = uncovered.map((e) => ({
99
+ route: e.route,
100
+ method: e.method,
101
+ statuses: e.status,
102
+ }));
103
+ writeFileSync(filePath, JSON.stringify(data, null, 2));
104
+ console.log(chalk.dim(`Coverage data exported to ${filePath}`));
105
+ }
106
+ makeKey(route, method, status) {
107
+ return `${method.toUpperCase()}:${route}:${status}`;
108
+ }
109
+ registerExitHandler() {
110
+ this.exitHandlerRegistered = true;
111
+ process.on('beforeExit', () => {
112
+ if (this.reportOnExit) {
113
+ this.report();
114
+ }
115
+ if (this.exportOnExit) {
116
+ this.export();
117
+ }
118
+ });
119
+ }
120
+ }
121
+ // Singleton instance for global coverage tracking
122
+ export const coverageTracker = new CoverageTracker();
123
+ //# sourceMappingURL=coverage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"coverage.js","sourceRoot":"","sources":["../src/coverage.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ import type { PluginConfig } from './types.js';
2
+ export { OpenApiAssertions } from './openapi_assertions.js';
3
+ export type { PluginConfig } from './types.js';
4
+ declare module '@japa/assert' {
5
+ interface Assert {
6
+ isValidApiResponse(response: unknown): void;
7
+ }
8
+ }
9
+ /**
10
+ * OpenAPI assertions plugin for Japa.
11
+ *
12
+ * This plugin validates HTTP responses against OpenAPI 3.0/3.1 specifications.
13
+ * It's a drop-in replacement for @japa/openapi-assertions with full OpenAPI 3.1 support.
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * import { openapi } from '@drew-daniels/japa-openapi-assertions'
18
+ *
19
+ * export const plugins = [
20
+ * assert(),
21
+ * openapi({
22
+ * schemas: ['./openapi.json'],
23
+ * reportCoverage: true,
24
+ * }),
25
+ * apiClient(),
26
+ * ]
27
+ * ```
28
+ *
29
+ * @param options - Plugin configuration
30
+ * @returns Japa plugin function
31
+ */
32
+ export declare function openapi(options: PluginConfig): () => void;
33
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAG9C,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAA;AAC3D,YAAY,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAG9C,OAAO,QAAQ,cAAc,CAAC;IAC5B,UAAU,MAAM;QACd,kBAAkB,CAAC,QAAQ,EAAE,OAAO,GAAG,IAAI,CAAA;KAC5C;CACF;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,OAAO,CAAC,OAAO,EAAE,YAAY,cAc5C"}
package/build/index.js ADDED
@@ -0,0 +1,42 @@
1
+ import { Assert } from '@japa/assert';
2
+ import { OpenApiAssertions } from './openapi_assertions.js';
3
+ // Re-export types and classes
4
+ export { OpenApiAssertions } from './openapi_assertions.js';
5
+ /**
6
+ * OpenAPI assertions plugin for Japa.
7
+ *
8
+ * This plugin validates HTTP responses against OpenAPI 3.0/3.1 specifications.
9
+ * It's a drop-in replacement for @japa/openapi-assertions with full OpenAPI 3.1 support.
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * import { openapi } from '@drew-daniels/japa-openapi-assertions'
14
+ *
15
+ * export const plugins = [
16
+ * assert(),
17
+ * openapi({
18
+ * schemas: ['./openapi.json'],
19
+ * reportCoverage: true,
20
+ * }),
21
+ * apiClient(),
22
+ * ]
23
+ * ```
24
+ *
25
+ * @param options - Plugin configuration
26
+ * @returns Japa plugin function
27
+ */
28
+ export function openapi(options) {
29
+ // Register specs immediately (async loading happens in background)
30
+ OpenApiAssertions.registerSpecs(options.schemas, {
31
+ reportCoverage: options.reportCoverage,
32
+ exportCoverage: options.exportCoverage,
33
+ });
34
+ // Return the Japa plugin function
35
+ return function japaOpenapiPlugin() {
36
+ // Register the assertion macro
37
+ Assert.macro('isValidApiResponse', function (response) {
38
+ return new OpenApiAssertions().isValidResponse(response);
39
+ });
40
+ };
41
+ }
42
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAA;AACrC,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAA;AAG3D,8BAA8B;AAC9B,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAA;AAU3D;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,UAAU,OAAO,CAAC,OAAqB;IAC3C,mEAAmE;IACnE,iBAAiB,CAAC,aAAa,CAAC,OAAO,CAAC,OAAO,EAAE;QAC/C,cAAc,EAAE,OAAO,CAAC,cAAc;QACtC,cAAc,EAAE,OAAO,CAAC,cAAc;KACvC,CAAC,CAAA;IAEF,kCAAkC;IAClC,OAAO,SAAS,iBAAiB;QAC/B,+BAA+B;QAC/B,MAAM,CAAC,KAAK,CAAC,oBAAoB,EAAE,UAAwB,QAAiB;YAC1E,OAAO,IAAI,iBAAiB,EAAE,CAAC,eAAe,CAAC,QAAQ,CAAC,CAAA;QAC1D,CAAC,CAAC,CAAA;IACJ,CAAC,CAAA;AACH,CAAC"}
@@ -0,0 +1,49 @@
1
+ interface RegisterOptions {
2
+ reportCoverage?: boolean;
3
+ exportCoverage?: boolean;
4
+ }
5
+ /**
6
+ * OpenAPI assertions for validating HTTP responses against OpenAPI specifications.
7
+ *
8
+ * This is a drop-in replacement for @japa/openapi-assertions with OpenAPI 3.1 support.
9
+ */
10
+ export declare class OpenApiAssertions {
11
+ /**
12
+ * Compiled validators from registered specs.
13
+ */
14
+ private static validators;
15
+ /**
16
+ * Flag indicating if specs have been registered.
17
+ */
18
+ private static hasRegistered;
19
+ /**
20
+ * Coverage options.
21
+ */
22
+ private static coverageOptions;
23
+ /**
24
+ * Register OpenAPI specs to validate responses against.
25
+ * This method is SYNCHRONOUS to match the original @japa/openapi-assertions API.
26
+ *
27
+ * @param schemaPathsOrURLs - Paths or URLs to OpenAPI spec files
28
+ * @param options - Coverage reporting options
29
+ */
30
+ static registerSpecs(schemaPathsOrURLs: (string | URL)[], options?: RegisterOptions): void;
31
+ /**
32
+ * Reset the assertions state (useful for testing).
33
+ */
34
+ static reset(): void;
35
+ /**
36
+ * Get validators, throwing if not registered.
37
+ */
38
+ private static getValidators;
39
+ /**
40
+ * Validate that a response matches the OpenAPI specification.
41
+ * This method is SYNCHRONOUS to match the original @japa/openapi-assertions API.
42
+ *
43
+ * @param response - HTTP response object from axios, supertest, or Japa api-client
44
+ * @throws AssertionError if the response does not match the spec
45
+ */
46
+ isValidResponse(response: unknown): void;
47
+ }
48
+ export {};
49
+ //# sourceMappingURL=openapi_assertions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openapi_assertions.d.ts","sourceRoot":"","sources":["../src/openapi_assertions.ts"],"names":[],"mappings":"AAQA,UAAU,eAAe;IACvB,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,cAAc,CAAC,EAAE,OAAO,CAAA;CACzB;AAED;;;;GAIG;AACH,qBAAa,iBAAiB;IAC5B;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,UAAU,CAAkC;IAE3D;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,aAAa,CAAiB;IAE7C;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,eAAe,CAAsB;IAEpD;;;;;;OAMG;IACH,MAAM,CAAC,aAAa,CAClB,iBAAiB,EAAE,CAAC,MAAM,GAAG,GAAG,CAAC,EAAE,EACnC,OAAO,GAAE,eAAoB,GAC5B,IAAI;IAuBP;;OAEG;IACH,MAAM,CAAC,KAAK,IAAI,IAAI;IAMpB;;OAEG;IACH,OAAO,CAAC,MAAM,CAAC,aAAa;IAU5B;;;;;;OAMG;IACH,eAAe,CAAC,QAAQ,EAAE,OAAO,GAAG,IAAI;CAsDzC"}
@@ -0,0 +1,114 @@
1
+ import { AssertionError } from 'node:assert';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { buildValidatorsSync } from './schema_builder.js';
4
+ import { validateResponse, parseResponse } from './response_validator.js';
5
+ import { matchPath } from './path_matcher.js';
6
+ import { coverageTracker } from './coverage.js';
7
+ /**
8
+ * OpenAPI assertions for validating HTTP responses against OpenAPI specifications.
9
+ *
10
+ * This is a drop-in replacement for @japa/openapi-assertions with OpenAPI 3.1 support.
11
+ */
12
+ export class OpenApiAssertions {
13
+ /**
14
+ * Compiled validators from registered specs.
15
+ */
16
+ static validators = null;
17
+ /**
18
+ * Flag indicating if specs have been registered.
19
+ */
20
+ static hasRegistered = false;
21
+ /**
22
+ * Coverage options.
23
+ */
24
+ static coverageOptions = {};
25
+ /**
26
+ * Register OpenAPI specs to validate responses against.
27
+ * This method is SYNCHRONOUS to match the original @japa/openapi-assertions API.
28
+ *
29
+ * @param schemaPathsOrURLs - Paths or URLs to OpenAPI spec files
30
+ * @param options - Coverage reporting options
31
+ */
32
+ static registerSpecs(schemaPathsOrURLs, options = {}) {
33
+ this.hasRegistered = true;
34
+ this.coverageOptions = options;
35
+ // Convert URLs to file paths
36
+ const paths = schemaPathsOrURLs.map((p) => p instanceof URL ? fileURLToPath(p) : p);
37
+ // Build validators synchronously
38
+ const result = buildValidatorsSync(paths);
39
+ this.validators = result.validators;
40
+ // Register coverage entries
41
+ if (options.reportCoverage || options.exportCoverage) {
42
+ coverageTracker.registerEndpoints(result.coverageEntries);
43
+ coverageTracker.enableReporting(options.reportCoverage ?? false, options.exportCoverage ?? false);
44
+ }
45
+ }
46
+ /**
47
+ * Reset the assertions state (useful for testing).
48
+ */
49
+ static reset() {
50
+ this.validators = null;
51
+ this.hasRegistered = false;
52
+ this.coverageOptions = {};
53
+ }
54
+ /**
55
+ * Get validators, throwing if not registered.
56
+ */
57
+ static getValidators() {
58
+ if (!this.validators) {
59
+ throw new Error('Cannot validate responses without defining API schemas. '
60
+ + 'Please configure the plugin with schemas.');
61
+ }
62
+ return this.validators;
63
+ }
64
+ /**
65
+ * Validate that a response matches the OpenAPI specification.
66
+ * This method is SYNCHRONOUS to match the original @japa/openapi-assertions API.
67
+ *
68
+ * @param response - HTTP response object from axios, supertest, or Japa api-client
69
+ * @throws AssertionError if the response does not match the spec
70
+ */
71
+ isValidResponse(response) {
72
+ if (!OpenApiAssertions.hasRegistered) {
73
+ throw new Error('Cannot validate responses without defining API schemas. '
74
+ + 'Please configure the plugin with schemas.');
75
+ }
76
+ const validators = OpenApiAssertions.getValidators();
77
+ const result = validateResponse(response, validators);
78
+ // Record coverage if enabled
79
+ if (OpenApiAssertions.coverageOptions.reportCoverage
80
+ || OpenApiAssertions.coverageOptions.exportCoverage) {
81
+ try {
82
+ const parsed = parseResponse(response);
83
+ const specPaths = Object.keys(validators);
84
+ const pathMatch = matchPath(parsed.path, specPaths);
85
+ if (pathMatch) {
86
+ coverageTracker.recordCoverage(pathMatch.matched, parsed.method, String(parsed.status));
87
+ }
88
+ }
89
+ catch {
90
+ // Ignore coverage tracking errors
91
+ }
92
+ }
93
+ if (!result.valid) {
94
+ const errorDetails = result.errors
95
+ ?.map((e) => {
96
+ let msg = e.path ? `${e.path}: ${e.message}` : e.message;
97
+ if (e.expected !== undefined) {
98
+ msg += ` (expected: ${JSON.stringify(e.expected)})`;
99
+ }
100
+ if (e.actual !== undefined) {
101
+ msg += ` (actual: ${JSON.stringify(e.actual)})`;
102
+ }
103
+ return msg;
104
+ })
105
+ .join('\n ');
106
+ throw new AssertionError({
107
+ message: `Response does not match API schema:\n ${errorDetails}`,
108
+ actual: result.errors,
109
+ expected: 'valid response matching OpenAPI schema',
110
+ });
111
+ }
112
+ }
113
+ }
114
+ //# sourceMappingURL=openapi_assertions.js.map