@powerduck/openapi-cli 0.2.9 → 0.2.13
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 +77 -35
- package/dist/chunk-LTG44T66.js +1 -0
- package/dist/cli.cjs +1 -1
- package/dist/cli.js +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +77 -6
- package/dist/index.d.ts +77 -6
- package/dist/index.js +1 -1
- package/package.json +5 -4
- package/dist/chunk-674PEZ5S.js +0 -1
package/README.md
CHANGED
|
@@ -10,13 +10,13 @@ CI-ready command-line tool for batch-testing OpenAPI 3.2 documents across six pr
|
|
|
10
10
|
|
|
11
11
|
Powerduck is an open-source developer tooling platform for teams building modern API workflows.
|
|
12
12
|
|
|
13
|
-
- **6 Protocols** — HTTP
|
|
13
|
+
- **6 Protocols** — HTTP, SSE, WebSocket, GraphQL, gRPC, and MCP
|
|
14
14
|
- **Batch Testing** — Run every operation in your OpenAPI spec concurrently
|
|
15
15
|
- **Assertion Engine** — Validate status codes, headers, response bodies, and schemas
|
|
16
16
|
- **3 Report Formats** — JSON for CI, CLI for terminal, HTML for dashboards
|
|
17
17
|
- **Authentication** — Bearer tokens, API keys, Basic auth, OAuth2, and custom headers
|
|
18
18
|
- **Filtering** — Run tests by tag, path, method, operationId, or regex
|
|
19
|
-
- **Concurrency Control** — Configurable parallelism
|
|
19
|
+
- **Concurrency Control** — Configurable bounded parallelism
|
|
20
20
|
- **gRPC Reflection** — Auto-discover gRPC services via server reflection
|
|
21
21
|
- **MCP Integration** — Streamable HTTP and stdio MCP transport support
|
|
22
22
|
- **GitHub Actions** — First-class CI integration with annotations and summaries
|
|
@@ -59,7 +59,7 @@ const report = await runTests(config);
|
|
|
59
59
|
const jsonPath = generateJsonReport(report, config.outputDir);
|
|
60
60
|
const htmlPath = generateHtmlReport(report, config.outputDir);
|
|
61
61
|
|
|
62
|
-
console.log("Passed:", report.passed, "/", report.total);
|
|
62
|
+
console.log("Passed:", report.summary.passed, "/", report.summary.total);
|
|
63
63
|
```
|
|
64
64
|
|
|
65
65
|
### GitHub Actions CI
|
|
@@ -102,13 +102,13 @@ jobs:
|
|
|
102
102
|
|
|
103
103
|
## Features
|
|
104
104
|
|
|
105
|
-
- **6 protocols** — HTTP
|
|
105
|
+
- **6 protocols** — HTTP, SSE, WebSocket, GraphQL, gRPC, and MCP
|
|
106
106
|
- **Batch testing** — Run every operation in your OpenAPI spec concurrently
|
|
107
107
|
- **Assertion engine** — Validate status codes, headers, response bodies, and schemas
|
|
108
108
|
- **3 report formats** — JSON for CI, CLI for terminal, HTML for dashboards
|
|
109
109
|
- **Authentication** — Bearer tokens, API keys, Basic auth, OAuth2, and custom headers
|
|
110
110
|
- **Filtering** — Run tests by tag, path, method, operationId, or regex
|
|
111
|
-
- **Concurrency control** — Configurable parallelism
|
|
111
|
+
- **Concurrency control** — Configurable bounded parallelism
|
|
112
112
|
- **gRPC reflection** — Auto-discover gRPC services via server reflection
|
|
113
113
|
- **MCP integration** — Streamable HTTP and stdio MCP transport support
|
|
114
114
|
- **GitHub Actions** — First-class CI integration with annotations and summaries
|
|
@@ -148,35 +148,35 @@ openapi-cli --config ./openapi-cli.config.json
|
|
|
148
148
|
|
|
149
149
|
### Options
|
|
150
150
|
|
|
151
|
-
| Option
|
|
152
|
-
|
|
|
153
|
-
| `--spec`
|
|
154
|
-
| `--server`
|
|
155
|
-
| `--output`
|
|
156
|
-
| `--format`
|
|
157
|
-
| `--method`
|
|
158
|
-
| `--path`
|
|
159
|
-
| `--tag`
|
|
160
|
-
| `--
|
|
161
|
-
| `--concurrency`
|
|
162
|
-
| `--timeout`
|
|
163
|
-
| `--bearer`
|
|
164
|
-
| `--header`
|
|
165
|
-
| `--variable`
|
|
166
|
-
| `--config`
|
|
167
|
-
| `--env`
|
|
168
|
-
| `--
|
|
169
|
-
| `--
|
|
170
|
-
| `--
|
|
171
|
-
| `--
|
|
172
|
-
| `--
|
|
173
|
-
| `--
|
|
174
|
-
| `--
|
|
175
|
-
| `--proxy`
|
|
176
|
-
| `--ca`
|
|
177
|
-
| `--cert`
|
|
178
|
-
| `--key`
|
|
179
|
-
| `--insecure`
|
|
151
|
+
| Option | Type | Default | Description |
|
|
152
|
+
| --------------------- | ---------- | ---------------------- | ----------------------------------------------------------- |
|
|
153
|
+
| `--spec` | `string` | - | Path or URL to OpenAPI spec (required) |
|
|
154
|
+
| `--server` | `string` | - | Server URL override |
|
|
155
|
+
| `--output` | `string` | `./openapi-cli-report` | Output directory |
|
|
156
|
+
| `--format` | `string` | `json,cli,html` | Output formats (comma-separated) |
|
|
157
|
+
| `--method` | `string` | - | Filter by HTTP method (comma-separated) |
|
|
158
|
+
| `--path` | `string` | - | Filter by path pattern (comma-separated) |
|
|
159
|
+
| `--tag` | `string` | - | Filter by tag (comma-separated) |
|
|
160
|
+
| `--operation-id` | `string` | - | Filter by operationId (comma-separated) |
|
|
161
|
+
| `--concurrency` | `number` | `5` | Max concurrent requests |
|
|
162
|
+
| `--timeout` | `number` | `30000` | Request timeout in ms |
|
|
163
|
+
| `--bearer` | `string` | - | Bearer token for authentication |
|
|
164
|
+
| `--header` | `string[]` | - | Custom headers (Key: Value) |
|
|
165
|
+
| `--variable` | `string[]` | - | Server variables (key=value) |
|
|
166
|
+
| `--config` | `string` | - | Path to JSON config file |
|
|
167
|
+
| `--env` | `string` | - | Path to .env file |
|
|
168
|
+
| `--no-fail-on-error` | `flag` | off | Exit 0 even when tests fail (default: non-zero on failure) |
|
|
169
|
+
| `--grpc-no-reflection`| `flag` | off | Disable gRPC reflection and use proto files |
|
|
170
|
+
| `--grpc-proto` | `string[]` | - | gRPC proto file paths |
|
|
171
|
+
| `--mcp-transport` | `string` | `streamable-http` | MCP transport type |
|
|
172
|
+
| `--mcp-command` | `string` | - | MCP server command (stdio) |
|
|
173
|
+
| `--mcp-args` | `string` | - | MCP server arguments |
|
|
174
|
+
| `--mcp-cwd` | `string` | - | MCP server working directory |
|
|
175
|
+
| `--proxy` | `string` | - | Proxy URL |
|
|
176
|
+
| `--ca` | `string` | - | CA certificate path |
|
|
177
|
+
| `--cert` | `string` | - | Client certificate path |
|
|
178
|
+
| `--key` | `string` | - | Client key path |
|
|
179
|
+
| `--insecure` | `flag` | off | Disable SSL verification |
|
|
180
180
|
|
|
181
181
|
---
|
|
182
182
|
|
|
@@ -228,10 +228,52 @@ import type {
|
|
|
228
228
|
|
|
229
229
|
- [Official Website](https://www.powerduck.com/opensource/openapi-cli.html)
|
|
230
230
|
- [Documentation](https://www.powerduck.com/docs/openapi-cli/introduction/)
|
|
231
|
-
- [Live Demo](https://www.powerduck.com/demo/
|
|
231
|
+
- [Live Demo](https://www.powerduck.com/demo/)
|
|
232
232
|
- [GitHub](https://github.com/powerducklab/openapi-cli)
|
|
233
233
|
- [npm](https://www.npmjs.com/package/@powerduck/openapi-cli)
|
|
234
234
|
|
|
235
235
|
## License
|
|
236
236
|
|
|
237
237
|
MIT © [POWERDUCK LIMITED](https://www.powerduck.com)
|
|
238
|
+
|
|
239
|
+
## Transport and scenario contract (0.2.13)
|
|
240
|
+
|
|
241
|
+
Requires Node.js 18.17 or newer. CLI arguments override JSON configuration only
|
|
242
|
+
when explicitly supplied; `openapi-cli --config config.json` accepts `specPath`
|
|
243
|
+
from that file. Concurrency must be an integer from 1–100 and timeout from
|
|
244
|
+
1–600000 milliseconds. Report versions match the installed package.
|
|
245
|
+
|
|
246
|
+
- HTTP/SSE use the request engine's agents for HTTP(S) CONNECT proxies (including
|
|
247
|
+
credentials), custom CA, PEM client certificates and PFX. JSON `tls` accepts
|
|
248
|
+
`caCert`, `clientCert`, `clientKey`, `pfx`, `passphrase`, and `strictSSL`.
|
|
249
|
+
Custom proxy/client certificates force HTTP/1 to use those agents.
|
|
250
|
+
- WebSocket reads `x-websocket` (legacy `x-ws` is accepted), preserves the endpoint
|
|
251
|
+
path when overriding the server, and uses `timeout` for opening the connection.
|
|
252
|
+
The smoke test sends up to three extension messages or one step `values.body`,
|
|
253
|
+
then collects messages for up to 500 ms.
|
|
254
|
+
- gRPC forwards metadata/authentication and `values.body`, uses `timeout` for
|
|
255
|
+
unary deadlines and `maxStreamMs` (or timeout) for streaming deadlines, and
|
|
256
|
+
half-closes client streams before reading their final result. PEM TLS supports
|
|
257
|
+
encrypted private keys; PFX and HTTPS proxies are rejected. HTTP CONNECT proxy
|
|
258
|
+
settings are per channel and never change process environment variables.
|
|
259
|
+
Proto paths disable reflection by default unless explicitly overridden.
|
|
260
|
+
- MCP honors operation transport/command settings unless explicitly overridden.
|
|
261
|
+
A step body `{ "method": "tools/list", "params": {} }` invokes that method;
|
|
262
|
+
a plain body supplies `tools/call` arguments. Results retain the `{result}`
|
|
263
|
+
envelope; tool `isError` responses fail implicit assertions.
|
|
264
|
+
- GraphQL and MCP HTTP currently reject custom proxy/TLS settings because their
|
|
265
|
+
bundled fetch transport cannot apply them. They never silently ignore these settings.
|
|
266
|
+
- HTTP/SSE forward the request library's complete static auth configuration,
|
|
267
|
+
including `parameters` for signing schemes. Manual WebSocket/gRPC/MCP sessions
|
|
268
|
+
support Basic, Bearer and API key auth; query API keys require a URL transport.
|
|
269
|
+
OAuth token acquisition is owned by the caller; pass a resolved token.
|
|
270
|
+
- Scenario values and variables reach manual sessions as well as HTTP. WebSocket
|
|
271
|
+
and MCP support `values.query`; gRPC uses `values.header` as metadata.
|
|
272
|
+
`AbortSignal` cancels the active request and marks the scenario cancelled;
|
|
273
|
+
sessions and owned agents are closed after completion or failure.
|
|
274
|
+
|
|
275
|
+
Programmatic hosts can supply `resolveTransport({protocol, url})` to resolve
|
|
276
|
+
per-target `{proxy, tls}` settings, for example system proxies, bypass rules and
|
|
277
|
+
host-specific client certificates. JSON configuration also forwards `requester`
|
|
278
|
+
(`followRedirects`, `maxRedirects`, `disableCookies`, `protocolVersion`) and
|
|
279
|
+
`maxEvents`, `maxStreamMs`, `maxResponseSize` to supported request transports.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var e={name:"@powerduck/openapi-cli",version:"0.2.13",description:"CI-ready CLI for batch-testing OpenAPI documents across HTTP, SSE, WebSocket, GraphQL, gRPC and MCP",type:"module",main:"./dist/index.cjs",module:"./dist/index.js",types:"./dist/index.d.ts",exports:{".":{types:"./dist/index.d.ts",import:"./dist/index.js",require:"./dist/index.cjs"},"./package.json":"./package.json"},bin:{"openapi-cli":"./dist/cli.js"},files:["dist","README.md","LICENSE"],sideEffects:!1,engines:{node:">=18.17"},scripts:{build:"tsc --noEmit && tsup",typecheck:"tsc --noEmit",test:"vitest run","test:watch":"vitest","test:coverage":"vitest run --coverage",prepublishOnly:"npm run build",check:"tsc --noEmit && vitest run"},keywords:["openapi","cli","testing","grpc","mcp","graphql","sse","websocket","ci","api-testing","contract-testing"],license:"MIT",dependencies:{"@powerduck/openapi-parser":"^0.3.5","@powerduck/openapi-request":"^0.2.12",commander:"^12.1.0","https-proxy-agent":"^5.0.1"},devDependencies:{"@types/node":"^20.14.0","@vitest/coverage-v8":"^3.2.7",terser:"^5.51.2",tsup:"^8.0.0",typescript:"^5.5.0",vitest:"^3.2.7"},repository:{type:"git",url:"git+https://github.com/powerducklab/openapi-cli.git"},bugs:{url:"https://github.com/powerducklab/openapi-cli/issues"},author:"POWERDUCK LIMITED",homepage:"https://www.powerduck.com/",publishConfig:{access:"public"}};import t from"fs";import r from"path";import s from"http";import o from"https";import{dereference as n}from"@powerduck/openapi-parser";var a={outputDir:"./openapi-cli-report",formats:["json","cli","html"],concurrency:5,timeout:3e4,failOnError:!0,grpcReflection:!0,mcpTransport:"streamable-http"};function i(e){for(const[t,r,s]of[["concurrency",e.concurrency??5,100],["timeout",e.timeout??3e4,6e5]])if(!Number.isInteger(r)||r<1||r>s)throw new Error(`${t} must be an integer between 1 and ${s}.`);if(e.formats?.some(e=>!["json","cli","html"].includes(e)))throw new Error("Unsupported report format.");if(e.mcpTransport&&!["stdio","streamable-http"].includes(e.mcpTransport))throw new Error("Unsupported MCP transport.")}function c(e){const s=e.config?function(e){const s=r.resolve(e);if(!t.existsSync(s))throw new Error(`Config file not found: ${s}`);const o=t.readFileSync(s,"utf-8");try{const e=JSON.parse(o);if(!e||"object"!=typeof e||Array.isArray(e))throw new Error("Config must be an object.");return e}catch(e){throw new Error(`Invalid config file (must be JSON): ${e.message}`)}}(e.config):{},o={specPath:e.spec??s.specPath??"",serverUrl:e.server??s.serverUrl,outputDir:e.output??s.outputDir??a.outputDir,formats:p(e.format??s.formats),filter:d(e,s),concurrency:e.concurrency??s.concurrency??a.concurrency,timeout:e.timeout??s.timeout??a.timeout,proxy:e.proxy??s.proxy,tls:{caCert:e.ca??s.tls?.caCert,clientCert:e.cert??s.tls?.clientCert,clientKey:e.key??s.tls?.clientKey,pfx:s.tls?.pfx,passphrase:s.tls?.passphrase,strictSSL:void 0!==e.insecure?!e.insecure:s.tls?.strictSSL},requester:s.requester,maxEvents:s.maxEvents,maxStreamMs:s.maxStreamMs,maxResponseSize:s.maxResponseSize,headers:l(e.header,s.headers),auth:u(e,s),variables:f(e.variable,s.variables),failOnError:e.failOnError??s.failOnError??a.failOnError,envFile:e.env??s.envFile,grpcReflection:e.grpcReflection??s.grpcReflection,grpcProtoPaths:e.grpcProto??s.grpcProtoPaths,mcpTransport:e.mcpTransport??s.mcpTransport,mcpCommand:e.mcpCommand??s.mcpCommand,mcpArgs:e.mcpArgs?e.mcpArgs.split(/[\s,]+/).filter(Boolean):s.mcpArgs,mcpCwd:e.mcpCwd??s.mcpCwd};if(o.envFile&&function(e){const s=r.resolve(e);if(!t.existsSync(s))return;const o=t.readFileSync(s,"utf-8").split("\n");for(const e of o){const t=e.trim();if(!t||t.startsWith("#"))continue;const r=t.indexOf("=");if(r>0){const e=t.slice(0,r).trim(),s=t.slice(r+1).trim().replace(/^["']|["']$/g,"");e in process.env||(process.env[e]=s)}}}(o.envFile),function(e){const t=e=>"string"!=typeof e?e:e.replace(/\$\{([^}]+)\}/g,(e,t)=>process.env[t]??"");if(e.serverUrl=t(e.serverUrl),e.proxy=t(e.proxy),e.headers)for(const[r,s]of Object.entries(e.headers))e.headers[r]=t(s);e.auth?.token&&(e.auth.token=t(e.auth.token));if(e.variables)for(const[r,s]of Object.entries(e.variables))e.variables[r]=t(s)}(o),!o.specPath)throw new Error("No OpenAPI spec path provided. Use --spec <path> or set specPath in config.");if(o.proxy)try{new URL(o.proxy)}catch{throw new Error(`Invalid proxy URL: ${o.proxy}`)}return i(o),o}function p(e){return Array.isArray(e)?e:"string"==typeof e?e.split(",").map(e=>e.trim().toLowerCase()).filter(Boolean):a.formats}function d(e,t){const r=e.method?e.method.split(",").map(e=>e.trim().toLowerCase()):t.filter?.methods,s=e.path?e.path.split(",").map(e=>e.trim()):t.filter?.paths,o=e.tag?e.tag.split(",").map(e=>e.trim()):t.filter?.tags,n=e.operationId?e.operationId.split(",").map(e=>e.trim()):t.filter?.operationIds;if(r||s||o||n)return{methods:r,paths:s,tags:o,operationIds:n}}function l(e,t){const r={...t??{}};for(const t of e??[]){const e=t.indexOf(":");if(e>0){const s=t.slice(0,e).trim(),o=t.slice(e+1).trim();s&&(r[s]=o)}}return Object.keys(r).length?r:void 0}function u(e,t){return e.bearer?{type:"bearer",token:e.bearer}:t.auth}function f(e,t){const r={...t??{}};for(const t of e??[]){const e=t.indexOf("=");e>0&&(r[t.slice(0,e).trim()]=t.slice(e+1).trim())}return Object.keys(r).length?r:void 0}function m(e){return/^https?:\/\//i.test(e.trim())}async function h(e){const i=e.specPath.trim();let c;if(m(i))c=await function(e,t){let r=0;return new Promise((n,a)=>{const i=c=>{const p=("https:"===new URL(c).protocol?o:s).get(c,{timeout:t,headers:{Accept:"application/json, text/plain, */*","User-Agent":"@powerduck/openapi-cli"}},t=>{if(t.statusCode&&t.statusCode>=300&&t.statusCode<400&&t.headers.location){if(++r>5)return t.resume(),void a(new Error(`Too many redirects (>5) while fetching spec: ${e}`));const s=new URL(t.headers.location,c).toString();return t.resume(),void i(s)}if(200!==t.statusCode)return t.resume(),void a(new Error(`Failed to fetch spec from ${c}: HTTP ${t.statusCode} ${t.statusMessage??""}`.trim()));const s=[];t.on("data",e=>s.push(e)),t.on("end",()=>n(Buffer.concat(s).toString("utf-8"))),t.on("error",e=>a(new Error(`Error reading spec response: ${e.message}`)))});p.on("timeout",()=>{p.destroy(new Error(`Timed out after ${t}ms while fetching spec: ${e}`))}),p.on("error",t=>a(new Error(`Network error fetching spec ${e}: ${t.message}`)))};i(e)})}(i,e.timeout??a.timeout);else{const e=r.resolve(i);if(!t.existsSync(e))throw new Error(`OpenAPI spec not found: ${e}`);if(!t.statSync(e).isFile())throw new Error(`OpenAPI spec path is not a file: ${e}`);c=t.readFileSync(e,"utf-8")}const p=function(e,t){let r;try{r=JSON.parse(e)}catch(e){throw new Error(`Spec from ${t} is not valid JSON: ${e.message}`)}if(!r||"object"!=typeof r||Array.isArray(r))throw new Error(`Spec from ${t} is not a JSON object`);if(!r.paths||"object"!=typeof r.paths)throw new Error(`Spec from ${t} does not contain a valid 'paths' object`);return r}(c,i);return async function(e){try{const t=await n(e);return t.errors&&t.errors.length>0?e:t.schema??t.specification??e}catch(t){return e}}(p)}function g(e,t){return e.map(e=>{try{const r=function(e,t){switch(e.assert){case"status":if(void 0===e.value)throw new Error("status assertion requires 'value'");return t.status===e.value;case"header":{if(!e.key)throw new Error("header assertion requires 'key'");const r=function(e,t){const r=t.toLowerCase();for(const[t,s]of Object.entries(e))if(t.toLowerCase()===r)return s;return}(t.headers??{},e.key);return void 0!==r&&(void 0===e.contains||r.toLowerCase().includes(e.contains.toLowerCase()))}case"bodyContains":if(void 0===e.contains)throw new Error("bodyContains assertion requires 'contains'");return(t.bodyText??JSON.stringify(t.body??"")).includes(e.contains);case"bodyEquals":if(void 0===e.body)throw new Error("bodyEquals assertion requires 'body'");return(t.bodyText??JSON.stringify(t.body??""))===e.body;case"jsonPath":{if(!e.path)throw new Error("jsonPath assertion requires 'path'");const r=v(t.body,e.path);return void 0!==e.exists?e.exists?void 0!==r:void 0===r:void 0!==e.equals?b(r,e.equals):void 0!==r}case"responseTime":if(void 0===e.max)throw new Error("responseTime assertion requires 'max'");return t.durationMs<=e.max;default:throw new Error(`Unknown assertion type: ${e.assert}`)}}(e,t);return{name:e.name,passed:r}}catch(t){return{name:e.name,passed:!1,error:t.message}}})}function v(e,t){const r=t.replace(/^\$\.?/,"");if(!r)return e;const s=r.split(/\.|\[(\d+)\]/).filter(e=>void 0!==e&&""!==e);let o=e;for(const e of s){if(null==o)return;o=/^\d+$/.test(e)?o[Number(e)]:o[e]}return o}function b(e,t){if(e===t)return!0;if(typeof e!=typeof t)return!1;if(null===e||null===t)return e===t;if("object"!=typeof e)return e===t;if(Array.isArray(e)!==Array.isArray(t))return!1;if(Array.isArray(e)&&Array.isArray(t))return e.length===t.length&&e.every((e,r)=>b(e,t[r]));const r=Object.keys(e),s=Object.keys(t);return r.length===s.length&&r.every(r=>b(e[r],t[r]))}function y(e,t){const r=e?.["x-tests"],s=t?.["x-tests"];return[...Array.isArray(s)?s:[],...Array.isArray(r)?r:[]].filter(e=>e&&"object"==typeof e&&e.assert&&e.name)}function w(e){const t=e?.["x-postman-scripts"];if(t&&"object"==typeof t&&"string"==typeof t.test)return t.test}import{createClient as x}from"@powerduck/openapi-request";import $ from"fs";import{createPrivateKey as k}from"crypto";import E from"http";import S from"https";import C from"https-proxy-agent";var{HttpsProxyAgent:O}=C;function j(e){const t=e.tls;return{...t?.caCert?{ca:$.readFileSync(t.caCert)}:{},...t?.clientCert?{cert:$.readFileSync(t.clientCert)}:{},...t?.clientKey?{key:$.readFileSync(t.clientKey)}:{},...t?.pfx?{pfx:$.readFileSync(t.pfx)}:{},...t?.passphrase?{passphrase:t.passphrase}:{},rejectUnauthorized:!1!==t?.strictSSL}}function P(e){const t=j(e);if(e.proxy){const r=new URL(e.proxy);if(!["http:","https:"].includes(r.protocol))throw new Error("Only HTTP(S) CONNECT proxies are supported.");class s extends O{callback(e,r){return super.callback(e,{...r,...t})}}const o=new s({protocol:r.protocol,host:r.hostname,port:Number(r.port||("https:"===r.protocol?443:80)),...r.username||r.password?{auth:`${decodeURIComponent(r.username)}:${decodeURIComponent(r.password)}`}:{},ca:t.ca,rejectUnauthorized:t.rejectUnauthorized});return{http:o,https:o}}return{http:new E.Agent,https:new S.Agent(t)}}function R(e,t){if(e.proxy||Object.entries(e.tls||{}).some(([e,t])=>"strictSSL"===e?!1===t:Boolean(t)))throw new Error(`${t} custom proxy/TLS is not supported by the bundled request engine.`)}function T(e,t={}){return"string"==typeof e?e.replace(/\{\{([^{}]+)\}\}/g,(e,r)=>{if(!Object.prototype.hasOwnProperty.call(t,r))throw new Error(`Variable ${r} is not defined.`);return t[r]}):Array.isArray(e)?e.map(e=>T(e,t)):e&&"object"==typeof e?Object.fromEntries(Object.entries(e).map(([e,r])=>[e,T(r,t)])):e}function A(e,t,r){const s=e.auth,o=(e,r)=>{for(const r of Object.keys(t))r.toLowerCase()===e.toLowerCase()&&delete t[r];t[e]=r};if(!s||"none"===s.type)return r;if("basic"===s.type)o("Authorization",`Basic ${Buffer.from(`${s.username||""}:${s.password||""}`).toString("base64")}`);else if("bearer"===s.type)o("Authorization",`Bearer ${s.token||""}`);else{if("apikey"!==s.type)throw new Error(`Authentication ${s.type} is supported only by the HTTP/SSE runner.`);if(!s.key)throw new Error("API key name is required.");if("query"===s.in){if(!r)throw new Error("gRPC API keys require metadata headers.");const e=new URL(r);e.searchParams.set(s.key,s.value||""),r=e.href}else o(s.key,s.value||"")}return r}function U(e,t){const r={"grpc.enable_http_proxy":0};if(!e.proxy)return{address:t,channelOptions:r};const s=new URL(e.proxy);if("http:"!==s.protocol)throw new Error("gRPC supports only HTTP CONNECT proxies.");const o=new URL(`https://${t}`);return r["grpc.http_connect_target"]=`dns:///${t}`,r["grpc.default_authority"]=o.host,r["grpc.ssl_target_name_override"]=o.hostname,(s.username||s.password)&&(r["grpc.http_connect_creds"]=`${decodeURIComponent(s.username)}:${decodeURIComponent(s.password)}`),{address:`${s.hostname}:${s.port||80}`,channelOptions:r}}function q(e){return e.key&&e.passphrase?Buffer.from(k({key:e.key,passphrase:e.passphrase}).export({format:"pem",type:"pkcs8"})):e.key}function L(e,t){const r=new URL(e);for(const[e,s]of Object.entries(t||{}))if(null!=s){r.searchParams.delete(e);for(const t of Array.isArray(s)?s:[s])r.searchParams.append(e,String(t))}return r.href}var I=e.version;function M(e){if(!e)return;if("string"==typeof e)return e;if(e instanceof Error)return e.message;const t=e;if("string"==typeof t.message)return t.message;if("string"==typeof t.msg)return t.msg;try{return JSON.stringify(e)}catch{return String(e)}}function z(e,t){const r=[];if("http"===e||"sse"===e){const e=t?.status,s="number"==typeof e&&e>=200&&e<300;r.push({name:"HTTP status is 2xx (implicit)",passed:s,error:s?void 0:`expected 2xx, got ${e??"no status"}`})}else if("graphql"===e){const e=t?.status,s="number"==typeof e&&e>=200&&e<300,o=t?.body,n=Array.isArray(o?.errors)&&o.errors.length>0,a=s&&!n;let i;s?n&&(i=`GraphQL response contains ${o.errors.length} error(s)`):i=`expected HTTP 2xx, got ${e??"no status"}`,r.push({name:"GraphQL success (implicit)",passed:a,error:i})}else if("grpc"===e){const e=t?.statusCode??t?.code??t?.status,s=t?.statusName??t?.status,o=0===e;r.push({name:"gRPC status is OK (implicit)",passed:o,error:o?void 0:`expected code 0 (OK), got ${e??"unknown"}${s?` (${s})`:""}`})}else if("mcp"===e){const e=t?.body??{},s=void 0!==e.result,o=void 0!==e.error||!0===e.result?.isError||void 0!==t?.error,n=s&&!o;let a;o?a="MCP JSON-RPC response contains an error":s||(a="MCP JSON-RPC response missing 'result'"),r.push({name:"MCP JSON-RPC success (implicit)",passed:n,error:a})}else if("websocket"===e){const e=t?.events??[],s=e.some(e=>"open"===e.kind||"open"===e.state),o=e.some(e=>"error"===e.kind||"error"===e.state),n=s&&!o;r.push({name:"WebSocket connected (implicit)",passed:n,error:n?void 0:o?"WebSocket connection errored":"WebSocket did not reach open state"})}return r}async function N(e){i(e);const t=await h(e),r=x(),s=F(t,e),o=Date.now(),n=[];let a=0;const c=Math.min(e.concurrency??5,s.length),p=[];for(let o=0;o<c;o++)p.push((async()=>{for(;a<s.length;){if(e.signal?.aborted)throw new Error("Run cancelled.");const o=s[a++];if(!o)break;const i=await J(r,o,e,t);n.push(i)}})());await Promise.all(p);const d=Date.now()-o;return{summary:W(n,d),results:n.sort((e,t)=>e.path.localeCompare(t.path)||e.method.localeCompare(t.method)),config:e,generatedAt:(new Date).toISOString(),version:I}}function F(e,t){const r=[],s=["get","post","put","delete","patch","head","options"];for(const[o,n]of Object.entries(e.paths??{})){if(!n||"object"!=typeof n)continue;const e=n;for(const n of s){const s=e[n];if(!s||"object"!=typeof s)continue;const a={path:o,method:n,operation:s,pathItem:e,operationId:s.operationId??`${n.toUpperCase()} ${o}`,protocol:D(s,e)};B(a,t.filter)&&r.push(a)}}return r}function D(e,t){const r=e["x-protocol"]??t?.["x-protocol"];if(r)return String(r).toLowerCase();const s=e.responses??{};for(const e of Object.values(s)){if((e?.content??{})["text/event-stream"])return"sse"}return e["x-response-stream"]?"sse":"http"}function B(e,t){if(!t)return!0;if(t.methods?.length&&!t.methods.includes(e.method.toLowerCase()))return!1;if(t.operationIds?.length&&!t.operationIds.includes(e.operationId))return!1;if(t.tags?.length){const r=e.operation.tags??[];if(!t.tags.some(e=>r.includes(e)))return!1}if(t.paths?.length){if(!t.paths.some(t=>{try{return new RegExp(t).test(e.path)}catch{return e.path.includes(t)}}))return!1}return!0}async function J(e,t,r,s){return(await H(e,t,{config:r,spec:s,variables:r.variables})).result}async function H(e,t,r){let{config:s}=r;const{spec:o}=r,n=Date.now(),a={operationId:t.operationId,path:t.path,method:t.method.toUpperCase(),protocol:t.protocol};try{if((r.signal||s.signal)?.aborted)throw new Error("Run cancelled.");const i=r.variables||s.variables,c=t.operation["x-websocket"]||t.operation["x-ws"]||t.operation["x-grpc"]||t.operation["x-mcp"]||{},p=T(r.serverUrl??s.serverUrl??c.url??c.address??c.endpoint??o.servers?.[0]?.url,i);s.resolveTransport&&(s={...s,...await s.resolveTransport({protocol:t.protocol,url:p})});const d=["grpc","websocket","mcp"].includes(t.protocol),l={...s,serverUrl:T(s.serverUrl,i),auth:d?T(s.auth,i):s.auth,headers:d?T({...s.headers,...r.values?.header},i):s.headers,signal:r.signal||s.signal},u={...t,operation:d?{...t.operation,...Object.fromEntries(Object.entries(t.operation).filter(([e])=>["x-grpc","x-mcp","x-ws","x-websocket"].includes(e)).map(([e,t])=>[e,T(t,i)]))}:t.operation},f=d?T(r.values,i):r.values;if(!["http","sse","graphql","grpc","websocket","mcp"].includes(t.protocol))throw new Error(`Unsupported protocol: ${t.protocol}`);const m=[...y(t.operation,t.pathItem),...r.extraAssertions??[]],h=w(t.operation),v=[];let b,x,$={};switch(t.protocol){case"grpc":{const t=await async function(e,t,r,s,o){const n=t.operation["x-grpc"]??{},a=(s??r.serverUrl??n.address)?.replace(/^https?:\/\//,"");if(!a)throw new Error("gRPC operation requires x-grpc.address or --server");const i={...r.headers};A(r,i);const c=j(r);if(c.pfx)throw new Error("gRPC requires PEM certificates, not PFX.");const p=e.connect({kind:"grpc",deadlineMs:r.timeout??3e4,streamDeadlineMs:r.maxStreamMs??r.timeout??3e4,...U(r,a),tls:!(!0!==n.tls&&!/^https:/.test(s??r.serverUrl??""))&&{rootCerts:c.ca,privateKey:q(c),certChain:c.cert,rejectUnauthorized:c.rejectUnauthorized},service:n.service,method:n.method??t.operationId,reflection:r.grpcReflection??n.reflection??!(r.grpcProtoPaths??n.protoPaths)?.length,protoPaths:r.grpcProtoPaths??n.protoPaths,includeDirs:n.includeDirs,metadata:i}),d=()=>{p.close().catch(()=>{})};r.signal?.addEventListener("abort",d,{once:!0});try{if(r.signal?.aborted)throw new Error("Run cancelled.");if(await p.open(),r.signal?.aborted)throw new Error("Run cancelled.");const e=p.kind,s=o?.body??function(e){const t=e["x-grpc"]??{};if(t.sample&&"object"==typeof t.sample)return t.sample;const r=e.requestBody?.content?.["application/json"]?.schema;if(r?.properties){const e={};for(const[t,s]of Object.entries(r.properties))e[t]=s.default??K(s.type);return e}return{}}(t.operation);"unary"===e||"server_streaming"===e?await p.send(s):(await p.send(s),await p.finishSending());const c=p.events??[],d=c.find(e=>"status"===e.kind),l=c.filter(e=>"data"===e.kind&&"in"===e.direction);"unary"!==e&&"server_streaming"!==e||await p.close();const u={status:d?.meta?.code??-1,statusText:d?.meta?.statusName,body:1===l.length?l[0].data:l.map(e=>e.data),events:c.map(e=>({kind:e.kind,direction:e.direction,data:e.data,meta:e.meta}))},f=n.method??t.operationId;return{response:u,request:{method:f,target:n.service?`${n.service}/${f}`:f,url:a,...Object.keys(s).length?{body:s}:{},...Object.keys(i).length?{headers:i}:{}}}}finally{r.signal?.removeEventListener("abort",d),await p.close().catch(()=>{})}}(e,u,l,T(r.serverUrl,i),f);b=t.response,x=t.request;break}case"websocket":{const t=await async function(e,t,r,s,o){const n=t.operation["x-websocket"]??t.operation["x-ws"]??{};let a=_(n.url,s??r.serverUrl,t.path)?.replace(/^http/,"ws");if(!a)throw new Error("WebSocket operation requires x-ws.url or --server");const i={...r.headers};a=A(r,i,L(a,o?.query));const c=P(r),p=e.connect({kind:"websocket",openTimeoutMs:r.timeout??3e4,signal:r.signal,clientOptions:{...j(r),agent:a.startsWith("wss:")?c.https:c.http},url:a,subprotocols:n.subprotocols,headers:i}),d=()=>{p.close().catch(()=>{})};r.signal?.addEventListener("abort",d,{once:!0});try{if(r.signal?.aborted)throw new Error("Run cancelled.");if(await p.open(),r.signal?.aborted)throw new Error("Run cancelled.");const e=void 0!==o?.body?[o.body]:n.send??[];for(const t of e.slice(0,3))await p.send(t);await new Promise(e=>setTimeout(e,Math.min(500,r.timeout??500))),await p.close();const t=p.events??[],s=t.filter(e=>"in"===e.direction&&"open"!==e.kind);return{response:{status:101,statusText:"Switching Protocols",body:s.map(e=>e.data),events:t.map(e=>({kind:e.kind,direction:e.direction,data:e.data})),streaming:!0},request:{method:"WS",url:a,...e.length?{body:e.slice(0,3)}:{},...Object.keys(i).length?{headers:i}:{}}}}finally{r.signal?.removeEventListener("abort",d),await p.close().catch(()=>{});for(const e of new Set(Object.values(c)))e.destroy()}}(e,u,l,T(r.serverUrl,i),f);b=t.response,x=t.request;break}case"mcp":{const t=await async function(e,t,r,s,o){const n=t.operation["x-mcp"]??{},a=r.mcpTransport??n.transport??"streamable-http",i={...r.headers};let c=_(n.endpoint,s??r.serverUrl,t.path);"stdio"!==a&&(R(r,"MCP HTTP"),c=A(r,i,c?L(c,o?.query):c));const p=o?.body?.method??n.method??"tools/call",d=o?.body?.params??n.params??("tools/call"===p?{name:n.name,arguments:o?.body??n.arguments??{}}:{}),l=e.connect({kind:"mcp",transport:"stdio"===a?"stdio":"streamable-http",endpoint:c,headers:i,command:r.mcpCommand??n.command,args:r.mcpArgs??n.args,cwd:r.mcpCwd??n.cwd,timeoutMs:r.timeout??3e4}),u=()=>{l.close().catch(()=>{})};r.signal?.addEventListener("abort",u,{once:!0});try{if(r.signal?.aborted)throw new Error("Run cancelled.");if(await l.open(),r.signal?.aborted)throw new Error("Run cancelled.");return{response:{status:200,statusText:"OK",body:{result:await l.request(p,d,{signal:r.signal,timeoutMs:r.timeout})}},request:{method:p,url:c,headers:i,body:d}}}finally{r.signal?.removeEventListener("abort",u),await l.close().catch(()=>{})}}(e,u,l,T(r.serverUrl,i),f);b=t.response,x=t.request;break}default:{const n=await async function(e,t,r,s,o){"graphql"===t.protocol&&R(r,"GraphQL");const n="graphql"===t.protocol?void 0:P(r),a={...r.httpRuntime,requester:{...r.requester,strictSSL:!1!==r.tls?.strictSSL,ignoreProxyEnvironmentVariables:!0,...n?{agents:n,...r.proxy||r.tls?.caCert||r.tls?.clientCert||r.tls?.pfx||r.httpRuntime?.certificates?{protocolVersion:"http1"}:{}}:{}}},i=o.values,c={...r.headers??{},...i?.header??{}},p={...r.headers?{header:r.headers}:{},...i??{},...Object.keys(c).length?{header:c}:{}};let d;try{d=await e.send({writeBack:!1,signal:o.signal,spec:s,target:{path:t.path,method:t.method},serverUrl:o.serverUrl??r.serverUrl,values:Object.keys(p).length?p:void 0,auth:r.auth,variables:o.variables,timeout:r.timeout,runner:Object.keys(a).length?a:void 0,scripts:o.postmanTest?{test:{exec:o.postmanTest},fromSpecExtensions:!1}:void 0,onAssertion:e=>{o.postmanAssertions.push({name:e.name??"assertion",passed:e.passed,error:M(e.error)})},maxStreamMs:r.maxStreamMs??r.timeout,maxEvents:r.maxEvents??50,maxResponseSize:r.maxResponseSize})}finally{for(const e of new Set(Object.values(n||{})))e.destroy()}if(o.signal?.aborted)throw new Error("Run cancelled.");if(d.error)throw new Error(d.error.message);const l={},u=d.scripts;if(u)for(const e of[...u.prerequest??[],...u.test??[]])e.environment&&Object.assign(l,e.environment);const f=d.request;return{response:{status:d.response?.status,statusText:d.response?.statusText,contentType:d.response?.contentType,sizeBytes:d.response?.sizeBytes,body:d.response?.body,text:d.response?.text,headers:d.response?.headers,events:d.response?.events,streaming:d.response?.streaming},request:f?{method:f.method,url:f.url,headers:f.headers,...void 0!==f.body?{body:f.body}:{}}:void 0,env:l}}(e,t,s,o,{postmanTest:h,postmanAssertions:v,variables:r.variables,values:r.values,serverUrl:r.serverUrl,signal:r.signal||s.signal});b=n.response,x=n.request,$=n.env;break}}if((r.signal||s.signal)?.aborted)throw new Error("Run cancelled.");const k=Date.now()-n,E=m.length>0||v.length>0?[]:z(t.protocol,b),S=m.length?g(m,{status:b?.status,headers:b?b.headers:void 0,body:b?.body,bodyText:b?b.text:void 0,durationMs:k}):[],C=new Set,O=v.filter(e=>{const t=`${e.name}|${e.passed}|${e.error??""}`;return!C.has(t)&&(C.add(t),!0)}),I=[...S,...O,...E],N=I.some(e=>!e.passed);return{result:{...a,status:N?"failed":"passed",durationMs:k,request:x,response:b,assertions:I.length?I:void 0,timestamp:(new Date).toISOString()},response:b,request:x,env:$}}catch(e){const o=Date.now()-n,i=r.values,c={...s.headers??{},...i?.header??{}},p=Object.fromEntries(Object.entries(c).map(([e,t])=>[e,null==t?"":String(t)])),d={method:t.method.toUpperCase(),url:r.serverUrl??s.serverUrl,...Object.keys(p).length?{headers:p}:{},...void 0!==i?.body?{body:i.body}:{}};return{result:{...a,status:"error",durationMs:o,request:d,error:e.message??String(e),timestamp:(new Date).toISOString()},response:void 0,request:d,env:{}}}}function _(e,t,r){if(!t)return e;const s=new URL(t),o=new URL(e||r,t);return o.host=s.host,o.protocol=s.protocol,e||"/"===s.pathname||(o.pathname=s.pathname.replace(/\/$/,"")+r),o.href}function K(e){switch(e){case"string":default:return"";case"number":case"integer":return 0;case"boolean":return!1;case"array":return[];case"object":return{}}}function W(e,t){const r=e.length,s=e.filter(e=>"passed"===e.status).length;return{total:r,passed:s,failed:e.filter(e=>"failed"===e.status).length,errors:e.filter(e=>"error"===e.status).length,skipped:e.filter(e=>"skipped"===e.status).length,durationMs:t,passRate:r>0?Math.round(s/r*1e3)/10:0}}import G from"fs";import Q from"path";function V(e,t){const r=Q.resolve(t);G.mkdirSync(r,{recursive:!0});const s=Q.join(r,"report.json");return G.writeFileSync(s,JSON.stringify(e,null,2),"utf-8"),s}var X="[0m",Y="[1m",Z="[2m",ee="[32m",te="[31m",re="[33m",se="[36m",oe="[90m",ne="[42m",ae="[41m",ie="[43m";function ce(e){if(null==e||!Number.isFinite(e))return`${Z}--ms${X}`;return`${e>500?`${Y}${re}`:Y}${e}${X}${Z}ms${X}`}function pe(e){const t=[],{summary:r,results:s}=e;t.push(""),t.push(`${Y}${se}╔══════════════════════════════════════════════════════╗${X}`),t.push(`${Y}${se}║ Powerduck Test Report ║${X}`),t.push(`${Y}${se}╚══════════════════════════════════════════════════════╝${X}`),t.push("");const o=r.passRate>=80?ee:r.passRate>=50?re:te;t.push(`${Y} Total:${X} ${r.total} ${ee}Passed:${X} ${r.passed} ${te}Failed:${X} ${r.failed} ${re}Errors:${X} ${r.errors} ${o}Rate:${X} ${r.passRate}% ${Z}Time:${X} ${ce(r.durationMs)}`),t.push("");const n=r.total>0?Math.round(r.passed/r.total*50):0,a=r.total>0?Math.round(r.failed/r.total*50):0,i=r.total>0?Math.round(r.errors/r.total*50):0,c=ee+"█".repeat(n)+te+"█".repeat(a)+re+"█".repeat(i)+oe+"░".repeat(Math.max(0,50-n-a-i))+X;t.push(` [${c}]`),t.push("");for(const e of s)t.push(de(e));return t.push(""),r.failed>0||r.errors>0?t.push(`${Y}${te} ✗ ${r.failed+r.errors} test(s) did not pass${X}`):t.push(`${Y}${ee} ✓ All ${r.total} test(s) passed${X}`),t.push(` ${Z}Generated: ${e.generatedAt}${X}`),t.push(""),t.join("\n")}function de(e){const t="passed"===e.status?`${ee}✓${X}`:"failed"===e.status?`${te}✗${X}`:"error"===e.status?`${re}!${X}`:`${oe}-${X}`,r="passed"===e.status?`${ne}${Y} PASS ${X}`:"failed"===e.status?`${ae}${Y} FAIL ${X}`:"error"===e.status?`${ie}${Y} ERROR ${X}`:`${oe} SKIP ${X}`,s=e.protocol?` ${se}[${e.protocol}]${X}`:"",o=null!=e.response?.status?` ${Z}${e.response.status}${X}`:"";let n=` ${t} ${r} ${Y}${e.method}${X} ${e.path}${s}${o} ${ce(e.durationMs)}`;if(e.error&&(n+=`\n ${te}${e.error}${X}`),e.assertions?.length)for(const t of e.assertions){n+=`\n ${t.passed?`${ee} ✓${X}`:`${te} ✗${X}`} ${t.name}${t.error?` — ${te}${t.error}${X}`:""}`}return n}function le(e){const t=pe(e);return console.log(t),t}import ue from"fs";import fe from"path";function me(e,t){const r=fe.resolve(t);ue.mkdirSync(r,{recursive:!0});const s=fe.join(r,"report.html");return ue.writeFileSync(s,function(e){const{summary:t,results:r}=e,s=JSON.stringify({summary:t,results:r});return`<!DOCTYPE html>\n<html lang="en">\n<head>\n<meta charset="UTF-8">\n<meta name="viewport" content="width=device-width, initial-scale=1.0">\n<title>Test Report — Powerduck</title>\n<style>\n *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}\n :root{\n --bg:#ffffff;--fg:#0f172a;--muted:#64748b;--border:#e2e8f0;\n --bg-subtle:#f8fafc;--bg-hover:#f1f5f9;\n --green:#16a34a;--green-bg:#f0fdf4;--green-border:#bbf7d0;\n --red:#dc2626;--red-bg:#fef2f2;--red-border:#fecaca;\n --amber:#d97706;--amber-bg:#fffbeb;--amber-border:#fde68a;\n --blue:#2563eb;--mono:ui-monospace,SFMono-Regular,'SF Mono',Menlo,Consolas,monospace;\n --sans:ui-sans-serif,system-ui,-apple-system,'Segoe UI',Roboto,Inter,sans-serif;\n }\n @media (prefers-color-scheme:dark){\n :root{\n --bg:#0f172a;--fg:#f1f5f9;--muted:#94a3b8;--border:#1e293b;\n --bg-subtle:#1e293b;--bg-hover:#334155;\n --green:#4ade80;--green-bg:rgba(74,222,128,.1);--green-border:rgba(74,222,128,.25);\n --red:#f87171;--red-bg:rgba(248,113,113,.1);--red-border:rgba(248,113,113,.25);\n --amber:#fbbf24;--amber-bg:rgba(251,191,36,.1);--amber-border:rgba(251,191,36,.25);\n --blue:#60a5fa;\n }\n }\n body{font-family:var(--sans);background:var(--bg);color:var(--fg);line-height:1.5;font-size:14px;-webkit-font-smoothing:antialiased}\n .wrap{max-width:960px;margin:0 auto;padding:32px 24px 64px}\n\n /* Header */\n .hdr{display:flex;align-items:baseline;justify-content:space-between;margin-bottom:24px;padding-bottom:16px;border-bottom:1px solid var(--border)}\n .hdr h1{font-size:18px;font-weight:600;letter-spacing:-.01em}\n .hdr .meta{font-size:12px;color:var(--muted);font-family:var(--mono)}\n\n /* Summary strip */\n .strip{display:grid;grid-template-columns:repeat(6,1fr);gap:0;border:1px solid var(--border);border-radius:8px;overflow:hidden;margin-bottom:24px}\n .stat{padding:14px 16px;border-right:1px solid var(--border);text-align:center}\n .stat:last-child{border-right:none}\n .stat .v{font-size:22px;font-weight:700;letter-spacing:-.02em;line-height:1.2}\n .stat .l{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;margin-top:2px}\n .stat.total .v{color:var(--fg)}\n .stat.pass .v{color:var(--green)}\n .stat.fail .v{color:var(--red)}\n .stat.err .v{color:var(--amber)}\n .stat.rate .v{color:var(--blue)}\n .stat.time .v{font-size:16px;font-weight:600}\n\n /* Progress */\n .bar{height:4px;background:var(--border);border-radius:2px;overflow:hidden;margin-bottom:24px;display:flex}\n .bar>div{height:100%}\n .bar .p{background:var(--green)}\n .bar .f{background:var(--red)}\n .bar .e{background:var(--amber)}\n\n /* Filters */\n .filters{display:flex;gap:4px;margin-bottom:16px}\n .fbtn{padding:4px 12px;border-radius:6px;border:1px solid transparent;background:transparent;color:var(--muted);cursor:pointer;font-size:12px;font-weight:500;transition:all .12s}\n .fbtn:hover{color:var(--fg);background:var(--bg-hover)}\n .fbtn.active{background:var(--bg-subtle);color:var(--fg);border-color:var(--border)}\n .fbtn .n{color:var(--muted);margin-left:4px;font-variant-numeric:tabular-nums}\n\n /* Test rows */\n .row{border:1px solid var(--border);border-radius:8px;margin-bottom:6px;overflow:hidden;transition:border-color .12s}\n .row:hover{border-color:var(--muted)}\n .row-head{display:flex;align-items:center;gap:10px;padding:10px 14px;cursor:pointer;user-select:none}\n .dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}\n .dot.pass{background:var(--green)}\n .dot.fail{background:var(--red)}\n .dot.err{background:var(--amber)}\n .method{font-family:var(--mono);font-size:11px;font-weight:600;color:var(--muted);min-width:40px;text-transform:uppercase}\n .path{font-family:var(--mono);font-size:13px;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n .proto{font-size:10px;text-transform:uppercase;letter-spacing:.05em;color:var(--muted);padding:2px 8px;border:1px solid var(--border);border-radius:4px}\n .dur{font-family:var(--mono);font-size:12px;color:var(--muted);min-width:56px;text-align:right;font-variant-numeric:tabular-nums}\n .chev{color:var(--muted);font-size:10px;transition:transform .15s;flex-shrink:0}\n .row.open .chev{transform:rotate(90deg)}\n\n /* Row body */\n .row-body{display:none;padding:0 14px 12px 32px;border-top:1px solid var(--border)}\n .row.open .row-body{display:block}\n .sect{margin-top:10px}\n .sect-t{font-size:10px;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);font-weight:600;margin-bottom:6px}\n .asrt{display:flex;align-items:center;gap:8px;padding:3px 0;font-size:13px}\n .asrt .ck{width:14px;height:14px;border-radius:3px;display:flex;align-items:center;justify-content:center;font-size:9px;font-weight:700;flex-shrink:0;color:#fff}\n .asrt.pass .ck{background:var(--green)}\n .asrt.fail .ck{background:var(--red)}\n .asrt .an{color:var(--fg)}\n .asrt .ae{color:var(--red);font-size:12px;margin-left:6px;font-family:var(--mono)}\n .rmeta{display:flex;gap:20px;flex-wrap:wrap;font-size:12px}\n .rmeta span{color:var(--muted)}\n .rmeta strong{color:var(--fg);font-weight:600;font-family:var(--mono)}\n .errbox{background:var(--red-bg);border:1px solid var(--red-border);border-radius:6px;padding:8px 12px;color:var(--red);font-size:12px;font-family:var(--mono);margin-top:10px}\n .bprev{background:var(--bg-subtle);border:1px solid var(--border);border-radius:6px;padding:10px 12px;font-family:var(--mono);font-size:11px;color:var(--muted);max-height:180px;overflow:auto;white-space:pre-wrap;word-break:break-all;margin-top:6px}\n\n footer{margin-top:32px;padding-top:16px;border-top:1px solid var(--border);font-size:11px;color:var(--muted);display:flex;justify-content:space-between}\n\n @media(max-width:640px){\n .strip{grid-template-columns:repeat(3,1fr)}\n .stat:nth-child(3){border-right:none}\n .stat:nth-child(4),.stat:nth-child(5){border-top:1px solid var(--border)}\n .path{font-size:11px}\n }\n</style>\n</head>\n<body>\n<div class="wrap">\n <div class="hdr">\n <h1>Test Report</h1>\n <span class="meta">${e.generatedAt} · v${e.version}</span>\n </div>\n\n <div class="strip">\n <div class="stat total"><div class="v">${t.total}</div><div class="l">Total</div></div>\n <div class="stat pass"><div class="v">${t.passed}</div><div class="l">Passed</div></div>\n <div class="stat fail"><div class="v">${t.failed}</div><div class="l">Failed</div></div>\n <div class="stat err"><div class="v">${t.errors}</div><div class="l">Errors</div></div>\n <div class="stat rate"><div class="v">${t.passRate}%</div><div class="l">Pass Rate</div></div>\n <div class="stat time"><div class="v">${(t.durationMs/1e3).toFixed(2)}s</div><div class="l">Duration</div></div>\n </div>\n\n <div class="bar">\n <div class="p" style="width:${t.total?t.passed/t.total*100:0}%"></div>\n <div class="f" style="width:${t.total?t.failed/t.total*100:0}%"></div>\n <div class="e" style="width:${t.total?t.errors/t.total*100:0}%"></div>\n </div>\n\n <div class="filters">\n <button class="fbtn active" data-f="all">All<span class="n">${t.total}</span></button>\n <button class="fbtn" data-f="passed">Passed<span class="n">${t.passed}</span></button>\n <button class="fbtn" data-f="failed">Failed<span class="n">${t.failed}</span></button>\n <button class="fbtn" data-f="error">Errors<span class="n">${t.errors}</span></button>\n </div>\n\n <div id="list"></div>\n\n <footer>\n <span>@powerduck/openapi-cli v${e.version}</span>\n <span>${t.passed}/${t.total} passed</span>\n </footer>\n</div>\n\n<script>\nconst D=${s};\nfunction esc(s){const d=document.createElement('div');d.textContent=String(s??'');return d.innerHTML}\nfunction render(f){\n const list=document.getElementById('list');\n const items=f==='all'?D.results:D.results.filter(r=>r.status===f);\n list.innerHTML=items.map(r=>{\n const as=r.assertions?r.assertions.map(a=>\`\n <div class="asrt \${a.passed?'pass':'fail'}">\n <div class="ck">\${a.passed?'✓':'✗'}</div>\n <span class="an">\${esc(a.name)}</span>\n \${a.error?'<span class="ae">'+esc(a.error)+'</span>':''}\n </div>\`).join(''):'';\n const bp=r.response?.body?'<div class="bprev">'+esc(JSON.stringify(r.response.body,null,2).slice(0,3000))+'</div>':'';\n const eb=r.error?'<div class="errbox">'+esc(r.error)+'</div>':'';\n const rm=r.response?\`\n <div class="rmeta">\n \${r.response.status!=null?'<span>Status <strong>'+r.response.status+(r.response.statusText?' '+r.response.statusText:'')+'</strong></span>':''}\n \${r.response.contentType?'<span>Type <strong>'+esc(r.response.contentType)+'</strong></span>':''}\n \${r.response.sizeBytes!=null?'<span>Size <strong>'+r.response.sizeBytes+'B</strong></span>':''}\n \${r.response.streaming?'<span>Streaming <strong>yes</strong></span>':''}\n </div>\`:'';\n return \`\n <div class="row \${r.status}" data-s="\${r.status}">\n <div class="row-head" onclick="this.parentElement.classList.toggle('open')">\n <div class="dot \${r.status==='passed'?'pass':r.status==='failed'?'fail':'err'}"></div>\n <span class="method">\${r.method}</span>\n <span class="path">\${esc(r.path)}</span>\n \${r.protocol?'<span class="proto">'+r.protocol+'</span>':''}\n <span class="dur">\${r.durationMs}ms</span>\n <span class="chev">▶</span>\n </div>\n <div class="row-body">\n \${eb}\n \${rm}\n \${as?'<div class="sect"><div class="sect-t">Assertions</div>'+as+'</div>':''}\n \${bp?'<div class="sect"><div class="sect-t">Response</div>'+bp+'</div>':''}\n </div>\n </div>\`;\n }).join('');\n}\ndocument.querySelectorAll('.fbtn').forEach(b=>b.addEventListener('click',()=>{\n document.querySelectorAll('.fbtn').forEach(x=>x.classList.remove('active'));\n b.classList.add('active');render(b.dataset.f);\n}));\nrender('all');\n<\/script>\n</body>\n</html>`}(e),"utf-8"),s}export{e as package_default,a as DEFAULT_CONFIG,i as validateRunConfig,c as resolveConfig,m as isRemoteSpec,h as loadSpec,g as runDeclarativeAssertions,v as resolveJsonPath,y as extractAssertions,w as extractPostmanScripts,M as formatAssertionError,z as buildImplicitAssertions,N as runTests,F as collectOperations,H as executeStep,W as buildSummary,V as generateJsonReport,pe as generateCliReport,le as printCliReport,me as generateHtmlReport};
|
package/dist/cli.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
"use strict";var e=Object.create,t=Object.defineProperty,r=Object.getOwnPropertyDescriptor,s=Object.getOwnPropertyNames,o=Object.getPrototypeOf,n=Object.prototype.hasOwnProperty,a=(a,i,c)=>(c=null!=a?e(o(a)):{},((e,o,a,i)=>{if(o&&"object"==typeof o||"function"==typeof o)for(let c of s(o))n.call(e,c)||c===a||t(e,c,{get:()=>o[c],enumerable:!(i=r(o,c))||i.enumerable});return e})(!i&&a&&a.__esModule?c:t(c,"default",{value:a,enumerable:!0}),a)),i=require("commander"),c=a(require("fs"),1),p=a(require("path"),1),d=a(require("http"),1),l=a(require("https"),1),u=require("@powerduck/openapi-parser"),f="./openapi-cli-report",m=["json","cli","html"],h=5,v=3e4,g=!0,b=!0,y="streamable-http";function x(e){const t=e.config?function(e){const t=p.default.resolve(e);if(!c.default.existsSync(t))throw new Error(`Config file not found: ${t}`);const r=c.default.readFileSync(t,"utf-8");try{return JSON.parse(r)}catch(e){throw new Error(`Invalid config file (must be JSON): ${e.message}`)}}(e.config):{},r={specPath:e.spec??t.specPath??"",serverUrl:e.server??t.serverUrl,outputDir:e.output??t.outputDir??f,formats:w(e.format??t.formats),filter:$(e,t),concurrency:e.concurrency??t.concurrency??h,timeout:e.timeout??t.timeout??v,proxy:e.proxy??t.proxy,tls:{caCert:e.ca??t.tls?.caCert,clientCert:e.cert??t.tls?.clientCert,clientKey:e.key??t.tls?.clientKey,strictSSL:void 0!==e.insecure?!e.insecure:t.tls?.strictSSL},headers:k(e.header,t.headers),auth:S(e,t),variables:P(e.variable,t.variables),failOnError:e.failOnError??t.failOnError??g,envFile:e.env??t.envFile,grpcReflection:e.grpcReflection??t.grpcReflection??b,grpcProtoPaths:e.grpcProto??t.grpcProtoPaths,mcpTransport:e.mcpTransport??t.mcpTransport??y,mcpCommand:e.mcpCommand??t.mcpCommand,mcpArgs:e.mcpArgs?e.mcpArgs.split(/[\s,]+/).filter(Boolean):t.mcpArgs,mcpCwd:e.mcpCwd??t.mcpCwd};if(r.envFile&&function(e){const t=p.default.resolve(e);if(!c.default.existsSync(t))return;const r=c.default.readFileSync(t,"utf-8").split("\n");for(const e of r){const t=e.trim();if(!t||t.startsWith("#"))continue;const r=t.indexOf("=");if(r>0){const e=t.slice(0,r).trim(),s=t.slice(r+1).trim().replace(/^["']|["']$/g,"");e in process.env||(process.env[e]=s)}}}(r.envFile),function(e){const t=e=>"string"!=typeof e?e:e.replace(/\$\{([^}]+)\}/g,(e,t)=>process.env[t]??"");if(e.serverUrl=t(e.serverUrl),e.proxy=t(e.proxy),e.headers)for(const[r,s]of Object.entries(e.headers))e.headers[r]=t(s);e.auth?.token&&(e.auth.token=t(e.auth.token));if(e.variables)for(const[r,s]of Object.entries(e.variables))e.variables[r]=t(s)}(r),!r.specPath)throw new Error("No OpenAPI spec path provided. Use --spec <path> or set specPath in config.");if(r.proxy)try{new URL(r.proxy)}catch{throw new Error(`Invalid proxy URL: ${r.proxy}`)}return r}function w(e){return Array.isArray(e)?e:"string"==typeof e?e.split(",").map(e=>e.trim().toLowerCase()).filter(Boolean):m}function $(e,t){const r=e.method?e.method.split(",").map(e=>e.trim().toLowerCase()):t.filter?.methods,s=e.path?e.path.split(",").map(e=>e.trim()):t.filter?.paths,o=e.tag?e.tag.split(",").map(e=>e.trim()):t.filter?.tags,n=e.operationId?e.operationId.split(",").map(e=>e.trim()):t.filter?.operationIds;if(r||s||o||n)return{methods:r,paths:s,tags:o,operationIds:n}}function k(e,t){const r={...t??{}};for(const t of e??[]){const e=t.indexOf(":");if(e>0){const s=t.slice(0,e).trim(),o=t.slice(e+1).trim();s&&(r[s]=o)}}return Object.keys(r).length?r:void 0}function S(e,t){return e.bearer?{type:"bearer",token:e.bearer}:t.auth}function P(e,t){const r={...t??{}};for(const t of e??[]){const e=t.indexOf("=");e>0&&(r[t.slice(0,e).trim()]=t.slice(e+1).trim())}return Object.keys(r).length?r:void 0}async function C(e){const t=e.specPath.trim();let r;if(/^https?:\/\//i.test(t.trim()))r=await function(e,t){let r=0;return new Promise((s,o)=>{const n=a=>{const i=("https:"===new URL(a).protocol?l.default:d.default).get(a,{timeout:t,headers:{Accept:"application/json, text/plain, */*","User-Agent":"@powerduck/openapi-cli"}},t=>{if(t.statusCode&&t.statusCode>=300&&t.statusCode<400&&t.headers.location){if(++r>5)return t.resume(),void o(new Error(`Too many redirects (>5) while fetching spec: ${e}`));const s=new URL(t.headers.location,a).toString();return t.resume(),void n(s)}if(200!==t.statusCode)return t.resume(),void o(new Error(`Failed to fetch spec from ${a}: HTTP ${t.statusCode} ${t.statusMessage??""}`.trim()));const i=[];t.on("data",e=>i.push(e)),t.on("end",()=>s(Buffer.concat(i).toString("utf-8"))),t.on("error",e=>o(new Error(`Error reading spec response: ${e.message}`)))});i.on("timeout",()=>{i.destroy(new Error(`Timed out after ${t}ms while fetching spec: ${e}`))}),i.on("error",t=>o(new Error(`Network error fetching spec ${e}: ${t.message}`)))};n(e)})}(t,e.timeout??v);else{const e=p.default.resolve(t);if(!c.default.existsSync(e))throw new Error(`OpenAPI spec not found: ${e}`);if(!c.default.statSync(e).isFile())throw new Error(`OpenAPI spec path is not a file: ${e}`);r=c.default.readFileSync(e,"utf-8")}const s=function(e,t){let r;try{r=JSON.parse(e)}catch(e){throw new Error(`Spec from ${t} is not valid JSON: ${e.message}`)}if(!r||"object"!=typeof r||Array.isArray(r))throw new Error(`Spec from ${t} is not a JSON object`);if(!r.paths||"object"!=typeof r.paths)throw new Error(`Spec from ${t} does not contain a valid 'paths' object`);return r}(r,t);return async function(e){try{const t=await(0,u.dereference)(e);return t.errors&&t.errors.length>0?e:t.schema??t.specification??e}catch(t){return e}}(s)}var O=require("@powerduck/openapi-request"),T=a(require("fs"),1);function E(e,t){return e.map(e=>{try{const r=function(e,t){switch(e.assert){case"status":if(void 0===e.value)throw new Error("status assertion requires 'value'");return t.status===e.value;case"header":{if(!e.key)throw new Error("header assertion requires 'key'");const r=function(e,t){const r=t.toLowerCase();for(const[t,s]of Object.entries(e))if(t.toLowerCase()===r)return s;return}(t.headers??{},e.key);return void 0!==r&&(void 0===e.contains||r.toLowerCase().includes(e.contains.toLowerCase()))}case"bodyContains":if(void 0===e.contains)throw new Error("bodyContains assertion requires 'contains'");return(t.bodyText??JSON.stringify(t.body??"")).includes(e.contains);case"bodyEquals":if(void 0===e.body)throw new Error("bodyEquals assertion requires 'body'");return(t.bodyText??JSON.stringify(t.body??""))===e.body;case"jsonPath":{if(!e.path)throw new Error("jsonPath assertion requires 'path'");const r=function(e,t){const r=t.replace(/^\$\.?/,"");if(!r)return e;const s=r.split(/\.|\[(\d+)\]/).filter(e=>void 0!==e&&""!==e);let o=e;for(const e of s){if(null==o)return;o=/^\d+$/.test(e)?o[Number(e)]:o[e]}return o}(t.body,e.path);return void 0!==e.exists?e.exists?void 0!==r:void 0===r:void 0!==e.equals?j(r,e.equals):void 0!==r}case"responseTime":if(void 0===e.max)throw new Error("responseTime assertion requires 'max'");return t.durationMs<=e.max;default:throw new Error(`Unknown assertion type: ${e.assert}`)}}(e,t);return{name:e.name,passed:r}}catch(t){return{name:e.name,passed:!1,error:t.message}}})}function j(e,t){if(e===t)return!0;if(typeof e!=typeof t)return!1;if(null===e||null===t)return e===t;if("object"!=typeof e)return e===t;if(Array.isArray(e)!==Array.isArray(t))return!1;if(Array.isArray(e)&&Array.isArray(t))return e.length===t.length&&e.every((e,r)=>j(e,t[r]));const r=Object.keys(e),s=Object.keys(t);return r.length===s.length&&r.every(r=>j(e[r],t[r]))}function q(e){if(!e)return;if("string"==typeof e)return e;if(e instanceof Error)return e.message;const t=e;if("string"==typeof t.message)return t.message;if("string"==typeof t.msg)return t.msg;try{return JSON.stringify(e)}catch{return String(e)}}async function A(e){const t=await C(e),r=(0,O.createClient)(),s=function(e,t){const r=[],s=["get","post","put","delete","patch","head","options"];for(const[o,n]of Object.entries(e.paths??{})){const e=n;for(const n of s){const s=e[n];if(!s||"object"!=typeof s)continue;const a={path:o,method:n,operation:s,pathItem:e,operationId:s.operationId??`${n.toUpperCase()} ${o}`,protocol:I(s,e)};R(a,t.filter)&&r.push(a)}}return r}(t,e),o=Date.now(),n=[],a=[...s],i=Math.min(e.concurrency??5,a.length),c=[];for(let s=0;s<i;s++)c.push((async()=>{for(;a.length>0;){const s=a.shift();if(!s)break;const o=await L(r,s,e,t);n.push(o)}})());await Promise.all(c);const p=Date.now()-o,d=function(e,t){const r=e.length,s=e.filter(e=>"passed"===e.status).length,o=e.filter(e=>"failed"===e.status).length,n=e.filter(e=>"error"===e.status).length,a=e.filter(e=>"skipped"===e.status).length;return{total:r,passed:s,failed:o,errors:n,skipped:a,durationMs:t,passRate:r>0?Math.round(s/r*1e3)/10:0}}(n,p);return{summary:d,results:n.sort((e,t)=>e.path.localeCompare(t.path)||e.method.localeCompare(t.method)),config:e,generatedAt:(new Date).toISOString(),version:"0.1.0"}}function I(e,t){const r=e["x-protocol"]??t?.["x-protocol"];if(r)return String(r).toLowerCase();const s=e.responses??{};for(const e of Object.values(s)){if((e?.content??{})["text/event-stream"])return"sse"}return e["x-response-stream"]?"sse":"http"}function R(e,t){if(!t)return!0;if(t.methods?.length&&!t.methods.includes(e.method.toLowerCase()))return!1;if(t.operationIds?.length&&!t.operationIds.includes(e.operationId))return!1;if(t.tags?.length){const r=e.operation.tags??[];if(!t.tags.some(e=>r.includes(e)))return!1}if(t.paths?.length){if(!t.paths.some(t=>{try{return new RegExp(t).test(e.path)}catch{return e.path.includes(t)}}))return!1}return!0}async function L(e,t,r,s){const o=await async function(e,t,r){const{config:s,spec:o}=r,n=Date.now(),a={operationId:t.operationId,path:t.path,method:t.method.toUpperCase(),protocol:t.protocol};try{const i=[...function(e,t){const r=e?.["x-tests"],s=t?.["x-tests"];return[...Array.isArray(s)?s:[],...Array.isArray(r)?r:[]].filter(e=>e&&"object"==typeof e&&e.assert&&e.name)}(t.operation,t.pathItem),...r.extraAssertions??[]],c=function(e){const t=e?.["x-postman-scripts"];if(t&&"object"==typeof t&&"string"==typeof t.test)return t.test}(t.operation),p=[];let d,l,u={};switch(t.protocol){case"grpc":{const o=await async function(e,t,r,s){const o=t.operation["x-grpc"]??{},n=o.address??(s??r.serverUrl)?.replace(/^https?:\/\//,"");if(!n)throw new Error("gRPC operation requires x-grpc.address or --server");const a=e.connect({kind:"grpc",address:n,service:o.service,method:o.method??t.operationId,reflection:r.grpcReflection,protoPaths:r.grpcProtoPaths,metadata:r.headers});await a.open();const i=a.kind,c=function(e){const t=e["x-grpc"]??{};if(t.sample&&"object"==typeof t.sample)return t.sample;const r=e.requestBody?.content?.["application/json"]?.schema;if(r?.properties){const e={};for(const[t,s]of Object.entries(r.properties))e[t]=s.default??M(s.type);return e}return{}}(t.operation);"unary"===i||"server_streaming"===i?await a.send(c):(await a.send(c),await a.close());const p=a.events??[],d=p.find(e=>"status"===e.kind),l=p.filter(e=>"data"===e.kind&&"inbound"===e.direction);"unary"!==i&&"server_streaming"!==i||await a.close();const u={status:d?.meta?.code??0,statusText:d?.meta?.statusName,body:1===l.length?l[0].data:l.map(e=>e.data),events:p.map(e=>({kind:e.kind,direction:e.direction,data:e.data,meta:e.meta}))},f=o.method??t.operationId;return{response:u,request:{method:f,target:o.service?`${o.service}/${f}`:f,url:n,...Object.keys(c).length?{body:c}:{},...r.headers?{headers:r.headers}:{}}}}(e,t,s,r.serverUrl);d=o.response,l=o.request;break}case"websocket":{const o=await async function(e,t,r,s){const o=t.operation["x-ws"]??{},n=o.url??(s??r.serverUrl)?.replace(/^http/,"ws");if(!n)throw new Error("WebSocket operation requires x-ws.url or --server");const a=e.connect({kind:"websocket",url:n,subprotocols:o.subprotocols,headers:r.headers});await a.open();const i=o.send??[];for(const e of i.slice(0,3))await a.send(e);await new Promise(e=>setTimeout(e,500)),await a.close();const c=a.events??[],p=c.filter(e=>"in"===e.direction&&"open"!==e.kind);return{response:{status:101,statusText:"Switching Protocols",body:p.map(e=>e.data),events:c.map(e=>({kind:e.kind,direction:e.direction,data:e.data})),streaming:!0},request:{method:"WS",url:n,...i.length?{body:i.slice(0,3)}:{},...r.headers?{headers:r.headers}:{}}}}(e,t,s,r.serverUrl);d=o.response,l=o.request;break}case"mcp":{const o=await async function(e,t,r,s){const o=t.operation["x-mcp"]??{},n=o.endpoint??s??r.serverUrl,a=o.method??"tools/call",i=o.name,c=o.arguments??{};if("stdio"===r.mcpTransport){if(!r.mcpCommand)throw new Error("MCP stdio requires --mcp-command");const t=e.connect({kind:"mcp",transport:"stdio",command:r.mcpCommand,args:r.mcpArgs,cwd:r.mcpCwd,timeoutMs:r.timeout});await t.open();const s=await t.request(a,{name:i,arguments:c});return await t.close(),{response:{status:200,statusText:"OK",body:s},request:{method:a,target:i,body:{name:i,arguments:c}}}}const p={openapi:"3.2.0",info:{title:"mcp",version:"1.0.0"},paths:{"/mcp":{post:{operationId:t.operationId,"x-protocol":"mcp","x-mcp":{endpoint:n,method:a,name:i,arguments:c},responses:{200:{description:"ok"}}}}}},d=await e.send({spec:p,target:{path:"/mcp",method:"post"},timeout:r.timeout,mcp:r.headers?{headers:r.headers}:void 0}),l=d.request;return{response:{status:d.response?.status,statusText:d.response?.statusText,body:d.response?.body},request:{...l?{url:l.url,headers:l.headers}:{},method:a,target:i,body:{name:i,arguments:c}}}}(e,t,s,r.serverUrl);d=o.response,l=o.request;break}default:{const n=await async function(e,t,r,s,o){const n={};if(r.proxy)try{const e=new URL(r.proxy);n.proxies=[{match:"*://*/*",host:e.hostname,port:e.port?Number(e.port):"https:"===e.protocol?443:80,tunnel:!0}]}catch{throw new Error(`Invalid proxy URL: ${r.proxy}`)}const a=r.tls;if(a){const e={...n.requester??{}};!1===a.strictSSL&&(e.strictSSL=!1),a.caCert&&(e.ca=T.default.readFileSync(a.caCert)),a.clientCert&&(e.cert=T.default.readFileSync(a.clientCert)),a.clientKey&&(e.key=T.default.readFileSync(a.clientKey)),Object.keys(e).length&&(n.requester=e)}const i=o.values,c={...r.headers??{},...i?.header??{}},p={...r.headers?{header:r.headers}:{},...i??{},...Object.keys(c).length?{header:c}:{}},d=await e.send({spec:s,target:{path:t.path,method:t.method},serverUrl:o.serverUrl??r.serverUrl,values:Object.keys(p).length?p:void 0,auth:r.auth,variables:o.variables,timeout:r.timeout,runner:Object.keys(n).length?n:void 0,scripts:o.postmanTest?{test:{exec:o.postmanTest},fromSpecExtensions:!1}:void 0,onAssertion:e=>{o.postmanAssertions.push({name:e.name??"assertion",passed:e.passed,error:q(e.error)})},maxStreamMs:r.timeout,maxEvents:50}),l={},u=d.scripts;if(u)for(const e of[...u.prerequest??[],...u.test??[]])e.environment&&Object.assign(l,e.environment);const f=d.request;return{response:{status:d.response?.status,statusText:d.response?.statusText,contentType:d.response?.contentType,sizeBytes:d.response?.sizeBytes,body:d.response?.body,text:d.response?.text,headers:d.response?.headers,events:d.response?.events,streaming:d.response?.streaming},request:f?{method:f.method,url:f.url,headers:f.headers,...void 0!==f.body?{body:f.body}:{}}:void 0,env:l}}(e,t,s,o,{postmanTest:c,postmanAssertions:p,variables:r.variables,values:r.values,serverUrl:r.serverUrl});d=n.response,l=n.request,u=n.env;break}}const f=Date.now()-n,m=i.length>0||p.length>0?[]:function(e,t){const r=[];if("http"===e||"sse"===e){const e=t?.status,s="number"==typeof e&&e>=200&&e<300;r.push({name:"HTTP status is 2xx (implicit)",passed:s,error:s?void 0:`expected 2xx, got ${e??"no status"}`})}else if("graphql"===e){const e=t?.status,s="number"==typeof e&&e>=200&&e<300,o=t?.body,n=Array.isArray(o?.errors)&&o.errors.length>0,a=s&&!n;let i;s?n&&(i=`GraphQL response contains ${o.errors.length} error(s)`):i=`expected HTTP 2xx, got ${e??"no status"}`,r.push({name:"GraphQL success (implicit)",passed:a,error:i})}else if("grpc"===e){const e=t?.statusCode??t?.code,s=t?.statusName??t?.status,o=0===e;r.push({name:"gRPC status is OK (implicit)",passed:o,error:o?void 0:`expected code 0 (OK), got ${e??"unknown"}${s?` (${s})`:""}`})}else if("mcp"===e){const e=t?.body??{},s=void 0!==e.result,o=void 0!==e.error||void 0!==t?.error,n=s&&!o;let a;o?a="MCP JSON-RPC response contains an error":s||(a="MCP JSON-RPC response missing 'result'"),r.push({name:"MCP JSON-RPC success (implicit)",passed:n,error:a})}else if("websocket"===e){const e=t?.events??[],s=e.some(e=>"open"===e.kind||"open"===e.state),o=e.some(e=>"error"===e.kind||"error"===e.state),n=s&&!o;r.push({name:"WebSocket connected (implicit)",passed:n,error:n?void 0:o?"WebSocket connection errored":"WebSocket did not reach open state"})}return r}(t.protocol,d),h=i.length?E(i,{status:d?.status,headers:d?d.headers:void 0,body:d?.body,bodyText:d?d.text:void 0,durationMs:f}):[],v=new Set,g=p.filter(e=>{const t=`${e.name}|${e.passed}|${e.error??""}`;return!v.has(t)&&(v.add(t),!0)}),b=[...h,...g,...m],y=b.some(e=>!e.passed);return{result:{...a,status:y?"failed":"passed",durationMs:f,request:l,response:d,assertions:b.length?b:void 0,timestamp:(new Date).toISOString()},response:d,request:l,env:u}}catch(e){const o=Date.now()-n,i=r.values,c={...s.headers??{},...i?.header??{}},p=Object.fromEntries(Object.entries(c).map(([e,t])=>[e,null==t?"":String(t)])),d={method:t.method.toUpperCase(),url:r.serverUrl??s.serverUrl,...Object.keys(p).length?{headers:p}:{},...void 0!==i?.body?{body:i.body}:{}};return{result:{...a,status:"error",durationMs:o,request:d,error:e.message??String(e),timestamp:(new Date).toISOString()},response:void 0,request:d,env:{}}}}(e,t,{config:r,spec:s,variables:r.variables});return o.result}function M(e){switch(e){case"string":default:return"";case"number":case"integer":return 0;case"boolean":return!1;case"array":return[];case"object":return{}}}var U=a(require("fs"),1),F=a(require("path"),1);var z="[0m",N="[1m",D="[2m",J="[32m",B="[31m",H="[33m",K="[36m",W="[90m",G="[42m",_="[41m",Q="[43m";function V(e){if(null==e||!Number.isFinite(e))return`${D}--ms${z}`;return`${e>500?`${N}${H}`:N}${e}${z}${D}ms${z}`}function Y(e){const t="passed"===e.status?`${J}✓${z}`:"failed"===e.status?`${B}✗${z}`:"error"===e.status?`${H}!${z}`:`${W}-${z}`,r="passed"===e.status?`${G}${N} PASS ${z}`:"failed"===e.status?`${_}${N} FAIL ${z}`:"error"===e.status?`${Q}${N} ERROR ${z}`:`${W} SKIP ${z}`,s=e.protocol?` ${K}[${e.protocol}]${z}`:"",o=null!=e.response?.status?` ${D}${e.response.status}${z}`:"";let n=` ${t} ${r} ${N}${e.method}${z} ${e.path}${s}${o} ${V(e.durationMs)}`;if(e.error&&(n+=`\n ${B}${e.error}${z}`),e.assertions?.length)for(const t of e.assertions){n+=`\n ${t.passed?`${J} ✓${z}`:`${B} ✗${z}`} ${t.name}${t.error?` — ${B}${t.error}${z}`:""}`}return n}function X(e){const t=function(e){const t=[],{summary:r,results:s}=e;t.push(""),t.push(`${N}${K}╔══════════════════════════════════════════════════════╗${z}`),t.push(`${N}${K}║ Powerduck Test Report ║${z}`),t.push(`${N}${K}╚══════════════════════════════════════════════════════╝${z}`),t.push("");const o=r.passRate>=80?J:r.passRate>=50?H:B;t.push(`${N} Total:${z} ${r.total} ${J}Passed:${z} ${r.passed} ${B}Failed:${z} ${r.failed} ${H}Errors:${z} ${r.errors} ${o}Rate:${z} ${r.passRate}% ${D}Time:${z} ${V(r.durationMs)}`),t.push("");const n=r.total>0?Math.round(r.passed/r.total*50):0,a=r.total>0?Math.round(r.failed/r.total*50):0,i=r.total>0?Math.round(r.errors/r.total*50):0,c=J+"█".repeat(n)+B+"█".repeat(a)+H+"█".repeat(i)+W+"░".repeat(Math.max(0,50-n-a-i))+z;t.push(` [${c}]`),t.push("");for(const e of s)t.push(Y(e));return t.push(""),r.failed>0||r.errors>0?t.push(`${N}${B} ✗ ${r.failed+r.errors} test(s) did not pass${z}`):t.push(`${N}${J} ✓ All ${r.total} test(s) passed${z}`),t.push(` ${D}Generated: ${e.generatedAt}${z}`),t.push(""),t.join("\n")}(e);return console.log(t),t}var Z=a(require("fs"),1),ee=a(require("path"),1);function te(e,t){const r=ee.default.resolve(t);Z.default.mkdirSync(r,{recursive:!0});const s=ee.default.join(r,"report.html");return Z.default.writeFileSync(s,function(e){const{summary:t,results:r}=e,s=JSON.stringify({summary:t,results:r});return`<!DOCTYPE html>\n<html lang="en">\n<head>\n<meta charset="UTF-8">\n<meta name="viewport" content="width=device-width, initial-scale=1.0">\n<title>Test Report — Powerduck</title>\n<style>\n *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}\n :root{\n --bg:#ffffff;--fg:#0f172a;--muted:#64748b;--border:#e2e8f0;\n --bg-subtle:#f8fafc;--bg-hover:#f1f5f9;\n --green:#16a34a;--green-bg:#f0fdf4;--green-border:#bbf7d0;\n --red:#dc2626;--red-bg:#fef2f2;--red-border:#fecaca;\n --amber:#d97706;--amber-bg:#fffbeb;--amber-border:#fde68a;\n --blue:#2563eb;--mono:ui-monospace,SFMono-Regular,'SF Mono',Menlo,Consolas,monospace;\n --sans:ui-sans-serif,system-ui,-apple-system,'Segoe UI',Roboto,Inter,sans-serif;\n }\n @media (prefers-color-scheme:dark){\n :root{\n --bg:#0f172a;--fg:#f1f5f9;--muted:#94a3b8;--border:#1e293b;\n --bg-subtle:#1e293b;--bg-hover:#334155;\n --green:#4ade80;--green-bg:rgba(74,222,128,.1);--green-border:rgba(74,222,128,.25);\n --red:#f87171;--red-bg:rgba(248,113,113,.1);--red-border:rgba(248,113,113,.25);\n --amber:#fbbf24;--amber-bg:rgba(251,191,36,.1);--amber-border:rgba(251,191,36,.25);\n --blue:#60a5fa;\n }\n }\n body{font-family:var(--sans);background:var(--bg);color:var(--fg);line-height:1.5;font-size:14px;-webkit-font-smoothing:antialiased}\n .wrap{max-width:960px;margin:0 auto;padding:32px 24px 64px}\n\n /* Header */\n .hdr{display:flex;align-items:baseline;justify-content:space-between;margin-bottom:24px;padding-bottom:16px;border-bottom:1px solid var(--border)}\n .hdr h1{font-size:18px;font-weight:600;letter-spacing:-.01em}\n .hdr .meta{font-size:12px;color:var(--muted);font-family:var(--mono)}\n\n /* Summary strip */\n .strip{display:grid;grid-template-columns:repeat(6,1fr);gap:0;border:1px solid var(--border);border-radius:8px;overflow:hidden;margin-bottom:24px}\n .stat{padding:14px 16px;border-right:1px solid var(--border);text-align:center}\n .stat:last-child{border-right:none}\n .stat .v{font-size:22px;font-weight:700;letter-spacing:-.02em;line-height:1.2}\n .stat .l{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;margin-top:2px}\n .stat.total .v{color:var(--fg)}\n .stat.pass .v{color:var(--green)}\n .stat.fail .v{color:var(--red)}\n .stat.err .v{color:var(--amber)}\n .stat.rate .v{color:var(--blue)}\n .stat.time .v{font-size:16px;font-weight:600}\n\n /* Progress */\n .bar{height:4px;background:var(--border);border-radius:2px;overflow:hidden;margin-bottom:24px;display:flex}\n .bar>div{height:100%}\n .bar .p{background:var(--green)}\n .bar .f{background:var(--red)}\n .bar .e{background:var(--amber)}\n\n /* Filters */\n .filters{display:flex;gap:4px;margin-bottom:16px}\n .fbtn{padding:4px 12px;border-radius:6px;border:1px solid transparent;background:transparent;color:var(--muted);cursor:pointer;font-size:12px;font-weight:500;transition:all .12s}\n .fbtn:hover{color:var(--fg);background:var(--bg-hover)}\n .fbtn.active{background:var(--bg-subtle);color:var(--fg);border-color:var(--border)}\n .fbtn .n{color:var(--muted);margin-left:4px;font-variant-numeric:tabular-nums}\n\n /* Test rows */\n .row{border:1px solid var(--border);border-radius:8px;margin-bottom:6px;overflow:hidden;transition:border-color .12s}\n .row:hover{border-color:var(--muted)}\n .row-head{display:flex;align-items:center;gap:10px;padding:10px 14px;cursor:pointer;user-select:none}\n .dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}\n .dot.pass{background:var(--green)}\n .dot.fail{background:var(--red)}\n .dot.err{background:var(--amber)}\n .method{font-family:var(--mono);font-size:11px;font-weight:600;color:var(--muted);min-width:40px;text-transform:uppercase}\n .path{font-family:var(--mono);font-size:13px;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n .proto{font-size:10px;text-transform:uppercase;letter-spacing:.05em;color:var(--muted);padding:2px 8px;border:1px solid var(--border);border-radius:4px}\n .dur{font-family:var(--mono);font-size:12px;color:var(--muted);min-width:56px;text-align:right;font-variant-numeric:tabular-nums}\n .chev{color:var(--muted);font-size:10px;transition:transform .15s;flex-shrink:0}\n .row.open .chev{transform:rotate(90deg)}\n\n /* Row body */\n .row-body{display:none;padding:0 14px 12px 32px;border-top:1px solid var(--border)}\n .row.open .row-body{display:block}\n .sect{margin-top:10px}\n .sect-t{font-size:10px;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);font-weight:600;margin-bottom:6px}\n .asrt{display:flex;align-items:center;gap:8px;padding:3px 0;font-size:13px}\n .asrt .ck{width:14px;height:14px;border-radius:3px;display:flex;align-items:center;justify-content:center;font-size:9px;font-weight:700;flex-shrink:0;color:#fff}\n .asrt.pass .ck{background:var(--green)}\n .asrt.fail .ck{background:var(--red)}\n .asrt .an{color:var(--fg)}\n .asrt .ae{color:var(--red);font-size:12px;margin-left:6px;font-family:var(--mono)}\n .rmeta{display:flex;gap:20px;flex-wrap:wrap;font-size:12px}\n .rmeta span{color:var(--muted)}\n .rmeta strong{color:var(--fg);font-weight:600;font-family:var(--mono)}\n .errbox{background:var(--red-bg);border:1px solid var(--red-border);border-radius:6px;padding:8px 12px;color:var(--red);font-size:12px;font-family:var(--mono);margin-top:10px}\n .bprev{background:var(--bg-subtle);border:1px solid var(--border);border-radius:6px;padding:10px 12px;font-family:var(--mono);font-size:11px;color:var(--muted);max-height:180px;overflow:auto;white-space:pre-wrap;word-break:break-all;margin-top:6px}\n\n footer{margin-top:32px;padding-top:16px;border-top:1px solid var(--border);font-size:11px;color:var(--muted);display:flex;justify-content:space-between}\n\n @media(max-width:640px){\n .strip{grid-template-columns:repeat(3,1fr)}\n .stat:nth-child(3){border-right:none}\n .stat:nth-child(4),.stat:nth-child(5){border-top:1px solid var(--border)}\n .path{font-size:11px}\n }\n</style>\n</head>\n<body>\n<div class="wrap">\n <div class="hdr">\n <h1>Test Report</h1>\n <span class="meta">${e.generatedAt} · v${e.version}</span>\n </div>\n\n <div class="strip">\n <div class="stat total"><div class="v">${t.total}</div><div class="l">Total</div></div>\n <div class="stat pass"><div class="v">${t.passed}</div><div class="l">Passed</div></div>\n <div class="stat fail"><div class="v">${t.failed}</div><div class="l">Failed</div></div>\n <div class="stat err"><div class="v">${t.errors}</div><div class="l">Errors</div></div>\n <div class="stat rate"><div class="v">${t.passRate}%</div><div class="l">Pass Rate</div></div>\n <div class="stat time"><div class="v">${(t.durationMs/1e3).toFixed(2)}s</div><div class="l">Duration</div></div>\n </div>\n\n <div class="bar">\n <div class="p" style="width:${t.total?t.passed/t.total*100:0}%"></div>\n <div class="f" style="width:${t.total?t.failed/t.total*100:0}%"></div>\n <div class="e" style="width:${t.total?t.errors/t.total*100:0}%"></div>\n </div>\n\n <div class="filters">\n <button class="fbtn active" data-f="all">All<span class="n">${t.total}</span></button>\n <button class="fbtn" data-f="passed">Passed<span class="n">${t.passed}</span></button>\n <button class="fbtn" data-f="failed">Failed<span class="n">${t.failed}</span></button>\n <button class="fbtn" data-f="error">Errors<span class="n">${t.errors}</span></button>\n </div>\n\n <div id="list"></div>\n\n <footer>\n <span>@powerduck/openapi-cli v${e.version}</span>\n <span>${t.passed}/${t.total} passed</span>\n </footer>\n</div>\n\n<script>\nconst D=${s};\nfunction esc(s){const d=document.createElement('div');d.textContent=String(s??'');return d.innerHTML}\nfunction render(f){\n const list=document.getElementById('list');\n const items=f==='all'?D.results:D.results.filter(r=>r.status===f);\n list.innerHTML=items.map(r=>{\n const as=r.assertions?r.assertions.map(a=>\`\n <div class="asrt \${a.passed?'pass':'fail'}">\n <div class="ck">\${a.passed?'✓':'✗'}</div>\n <span class="an">\${esc(a.name)}</span>\n \${a.error?'<span class="ae">'+esc(a.error)+'</span>':''}\n </div>\`).join(''):'';\n const bp=r.response?.body?'<div class="bprev">'+esc(JSON.stringify(r.response.body,null,2).slice(0,3000))+'</div>':'';\n const eb=r.error?'<div class="errbox">'+esc(r.error)+'</div>':'';\n const rm=r.response?\`\n <div class="rmeta">\n \${r.response.status!=null?'<span>Status <strong>'+r.response.status+(r.response.statusText?' '+r.response.statusText:'')+'</strong></span>':''}\n \${r.response.contentType?'<span>Type <strong>'+esc(r.response.contentType)+'</strong></span>':''}\n \${r.response.sizeBytes!=null?'<span>Size <strong>'+r.response.sizeBytes+'B</strong></span>':''}\n \${r.response.streaming?'<span>Streaming <strong>yes</strong></span>':''}\n </div>\`:'';\n return \`\n <div class="row \${r.status}" data-s="\${r.status}">\n <div class="row-head" onclick="this.parentElement.classList.toggle('open')">\n <div class="dot \${r.status==='passed'?'pass':r.status==='failed'?'fail':'err'}"></div>\n <span class="method">\${r.method}</span>\n <span class="path">\${esc(r.path)}</span>\n \${r.protocol?'<span class="proto">'+r.protocol+'</span>':''}\n <span class="dur">\${r.durationMs}ms</span>\n <span class="chev">▶</span>\n </div>\n <div class="row-body">\n \${eb}\n \${rm}\n \${as?'<div class="sect"><div class="sect-t">Assertions</div>'+as+'</div>':''}\n \${bp?'<div class="sect"><div class="sect-t">Response</div>'+bp+'</div>':''}\n </div>\n </div>\`;\n }).join('');\n}\ndocument.querySelectorAll('.fbtn').forEach(b=>b.addEventListener('click',()=>{\n document.querySelectorAll('.fbtn').forEach(x=>x.classList.remove('active'));\n b.classList.add('active');render(b.dataset.f);\n}));\nrender('all');\n<\/script>\n</body>\n</html>`}(e),"utf-8"),s}var re=new i.Command;re.name("openapi-cli").description("CI-ready CLI for batch-testing OpenAPI 3.2 documents across HTTP, SSE, WebSocket, GraphQL, gRPC and MCP").version("0.2.0").requiredOption("-s, --spec <path-or-url>","Path or URL (http/https) to OpenAPI 3.2 JSON spec").option("-S, --server <url>","Override server URL from the spec").option("-o, --output <dir>","Output directory for reports","./openapi-cli-report").option("-f, --format <formats>","Comma-separated report formats: json,cli,html","json,cli,html").option("-c, --config <path>","Path to JSON config file (CLI args take precedence)").option("--method <methods>","Filter by HTTP methods (comma-separated)").option("--path <patterns>","Filter by path regex patterns (comma-separated)").option("--tag <tags>","Filter by OpenAPI tags (comma-separated)").option("--operation-id <ids>","Filter by operationId (comma-separated)").option("-n, --concurrency <n>","Max concurrent requests","5").option("-t, --timeout <ms>","Per-request timeout in ms","30000").option("-p, --proxy <url>","HTTP(S) proxy URL").option("--ca <path>","CA certificate bundle (PEM)").option("--cert <path>","Client certificate (PEM) for mTLS").option("--key <path>","Client private key (PEM) for mTLS").option("-k, --insecure","Skip TLS certificate verification").option("-H, --header <headers...>","Extra headers (Key: Value, repeatable)").option("--bearer <token>","Bearer token for Authorization header").option("-v, --variable <vars...>","Postman variables (KEY=VALUE, repeatable)").option("--env <path>","Path to .env file").option("--no-fail-on-error","Exit 0 even if tests fail").option("--grpc-no-reflection","Disable gRPC server reflection (use proto files)").option("--grpc-proto <paths...>","Paths to .proto files or directories").option("--mcp-transport <transport>","MCP transport: streamable-http or stdio","streamable-http").option("--mcp-command <command>","MCP stdio command").option("--mcp-args <args>","MCP stdio arguments (space or comma separated)").option("--mcp-cwd <dir>","MCP stdio working directory"),re.parse(process.argv);var se=re.opts();(async function(){const e={spec:se.spec,server:se.server,output:se.output,format:se.format,config:se.config,method:se.method,path:se.path,tag:se.tag,operationId:se.operationId,concurrency:Number(se.concurrency),timeout:Number(se.timeout),proxy:se.proxy,ca:se.ca,cert:se.cert,key:se.key,insecure:se.insecure,header:se.header,bearer:se.bearer,variable:se.variable,env:se.env,failOnError:!process.argv.includes("--no-fail-on-error")&&void 0,grpcReflection:!se.grpcNoReflection&&void 0,grpcProto:se.grpcProto,mcpTransport:se.mcpTransport,mcpCommand:se.mcpCommand,mcpArgs:se.mcpArgs,mcpCwd:se.mcpCwd};let t,r;try{t=x(e)}catch(e){console.error(`[31mError: ${e.message}[0m`),process.exit(2)}console.log(`[36mopenapi-cli v0.2.0 — testing ${t.specPath}[0m`),console.log(`[90mServer: ${t.serverUrl??"(from spec)"} | Concurrency: ${t.concurrency} | Timeout: ${t.timeout}ms[0m\n`);try{r=await A(t)}catch(e){console.error(`[31mFatal error: ${e.message}[0m`),process.exit(2)}const s=t.formats??["json","cli","html"];if(s.includes("cli")&&X(r),s.includes("json")){const e=function(e,t){const r=F.default.resolve(t);U.default.mkdirSync(r,{recursive:!0});const s=F.default.join(r,"report.json");return U.default.writeFileSync(s,JSON.stringify(e,null,2),"utf-8"),s}(r,t.outputDir??"./openapi-cli-report");console.log(`[90mJSON report: ${e}[0m`)}if(s.includes("html")){const e=te(r,t.outputDir??"./openapi-cli-report");console.log(`[90mHTML report: ${e}[0m`)}(r.summary.failed>0||r.summary.errors>0)&&!1!==t.failOnError&&process.exit(1)})().catch(e=>{console.error(`[31mUnexpected error: ${e.message}[0m`),console.error(e.stack),process.exit(2)});
|
|
2
|
+
"use strict";var e=Object.create,t=Object.defineProperty,r=Object.getOwnPropertyDescriptor,o=Object.getOwnPropertyNames,s=Object.getPrototypeOf,n=Object.prototype.hasOwnProperty,a=(a,i,c)=>(c=null!=a?e(s(a)):{},((e,s,a,i)=>{if(s&&"object"==typeof s||"function"==typeof s)for(let c of o(s))n.call(e,c)||c===a||t(e,c,{get:()=>s[c],enumerable:!(i=r(s,c))||i.enumerable});return e})(!i&&a&&a.__esModule?c:t(c,"default",{value:a,enumerable:!0}),a)),i=require("commander"),c=a(require("fs"),1),p=a(require("path"),1),d=a(require("http"),1),l=a(require("https"),1),u=require("@powerduck/openapi-parser"),f="./openapi-cli-report",m=["json","cli","html"],h=5,g=3e4,v=!0;function b(e){for(const[t,r,o]of[["concurrency",e.concurrency??5,100],["timeout",e.timeout??3e4,6e5]])if(!Number.isInteger(r)||r<1||r>o)throw new Error(`${t} must be an integer between 1 and ${o}.`);if(e.formats?.some(e=>!["json","cli","html"].includes(e)))throw new Error("Unsupported report format.");if(e.mcpTransport&&!["stdio","streamable-http"].includes(e.mcpTransport))throw new Error("Unsupported MCP transport.")}function y(e){const t=e.config?function(e){const t=p.default.resolve(e);if(!c.default.existsSync(t))throw new Error(`Config file not found: ${t}`);const r=c.default.readFileSync(t,"utf-8");try{const e=JSON.parse(r);if(!e||"object"!=typeof e||Array.isArray(e))throw new Error("Config must be an object.");return e}catch(e){throw new Error(`Invalid config file (must be JSON): ${e.message}`)}}(e.config):{},r={specPath:e.spec??t.specPath??"",serverUrl:e.server??t.serverUrl,outputDir:e.output??t.outputDir??f,formats:w(e.format??t.formats),filter:x(e,t),concurrency:e.concurrency??t.concurrency??h,timeout:e.timeout??t.timeout??g,proxy:e.proxy??t.proxy,tls:{caCert:e.ca??t.tls?.caCert,clientCert:e.cert??t.tls?.clientCert,clientKey:e.key??t.tls?.clientKey,pfx:t.tls?.pfx,passphrase:t.tls?.passphrase,strictSSL:void 0!==e.insecure?!e.insecure:t.tls?.strictSSL},requester:t.requester,maxEvents:t.maxEvents,maxStreamMs:t.maxStreamMs,maxResponseSize:t.maxResponseSize,headers:$(e.header,t.headers),auth:k(e,t),variables:S(e.variable,t.variables),failOnError:e.failOnError??t.failOnError??v,envFile:e.env??t.envFile,grpcReflection:e.grpcReflection??t.grpcReflection,grpcProtoPaths:e.grpcProto??t.grpcProtoPaths,mcpTransport:e.mcpTransport??t.mcpTransport,mcpCommand:e.mcpCommand??t.mcpCommand,mcpArgs:e.mcpArgs?e.mcpArgs.split(/[\s,]+/).filter(Boolean):t.mcpArgs,mcpCwd:e.mcpCwd??t.mcpCwd};if(r.envFile&&function(e){const t=p.default.resolve(e);if(!c.default.existsSync(t))return;const r=c.default.readFileSync(t,"utf-8").split("\n");for(const e of r){const t=e.trim();if(!t||t.startsWith("#"))continue;const r=t.indexOf("=");if(r>0){const e=t.slice(0,r).trim(),o=t.slice(r+1).trim().replace(/^["']|["']$/g,"");e in process.env||(process.env[e]=o)}}}(r.envFile),function(e){const t=e=>"string"!=typeof e?e:e.replace(/\$\{([^}]+)\}/g,(e,t)=>process.env[t]??"");if(e.serverUrl=t(e.serverUrl),e.proxy=t(e.proxy),e.headers)for(const[r,o]of Object.entries(e.headers))e.headers[r]=t(o);e.auth?.token&&(e.auth.token=t(e.auth.token));if(e.variables)for(const[r,o]of Object.entries(e.variables))e.variables[r]=t(o)}(r),!r.specPath)throw new Error("No OpenAPI spec path provided. Use --spec <path> or set specPath in config.");if(r.proxy)try{new URL(r.proxy)}catch{throw new Error(`Invalid proxy URL: ${r.proxy}`)}return b(r),r}function w(e){return Array.isArray(e)?e:"string"==typeof e?e.split(",").map(e=>e.trim().toLowerCase()).filter(Boolean):m}function x(e,t){const r=e.method?e.method.split(",").map(e=>e.trim().toLowerCase()):t.filter?.methods,o=e.path?e.path.split(",").map(e=>e.trim()):t.filter?.paths,s=e.tag?e.tag.split(",").map(e=>e.trim()):t.filter?.tags,n=e.operationId?e.operationId.split(",").map(e=>e.trim()):t.filter?.operationIds;if(r||o||s||n)return{methods:r,paths:o,tags:s,operationIds:n}}function $(e,t){const r={...t??{}};for(const t of e??[]){const e=t.indexOf(":");if(e>0){const o=t.slice(0,e).trim(),s=t.slice(e+1).trim();o&&(r[o]=s)}}return Object.keys(r).length?r:void 0}function k(e,t){return e.bearer?{type:"bearer",token:e.bearer}:t.auth}function S(e,t){const r={...t??{}};for(const t of e??[]){const e=t.indexOf("=");e>0&&(r[t.slice(0,e).trim()]=t.slice(e+1).trim())}return Object.keys(r).length?r:void 0}async function E(e){const t=e.specPath.trim();let r;if(/^https?:\/\//i.test(t.trim()))r=await function(e,t){let r=0;return new Promise((o,s)=>{const n=a=>{const i=("https:"===new URL(a).protocol?l.default:d.default).get(a,{timeout:t,headers:{Accept:"application/json, text/plain, */*","User-Agent":"@powerduck/openapi-cli"}},t=>{if(t.statusCode&&t.statusCode>=300&&t.statusCode<400&&t.headers.location){if(++r>5)return t.resume(),void s(new Error(`Too many redirects (>5) while fetching spec: ${e}`));const o=new URL(t.headers.location,a).toString();return t.resume(),void n(o)}if(200!==t.statusCode)return t.resume(),void s(new Error(`Failed to fetch spec from ${a}: HTTP ${t.statusCode} ${t.statusMessage??""}`.trim()));const i=[];t.on("data",e=>i.push(e)),t.on("end",()=>o(Buffer.concat(i).toString("utf-8"))),t.on("error",e=>s(new Error(`Error reading spec response: ${e.message}`)))});i.on("timeout",()=>{i.destroy(new Error(`Timed out after ${t}ms while fetching spec: ${e}`))}),i.on("error",t=>s(new Error(`Network error fetching spec ${e}: ${t.message}`)))};n(e)})}(t,e.timeout??g);else{const e=p.default.resolve(t);if(!c.default.existsSync(e))throw new Error(`OpenAPI spec not found: ${e}`);if(!c.default.statSync(e).isFile())throw new Error(`OpenAPI spec path is not a file: ${e}`);r=c.default.readFileSync(e,"utf-8")}const o=function(e,t){let r;try{r=JSON.parse(e)}catch(e){throw new Error(`Spec from ${t} is not valid JSON: ${e.message}`)}if(!r||"object"!=typeof r||Array.isArray(r))throw new Error(`Spec from ${t} is not a JSON object`);if(!r.paths||"object"!=typeof r.paths)throw new Error(`Spec from ${t} does not contain a valid 'paths' object`);return r}(r,t);return async function(e){try{const t=await(0,u.dereference)(e);return t.errors&&t.errors.length>0?e:t.schema??t.specification??e}catch(t){return e}}(o)}var P=require("@powerduck/openapi-request"),C="0.2.13",O=a(require("fs"),1),j=require("crypto"),T=a(require("http"),1),R=a(require("https"),1),q=a(require("https-proxy-agent"),1),{HttpsProxyAgent:A}=q.default;function U(e){const t=e.tls;return{...t?.caCert?{ca:O.default.readFileSync(t.caCert)}:{},...t?.clientCert?{cert:O.default.readFileSync(t.clientCert)}:{},...t?.clientKey?{key:O.default.readFileSync(t.clientKey)}:{},...t?.pfx?{pfx:O.default.readFileSync(t.pfx)}:{},...t?.passphrase?{passphrase:t.passphrase}:{},rejectUnauthorized:!1!==t?.strictSSL}}function L(e){const t=U(e);if(e.proxy){const r=new URL(e.proxy);if(!["http:","https:"].includes(r.protocol))throw new Error("Only HTTP(S) CONNECT proxies are supported.");class o extends A{callback(e,r){return super.callback(e,{...r,...t})}}const s=new o({protocol:r.protocol,host:r.hostname,port:Number(r.port||("https:"===r.protocol?443:80)),...r.username||r.password?{auth:`${decodeURIComponent(r.username)}:${decodeURIComponent(r.password)}`}:{},ca:t.ca,rejectUnauthorized:t.rejectUnauthorized});return{http:s,https:s}}return{http:new T.default.Agent,https:new R.default.Agent(t)}}function M(e,t){if(e.proxy||Object.entries(e.tls||{}).some(([e,t])=>"strictSSL"===e?!1===t:Boolean(t)))throw new Error(`${t} custom proxy/TLS is not supported by the bundled request engine.`)}function I(e,t={}){return"string"==typeof e?e.replace(/\{\{([^{}]+)\}\}/g,(e,r)=>{if(!Object.prototype.hasOwnProperty.call(t,r))throw new Error(`Variable ${r} is not defined.`);return t[r]}):Array.isArray(e)?e.map(e=>I(e,t)):e&&"object"==typeof e?Object.fromEntries(Object.entries(e).map(([e,r])=>[e,I(r,t)])):e}function z(e,t,r){const o=e.auth,s=(e,r)=>{for(const r of Object.keys(t))r.toLowerCase()===e.toLowerCase()&&delete t[r];t[e]=r};if(!o||"none"===o.type)return r;if("basic"===o.type)s("Authorization",`Basic ${Buffer.from(`${o.username||""}:${o.password||""}`).toString("base64")}`);else if("bearer"===o.type)s("Authorization",`Bearer ${o.token||""}`);else{if("apikey"!==o.type)throw new Error(`Authentication ${o.type} is supported only by the HTTP/SSE runner.`);if(!o.key)throw new Error("API key name is required.");if("query"===o.in){if(!r)throw new Error("gRPC API keys require metadata headers.");const e=new URL(r);e.searchParams.set(o.key,o.value||""),r=e.href}else s(o.key,o.value||"")}return r}function N(e,t){const r={"grpc.enable_http_proxy":0};if(!e.proxy)return{address:t,channelOptions:r};const o=new URL(e.proxy);if("http:"!==o.protocol)throw new Error("gRPC supports only HTTP CONNECT proxies.");const s=new URL(`https://${t}`);return r["grpc.http_connect_target"]=`dns:///${t}`,r["grpc.default_authority"]=s.host,r["grpc.ssl_target_name_override"]=s.hostname,(o.username||o.password)&&(r["grpc.http_connect_creds"]=`${decodeURIComponent(o.username)}:${decodeURIComponent(o.password)}`),{address:`${o.hostname}:${o.port||80}`,channelOptions:r}}function F(e){return e.key&&e.passphrase?Buffer.from((0,j.createPrivateKey)({key:e.key,passphrase:e.passphrase}).export({format:"pem",type:"pkcs8"})):e.key}function D(e,t){const r=new URL(e);for(const[e,o]of Object.entries(t||{}))if(null!=o){r.searchParams.delete(e);for(const t of Array.isArray(o)?o:[o])r.searchParams.append(e,String(t))}return r.href}function B(e,t){return e.map(e=>{try{const r=function(e,t){switch(e.assert){case"status":if(void 0===e.value)throw new Error("status assertion requires 'value'");return t.status===e.value;case"header":{if(!e.key)throw new Error("header assertion requires 'key'");const r=function(e,t){const r=t.toLowerCase();for(const[t,o]of Object.entries(e))if(t.toLowerCase()===r)return o;return}(t.headers??{},e.key);return void 0!==r&&(void 0===e.contains||r.toLowerCase().includes(e.contains.toLowerCase()))}case"bodyContains":if(void 0===e.contains)throw new Error("bodyContains assertion requires 'contains'");return(t.bodyText??JSON.stringify(t.body??"")).includes(e.contains);case"bodyEquals":if(void 0===e.body)throw new Error("bodyEquals assertion requires 'body'");return(t.bodyText??JSON.stringify(t.body??""))===e.body;case"jsonPath":{if(!e.path)throw new Error("jsonPath assertion requires 'path'");const r=function(e,t){const r=t.replace(/^\$\.?/,"");if(!r)return e;const o=r.split(/\.|\[(\d+)\]/).filter(e=>void 0!==e&&""!==e);let s=e;for(const e of o){if(null==s)return;s=/^\d+$/.test(e)?s[Number(e)]:s[e]}return s}(t.body,e.path);return void 0!==e.exists?e.exists?void 0!==r:void 0===r:void 0!==e.equals?J(r,e.equals):void 0!==r}case"responseTime":if(void 0===e.max)throw new Error("responseTime assertion requires 'max'");return t.durationMs<=e.max;default:throw new Error(`Unknown assertion type: ${e.assert}`)}}(e,t);return{name:e.name,passed:r}}catch(t){return{name:e.name,passed:!1,error:t.message}}})}function J(e,t){if(e===t)return!0;if(typeof e!=typeof t)return!1;if(null===e||null===t)return e===t;if("object"!=typeof e)return e===t;if(Array.isArray(e)!==Array.isArray(t))return!1;if(Array.isArray(e)&&Array.isArray(t))return e.length===t.length&&e.every((e,r)=>J(e,t[r]));const r=Object.keys(e),o=Object.keys(t);return r.length===o.length&&r.every(r=>J(e[r],t[r]))}var H=C;function _(e){if(!e)return;if("string"==typeof e)return e;if(e instanceof Error)return e.message;const t=e;if("string"==typeof t.message)return t.message;if("string"==typeof t.msg)return t.msg;try{return JSON.stringify(e)}catch{return String(e)}}async function K(e){b(e);const t=await E(e),r=(0,P.createClient)(),o=function(e,t){const r=[],o=["get","post","put","delete","patch","head","options"];for(const[s,n]of Object.entries(e.paths??{})){if(!n||"object"!=typeof n)continue;const e=n;for(const n of o){const o=e[n];if(!o||"object"!=typeof o)continue;const a={path:s,method:n,operation:o,pathItem:e,operationId:o.operationId??`${n.toUpperCase()} ${s}`,protocol:W(o,e)};V(a,t.filter)&&r.push(a)}}return r}(t,e),s=Date.now(),n=[];let a=0;const i=Math.min(e.concurrency??5,o.length),c=[];for(let s=0;s<i;s++)c.push((async()=>{for(;a<o.length;){if(e.signal?.aborted)throw new Error("Run cancelled.");const s=o[a++];if(!s)break;const i=await G(r,s,e,t);n.push(i)}})());await Promise.all(c);const p=Date.now()-s,d=function(e,t){const r=e.length,o=e.filter(e=>"passed"===e.status).length,s=e.filter(e=>"failed"===e.status).length,n=e.filter(e=>"error"===e.status).length,a=e.filter(e=>"skipped"===e.status).length;return{total:r,passed:o,failed:s,errors:n,skipped:a,durationMs:t,passRate:r>0?Math.round(o/r*1e3)/10:0}}(n,p);return{summary:d,results:n.sort((e,t)=>e.path.localeCompare(t.path)||e.method.localeCompare(t.method)),config:e,generatedAt:(new Date).toISOString(),version:H}}function W(e,t){const r=e["x-protocol"]??t?.["x-protocol"];if(r)return String(r).toLowerCase();const o=e.responses??{};for(const e of Object.values(o)){if((e?.content??{})["text/event-stream"])return"sse"}return e["x-response-stream"]?"sse":"http"}function V(e,t){if(!t)return!0;if(t.methods?.length&&!t.methods.includes(e.method.toLowerCase()))return!1;if(t.operationIds?.length&&!t.operationIds.includes(e.operationId))return!1;if(t.tags?.length){const r=e.operation.tags??[];if(!t.tags.some(e=>r.includes(e)))return!1}if(t.paths?.length){if(!t.paths.some(t=>{try{return new RegExp(t).test(e.path)}catch{return e.path.includes(t)}}))return!1}return!0}async function G(e,t,r,o){const s=await async function(e,t,r){let{config:o}=r;const{spec:s}=r,n=Date.now(),a={operationId:t.operationId,path:t.path,method:t.method.toUpperCase(),protocol:t.protocol};try{if((r.signal||o.signal)?.aborted)throw new Error("Run cancelled.");const i=r.variables||o.variables,c=t.operation["x-websocket"]||t.operation["x-ws"]||t.operation["x-grpc"]||t.operation["x-mcp"]||{},p=I(r.serverUrl??o.serverUrl??c.url??c.address??c.endpoint??s.servers?.[0]?.url,i);o.resolveTransport&&(o={...o,...await o.resolveTransport({protocol:t.protocol,url:p})});const d=["grpc","websocket","mcp"].includes(t.protocol),l={...o,serverUrl:I(o.serverUrl,i),auth:d?I(o.auth,i):o.auth,headers:d?I({...o.headers,...r.values?.header},i):o.headers,signal:r.signal||o.signal},u={...t,operation:d?{...t.operation,...Object.fromEntries(Object.entries(t.operation).filter(([e])=>["x-grpc","x-mcp","x-ws","x-websocket"].includes(e)).map(([e,t])=>[e,I(t,i)]))}:t.operation},f=d?I(r.values,i):r.values;if(!["http","sse","graphql","grpc","websocket","mcp"].includes(t.protocol))throw new Error(`Unsupported protocol: ${t.protocol}`);const m=[...function(e,t){const r=e?.["x-tests"],o=t?.["x-tests"];return[...Array.isArray(o)?o:[],...Array.isArray(r)?r:[]].filter(e=>e&&"object"==typeof e&&e.assert&&e.name)}(t.operation,t.pathItem),...r.extraAssertions??[]],h=function(e){const t=e?.["x-postman-scripts"];if(t&&"object"==typeof t&&"string"==typeof t.test)return t.test}(t.operation),g=[];let v,b,y={};switch(t.protocol){case"grpc":{const t=await async function(e,t,r,o,s){const n=t.operation["x-grpc"]??{},a=(o??r.serverUrl??n.address)?.replace(/^https?:\/\//,"");if(!a)throw new Error("gRPC operation requires x-grpc.address or --server");const i={...r.headers};z(r,i);const c=U(r);if(c.pfx)throw new Error("gRPC requires PEM certificates, not PFX.");const p=e.connect({kind:"grpc",deadlineMs:r.timeout??3e4,streamDeadlineMs:r.maxStreamMs??r.timeout??3e4,...N(r,a),tls:!(!0!==n.tls&&!/^https:/.test(o??r.serverUrl??""))&&{rootCerts:c.ca,privateKey:F(c),certChain:c.cert,rejectUnauthorized:c.rejectUnauthorized},service:n.service,method:n.method??t.operationId,reflection:r.grpcReflection??n.reflection??!(r.grpcProtoPaths??n.protoPaths)?.length,protoPaths:r.grpcProtoPaths??n.protoPaths,includeDirs:n.includeDirs,metadata:i}),d=()=>{p.close().catch(()=>{})};r.signal?.addEventListener("abort",d,{once:!0});try{if(r.signal?.aborted)throw new Error("Run cancelled.");if(await p.open(),r.signal?.aborted)throw new Error("Run cancelled.");const e=p.kind,o=s?.body??function(e){const t=e["x-grpc"]??{};if(t.sample&&"object"==typeof t.sample)return t.sample;const r=e.requestBody?.content?.["application/json"]?.schema;if(r?.properties){const e={};for(const[t,o]of Object.entries(r.properties))e[t]=o.default??Y(o.type);return e}return{}}(t.operation);"unary"===e||"server_streaming"===e?await p.send(o):(await p.send(o),await p.finishSending());const c=p.events??[],d=c.find(e=>"status"===e.kind),l=c.filter(e=>"data"===e.kind&&"in"===e.direction);"unary"!==e&&"server_streaming"!==e||await p.close();const u={status:d?.meta?.code??-1,statusText:d?.meta?.statusName,body:1===l.length?l[0].data:l.map(e=>e.data),events:c.map(e=>({kind:e.kind,direction:e.direction,data:e.data,meta:e.meta}))},f=n.method??t.operationId;return{response:u,request:{method:f,target:n.service?`${n.service}/${f}`:f,url:a,...Object.keys(o).length?{body:o}:{},...Object.keys(i).length?{headers:i}:{}}}}finally{r.signal?.removeEventListener("abort",d),await p.close().catch(()=>{})}}(e,u,l,I(r.serverUrl,i),f);v=t.response,b=t.request;break}case"websocket":{const t=await async function(e,t,r,o,s){const n=t.operation["x-websocket"]??t.operation["x-ws"]??{};let a=Q(n.url,o??r.serverUrl,t.path)?.replace(/^http/,"ws");if(!a)throw new Error("WebSocket operation requires x-ws.url or --server");const i={...r.headers};a=z(r,i,D(a,s?.query));const c=L(r),p=e.connect({kind:"websocket",openTimeoutMs:r.timeout??3e4,signal:r.signal,clientOptions:{...U(r),agent:a.startsWith("wss:")?c.https:c.http},url:a,subprotocols:n.subprotocols,headers:i}),d=()=>{p.close().catch(()=>{})};r.signal?.addEventListener("abort",d,{once:!0});try{if(r.signal?.aborted)throw new Error("Run cancelled.");if(await p.open(),r.signal?.aborted)throw new Error("Run cancelled.");const e=void 0!==s?.body?[s.body]:n.send??[];for(const t of e.slice(0,3))await p.send(t);await new Promise(e=>setTimeout(e,Math.min(500,r.timeout??500))),await p.close();const t=p.events??[],o=t.filter(e=>"in"===e.direction&&"open"!==e.kind);return{response:{status:101,statusText:"Switching Protocols",body:o.map(e=>e.data),events:t.map(e=>({kind:e.kind,direction:e.direction,data:e.data})),streaming:!0},request:{method:"WS",url:a,...e.length?{body:e.slice(0,3)}:{},...Object.keys(i).length?{headers:i}:{}}}}finally{r.signal?.removeEventListener("abort",d),await p.close().catch(()=>{});for(const e of new Set(Object.values(c)))e.destroy()}}(e,u,l,I(r.serverUrl,i),f);v=t.response,b=t.request;break}case"mcp":{const t=await async function(e,t,r,o,s){const n=t.operation["x-mcp"]??{},a=r.mcpTransport??n.transport??"streamable-http",i={...r.headers};let c=Q(n.endpoint,o??r.serverUrl,t.path);"stdio"!==a&&(M(r,"MCP HTTP"),c=z(r,i,c?D(c,s?.query):c));const p=s?.body?.method??n.method??"tools/call",d=s?.body?.params??n.params??("tools/call"===p?{name:n.name,arguments:s?.body??n.arguments??{}}:{}),l=e.connect({kind:"mcp",transport:"stdio"===a?"stdio":"streamable-http",endpoint:c,headers:i,command:r.mcpCommand??n.command,args:r.mcpArgs??n.args,cwd:r.mcpCwd??n.cwd,timeoutMs:r.timeout??3e4}),u=()=>{l.close().catch(()=>{})};r.signal?.addEventListener("abort",u,{once:!0});try{if(r.signal?.aborted)throw new Error("Run cancelled.");if(await l.open(),r.signal?.aborted)throw new Error("Run cancelled.");return{response:{status:200,statusText:"OK",body:{result:await l.request(p,d,{signal:r.signal,timeoutMs:r.timeout})}},request:{method:p,url:c,headers:i,body:d}}}finally{r.signal?.removeEventListener("abort",u),await l.close().catch(()=>{})}}(e,u,l,I(r.serverUrl,i),f);v=t.response,b=t.request;break}default:{const n=await async function(e,t,r,o,s){"graphql"===t.protocol&&M(r,"GraphQL");const n="graphql"===t.protocol?void 0:L(r),a={...r.httpRuntime,requester:{...r.requester,strictSSL:!1!==r.tls?.strictSSL,ignoreProxyEnvironmentVariables:!0,...n?{agents:n,...r.proxy||r.tls?.caCert||r.tls?.clientCert||r.tls?.pfx||r.httpRuntime?.certificates?{protocolVersion:"http1"}:{}}:{}}},i=s.values,c={...r.headers??{},...i?.header??{}},p={...r.headers?{header:r.headers}:{},...i??{},...Object.keys(c).length?{header:c}:{}};let d;try{d=await e.send({writeBack:!1,signal:s.signal,spec:o,target:{path:t.path,method:t.method},serverUrl:s.serverUrl??r.serverUrl,values:Object.keys(p).length?p:void 0,auth:r.auth,variables:s.variables,timeout:r.timeout,runner:Object.keys(a).length?a:void 0,scripts:s.postmanTest?{test:{exec:s.postmanTest},fromSpecExtensions:!1}:void 0,onAssertion:e=>{s.postmanAssertions.push({name:e.name??"assertion",passed:e.passed,error:_(e.error)})},maxStreamMs:r.maxStreamMs??r.timeout,maxEvents:r.maxEvents??50,maxResponseSize:r.maxResponseSize})}finally{for(const e of new Set(Object.values(n||{})))e.destroy()}if(s.signal?.aborted)throw new Error("Run cancelled.");if(d.error)throw new Error(d.error.message);const l={},u=d.scripts;if(u)for(const e of[...u.prerequest??[],...u.test??[]])e.environment&&Object.assign(l,e.environment);const f=d.request;return{response:{status:d.response?.status,statusText:d.response?.statusText,contentType:d.response?.contentType,sizeBytes:d.response?.sizeBytes,body:d.response?.body,text:d.response?.text,headers:d.response?.headers,events:d.response?.events,streaming:d.response?.streaming},request:f?{method:f.method,url:f.url,headers:f.headers,...void 0!==f.body?{body:f.body}:{}}:void 0,env:l}}(e,t,o,s,{postmanTest:h,postmanAssertions:g,variables:r.variables,values:r.values,serverUrl:r.serverUrl,signal:r.signal||o.signal});v=n.response,b=n.request,y=n.env;break}}if((r.signal||o.signal)?.aborted)throw new Error("Run cancelled.");const w=Date.now()-n,x=m.length>0||g.length>0?[]:function(e,t){const r=[];if("http"===e||"sse"===e){const e=t?.status,o="number"==typeof e&&e>=200&&e<300;r.push({name:"HTTP status is 2xx (implicit)",passed:o,error:o?void 0:`expected 2xx, got ${e??"no status"}`})}else if("graphql"===e){const e=t?.status,o="number"==typeof e&&e>=200&&e<300,s=t?.body,n=Array.isArray(s?.errors)&&s.errors.length>0,a=o&&!n;let i;o?n&&(i=`GraphQL response contains ${s.errors.length} error(s)`):i=`expected HTTP 2xx, got ${e??"no status"}`,r.push({name:"GraphQL success (implicit)",passed:a,error:i})}else if("grpc"===e){const e=t?.statusCode??t?.code??t?.status,o=t?.statusName??t?.status,s=0===e;r.push({name:"gRPC status is OK (implicit)",passed:s,error:s?void 0:`expected code 0 (OK), got ${e??"unknown"}${o?` (${o})`:""}`})}else if("mcp"===e){const e=t?.body??{},o=void 0!==e.result,s=void 0!==e.error||!0===e.result?.isError||void 0!==t?.error,n=o&&!s;let a;s?a="MCP JSON-RPC response contains an error":o||(a="MCP JSON-RPC response missing 'result'"),r.push({name:"MCP JSON-RPC success (implicit)",passed:n,error:a})}else if("websocket"===e){const e=t?.events??[],o=e.some(e=>"open"===e.kind||"open"===e.state),s=e.some(e=>"error"===e.kind||"error"===e.state),n=o&&!s;r.push({name:"WebSocket connected (implicit)",passed:n,error:n?void 0:s?"WebSocket connection errored":"WebSocket did not reach open state"})}return r}(t.protocol,v),$=m.length?B(m,{status:v?.status,headers:v?v.headers:void 0,body:v?.body,bodyText:v?v.text:void 0,durationMs:w}):[],k=new Set,S=g.filter(e=>{const t=`${e.name}|${e.passed}|${e.error??""}`;return!k.has(t)&&(k.add(t),!0)}),E=[...$,...S,...x],P=E.some(e=>!e.passed);return{result:{...a,status:P?"failed":"passed",durationMs:w,request:b,response:v,assertions:E.length?E:void 0,timestamp:(new Date).toISOString()},response:v,request:b,env:y}}catch(e){const s=Date.now()-n,i=r.values,c={...o.headers??{},...i?.header??{}},p=Object.fromEntries(Object.entries(c).map(([e,t])=>[e,null==t?"":String(t)])),d={method:t.method.toUpperCase(),url:r.serverUrl??o.serverUrl,...Object.keys(p).length?{headers:p}:{},...void 0!==i?.body?{body:i.body}:{}};return{result:{...a,status:"error",durationMs:s,request:d,error:e.message??String(e),timestamp:(new Date).toISOString()},response:void 0,request:d,env:{}}}}(e,t,{config:r,spec:o,variables:r.variables});return s.result}function Q(e,t,r){if(!t)return e;const o=new URL(t),s=new URL(e||r,t);return s.host=o.host,s.protocol=o.protocol,e||"/"===o.pathname||(s.pathname=o.pathname.replace(/\/$/,"")+r),s.href}function Y(e){switch(e){case"string":default:return"";case"number":case"integer":return 0;case"boolean":return!1;case"array":return[];case"object":return{}}}var X=a(require("fs"),1),Z=a(require("path"),1);var ee="[0m",te="[1m",re="[2m",oe="[32m",se="[31m",ne="[33m",ae="[36m",ie="[90m",ce="[42m",pe="[41m",de="[43m";function le(e){if(null==e||!Number.isFinite(e))return`${re}--ms${ee}`;return`${e>500?`${te}${ne}`:te}${e}${ee}${re}ms${ee}`}function ue(e){const t="passed"===e.status?`${oe}✓${ee}`:"failed"===e.status?`${se}✗${ee}`:"error"===e.status?`${ne}!${ee}`:`${ie}-${ee}`,r="passed"===e.status?`${ce}${te} PASS ${ee}`:"failed"===e.status?`${pe}${te} FAIL ${ee}`:"error"===e.status?`${de}${te} ERROR ${ee}`:`${ie} SKIP ${ee}`,o=e.protocol?` ${ae}[${e.protocol}]${ee}`:"",s=null!=e.response?.status?` ${re}${e.response.status}${ee}`:"";let n=` ${t} ${r} ${te}${e.method}${ee} ${e.path}${o}${s} ${le(e.durationMs)}`;if(e.error&&(n+=`\n ${se}${e.error}${ee}`),e.assertions?.length)for(const t of e.assertions){n+=`\n ${t.passed?`${oe} ✓${ee}`:`${se} ✗${ee}`} ${t.name}${t.error?` — ${se}${t.error}${ee}`:""}`}return n}function fe(e){const t=function(e){const t=[],{summary:r,results:o}=e;t.push(""),t.push(`${te}${ae}╔══════════════════════════════════════════════════════╗${ee}`),t.push(`${te}${ae}║ Powerduck Test Report ║${ee}`),t.push(`${te}${ae}╚══════════════════════════════════════════════════════╝${ee}`),t.push("");const s=r.passRate>=80?oe:r.passRate>=50?ne:se;t.push(`${te} Total:${ee} ${r.total} ${oe}Passed:${ee} ${r.passed} ${se}Failed:${ee} ${r.failed} ${ne}Errors:${ee} ${r.errors} ${s}Rate:${ee} ${r.passRate}% ${re}Time:${ee} ${le(r.durationMs)}`),t.push("");const n=r.total>0?Math.round(r.passed/r.total*50):0,a=r.total>0?Math.round(r.failed/r.total*50):0,i=r.total>0?Math.round(r.errors/r.total*50):0,c=oe+"█".repeat(n)+se+"█".repeat(a)+ne+"█".repeat(i)+ie+"░".repeat(Math.max(0,50-n-a-i))+ee;t.push(` [${c}]`),t.push("");for(const e of o)t.push(ue(e));return t.push(""),r.failed>0||r.errors>0?t.push(`${te}${se} ✗ ${r.failed+r.errors} test(s) did not pass${ee}`):t.push(`${te}${oe} ✓ All ${r.total} test(s) passed${ee}`),t.push(` ${re}Generated: ${e.generatedAt}${ee}`),t.push(""),t.join("\n")}(e);return console.log(t),t}var me=a(require("fs"),1),he=a(require("path"),1);function ge(e,t){const r=he.default.resolve(t);me.default.mkdirSync(r,{recursive:!0});const o=he.default.join(r,"report.html");return me.default.writeFileSync(o,function(e){const{summary:t,results:r}=e,o=JSON.stringify({summary:t,results:r});return`<!DOCTYPE html>\n<html lang="en">\n<head>\n<meta charset="UTF-8">\n<meta name="viewport" content="width=device-width, initial-scale=1.0">\n<title>Test Report — Powerduck</title>\n<style>\n *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}\n :root{\n --bg:#ffffff;--fg:#0f172a;--muted:#64748b;--border:#e2e8f0;\n --bg-subtle:#f8fafc;--bg-hover:#f1f5f9;\n --green:#16a34a;--green-bg:#f0fdf4;--green-border:#bbf7d0;\n --red:#dc2626;--red-bg:#fef2f2;--red-border:#fecaca;\n --amber:#d97706;--amber-bg:#fffbeb;--amber-border:#fde68a;\n --blue:#2563eb;--mono:ui-monospace,SFMono-Regular,'SF Mono',Menlo,Consolas,monospace;\n --sans:ui-sans-serif,system-ui,-apple-system,'Segoe UI',Roboto,Inter,sans-serif;\n }\n @media (prefers-color-scheme:dark){\n :root{\n --bg:#0f172a;--fg:#f1f5f9;--muted:#94a3b8;--border:#1e293b;\n --bg-subtle:#1e293b;--bg-hover:#334155;\n --green:#4ade80;--green-bg:rgba(74,222,128,.1);--green-border:rgba(74,222,128,.25);\n --red:#f87171;--red-bg:rgba(248,113,113,.1);--red-border:rgba(248,113,113,.25);\n --amber:#fbbf24;--amber-bg:rgba(251,191,36,.1);--amber-border:rgba(251,191,36,.25);\n --blue:#60a5fa;\n }\n }\n body{font-family:var(--sans);background:var(--bg);color:var(--fg);line-height:1.5;font-size:14px;-webkit-font-smoothing:antialiased}\n .wrap{max-width:960px;margin:0 auto;padding:32px 24px 64px}\n\n /* Header */\n .hdr{display:flex;align-items:baseline;justify-content:space-between;margin-bottom:24px;padding-bottom:16px;border-bottom:1px solid var(--border)}\n .hdr h1{font-size:18px;font-weight:600;letter-spacing:-.01em}\n .hdr .meta{font-size:12px;color:var(--muted);font-family:var(--mono)}\n\n /* Summary strip */\n .strip{display:grid;grid-template-columns:repeat(6,1fr);gap:0;border:1px solid var(--border);border-radius:8px;overflow:hidden;margin-bottom:24px}\n .stat{padding:14px 16px;border-right:1px solid var(--border);text-align:center}\n .stat:last-child{border-right:none}\n .stat .v{font-size:22px;font-weight:700;letter-spacing:-.02em;line-height:1.2}\n .stat .l{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;margin-top:2px}\n .stat.total .v{color:var(--fg)}\n .stat.pass .v{color:var(--green)}\n .stat.fail .v{color:var(--red)}\n .stat.err .v{color:var(--amber)}\n .stat.rate .v{color:var(--blue)}\n .stat.time .v{font-size:16px;font-weight:600}\n\n /* Progress */\n .bar{height:4px;background:var(--border);border-radius:2px;overflow:hidden;margin-bottom:24px;display:flex}\n .bar>div{height:100%}\n .bar .p{background:var(--green)}\n .bar .f{background:var(--red)}\n .bar .e{background:var(--amber)}\n\n /* Filters */\n .filters{display:flex;gap:4px;margin-bottom:16px}\n .fbtn{padding:4px 12px;border-radius:6px;border:1px solid transparent;background:transparent;color:var(--muted);cursor:pointer;font-size:12px;font-weight:500;transition:all .12s}\n .fbtn:hover{color:var(--fg);background:var(--bg-hover)}\n .fbtn.active{background:var(--bg-subtle);color:var(--fg);border-color:var(--border)}\n .fbtn .n{color:var(--muted);margin-left:4px;font-variant-numeric:tabular-nums}\n\n /* Test rows */\n .row{border:1px solid var(--border);border-radius:8px;margin-bottom:6px;overflow:hidden;transition:border-color .12s}\n .row:hover{border-color:var(--muted)}\n .row-head{display:flex;align-items:center;gap:10px;padding:10px 14px;cursor:pointer;user-select:none}\n .dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}\n .dot.pass{background:var(--green)}\n .dot.fail{background:var(--red)}\n .dot.err{background:var(--amber)}\n .method{font-family:var(--mono);font-size:11px;font-weight:600;color:var(--muted);min-width:40px;text-transform:uppercase}\n .path{font-family:var(--mono);font-size:13px;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n .proto{font-size:10px;text-transform:uppercase;letter-spacing:.05em;color:var(--muted);padding:2px 8px;border:1px solid var(--border);border-radius:4px}\n .dur{font-family:var(--mono);font-size:12px;color:var(--muted);min-width:56px;text-align:right;font-variant-numeric:tabular-nums}\n .chev{color:var(--muted);font-size:10px;transition:transform .15s;flex-shrink:0}\n .row.open .chev{transform:rotate(90deg)}\n\n /* Row body */\n .row-body{display:none;padding:0 14px 12px 32px;border-top:1px solid var(--border)}\n .row.open .row-body{display:block}\n .sect{margin-top:10px}\n .sect-t{font-size:10px;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);font-weight:600;margin-bottom:6px}\n .asrt{display:flex;align-items:center;gap:8px;padding:3px 0;font-size:13px}\n .asrt .ck{width:14px;height:14px;border-radius:3px;display:flex;align-items:center;justify-content:center;font-size:9px;font-weight:700;flex-shrink:0;color:#fff}\n .asrt.pass .ck{background:var(--green)}\n .asrt.fail .ck{background:var(--red)}\n .asrt .an{color:var(--fg)}\n .asrt .ae{color:var(--red);font-size:12px;margin-left:6px;font-family:var(--mono)}\n .rmeta{display:flex;gap:20px;flex-wrap:wrap;font-size:12px}\n .rmeta span{color:var(--muted)}\n .rmeta strong{color:var(--fg);font-weight:600;font-family:var(--mono)}\n .errbox{background:var(--red-bg);border:1px solid var(--red-border);border-radius:6px;padding:8px 12px;color:var(--red);font-size:12px;font-family:var(--mono);margin-top:10px}\n .bprev{background:var(--bg-subtle);border:1px solid var(--border);border-radius:6px;padding:10px 12px;font-family:var(--mono);font-size:11px;color:var(--muted);max-height:180px;overflow:auto;white-space:pre-wrap;word-break:break-all;margin-top:6px}\n\n footer{margin-top:32px;padding-top:16px;border-top:1px solid var(--border);font-size:11px;color:var(--muted);display:flex;justify-content:space-between}\n\n @media(max-width:640px){\n .strip{grid-template-columns:repeat(3,1fr)}\n .stat:nth-child(3){border-right:none}\n .stat:nth-child(4),.stat:nth-child(5){border-top:1px solid var(--border)}\n .path{font-size:11px}\n }\n</style>\n</head>\n<body>\n<div class="wrap">\n <div class="hdr">\n <h1>Test Report</h1>\n <span class="meta">${e.generatedAt} · v${e.version}</span>\n </div>\n\n <div class="strip">\n <div class="stat total"><div class="v">${t.total}</div><div class="l">Total</div></div>\n <div class="stat pass"><div class="v">${t.passed}</div><div class="l">Passed</div></div>\n <div class="stat fail"><div class="v">${t.failed}</div><div class="l">Failed</div></div>\n <div class="stat err"><div class="v">${t.errors}</div><div class="l">Errors</div></div>\n <div class="stat rate"><div class="v">${t.passRate}%</div><div class="l">Pass Rate</div></div>\n <div class="stat time"><div class="v">${(t.durationMs/1e3).toFixed(2)}s</div><div class="l">Duration</div></div>\n </div>\n\n <div class="bar">\n <div class="p" style="width:${t.total?t.passed/t.total*100:0}%"></div>\n <div class="f" style="width:${t.total?t.failed/t.total*100:0}%"></div>\n <div class="e" style="width:${t.total?t.errors/t.total*100:0}%"></div>\n </div>\n\n <div class="filters">\n <button class="fbtn active" data-f="all">All<span class="n">${t.total}</span></button>\n <button class="fbtn" data-f="passed">Passed<span class="n">${t.passed}</span></button>\n <button class="fbtn" data-f="failed">Failed<span class="n">${t.failed}</span></button>\n <button class="fbtn" data-f="error">Errors<span class="n">${t.errors}</span></button>\n </div>\n\n <div id="list"></div>\n\n <footer>\n <span>@powerduck/openapi-cli v${e.version}</span>\n <span>${t.passed}/${t.total} passed</span>\n </footer>\n</div>\n\n<script>\nconst D=${o};\nfunction esc(s){const d=document.createElement('div');d.textContent=String(s??'');return d.innerHTML}\nfunction render(f){\n const list=document.getElementById('list');\n const items=f==='all'?D.results:D.results.filter(r=>r.status===f);\n list.innerHTML=items.map(r=>{\n const as=r.assertions?r.assertions.map(a=>\`\n <div class="asrt \${a.passed?'pass':'fail'}">\n <div class="ck">\${a.passed?'✓':'✗'}</div>\n <span class="an">\${esc(a.name)}</span>\n \${a.error?'<span class="ae">'+esc(a.error)+'</span>':''}\n </div>\`).join(''):'';\n const bp=r.response?.body?'<div class="bprev">'+esc(JSON.stringify(r.response.body,null,2).slice(0,3000))+'</div>':'';\n const eb=r.error?'<div class="errbox">'+esc(r.error)+'</div>':'';\n const rm=r.response?\`\n <div class="rmeta">\n \${r.response.status!=null?'<span>Status <strong>'+r.response.status+(r.response.statusText?' '+r.response.statusText:'')+'</strong></span>':''}\n \${r.response.contentType?'<span>Type <strong>'+esc(r.response.contentType)+'</strong></span>':''}\n \${r.response.sizeBytes!=null?'<span>Size <strong>'+r.response.sizeBytes+'B</strong></span>':''}\n \${r.response.streaming?'<span>Streaming <strong>yes</strong></span>':''}\n </div>\`:'';\n return \`\n <div class="row \${r.status}" data-s="\${r.status}">\n <div class="row-head" onclick="this.parentElement.classList.toggle('open')">\n <div class="dot \${r.status==='passed'?'pass':r.status==='failed'?'fail':'err'}"></div>\n <span class="method">\${r.method}</span>\n <span class="path">\${esc(r.path)}</span>\n \${r.protocol?'<span class="proto">'+r.protocol+'</span>':''}\n <span class="dur">\${r.durationMs}ms</span>\n <span class="chev">▶</span>\n </div>\n <div class="row-body">\n \${eb}\n \${rm}\n \${as?'<div class="sect"><div class="sect-t">Assertions</div>'+as+'</div>':''}\n \${bp?'<div class="sect"><div class="sect-t">Response</div>'+bp+'</div>':''}\n </div>\n </div>\`;\n }).join('');\n}\ndocument.querySelectorAll('.fbtn').forEach(b=>b.addEventListener('click',()=>{\n document.querySelectorAll('.fbtn').forEach(x=>x.classList.remove('active'));\n b.classList.add('active');render(b.dataset.f);\n}));\nrender('all');\n<\/script>\n</body>\n</html>`}(e),"utf-8"),o}var ve=new i.Command;ve.name("openapi-cli").description("CI-ready CLI for batch-testing OpenAPI 3.2 documents across HTTP, SSE, WebSocket, GraphQL, gRPC and MCP").version(C).option("-s, --spec <path-or-url>","Path or URL (http/https) to OpenAPI 3.2 JSON spec").option("-S, --server <url>","Override server URL from the spec").option("-o, --output <dir>","Output directory for reports","./openapi-cli-report").option("-f, --format <formats>","Comma-separated report formats: json,cli,html","json,cli,html").option("-c, --config <path>","Path to JSON config file (CLI args take precedence)").option("--method <methods>","Filter by HTTP methods (comma-separated)").option("--path <patterns>","Filter by path regex patterns (comma-separated)").option("--tag <tags>","Filter by OpenAPI tags (comma-separated)").option("--operation-id <ids>","Filter by operationId (comma-separated)").option("-n, --concurrency <n>","Max concurrent requests","5").option("-t, --timeout <ms>","Per-request timeout in ms","30000").option("-p, --proxy <url>","HTTP(S) proxy URL").option("--ca <path>","CA certificate bundle (PEM)").option("--cert <path>","Client certificate (PEM) for mTLS").option("--key <path>","Client private key (PEM) for mTLS").option("-k, --insecure","Skip TLS certificate verification").option("-H, --header <headers...>","Extra headers (Key: Value, repeatable)").option("--bearer <token>","Bearer token for Authorization header").option("-v, --variable <vars...>","Postman variables (KEY=VALUE, repeatable)").option("--env <path>","Path to .env file").option("--no-fail-on-error","Exit 0 even if tests fail").option("--grpc-no-reflection","Disable gRPC server reflection (use proto files)").option("--grpc-proto <paths...>","Paths to .proto files or directories").option("--mcp-transport <transport>","MCP transport: streamable-http or stdio","streamable-http").option("--mcp-command <command>","MCP stdio command").option("--mcp-args <args>","MCP stdio arguments (space or comma separated)").option("--mcp-cwd <dir>","MCP stdio working directory"),ve.parse(process.argv);var be=ve.opts();(async function(){const e=e=>"cli"===ve.getOptionValueSource(e)?be[e]:void 0,t={spec:be.spec,server:be.server,output:e("output"),format:e("format"),config:be.config,method:be.method,path:be.path,tag:be.tag,operationId:be.operationId,concurrency:void 0===e("concurrency")?void 0:Number(be.concurrency),timeout:void 0===e("timeout")?void 0:Number(be.timeout),proxy:be.proxy,ca:be.ca,cert:be.cert,key:be.key,insecure:be.insecure,header:be.header,bearer:be.bearer,variable:be.variable,env:be.env,failOnError:!process.argv.includes("--no-fail-on-error")&&void 0,grpcReflection:!be.grpcNoReflection&&void 0,grpcProto:be.grpcProto,mcpTransport:e("mcpTransport"),mcpCommand:be.mcpCommand,mcpArgs:be.mcpArgs,mcpCwd:be.mcpCwd};let r,o;try{r=y(t)}catch(e){console.error(`[31mError: ${e.message}[0m`),process.exit(2)}console.log(`[36mopenapi-cli v${C} — testing ${r.specPath}[0m`),console.log(`[90mServer: ${r.serverUrl??"(from spec)"} | Concurrency: ${r.concurrency} | Timeout: ${r.timeout}ms[0m\n`);try{o=await K(r)}catch(e){console.error(`[31mFatal error: ${e.message}[0m`),process.exit(2)}const s=r.formats??["json","cli","html"];if(s.includes("cli")&&fe(o),s.includes("json")){const e=function(e,t){const r=Z.default.resolve(t);X.default.mkdirSync(r,{recursive:!0});const o=Z.default.join(r,"report.json");return X.default.writeFileSync(o,JSON.stringify(e,null,2),"utf-8"),o}(o,r.outputDir??"./openapi-cli-report");console.log(`[90mJSON report: ${e}[0m`)}if(s.includes("html")){const e=ge(o,r.outputDir??"./openapi-cli-report");console.log(`[90mHTML report: ${e}[0m`)}(o.summary.failed>0||o.summary.errors>0)&&!1!==r.failOnError&&process.exit(1)})().catch(e=>{console.error(`[31mUnexpected error: ${e.message}[0m`),console.error(e.stack),process.exit(2)});
|
package/dist/cli.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{generateHtmlReport as
|
|
2
|
+
import{generateHtmlReport as o,generateJsonReport as e,package_default as r,printCliReport as t,resolveConfig as a,runTests as i}from"./chunk-LTG44T66.js";import{Command as n}from"commander";var p=new n;p.name("openapi-cli").description("CI-ready CLI for batch-testing OpenAPI 3.2 documents across HTTP, SSE, WebSocket, GraphQL, gRPC and MCP").version(r.version).option("-s, --spec <path-or-url>","Path or URL (http/https) to OpenAPI 3.2 JSON spec").option("-S, --server <url>","Override server URL from the spec").option("-o, --output <dir>","Output directory for reports","./openapi-cli-report").option("-f, --format <formats>","Comma-separated report formats: json,cli,html","json,cli,html").option("-c, --config <path>","Path to JSON config file (CLI args take precedence)").option("--method <methods>","Filter by HTTP methods (comma-separated)").option("--path <patterns>","Filter by path regex patterns (comma-separated)").option("--tag <tags>","Filter by OpenAPI tags (comma-separated)").option("--operation-id <ids>","Filter by operationId (comma-separated)").option("-n, --concurrency <n>","Max concurrent requests","5").option("-t, --timeout <ms>","Per-request timeout in ms","30000").option("-p, --proxy <url>","HTTP(S) proxy URL").option("--ca <path>","CA certificate bundle (PEM)").option("--cert <path>","Client certificate (PEM) for mTLS").option("--key <path>","Client private key (PEM) for mTLS").option("-k, --insecure","Skip TLS certificate verification").option("-H, --header <headers...>","Extra headers (Key: Value, repeatable)").option("--bearer <token>","Bearer token for Authorization header").option("-v, --variable <vars...>","Postman variables (KEY=VALUE, repeatable)").option("--env <path>","Path to .env file").option("--no-fail-on-error","Exit 0 even if tests fail").option("--grpc-no-reflection","Disable gRPC server reflection (use proto files)").option("--grpc-proto <paths...>","Paths to .proto files or directories").option("--mcp-transport <transport>","MCP transport: streamable-http or stdio","streamable-http").option("--mcp-command <command>","MCP stdio command").option("--mcp-args <args>","MCP stdio arguments (space or comma separated)").option("--mcp-cwd <dir>","MCP stdio working directory"),p.parse(process.argv);var c=p.opts();(async function(){const n=o=>"cli"===p.getOptionValueSource(o)?c[o]:void 0,s={spec:c.spec,server:c.server,output:n("output"),format:n("format"),config:c.config,method:c.method,path:c.path,tag:c.tag,operationId:c.operationId,concurrency:void 0===n("concurrency")?void 0:Number(c.concurrency),timeout:void 0===n("timeout")?void 0:Number(c.timeout),proxy:c.proxy,ca:c.ca,cert:c.cert,key:c.key,insecure:c.insecure,header:c.header,bearer:c.bearer,variable:c.variable,env:c.env,failOnError:!process.argv.includes("--no-fail-on-error")&&void 0,grpcReflection:!c.grpcNoReflection&&void 0,grpcProto:c.grpcProto,mcpTransport:n("mcpTransport"),mcpCommand:c.mcpCommand,mcpArgs:c.mcpArgs,mcpCwd:c.mcpCwd};let m,l;try{m=a(s)}catch(o){console.error(`[31mError: ${o.message}[0m`),process.exit(2)}console.log(`[36mopenapi-cli v${r.version} — testing ${m.specPath}[0m`),console.log(`[90mServer: ${m.serverUrl??"(from spec)"} | Concurrency: ${m.concurrency} | Timeout: ${m.timeout}ms[0m\n`);try{l=await i(m)}catch(o){console.error(`[31mFatal error: ${o.message}[0m`),process.exit(2)}const d=m.formats??["json","cli","html"];if(d.includes("cli")&&t(l),d.includes("json")){const o=e(l,m.outputDir??"./openapi-cli-report");console.log(`[90mJSON report: ${o}[0m`)}if(d.includes("html")){const e=o(l,m.outputDir??"./openapi-cli-report");console.log(`[90mHTML report: ${e}[0m`)}(l.summary.failed>0||l.summary.errors>0)&&!1!==m.failOnError&&process.exit(1)})().catch(o=>{console.error(`[31mUnexpected error: ${o.message}[0m`),console.error(o.stack),process.exit(2)});
|
package/dist/index.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var e,t=Object.create,r=Object.defineProperty,s=Object.getOwnPropertyDescriptor,n=Object.getOwnPropertyNames,o=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,i=(e,t,o,i)=>{if(t&&"object"==typeof t||"function"==typeof t)for(let c of n(t))a.call(e,c)||c===o||r(e,c,{get:()=>t[c],enumerable:!(i=s(t,c))||i.enumerable});return e},c=(e,s,n)=>(n=null!=e?t(o(e)):{},i(!s&&e&&e.__esModule?n:r(n,"default",{value:e,enumerable:!0}),e)),p={};((e,t)=>{for(var s in t)r(e,s,{get:t[s],enumerable:!0})})(p,{DEFAULT_CONFIG:()=>g,applyExtracts:()=>W,buildImplicitAssertions:()=>R,buildSummary:()=>N,collectOperations:()=>U,executeStep:()=>F,extractAssertions:()=>q,extractPostmanScripts:()=>A,formatAssertionError:()=>T,generateCliReport:()=>re,generateHtmlReport:()=>ie,generateJsonReport:()=>Z,isRemoteSpec:()=>S,loadSpec:()=>O,printCliReport:()=>ne,resolveConfig:()=>b,resolveJsonPath:()=>j,resolveSteps:()=>H,runDeclarativeAssertions:()=>C,runScenario:()=>Q,runTests:()=>I,stepRef:()=>K}),module.exports=(e=p,i(r({},"__esModule",{value:!0}),e));var d=require("@powerduck/openapi-request"),l=c(require("fs"),1),u=c(require("fs"),1),f=c(require("path"),1),m=c(require("http"),1),h=c(require("https"),1),v=require("@powerduck/openapi-parser"),g={outputDir:"./openapi-cli-report",formats:["json","cli","html"],concurrency:5,timeout:3e4,failOnError:!0,grpcReflection:!0,mcpTransport:"streamable-http"};function b(e){const t=e.config?function(e){const t=f.default.resolve(e);if(!u.default.existsSync(t))throw new Error(`Config file not found: ${t}`);const r=u.default.readFileSync(t,"utf-8");try{return JSON.parse(r)}catch(e){throw new Error(`Invalid config file (must be JSON): ${e.message}`)}}(e.config):{},r={specPath:e.spec??t.specPath??"",serverUrl:e.server??t.serverUrl,outputDir:e.output??t.outputDir??g.outputDir,formats:y(e.format??t.formats),filter:x(e,t),concurrency:e.concurrency??t.concurrency??g.concurrency,timeout:e.timeout??t.timeout??g.timeout,proxy:e.proxy??t.proxy,tls:{caCert:e.ca??t.tls?.caCert,clientCert:e.cert??t.tls?.clientCert,clientKey:e.key??t.tls?.clientKey,strictSSL:void 0!==e.insecure?!e.insecure:t.tls?.strictSSL},headers:w(e.header,t.headers),auth:$(e,t),variables:k(e.variable,t.variables),failOnError:e.failOnError??t.failOnError??g.failOnError,envFile:e.env??t.envFile,grpcReflection:e.grpcReflection??t.grpcReflection??g.grpcReflection,grpcProtoPaths:e.grpcProto??t.grpcProtoPaths,mcpTransport:e.mcpTransport??t.mcpTransport??g.mcpTransport,mcpCommand:e.mcpCommand??t.mcpCommand,mcpArgs:e.mcpArgs?e.mcpArgs.split(/[\s,]+/).filter(Boolean):t.mcpArgs,mcpCwd:e.mcpCwd??t.mcpCwd};if(r.envFile&&function(e){const t=f.default.resolve(e);if(!u.default.existsSync(t))return;const r=u.default.readFileSync(t,"utf-8").split("\n");for(const e of r){const t=e.trim();if(!t||t.startsWith("#"))continue;const r=t.indexOf("=");if(r>0){const e=t.slice(0,r).trim(),s=t.slice(r+1).trim().replace(/^["']|["']$/g,"");e in process.env||(process.env[e]=s)}}}(r.envFile),function(e){const t=e=>"string"!=typeof e?e:e.replace(/\$\{([^}]+)\}/g,(e,t)=>process.env[t]??"");if(e.serverUrl=t(e.serverUrl),e.proxy=t(e.proxy),e.headers)for(const[r,s]of Object.entries(e.headers))e.headers[r]=t(s);e.auth?.token&&(e.auth.token=t(e.auth.token));if(e.variables)for(const[r,s]of Object.entries(e.variables))e.variables[r]=t(s)}(r),!r.specPath)throw new Error("No OpenAPI spec path provided. Use --spec <path> or set specPath in config.");if(r.proxy)try{new URL(r.proxy)}catch{throw new Error(`Invalid proxy URL: ${r.proxy}`)}return r}function y(e){return Array.isArray(e)?e:"string"==typeof e?e.split(",").map(e=>e.trim().toLowerCase()).filter(Boolean):g.formats}function x(e,t){const r=e.method?e.method.split(",").map(e=>e.trim().toLowerCase()):t.filter?.methods,s=e.path?e.path.split(",").map(e=>e.trim()):t.filter?.paths,n=e.tag?e.tag.split(",").map(e=>e.trim()):t.filter?.tags,o=e.operationId?e.operationId.split(",").map(e=>e.trim()):t.filter?.operationIds;if(r||s||n||o)return{methods:r,paths:s,tags:n,operationIds:o}}function w(e,t){const r={...t??{}};for(const t of e??[]){const e=t.indexOf(":");if(e>0){const s=t.slice(0,e).trim(),n=t.slice(e+1).trim();s&&(r[s]=n)}}return Object.keys(r).length?r:void 0}function $(e,t){return e.bearer?{type:"bearer",token:e.bearer}:t.auth}function k(e,t){const r={...t??{}};for(const t of e??[]){const e=t.indexOf("=");e>0&&(r[t.slice(0,e).trim()]=t.slice(e+1).trim())}return Object.keys(r).length?r:void 0}function S(e){return/^https?:\/\//i.test(e.trim())}async function O(e){const t=e.specPath.trim();let r;if(S(t))r=await function(e,t){let r=0;return new Promise((s,n)=>{const o=a=>{const i=("https:"===new URL(a).protocol?h.default:m.default).get(a,{timeout:t,headers:{Accept:"application/json, text/plain, */*","User-Agent":"@powerduck/openapi-cli"}},t=>{if(t.statusCode&&t.statusCode>=300&&t.statusCode<400&&t.headers.location){if(++r>5)return t.resume(),void n(new Error(`Too many redirects (>5) while fetching spec: ${e}`));const s=new URL(t.headers.location,a).toString();return t.resume(),void o(s)}if(200!==t.statusCode)return t.resume(),void n(new Error(`Failed to fetch spec from ${a}: HTTP ${t.statusCode} ${t.statusMessage??""}`.trim()));const i=[];t.on("data",e=>i.push(e)),t.on("end",()=>s(Buffer.concat(i).toString("utf-8"))),t.on("error",e=>n(new Error(`Error reading spec response: ${e.message}`)))});i.on("timeout",()=>{i.destroy(new Error(`Timed out after ${t}ms while fetching spec: ${e}`))}),i.on("error",t=>n(new Error(`Network error fetching spec ${e}: ${t.message}`)))};o(e)})}(t,e.timeout??g.timeout);else{const e=f.default.resolve(t);if(!u.default.existsSync(e))throw new Error(`OpenAPI spec not found: ${e}`);if(!u.default.statSync(e).isFile())throw new Error(`OpenAPI spec path is not a file: ${e}`);r=u.default.readFileSync(e,"utf-8")}const s=function(e,t){let r;try{r=JSON.parse(e)}catch(e){throw new Error(`Spec from ${t} is not valid JSON: ${e.message}`)}if(!r||"object"!=typeof r||Array.isArray(r))throw new Error(`Spec from ${t} is not a JSON object`);if(!r.paths||"object"!=typeof r.paths)throw new Error(`Spec from ${t} does not contain a valid 'paths' object`);return r}(r,t);return async function(e){try{const t=await(0,v.dereference)(e);return t.errors&&t.errors.length>0?e:t.schema??t.specification??e}catch(t){return e}}(s)}function C(e,t){return e.map(e=>{try{const r=function(e,t){switch(e.assert){case"status":if(void 0===e.value)throw new Error("status assertion requires 'value'");return t.status===e.value;case"header":{if(!e.key)throw new Error("header assertion requires 'key'");const r=function(e,t){const r=t.toLowerCase();for(const[t,s]of Object.entries(e))if(t.toLowerCase()===r)return s;return}(t.headers??{},e.key);return void 0!==r&&(void 0===e.contains||r.toLowerCase().includes(e.contains.toLowerCase()))}case"bodyContains":if(void 0===e.contains)throw new Error("bodyContains assertion requires 'contains'");return(t.bodyText??JSON.stringify(t.body??"")).includes(e.contains);case"bodyEquals":if(void 0===e.body)throw new Error("bodyEquals assertion requires 'body'");return(t.bodyText??JSON.stringify(t.body??""))===e.body;case"jsonPath":{if(!e.path)throw new Error("jsonPath assertion requires 'path'");const r=j(t.body,e.path);return void 0!==e.exists?e.exists?void 0!==r:void 0===r:void 0!==e.equals?E(r,e.equals):void 0!==r}case"responseTime":if(void 0===e.max)throw new Error("responseTime assertion requires 'max'");return t.durationMs<=e.max;default:throw new Error(`Unknown assertion type: ${e.assert}`)}}(e,t);return{name:e.name,passed:r}}catch(t){return{name:e.name,passed:!1,error:t.message}}})}function j(e,t){const r=t.replace(/^\$\.?/,"");if(!r)return e;const s=r.split(/\.|\[(\d+)\]/).filter(e=>void 0!==e&&""!==e);let n=e;for(const e of s){if(null==n)return;n=/^\d+$/.test(e)?n[Number(e)]:n[e]}return n}function E(e,t){if(e===t)return!0;if(typeof e!=typeof t)return!1;if(null===e||null===t)return e===t;if("object"!=typeof e)return e===t;if(Array.isArray(e)!==Array.isArray(t))return!1;if(Array.isArray(e)&&Array.isArray(t))return e.length===t.length&&e.every((e,r)=>E(e,t[r]));const r=Object.keys(e),s=Object.keys(t);return r.length===s.length&&r.every(r=>E(e[r],t[r]))}function q(e,t){const r=e?.["x-tests"],s=t?.["x-tests"];return[...Array.isArray(s)?s:[],...Array.isArray(r)?r:[]].filter(e=>e&&"object"==typeof e&&e.assert&&e.name)}function A(e){const t=e?.["x-postman-scripts"];if(t&&"object"==typeof t&&"string"==typeof t.test)return t.test}var P="0.1.0";function T(e){if(!e)return;if("string"==typeof e)return e;if(e instanceof Error)return e.message;const t=e;if("string"==typeof t.message)return t.message;if("string"==typeof t.msg)return t.msg;try{return JSON.stringify(e)}catch{return String(e)}}function R(e,t){const r=[];if("http"===e||"sse"===e){const e=t?.status,s="number"==typeof e&&e>=200&&e<300;r.push({name:"HTTP status is 2xx (implicit)",passed:s,error:s?void 0:`expected 2xx, got ${e??"no status"}`})}else if("graphql"===e){const e=t?.status,s="number"==typeof e&&e>=200&&e<300,n=t?.body,o=Array.isArray(n?.errors)&&n.errors.length>0,a=s&&!o;let i;s?o&&(i=`GraphQL response contains ${n.errors.length} error(s)`):i=`expected HTTP 2xx, got ${e??"no status"}`,r.push({name:"GraphQL success (implicit)",passed:a,error:i})}else if("grpc"===e){const e=t?.statusCode??t?.code,s=t?.statusName??t?.status,n=0===e;r.push({name:"gRPC status is OK (implicit)",passed:n,error:n?void 0:`expected code 0 (OK), got ${e??"unknown"}${s?` (${s})`:""}`})}else if("mcp"===e){const e=t?.body??{},s=void 0!==e.result,n=void 0!==e.error||void 0!==t?.error,o=s&&!n;let a;n?a="MCP JSON-RPC response contains an error":s||(a="MCP JSON-RPC response missing 'result'"),r.push({name:"MCP JSON-RPC success (implicit)",passed:o,error:a})}else if("websocket"===e){const e=t?.events??[],s=e.some(e=>"open"===e.kind||"open"===e.state),n=e.some(e=>"error"===e.kind||"error"===e.state),o=s&&!n;r.push({name:"WebSocket connected (implicit)",passed:o,error:o?void 0:n?"WebSocket connection errored":"WebSocket did not reach open state"})}return r}async function I(e){const t=await O(e),r=(0,d.createClient)(),s=U(t,e),n=Date.now(),o=[],a=[...s],i=Math.min(e.concurrency??5,a.length),c=[];for(let s=0;s<i;s++)c.push((async()=>{for(;a.length>0;){const s=a.shift();if(!s)break;const n=await z(r,s,e,t);o.push(n)}})());await Promise.all(c);const p=Date.now()-n;return{summary:N(o,p),results:o.sort((e,t)=>e.path.localeCompare(t.path)||e.method.localeCompare(t.method)),config:e,generatedAt:(new Date).toISOString(),version:P}}function U(e,t){const r=[],s=["get","post","put","delete","patch","head","options"];for(const[n,o]of Object.entries(e.paths??{})){const e=o;for(const o of s){const s=e[o];if(!s||"object"!=typeof s)continue;const a={path:n,method:o,operation:s,pathItem:e,operationId:s.operationId??`${o.toUpperCase()} ${n}`,protocol:M(s,e)};L(a,t.filter)&&r.push(a)}}return r}function M(e,t){const r=e["x-protocol"]??t?.["x-protocol"];if(r)return String(r).toLowerCase();const s=e.responses??{};for(const e of Object.values(s)){if((e?.content??{})["text/event-stream"])return"sse"}return e["x-response-stream"]?"sse":"http"}function L(e,t){if(!t)return!0;if(t.methods?.length&&!t.methods.includes(e.method.toLowerCase()))return!1;if(t.operationIds?.length&&!t.operationIds.includes(e.operationId))return!1;if(t.tags?.length){const r=e.operation.tags??[];if(!t.tags.some(e=>r.includes(e)))return!1}if(t.paths?.length){if(!t.paths.some(t=>{try{return new RegExp(t).test(e.path)}catch{return e.path.includes(t)}}))return!1}return!0}async function z(e,t,r,s){return(await F(e,t,{config:r,spec:s,variables:r.variables})).result}async function F(e,t,r){const{config:s,spec:n}=r,o=Date.now(),a={operationId:t.operationId,path:t.path,method:t.method.toUpperCase(),protocol:t.protocol};try{const i=[...q(t.operation,t.pathItem),...r.extraAssertions??[]],c=A(t.operation),p=[];let d,u,f={};switch(t.protocol){case"grpc":{const n=await async function(e,t,r,s){const n=t.operation["x-grpc"]??{},o=n.address??(s??r.serverUrl)?.replace(/^https?:\/\//,"");if(!o)throw new Error("gRPC operation requires x-grpc.address or --server");const a=e.connect({kind:"grpc",address:o,service:n.service,method:n.method??t.operationId,reflection:r.grpcReflection,protoPaths:r.grpcProtoPaths,metadata:r.headers});await a.open();const i=a.kind,c=function(e){const t=e["x-grpc"]??{};if(t.sample&&"object"==typeof t.sample)return t.sample;const r=e.requestBody?.content?.["application/json"]?.schema;if(r?.properties){const e={};for(const[t,s]of Object.entries(r.properties))e[t]=s.default??D(s.type);return e}return{}}(t.operation);"unary"===i||"server_streaming"===i?await a.send(c):(await a.send(c),await a.close());const p=a.events??[],d=p.find(e=>"status"===e.kind),l=p.filter(e=>"data"===e.kind&&"inbound"===e.direction);"unary"!==i&&"server_streaming"!==i||await a.close();const u={status:d?.meta?.code??0,statusText:d?.meta?.statusName,body:1===l.length?l[0].data:l.map(e=>e.data),events:p.map(e=>({kind:e.kind,direction:e.direction,data:e.data,meta:e.meta}))},f=n.method??t.operationId;return{response:u,request:{method:f,target:n.service?`${n.service}/${f}`:f,url:o,...Object.keys(c).length?{body:c}:{},...r.headers?{headers:r.headers}:{}}}}(e,t,s,r.serverUrl);d=n.response,u=n.request;break}case"websocket":{const n=await async function(e,t,r,s){const n=t.operation["x-ws"]??{},o=n.url??(s??r.serverUrl)?.replace(/^http/,"ws");if(!o)throw new Error("WebSocket operation requires x-ws.url or --server");const a=e.connect({kind:"websocket",url:o,subprotocols:n.subprotocols,headers:r.headers});await a.open();const i=n.send??[];for(const e of i.slice(0,3))await a.send(e);await new Promise(e=>setTimeout(e,500)),await a.close();const c=a.events??[],p=c.filter(e=>"in"===e.direction&&"open"!==e.kind);return{response:{status:101,statusText:"Switching Protocols",body:p.map(e=>e.data),events:c.map(e=>({kind:e.kind,direction:e.direction,data:e.data})),streaming:!0},request:{method:"WS",url:o,...i.length?{body:i.slice(0,3)}:{},...r.headers?{headers:r.headers}:{}}}}(e,t,s,r.serverUrl);d=n.response,u=n.request;break}case"mcp":{const n=await async function(e,t,r,s){const n=t.operation["x-mcp"]??{},o=n.endpoint??s??r.serverUrl,a=n.method??"tools/call",i=n.name,c=n.arguments??{};if("stdio"===r.mcpTransport){if(!r.mcpCommand)throw new Error("MCP stdio requires --mcp-command");const t=e.connect({kind:"mcp",transport:"stdio",command:r.mcpCommand,args:r.mcpArgs,cwd:r.mcpCwd,timeoutMs:r.timeout});await t.open();const s=await t.request(a,{name:i,arguments:c});return await t.close(),{response:{status:200,statusText:"OK",body:s},request:{method:a,target:i,body:{name:i,arguments:c}}}}const p={openapi:"3.2.0",info:{title:"mcp",version:"1.0.0"},paths:{"/mcp":{post:{operationId:t.operationId,"x-protocol":"mcp","x-mcp":{endpoint:o,method:a,name:i,arguments:c},responses:{200:{description:"ok"}}}}}},d=await e.send({spec:p,target:{path:"/mcp",method:"post"},timeout:r.timeout,mcp:r.headers?{headers:r.headers}:void 0}),l=d.request;return{response:{status:d.response?.status,statusText:d.response?.statusText,body:d.response?.body},request:{...l?{url:l.url,headers:l.headers}:{},method:a,target:i,body:{name:i,arguments:c}}}}(e,t,s,r.serverUrl);d=n.response,u=n.request;break}default:{const o=await async function(e,t,r,s,n){const o={};if(r.proxy)try{const e=new URL(r.proxy);o.proxies=[{match:"*://*/*",host:e.hostname,port:e.port?Number(e.port):"https:"===e.protocol?443:80,tunnel:!0}]}catch{throw new Error(`Invalid proxy URL: ${r.proxy}`)}const a=r.tls;if(a){const e={...o.requester??{}};!1===a.strictSSL&&(e.strictSSL=!1),a.caCert&&(e.ca=l.default.readFileSync(a.caCert)),a.clientCert&&(e.cert=l.default.readFileSync(a.clientCert)),a.clientKey&&(e.key=l.default.readFileSync(a.clientKey)),Object.keys(e).length&&(o.requester=e)}const i=n.values,c={...r.headers??{},...i?.header??{}},p={...r.headers?{header:r.headers}:{},...i??{},...Object.keys(c).length?{header:c}:{}},d=await e.send({spec:s,target:{path:t.path,method:t.method},serverUrl:n.serverUrl??r.serverUrl,values:Object.keys(p).length?p:void 0,auth:r.auth,variables:n.variables,timeout:r.timeout,runner:Object.keys(o).length?o:void 0,scripts:n.postmanTest?{test:{exec:n.postmanTest},fromSpecExtensions:!1}:void 0,onAssertion:e=>{n.postmanAssertions.push({name:e.name??"assertion",passed:e.passed,error:T(e.error)})},maxStreamMs:r.timeout,maxEvents:50}),u={},f=d.scripts;if(f)for(const e of[...f.prerequest??[],...f.test??[]])e.environment&&Object.assign(u,e.environment);const m=d.request;return{response:{status:d.response?.status,statusText:d.response?.statusText,contentType:d.response?.contentType,sizeBytes:d.response?.sizeBytes,body:d.response?.body,text:d.response?.text,headers:d.response?.headers,events:d.response?.events,streaming:d.response?.streaming},request:m?{method:m.method,url:m.url,headers:m.headers,...void 0!==m.body?{body:m.body}:{}}:void 0,env:u}}(e,t,s,n,{postmanTest:c,postmanAssertions:p,variables:r.variables,values:r.values,serverUrl:r.serverUrl});d=o.response,u=o.request,f=o.env;break}}const m=Date.now()-o,h=i.length>0||p.length>0?[]:R(t.protocol,d),v=i.length?C(i,{status:d?.status,headers:d?d.headers:void 0,body:d?.body,bodyText:d?d.text:void 0,durationMs:m}):[],g=new Set,b=p.filter(e=>{const t=`${e.name}|${e.passed}|${e.error??""}`;return!g.has(t)&&(g.add(t),!0)}),y=[...v,...b,...h],x=y.some(e=>!e.passed);return{result:{...a,status:x?"failed":"passed",durationMs:m,request:u,response:d,assertions:y.length?y:void 0,timestamp:(new Date).toISOString()},response:d,request:u,env:f}}catch(e){const n=Date.now()-o,i=r.values,c={...s.headers??{},...i?.header??{}},p=Object.fromEntries(Object.entries(c).map(([e,t])=>[e,null==t?"":String(t)])),d={method:t.method.toUpperCase(),url:r.serverUrl??s.serverUrl,...Object.keys(p).length?{headers:p}:{},...void 0!==i?.body?{body:i.body}:{}};return{result:{...a,status:"error",durationMs:n,request:d,error:e.message??String(e),timestamp:(new Date).toISOString()},response:void 0,request:d,env:{}}}}function D(e){switch(e){case"string":default:return"";case"number":case"integer":return 0;case"boolean":return!1;case"array":return[];case"object":return{}}}function N(e,t){const r=e.length,s=e.filter(e=>"passed"===e.status).length;return{total:r,passed:s,failed:e.filter(e=>"failed"===e.status).length,errors:e.filter(e=>"error"===e.status).length,skipped:e.filter(e=>"skipped"===e.status).length,durationMs:t,passRate:r>0?Math.round(s/r*1e3)/10:0}}var J=require("@powerduck/openapi-request"),B="0.1.0";function K(e){if(e.method&&e.path)return`${e.method.toUpperCase()} ${e.path}`;const t=(e.ref??"").trim(),r=t.indexOf(" ");return r>0?`${t.slice(0,r).toUpperCase()} ${t.slice(r+1).trim()}`:t}function H(e,t){const r=new Map,s=new Map;for(const e of t)r.set(`${e.method.toUpperCase()} ${e.path}`,e),s.set(e.operationId,e);const n=[],o=[];if(e.steps.forEach((e,t)=>{const a=K(e),i=r.get(a)??(a?s.get(a):void 0);i?n.push({step:e,op:i,ref:`${i.method.toUpperCase()} ${i.path}`}):o.push(`step ${t} ("${e.name??a}") -> ${a||"<empty ref>"}`)}),o.length)throw new Error(`Scenario references operations not present in the spec: ${o.join("; ")}`);return n}function _(e,t){if(!e)return;const r=t.toLowerCase();for(const[t,s]of Object.entries(e))if(t.toLowerCase()===r)return s}function G(e){if(null==e)return"";if("string"==typeof e)return e;if("number"==typeof e||"boolean"==typeof e)return String(e);try{return JSON.stringify(e)}catch{return String(e)}}function W(e,t){const r={};if(!e?.length)return r;for(const s of e){let e;e="status"===s.from?t?.status:"header"===s.from?s.key?_(t?.headers,s.key):void 0:j(t?.body,s.path??"$"),null!=e&&(r[s.name]=G(e))}return r}function Y(e,t,r,s){return{index:e,ref:t,name:r.name,operationId:t,path:t.includes(" ")?t.slice(t.indexOf(" ")+1):t,method:t.includes(" ")?t.slice(0,t.indexOf(" ")):"",protocol:"",status:"skipped",durationMs:0,timestamp:(new Date).toISOString(),notRun:!0,notRunReason:s}}async function Q(e,t){const{config:r,onEvent:s,signal:n}=t,o=Date.now(),a=new Date(o).toISOString(),i=e=>{try{s?.(e)}catch{}},c=t.spec??await O(r),p=H(e,U(c,{...r,filter:void 0})),d=(0,J.createClient)(),l={...r.variables??{},...e.variables??{}},u=!1!==e.stopOnFailure,f=[];let m="passed",h=!1;i({type:"scenario:start",scenario:e.name,status:m});for(let t=0;t<p.length;t++){const{step:s,op:o,ref:a}=p[t];if(n?.aborted){m="cancelled",f.push(Y(t,a,s,"Scenario cancelled")),i({type:"step:skip",scenario:e.name,stepIndex:t,step:f[t]}),h=!0;break}if(s.skip){const r=Y(t,a,s,"Skipped by plan");f.push(r),i({type:"step:skip",scenario:e.name,stepIndex:t,step:r});continue}i({type:"step:start",scenario:e.name,stepIndex:t});const v=await F(d,o,{config:r,spec:c,variables:{...l},values:s.request?.values,serverUrl:s.request?.serverUrl,extraAssertions:s.request?.assertions}),g={...l};Object.assign(l,v.env);const b={};for(const[e,t]of Object.entries(v.env))g[e]!==t&&(b[e]=t);const y=W(s.request?.extract,v.response);Object.assign(l,y);const x={...b,...y},w={...v.result,index:t,ref:a,name:s.name,extracted:Object.keys(x).length?x:void 0,variablesAfter:{...l}};if(f.push(w),i({type:"step:finish",scenario:e.name,stepIndex:t,step:w}),"error"===w.status?m="error":"failed"===w.status&&"error"!==m&&(m="failed"),("failed"===w.status||"error"===w.status)&&u){h=!0;for(let r=t+1;r<p.length;r++){const s=p[r],n=Y(r,s.ref,s.step,`Stopped after ${w.status} step ${t}`);f.push(n),i({type:"step:skip",scenario:e.name,stepIndex:r,step:n})}break}}n?.aborted&&"error"!==m&&"failed"!==m&&(m="cancelled");const v=Date.now()-o,g="cancelled"===m?"scenario:cancel":"scenario:finish",b={name:e.name,description:e.description,status:m,summary:N(f,v),steps:f,variables:l,config:r,startedAt:a,durationMs:v,generatedAt:(new Date).toISOString(),version:B};return i({type:g,scenario:e.name,status:m,message:h?"halted":void 0}),b}var V=c(require("fs"),1),X=c(require("path"),1);function Z(e,t){const r=X.default.resolve(t);V.default.mkdirSync(r,{recursive:!0});const s=X.default.join(r,"report.json");return V.default.writeFileSync(s,JSON.stringify(e,null,2),"utf-8"),s}var ee={reset:"[0m",bold:"[1m",dim:"[2m",green:"[32m",red:"[31m",yellow:"[33m",blue:"[34m",cyan:"[36m",gray:"[90m",bgGreen:"[42m",bgRed:"[41m",bgYellow:"[43m"};function te(e){if(null==e||!Number.isFinite(e))return`${ee.dim}--ms${ee.reset}`;return`${e>500?`${ee.bold}${ee.yellow}`:ee.bold}${e}${ee.reset}${ee.dim}ms${ee.reset}`}function re(e){const t=[],{summary:r,results:s}=e;t.push(""),t.push(`${ee.bold}${ee.cyan}╔══════════════════════════════════════════════════════╗${ee.reset}`),t.push(`${ee.bold}${ee.cyan}║ Powerduck Test Report ║${ee.reset}`),t.push(`${ee.bold}${ee.cyan}╚══════════════════════════════════════════════════════╝${ee.reset}`),t.push("");const n=r.passRate>=80?ee.green:r.passRate>=50?ee.yellow:ee.red;t.push(`${ee.bold} Total:${ee.reset} ${r.total} ${ee.green}Passed:${ee.reset} ${r.passed} ${ee.red}Failed:${ee.reset} ${r.failed} ${ee.yellow}Errors:${ee.reset} ${r.errors} ${n}Rate:${ee.reset} ${r.passRate}% ${ee.dim}Time:${ee.reset} ${te(r.durationMs)}`),t.push("");const o=r.total>0?Math.round(r.passed/r.total*50):0,a=r.total>0?Math.round(r.failed/r.total*50):0,i=r.total>0?Math.round(r.errors/r.total*50):0,c=ee.green+"█".repeat(o)+ee.red+"█".repeat(a)+ee.yellow+"█".repeat(i)+ee.gray+"░".repeat(Math.max(0,50-o-a-i))+ee.reset;t.push(` [${c}]`),t.push("");for(const e of s)t.push(se(e));return t.push(""),r.failed>0||r.errors>0?t.push(`${ee.bold}${ee.red} ✗ ${r.failed+r.errors} test(s) did not pass${ee.reset}`):t.push(`${ee.bold}${ee.green} ✓ All ${r.total} test(s) passed${ee.reset}`),t.push(` ${ee.dim}Generated: ${e.generatedAt}${ee.reset}`),t.push(""),t.join("\n")}function se(e){const t="passed"===e.status?`${ee.green}✓${ee.reset}`:"failed"===e.status?`${ee.red}✗${ee.reset}`:"error"===e.status?`${ee.yellow}!${ee.reset}`:`${ee.gray}-${ee.reset}`,r="passed"===e.status?`${ee.bgGreen}${ee.bold} PASS ${ee.reset}`:"failed"===e.status?`${ee.bgRed}${ee.bold} FAIL ${ee.reset}`:"error"===e.status?`${ee.bgYellow}${ee.bold} ERROR ${ee.reset}`:`${ee.gray} SKIP ${ee.reset}`,s=e.protocol?` ${ee.cyan}[${e.protocol}]${ee.reset}`:"",n=null!=e.response?.status?` ${ee.dim}${e.response.status}${ee.reset}`:"";let o=` ${t} ${r} ${ee.bold}${e.method}${ee.reset} ${e.path}${s}${n} ${te(e.durationMs)}`;if(e.error&&(o+=`\n ${ee.red}${e.error}${ee.reset}`),e.assertions?.length)for(const t of e.assertions){o+=`\n ${t.passed?`${ee.green} ✓${ee.reset}`:`${ee.red} ✗${ee.reset}`} ${t.name}${t.error?` — ${ee.red}${t.error}${ee.reset}`:""}`}return o}function ne(e){const t=re(e);return console.log(t),t}var oe=c(require("fs"),1),ae=c(require("path"),1);function ie(e,t){const r=ae.default.resolve(t);oe.default.mkdirSync(r,{recursive:!0});const s=ae.default.join(r,"report.html");return oe.default.writeFileSync(s,function(e){const{summary:t,results:r}=e,s=JSON.stringify({summary:t,results:r});return`<!DOCTYPE html>\n<html lang="en">\n<head>\n<meta charset="UTF-8">\n<meta name="viewport" content="width=device-width, initial-scale=1.0">\n<title>Test Report — Powerduck</title>\n<style>\n *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}\n :root{\n --bg:#ffffff;--fg:#0f172a;--muted:#64748b;--border:#e2e8f0;\n --bg-subtle:#f8fafc;--bg-hover:#f1f5f9;\n --green:#16a34a;--green-bg:#f0fdf4;--green-border:#bbf7d0;\n --red:#dc2626;--red-bg:#fef2f2;--red-border:#fecaca;\n --amber:#d97706;--amber-bg:#fffbeb;--amber-border:#fde68a;\n --blue:#2563eb;--mono:ui-monospace,SFMono-Regular,'SF Mono',Menlo,Consolas,monospace;\n --sans:ui-sans-serif,system-ui,-apple-system,'Segoe UI',Roboto,Inter,sans-serif;\n }\n @media (prefers-color-scheme:dark){\n :root{\n --bg:#0f172a;--fg:#f1f5f9;--muted:#94a3b8;--border:#1e293b;\n --bg-subtle:#1e293b;--bg-hover:#334155;\n --green:#4ade80;--green-bg:rgba(74,222,128,.1);--green-border:rgba(74,222,128,.25);\n --red:#f87171;--red-bg:rgba(248,113,113,.1);--red-border:rgba(248,113,113,.25);\n --amber:#fbbf24;--amber-bg:rgba(251,191,36,.1);--amber-border:rgba(251,191,36,.25);\n --blue:#60a5fa;\n }\n }\n body{font-family:var(--sans);background:var(--bg);color:var(--fg);line-height:1.5;font-size:14px;-webkit-font-smoothing:antialiased}\n .wrap{max-width:960px;margin:0 auto;padding:32px 24px 64px}\n\n /* Header */\n .hdr{display:flex;align-items:baseline;justify-content:space-between;margin-bottom:24px;padding-bottom:16px;border-bottom:1px solid var(--border)}\n .hdr h1{font-size:18px;font-weight:600;letter-spacing:-.01em}\n .hdr .meta{font-size:12px;color:var(--muted);font-family:var(--mono)}\n\n /* Summary strip */\n .strip{display:grid;grid-template-columns:repeat(6,1fr);gap:0;border:1px solid var(--border);border-radius:8px;overflow:hidden;margin-bottom:24px}\n .stat{padding:14px 16px;border-right:1px solid var(--border);text-align:center}\n .stat:last-child{border-right:none}\n .stat .v{font-size:22px;font-weight:700;letter-spacing:-.02em;line-height:1.2}\n .stat .l{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;margin-top:2px}\n .stat.total .v{color:var(--fg)}\n .stat.pass .v{color:var(--green)}\n .stat.fail .v{color:var(--red)}\n .stat.err .v{color:var(--amber)}\n .stat.rate .v{color:var(--blue)}\n .stat.time .v{font-size:16px;font-weight:600}\n\n /* Progress */\n .bar{height:4px;background:var(--border);border-radius:2px;overflow:hidden;margin-bottom:24px;display:flex}\n .bar>div{height:100%}\n .bar .p{background:var(--green)}\n .bar .f{background:var(--red)}\n .bar .e{background:var(--amber)}\n\n /* Filters */\n .filters{display:flex;gap:4px;margin-bottom:16px}\n .fbtn{padding:4px 12px;border-radius:6px;border:1px solid transparent;background:transparent;color:var(--muted);cursor:pointer;font-size:12px;font-weight:500;transition:all .12s}\n .fbtn:hover{color:var(--fg);background:var(--bg-hover)}\n .fbtn.active{background:var(--bg-subtle);color:var(--fg);border-color:var(--border)}\n .fbtn .n{color:var(--muted);margin-left:4px;font-variant-numeric:tabular-nums}\n\n /* Test rows */\n .row{border:1px solid var(--border);border-radius:8px;margin-bottom:6px;overflow:hidden;transition:border-color .12s}\n .row:hover{border-color:var(--muted)}\n .row-head{display:flex;align-items:center;gap:10px;padding:10px 14px;cursor:pointer;user-select:none}\n .dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}\n .dot.pass{background:var(--green)}\n .dot.fail{background:var(--red)}\n .dot.err{background:var(--amber)}\n .method{font-family:var(--mono);font-size:11px;font-weight:600;color:var(--muted);min-width:40px;text-transform:uppercase}\n .path{font-family:var(--mono);font-size:13px;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n .proto{font-size:10px;text-transform:uppercase;letter-spacing:.05em;color:var(--muted);padding:2px 8px;border:1px solid var(--border);border-radius:4px}\n .dur{font-family:var(--mono);font-size:12px;color:var(--muted);min-width:56px;text-align:right;font-variant-numeric:tabular-nums}\n .chev{color:var(--muted);font-size:10px;transition:transform .15s;flex-shrink:0}\n .row.open .chev{transform:rotate(90deg)}\n\n /* Row body */\n .row-body{display:none;padding:0 14px 12px 32px;border-top:1px solid var(--border)}\n .row.open .row-body{display:block}\n .sect{margin-top:10px}\n .sect-t{font-size:10px;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);font-weight:600;margin-bottom:6px}\n .asrt{display:flex;align-items:center;gap:8px;padding:3px 0;font-size:13px}\n .asrt .ck{width:14px;height:14px;border-radius:3px;display:flex;align-items:center;justify-content:center;font-size:9px;font-weight:700;flex-shrink:0;color:#fff}\n .asrt.pass .ck{background:var(--green)}\n .asrt.fail .ck{background:var(--red)}\n .asrt .an{color:var(--fg)}\n .asrt .ae{color:var(--red);font-size:12px;margin-left:6px;font-family:var(--mono)}\n .rmeta{display:flex;gap:20px;flex-wrap:wrap;font-size:12px}\n .rmeta span{color:var(--muted)}\n .rmeta strong{color:var(--fg);font-weight:600;font-family:var(--mono)}\n .errbox{background:var(--red-bg);border:1px solid var(--red-border);border-radius:6px;padding:8px 12px;color:var(--red);font-size:12px;font-family:var(--mono);margin-top:10px}\n .bprev{background:var(--bg-subtle);border:1px solid var(--border);border-radius:6px;padding:10px 12px;font-family:var(--mono);font-size:11px;color:var(--muted);max-height:180px;overflow:auto;white-space:pre-wrap;word-break:break-all;margin-top:6px}\n\n footer{margin-top:32px;padding-top:16px;border-top:1px solid var(--border);font-size:11px;color:var(--muted);display:flex;justify-content:space-between}\n\n @media(max-width:640px){\n .strip{grid-template-columns:repeat(3,1fr)}\n .stat:nth-child(3){border-right:none}\n .stat:nth-child(4),.stat:nth-child(5){border-top:1px solid var(--border)}\n .path{font-size:11px}\n }\n</style>\n</head>\n<body>\n<div class="wrap">\n <div class="hdr">\n <h1>Test Report</h1>\n <span class="meta">${e.generatedAt} · v${e.version}</span>\n </div>\n\n <div class="strip">\n <div class="stat total"><div class="v">${t.total}</div><div class="l">Total</div></div>\n <div class="stat pass"><div class="v">${t.passed}</div><div class="l">Passed</div></div>\n <div class="stat fail"><div class="v">${t.failed}</div><div class="l">Failed</div></div>\n <div class="stat err"><div class="v">${t.errors}</div><div class="l">Errors</div></div>\n <div class="stat rate"><div class="v">${t.passRate}%</div><div class="l">Pass Rate</div></div>\n <div class="stat time"><div class="v">${(t.durationMs/1e3).toFixed(2)}s</div><div class="l">Duration</div></div>\n </div>\n\n <div class="bar">\n <div class="p" style="width:${t.total?t.passed/t.total*100:0}%"></div>\n <div class="f" style="width:${t.total?t.failed/t.total*100:0}%"></div>\n <div class="e" style="width:${t.total?t.errors/t.total*100:0}%"></div>\n </div>\n\n <div class="filters">\n <button class="fbtn active" data-f="all">All<span class="n">${t.total}</span></button>\n <button class="fbtn" data-f="passed">Passed<span class="n">${t.passed}</span></button>\n <button class="fbtn" data-f="failed">Failed<span class="n">${t.failed}</span></button>\n <button class="fbtn" data-f="error">Errors<span class="n">${t.errors}</span></button>\n </div>\n\n <div id="list"></div>\n\n <footer>\n <span>@powerduck/openapi-cli v${e.version}</span>\n <span>${t.passed}/${t.total} passed</span>\n </footer>\n</div>\n\n<script>\nconst D=${s};\nfunction esc(s){const d=document.createElement('div');d.textContent=String(s??'');return d.innerHTML}\nfunction render(f){\n const list=document.getElementById('list');\n const items=f==='all'?D.results:D.results.filter(r=>r.status===f);\n list.innerHTML=items.map(r=>{\n const as=r.assertions?r.assertions.map(a=>\`\n <div class="asrt \${a.passed?'pass':'fail'}">\n <div class="ck">\${a.passed?'✓':'✗'}</div>\n <span class="an">\${esc(a.name)}</span>\n \${a.error?'<span class="ae">'+esc(a.error)+'</span>':''}\n </div>\`).join(''):'';\n const bp=r.response?.body?'<div class="bprev">'+esc(JSON.stringify(r.response.body,null,2).slice(0,3000))+'</div>':'';\n const eb=r.error?'<div class="errbox">'+esc(r.error)+'</div>':'';\n const rm=r.response?\`\n <div class="rmeta">\n \${r.response.status!=null?'<span>Status <strong>'+r.response.status+(r.response.statusText?' '+r.response.statusText:'')+'</strong></span>':''}\n \${r.response.contentType?'<span>Type <strong>'+esc(r.response.contentType)+'</strong></span>':''}\n \${r.response.sizeBytes!=null?'<span>Size <strong>'+r.response.sizeBytes+'B</strong></span>':''}\n \${r.response.streaming?'<span>Streaming <strong>yes</strong></span>':''}\n </div>\`:'';\n return \`\n <div class="row \${r.status}" data-s="\${r.status}">\n <div class="row-head" onclick="this.parentElement.classList.toggle('open')">\n <div class="dot \${r.status==='passed'?'pass':r.status==='failed'?'fail':'err'}"></div>\n <span class="method">\${r.method}</span>\n <span class="path">\${esc(r.path)}</span>\n \${r.protocol?'<span class="proto">'+r.protocol+'</span>':''}\n <span class="dur">\${r.durationMs}ms</span>\n <span class="chev">▶</span>\n </div>\n <div class="row-body">\n \${eb}\n \${rm}\n \${as?'<div class="sect"><div class="sect-t">Assertions</div>'+as+'</div>':''}\n \${bp?'<div class="sect"><div class="sect-t">Response</div>'+bp+'</div>':''}\n </div>\n </div>\`;\n }).join('');\n}\ndocument.querySelectorAll('.fbtn').forEach(b=>b.addEventListener('click',()=>{\n document.querySelectorAll('.fbtn').forEach(x=>x.classList.remove('active'));\n b.classList.add('active');render(b.dataset.f);\n}));\nrender('all');\n<\/script>\n</body>\n</html>`}(e),"utf-8"),s}
|
|
1
|
+
"use strict";var e,t=Object.create,r=Object.defineProperty,s=Object.getOwnPropertyDescriptor,n=Object.getOwnPropertyNames,o=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,i=(e,t,o,i)=>{if(t&&"object"==typeof t||"function"==typeof t)for(let c of n(t))a.call(e,c)||c===o||r(e,c,{get:()=>t[c],enumerable:!(i=s(t,c))||i.enumerable});return e},c=(e,s,n)=>(n=null!=e?t(o(e)):{},i(!s&&e&&e.__esModule?n:r(n,"default",{value:e,enumerable:!0}),e)),p={};((e,t)=>{for(var s in t)r(e,s,{get:t[s],enumerable:!0})})(p,{DEFAULT_CONFIG:()=>R,applyExtracts:()=>de,buildImplicitAssertions:()=>W,buildSummary:()=>se,collectOperations:()=>V,executeStep:()=>ee,extractAssertions:()=>B,extractPostmanScripts:()=>H,formatAssertionError:()=>G,generateCliReport:()=>ye,generateHtmlReport:()=>Se,generateJsonReport:()=>ge,isRemoteSpec:()=>N,loadSpec:()=>F,printCliReport:()=>xe,resolveConfig:()=>T,resolveJsonPath:()=>J,resolveSteps:()=>ce,runDeclarativeAssertions:()=>D,runScenario:()=>fe,runTests:()=>Q,stepRef:()=>ie}),module.exports=(e=p,i(r({},"__esModule",{value:!0}),e));var l=require("@powerduck/openapi-request"),d="0.2.13",u=c(require("fs"),1),f=require("crypto"),m=c(require("http"),1),h=c(require("https"),1),g=c(require("https-proxy-agent"),1),{HttpsProxyAgent:b}=g.default;function v(e){const t=e.tls;return{...t?.caCert?{ca:u.default.readFileSync(t.caCert)}:{},...t?.clientCert?{cert:u.default.readFileSync(t.clientCert)}:{},...t?.clientKey?{key:u.default.readFileSync(t.clientKey)}:{},...t?.pfx?{pfx:u.default.readFileSync(t.pfx)}:{},...t?.passphrase?{passphrase:t.passphrase}:{},rejectUnauthorized:!1!==t?.strictSSL}}function y(e){const t=v(e);if(e.proxy){const r=new URL(e.proxy);if(!["http:","https:"].includes(r.protocol))throw new Error("Only HTTP(S) CONNECT proxies are supported.");class s extends b{callback(e,r){return super.callback(e,{...r,...t})}}const n=new s({protocol:r.protocol,host:r.hostname,port:Number(r.port||("https:"===r.protocol?443:80)),...r.username||r.password?{auth:`${decodeURIComponent(r.username)}:${decodeURIComponent(r.password)}`}:{},ca:t.ca,rejectUnauthorized:t.rejectUnauthorized});return{http:n,https:n}}return{http:new m.default.Agent,https:new h.default.Agent(t)}}function w(e,t){if(e.proxy||Object.entries(e.tls||{}).some(([e,t])=>"strictSSL"===e?!1===t:Boolean(t)))throw new Error(`${t} custom proxy/TLS is not supported by the bundled request engine.`)}function x(e,t={}){return"string"==typeof e?e.replace(/\{\{([^{}]+)\}\}/g,(e,r)=>{if(!Object.prototype.hasOwnProperty.call(t,r))throw new Error(`Variable ${r} is not defined.`);return t[r]}):Array.isArray(e)?e.map(e=>x(e,t)):e&&"object"==typeof e?Object.fromEntries(Object.entries(e).map(([e,r])=>[e,x(r,t)])):e}function $(e,t,r){const s=e.auth,n=(e,r)=>{for(const r of Object.keys(t))r.toLowerCase()===e.toLowerCase()&&delete t[r];t[e]=r};if(!s||"none"===s.type)return r;if("basic"===s.type)n("Authorization",`Basic ${Buffer.from(`${s.username||""}:${s.password||""}`).toString("base64")}`);else if("bearer"===s.type)n("Authorization",`Bearer ${s.token||""}`);else{if("apikey"!==s.type)throw new Error(`Authentication ${s.type} is supported only by the HTTP/SSE runner.`);if(!s.key)throw new Error("API key name is required.");if("query"===s.in){if(!r)throw new Error("gRPC API keys require metadata headers.");const e=new URL(r);e.searchParams.set(s.key,s.value||""),r=e.href}else n(s.key,s.value||"")}return r}function k(e,t){const r={"grpc.enable_http_proxy":0};if(!e.proxy)return{address:t,channelOptions:r};const s=new URL(e.proxy);if("http:"!==s.protocol)throw new Error("gRPC supports only HTTP CONNECT proxies.");const n=new URL(`https://${t}`);return r["grpc.http_connect_target"]=`dns:///${t}`,r["grpc.default_authority"]=n.host,r["grpc.ssl_target_name_override"]=n.hostname,(s.username||s.password)&&(r["grpc.http_connect_creds"]=`${decodeURIComponent(s.username)}:${decodeURIComponent(s.password)}`),{address:`${s.hostname}:${s.port||80}`,channelOptions:r}}function S(e){return e.key&&e.passphrase?Buffer.from((0,f.createPrivateKey)({key:e.key,passphrase:e.passphrase}).export({format:"pem",type:"pkcs8"})):e.key}function E(e,t){const r=new URL(e);for(const[e,s]of Object.entries(t||{}))if(null!=s){r.searchParams.delete(e);for(const t of Array.isArray(s)?s:[s])r.searchParams.append(e,String(t))}return r.href}var O=c(require("fs"),1),C=c(require("path"),1),j=c(require("http"),1),q=c(require("https"),1),P=require("@powerduck/openapi-parser"),R={outputDir:"./openapi-cli-report",formats:["json","cli","html"],concurrency:5,timeout:3e4,failOnError:!0,grpcReflection:!0,mcpTransport:"streamable-http"};function A(e){for(const[t,r,s]of[["concurrency",e.concurrency??5,100],["timeout",e.timeout??3e4,6e5]])if(!Number.isInteger(r)||r<1||r>s)throw new Error(`${t} must be an integer between 1 and ${s}.`);if(e.formats?.some(e=>!["json","cli","html"].includes(e)))throw new Error("Unsupported report format.");if(e.mcpTransport&&!["stdio","streamable-http"].includes(e.mcpTransport))throw new Error("Unsupported MCP transport.")}function T(e){const t=e.config?function(e){const t=C.default.resolve(e);if(!O.default.existsSync(t))throw new Error(`Config file not found: ${t}`);const r=O.default.readFileSync(t,"utf-8");try{const e=JSON.parse(r);if(!e||"object"!=typeof e||Array.isArray(e))throw new Error("Config must be an object.");return e}catch(e){throw new Error(`Invalid config file (must be JSON): ${e.message}`)}}(e.config):{},r={specPath:e.spec??t.specPath??"",serverUrl:e.server??t.serverUrl,outputDir:e.output??t.outputDir??R.outputDir,formats:U(e.format??t.formats),filter:I(e,t),concurrency:e.concurrency??t.concurrency??R.concurrency,timeout:e.timeout??t.timeout??R.timeout,proxy:e.proxy??t.proxy,tls:{caCert:e.ca??t.tls?.caCert,clientCert:e.cert??t.tls?.clientCert,clientKey:e.key??t.tls?.clientKey,pfx:t.tls?.pfx,passphrase:t.tls?.passphrase,strictSSL:void 0!==e.insecure?!e.insecure:t.tls?.strictSSL},requester:t.requester,maxEvents:t.maxEvents,maxStreamMs:t.maxStreamMs,maxResponseSize:t.maxResponseSize,headers:L(e.header,t.headers),auth:M(e,t),variables:z(e.variable,t.variables),failOnError:e.failOnError??t.failOnError??R.failOnError,envFile:e.env??t.envFile,grpcReflection:e.grpcReflection??t.grpcReflection,grpcProtoPaths:e.grpcProto??t.grpcProtoPaths,mcpTransport:e.mcpTransport??t.mcpTransport,mcpCommand:e.mcpCommand??t.mcpCommand,mcpArgs:e.mcpArgs?e.mcpArgs.split(/[\s,]+/).filter(Boolean):t.mcpArgs,mcpCwd:e.mcpCwd??t.mcpCwd};if(r.envFile&&function(e){const t=C.default.resolve(e);if(!O.default.existsSync(t))return;const r=O.default.readFileSync(t,"utf-8").split("\n");for(const e of r){const t=e.trim();if(!t||t.startsWith("#"))continue;const r=t.indexOf("=");if(r>0){const e=t.slice(0,r).trim(),s=t.slice(r+1).trim().replace(/^["']|["']$/g,"");e in process.env||(process.env[e]=s)}}}(r.envFile),function(e){const t=e=>"string"!=typeof e?e:e.replace(/\$\{([^}]+)\}/g,(e,t)=>process.env[t]??"");if(e.serverUrl=t(e.serverUrl),e.proxy=t(e.proxy),e.headers)for(const[r,s]of Object.entries(e.headers))e.headers[r]=t(s);e.auth?.token&&(e.auth.token=t(e.auth.token));if(e.variables)for(const[r,s]of Object.entries(e.variables))e.variables[r]=t(s)}(r),!r.specPath)throw new Error("No OpenAPI spec path provided. Use --spec <path> or set specPath in config.");if(r.proxy)try{new URL(r.proxy)}catch{throw new Error(`Invalid proxy URL: ${r.proxy}`)}return A(r),r}function U(e){return Array.isArray(e)?e:"string"==typeof e?e.split(",").map(e=>e.trim().toLowerCase()).filter(Boolean):R.formats}function I(e,t){const r=e.method?e.method.split(",").map(e=>e.trim().toLowerCase()):t.filter?.methods,s=e.path?e.path.split(",").map(e=>e.trim()):t.filter?.paths,n=e.tag?e.tag.split(",").map(e=>e.trim()):t.filter?.tags,o=e.operationId?e.operationId.split(",").map(e=>e.trim()):t.filter?.operationIds;if(r||s||n||o)return{methods:r,paths:s,tags:n,operationIds:o}}function L(e,t){const r={...t??{}};for(const t of e??[]){const e=t.indexOf(":");if(e>0){const s=t.slice(0,e).trim(),n=t.slice(e+1).trim();s&&(r[s]=n)}}return Object.keys(r).length?r:void 0}function M(e,t){return e.bearer?{type:"bearer",token:e.bearer}:t.auth}function z(e,t){const r={...t??{}};for(const t of e??[]){const e=t.indexOf("=");e>0&&(r[t.slice(0,e).trim()]=t.slice(e+1).trim())}return Object.keys(r).length?r:void 0}function N(e){return/^https?:\/\//i.test(e.trim())}async function F(e){const t=e.specPath.trim();let r;if(N(t))r=await function(e,t){let r=0;return new Promise((s,n)=>{const o=a=>{const i=("https:"===new URL(a).protocol?q.default:j.default).get(a,{timeout:t,headers:{Accept:"application/json, text/plain, */*","User-Agent":"@powerduck/openapi-cli"}},t=>{if(t.statusCode&&t.statusCode>=300&&t.statusCode<400&&t.headers.location){if(++r>5)return t.resume(),void n(new Error(`Too many redirects (>5) while fetching spec: ${e}`));const s=new URL(t.headers.location,a).toString();return t.resume(),void o(s)}if(200!==t.statusCode)return t.resume(),void n(new Error(`Failed to fetch spec from ${a}: HTTP ${t.statusCode} ${t.statusMessage??""}`.trim()));const i=[];t.on("data",e=>i.push(e)),t.on("end",()=>s(Buffer.concat(i).toString("utf-8"))),t.on("error",e=>n(new Error(`Error reading spec response: ${e.message}`)))});i.on("timeout",()=>{i.destroy(new Error(`Timed out after ${t}ms while fetching spec: ${e}`))}),i.on("error",t=>n(new Error(`Network error fetching spec ${e}: ${t.message}`)))};o(e)})}(t,e.timeout??R.timeout);else{const e=C.default.resolve(t);if(!O.default.existsSync(e))throw new Error(`OpenAPI spec not found: ${e}`);if(!O.default.statSync(e).isFile())throw new Error(`OpenAPI spec path is not a file: ${e}`);r=O.default.readFileSync(e,"utf-8")}const s=function(e,t){let r;try{r=JSON.parse(e)}catch(e){throw new Error(`Spec from ${t} is not valid JSON: ${e.message}`)}if(!r||"object"!=typeof r||Array.isArray(r))throw new Error(`Spec from ${t} is not a JSON object`);if(!r.paths||"object"!=typeof r.paths)throw new Error(`Spec from ${t} does not contain a valid 'paths' object`);return r}(r,t);return async function(e){try{const t=await(0,P.dereference)(e);return t.errors&&t.errors.length>0?e:t.schema??t.specification??e}catch(t){return e}}(s)}function D(e,t){return e.map(e=>{try{const r=function(e,t){switch(e.assert){case"status":if(void 0===e.value)throw new Error("status assertion requires 'value'");return t.status===e.value;case"header":{if(!e.key)throw new Error("header assertion requires 'key'");const r=function(e,t){const r=t.toLowerCase();for(const[t,s]of Object.entries(e))if(t.toLowerCase()===r)return s;return}(t.headers??{},e.key);return void 0!==r&&(void 0===e.contains||r.toLowerCase().includes(e.contains.toLowerCase()))}case"bodyContains":if(void 0===e.contains)throw new Error("bodyContains assertion requires 'contains'");return(t.bodyText??JSON.stringify(t.body??"")).includes(e.contains);case"bodyEquals":if(void 0===e.body)throw new Error("bodyEquals assertion requires 'body'");return(t.bodyText??JSON.stringify(t.body??""))===e.body;case"jsonPath":{if(!e.path)throw new Error("jsonPath assertion requires 'path'");const r=J(t.body,e.path);return void 0!==e.exists?e.exists?void 0!==r:void 0===r:void 0!==e.equals?_(r,e.equals):void 0!==r}case"responseTime":if(void 0===e.max)throw new Error("responseTime assertion requires 'max'");return t.durationMs<=e.max;default:throw new Error(`Unknown assertion type: ${e.assert}`)}}(e,t);return{name:e.name,passed:r}}catch(t){return{name:e.name,passed:!1,error:t.message}}})}function J(e,t){const r=t.replace(/^\$\.?/,"");if(!r)return e;const s=r.split(/\.|\[(\d+)\]/).filter(e=>void 0!==e&&""!==e);let n=e;for(const e of s){if(null==n)return;n=/^\d+$/.test(e)?n[Number(e)]:n[e]}return n}function _(e,t){if(e===t)return!0;if(typeof e!=typeof t)return!1;if(null===e||null===t)return e===t;if("object"!=typeof e)return e===t;if(Array.isArray(e)!==Array.isArray(t))return!1;if(Array.isArray(e)&&Array.isArray(t))return e.length===t.length&&e.every((e,r)=>_(e,t[r]));const r=Object.keys(e),s=Object.keys(t);return r.length===s.length&&r.every(r=>_(e[r],t[r]))}function B(e,t){const r=e?.["x-tests"],s=t?.["x-tests"];return[...Array.isArray(s)?s:[],...Array.isArray(r)?r:[]].filter(e=>e&&"object"==typeof e&&e.assert&&e.name)}function H(e){const t=e?.["x-postman-scripts"];if(t&&"object"==typeof t&&"string"==typeof t.test)return t.test}var K=d;function G(e){if(!e)return;if("string"==typeof e)return e;if(e instanceof Error)return e.message;const t=e;if("string"==typeof t.message)return t.message;if("string"==typeof t.msg)return t.msg;try{return JSON.stringify(e)}catch{return String(e)}}function W(e,t){const r=[];if("http"===e||"sse"===e){const e=t?.status,s="number"==typeof e&&e>=200&&e<300;r.push({name:"HTTP status is 2xx (implicit)",passed:s,error:s?void 0:`expected 2xx, got ${e??"no status"}`})}else if("graphql"===e){const e=t?.status,s="number"==typeof e&&e>=200&&e<300,n=t?.body,o=Array.isArray(n?.errors)&&n.errors.length>0,a=s&&!o;let i;s?o&&(i=`GraphQL response contains ${n.errors.length} error(s)`):i=`expected HTTP 2xx, got ${e??"no status"}`,r.push({name:"GraphQL success (implicit)",passed:a,error:i})}else if("grpc"===e){const e=t?.statusCode??t?.code??t?.status,s=t?.statusName??t?.status,n=0===e;r.push({name:"gRPC status is OK (implicit)",passed:n,error:n?void 0:`expected code 0 (OK), got ${e??"unknown"}${s?` (${s})`:""}`})}else if("mcp"===e){const e=t?.body??{},s=void 0!==e.result,n=void 0!==e.error||!0===e.result?.isError||void 0!==t?.error,o=s&&!n;let a;n?a="MCP JSON-RPC response contains an error":s||(a="MCP JSON-RPC response missing 'result'"),r.push({name:"MCP JSON-RPC success (implicit)",passed:o,error:a})}else if("websocket"===e){const e=t?.events??[],s=e.some(e=>"open"===e.kind||"open"===e.state),n=e.some(e=>"error"===e.kind||"error"===e.state),o=s&&!n;r.push({name:"WebSocket connected (implicit)",passed:o,error:o?void 0:n?"WebSocket connection errored":"WebSocket did not reach open state"})}return r}async function Q(e){A(e);const t=await F(e),r=(0,l.createClient)(),s=V(t,e),n=Date.now(),o=[];let a=0;const i=Math.min(e.concurrency??5,s.length),c=[];for(let n=0;n<i;n++)c.push((async()=>{for(;a<s.length;){if(e.signal?.aborted)throw new Error("Run cancelled.");const n=s[a++];if(!n)break;const i=await Z(r,n,e,t);o.push(i)}})());await Promise.all(c);const p=Date.now()-n;return{summary:se(o,p),results:o.sort((e,t)=>e.path.localeCompare(t.path)||e.method.localeCompare(t.method)),config:e,generatedAt:(new Date).toISOString(),version:K}}function V(e,t){const r=[],s=["get","post","put","delete","patch","head","options"];for(const[n,o]of Object.entries(e.paths??{})){if(!o||"object"!=typeof o)continue;const e=o;for(const o of s){const s=e[o];if(!s||"object"!=typeof s)continue;const a={path:n,method:o,operation:s,pathItem:e,operationId:s.operationId??`${o.toUpperCase()} ${n}`,protocol:Y(s,e)};X(a,t.filter)&&r.push(a)}}return r}function Y(e,t){const r=e["x-protocol"]??t?.["x-protocol"];if(r)return String(r).toLowerCase();const s=e.responses??{};for(const e of Object.values(s)){if((e?.content??{})["text/event-stream"])return"sse"}return e["x-response-stream"]?"sse":"http"}function X(e,t){if(!t)return!0;if(t.methods?.length&&!t.methods.includes(e.method.toLowerCase()))return!1;if(t.operationIds?.length&&!t.operationIds.includes(e.operationId))return!1;if(t.tags?.length){const r=e.operation.tags??[];if(!t.tags.some(e=>r.includes(e)))return!1}if(t.paths?.length){if(!t.paths.some(t=>{try{return new RegExp(t).test(e.path)}catch{return e.path.includes(t)}}))return!1}return!0}async function Z(e,t,r,s){return(await ee(e,t,{config:r,spec:s,variables:r.variables})).result}async function ee(e,t,r){let{config:s}=r;const{spec:n}=r,o=Date.now(),a={operationId:t.operationId,path:t.path,method:t.method.toUpperCase(),protocol:t.protocol};try{if((r.signal||s.signal)?.aborted)throw new Error("Run cancelled.");const i=r.variables||s.variables,c=t.operation["x-websocket"]||t.operation["x-ws"]||t.operation["x-grpc"]||t.operation["x-mcp"]||{},p=x(r.serverUrl??s.serverUrl??c.url??c.address??c.endpoint??n.servers?.[0]?.url,i);s.resolveTransport&&(s={...s,...await s.resolveTransport({protocol:t.protocol,url:p})});const l=["grpc","websocket","mcp"].includes(t.protocol),d={...s,serverUrl:x(s.serverUrl,i),auth:l?x(s.auth,i):s.auth,headers:l?x({...s.headers,...r.values?.header},i):s.headers,signal:r.signal||s.signal},u={...t,operation:l?{...t.operation,...Object.fromEntries(Object.entries(t.operation).filter(([e])=>["x-grpc","x-mcp","x-ws","x-websocket"].includes(e)).map(([e,t])=>[e,x(t,i)]))}:t.operation},f=l?x(r.values,i):r.values;if(!["http","sse","graphql","grpc","websocket","mcp"].includes(t.protocol))throw new Error(`Unsupported protocol: ${t.protocol}`);const m=[...B(t.operation,t.pathItem),...r.extraAssertions??[]],h=H(t.operation),g=[];let b,O,C={};switch(t.protocol){case"grpc":{const t=await async function(e,t,r,s,n){const o=t.operation["x-grpc"]??{},a=(s??r.serverUrl??o.address)?.replace(/^https?:\/\//,"");if(!a)throw new Error("gRPC operation requires x-grpc.address or --server");const i={...r.headers};$(r,i);const c=v(r);if(c.pfx)throw new Error("gRPC requires PEM certificates, not PFX.");const p=e.connect({kind:"grpc",deadlineMs:r.timeout??3e4,streamDeadlineMs:r.maxStreamMs??r.timeout??3e4,...k(r,a),tls:!(!0!==o.tls&&!/^https:/.test(s??r.serverUrl??""))&&{rootCerts:c.ca,privateKey:S(c),certChain:c.cert,rejectUnauthorized:c.rejectUnauthorized},service:o.service,method:o.method??t.operationId,reflection:r.grpcReflection??o.reflection??!(r.grpcProtoPaths??o.protoPaths)?.length,protoPaths:r.grpcProtoPaths??o.protoPaths,includeDirs:o.includeDirs,metadata:i}),l=()=>{p.close().catch(()=>{})};r.signal?.addEventListener("abort",l,{once:!0});try{if(r.signal?.aborted)throw new Error("Run cancelled.");if(await p.open(),r.signal?.aborted)throw new Error("Run cancelled.");const e=p.kind,s=n?.body??function(e){const t=e["x-grpc"]??{};if(t.sample&&"object"==typeof t.sample)return t.sample;const r=e.requestBody?.content?.["application/json"]?.schema;if(r?.properties){const e={};for(const[t,s]of Object.entries(r.properties))e[t]=s.default??re(s.type);return e}return{}}(t.operation);"unary"===e||"server_streaming"===e?await p.send(s):(await p.send(s),await p.finishSending());const c=p.events??[],l=c.find(e=>"status"===e.kind),d=c.filter(e=>"data"===e.kind&&"in"===e.direction);"unary"!==e&&"server_streaming"!==e||await p.close();const u={status:l?.meta?.code??-1,statusText:l?.meta?.statusName,body:1===d.length?d[0].data:d.map(e=>e.data),events:c.map(e=>({kind:e.kind,direction:e.direction,data:e.data,meta:e.meta}))},f=o.method??t.operationId;return{response:u,request:{method:f,target:o.service?`${o.service}/${f}`:f,url:a,...Object.keys(s).length?{body:s}:{},...Object.keys(i).length?{headers:i}:{}}}}finally{r.signal?.removeEventListener("abort",l),await p.close().catch(()=>{})}}(e,u,d,x(r.serverUrl,i),f);b=t.response,O=t.request;break}case"websocket":{const t=await async function(e,t,r,s,n){const o=t.operation["x-websocket"]??t.operation["x-ws"]??{};let a=te(o.url,s??r.serverUrl,t.path)?.replace(/^http/,"ws");if(!a)throw new Error("WebSocket operation requires x-ws.url or --server");const i={...r.headers};a=$(r,i,E(a,n?.query));const c=y(r),p=e.connect({kind:"websocket",openTimeoutMs:r.timeout??3e4,signal:r.signal,clientOptions:{...v(r),agent:a.startsWith("wss:")?c.https:c.http},url:a,subprotocols:o.subprotocols,headers:i}),l=()=>{p.close().catch(()=>{})};r.signal?.addEventListener("abort",l,{once:!0});try{if(r.signal?.aborted)throw new Error("Run cancelled.");if(await p.open(),r.signal?.aborted)throw new Error("Run cancelled.");const e=void 0!==n?.body?[n.body]:o.send??[];for(const t of e.slice(0,3))await p.send(t);await new Promise(e=>setTimeout(e,Math.min(500,r.timeout??500))),await p.close();const t=p.events??[],s=t.filter(e=>"in"===e.direction&&"open"!==e.kind);return{response:{status:101,statusText:"Switching Protocols",body:s.map(e=>e.data),events:t.map(e=>({kind:e.kind,direction:e.direction,data:e.data})),streaming:!0},request:{method:"WS",url:a,...e.length?{body:e.slice(0,3)}:{},...Object.keys(i).length?{headers:i}:{}}}}finally{r.signal?.removeEventListener("abort",l),await p.close().catch(()=>{});for(const e of new Set(Object.values(c)))e.destroy()}}(e,u,d,x(r.serverUrl,i),f);b=t.response,O=t.request;break}case"mcp":{const t=await async function(e,t,r,s,n){const o=t.operation["x-mcp"]??{},a=r.mcpTransport??o.transport??"streamable-http",i={...r.headers};let c=te(o.endpoint,s??r.serverUrl,t.path);"stdio"!==a&&(w(r,"MCP HTTP"),c=$(r,i,c?E(c,n?.query):c));const p=n?.body?.method??o.method??"tools/call",l=n?.body?.params??o.params??("tools/call"===p?{name:o.name,arguments:n?.body??o.arguments??{}}:{}),d=e.connect({kind:"mcp",transport:"stdio"===a?"stdio":"streamable-http",endpoint:c,headers:i,command:r.mcpCommand??o.command,args:r.mcpArgs??o.args,cwd:r.mcpCwd??o.cwd,timeoutMs:r.timeout??3e4}),u=()=>{d.close().catch(()=>{})};r.signal?.addEventListener("abort",u,{once:!0});try{if(r.signal?.aborted)throw new Error("Run cancelled.");if(await d.open(),r.signal?.aborted)throw new Error("Run cancelled.");return{response:{status:200,statusText:"OK",body:{result:await d.request(p,l,{signal:r.signal,timeoutMs:r.timeout})}},request:{method:p,url:c,headers:i,body:l}}}finally{r.signal?.removeEventListener("abort",u),await d.close().catch(()=>{})}}(e,u,d,x(r.serverUrl,i),f);b=t.response,O=t.request;break}default:{const o=await async function(e,t,r,s,n){"graphql"===t.protocol&&w(r,"GraphQL");const o="graphql"===t.protocol?void 0:y(r),a={...r.httpRuntime,requester:{...r.requester,strictSSL:!1!==r.tls?.strictSSL,ignoreProxyEnvironmentVariables:!0,...o?{agents:o,...r.proxy||r.tls?.caCert||r.tls?.clientCert||r.tls?.pfx||r.httpRuntime?.certificates?{protocolVersion:"http1"}:{}}:{}}},i=n.values,c={...r.headers??{},...i?.header??{}},p={...r.headers?{header:r.headers}:{},...i??{},...Object.keys(c).length?{header:c}:{}};let l;try{l=await e.send({writeBack:!1,signal:n.signal,spec:s,target:{path:t.path,method:t.method},serverUrl:n.serverUrl??r.serverUrl,values:Object.keys(p).length?p:void 0,auth:r.auth,variables:n.variables,timeout:r.timeout,runner:Object.keys(a).length?a:void 0,scripts:n.postmanTest?{test:{exec:n.postmanTest},fromSpecExtensions:!1}:void 0,onAssertion:e=>{n.postmanAssertions.push({name:e.name??"assertion",passed:e.passed,error:G(e.error)})},maxStreamMs:r.maxStreamMs??r.timeout,maxEvents:r.maxEvents??50,maxResponseSize:r.maxResponseSize})}finally{for(const e of new Set(Object.values(o||{})))e.destroy()}if(n.signal?.aborted)throw new Error("Run cancelled.");if(l.error)throw new Error(l.error.message);const d={},u=l.scripts;if(u)for(const e of[...u.prerequest??[],...u.test??[]])e.environment&&Object.assign(d,e.environment);const f=l.request;return{response:{status:l.response?.status,statusText:l.response?.statusText,contentType:l.response?.contentType,sizeBytes:l.response?.sizeBytes,body:l.response?.body,text:l.response?.text,headers:l.response?.headers,events:l.response?.events,streaming:l.response?.streaming},request:f?{method:f.method,url:f.url,headers:f.headers,...void 0!==f.body?{body:f.body}:{}}:void 0,env:d}}(e,t,s,n,{postmanTest:h,postmanAssertions:g,variables:r.variables,values:r.values,serverUrl:r.serverUrl,signal:r.signal||s.signal});b=o.response,O=o.request,C=o.env;break}}if((r.signal||s.signal)?.aborted)throw new Error("Run cancelled.");const j=Date.now()-o,q=m.length>0||g.length>0?[]:W(t.protocol,b),P=m.length?D(m,{status:b?.status,headers:b?b.headers:void 0,body:b?.body,bodyText:b?b.text:void 0,durationMs:j}):[],R=new Set,A=g.filter(e=>{const t=`${e.name}|${e.passed}|${e.error??""}`;return!R.has(t)&&(R.add(t),!0)}),T=[...P,...A,...q],U=T.some(e=>!e.passed);return{result:{...a,status:U?"failed":"passed",durationMs:j,request:O,response:b,assertions:T.length?T:void 0,timestamp:(new Date).toISOString()},response:b,request:O,env:C}}catch(e){const n=Date.now()-o,i=r.values,c={...s.headers??{},...i?.header??{}},p=Object.fromEntries(Object.entries(c).map(([e,t])=>[e,null==t?"":String(t)])),l={method:t.method.toUpperCase(),url:r.serverUrl??s.serverUrl,...Object.keys(p).length?{headers:p}:{},...void 0!==i?.body?{body:i.body}:{}};return{result:{...a,status:"error",durationMs:n,request:l,error:e.message??String(e),timestamp:(new Date).toISOString()},response:void 0,request:l,env:{}}}}function te(e,t,r){if(!t)return e;const s=new URL(t),n=new URL(e||r,t);return n.host=s.host,n.protocol=s.protocol,e||"/"===s.pathname||(n.pathname=s.pathname.replace(/\/$/,"")+r),n.href}function re(e){switch(e){case"string":default:return"";case"number":case"integer":return 0;case"boolean":return!1;case"array":return[];case"object":return{}}}function se(e,t){const r=e.length,s=e.filter(e=>"passed"===e.status).length;return{total:r,passed:s,failed:e.filter(e=>"failed"===e.status).length,errors:e.filter(e=>"error"===e.status).length,skipped:e.filter(e=>"skipped"===e.status).length,durationMs:t,passRate:r>0?Math.round(s/r*1e3)/10:0}}var ne=require("@powerduck/openapi-request"),oe=d,ae=1e3;function ie(e){if(e.method&&e.path)return`${e.method.toUpperCase()} ${e.path}`;const t=(e.ref??"").trim(),r=t.indexOf(" ");return r>0?`${t.slice(0,r).toUpperCase()} ${t.slice(r+1).trim()}`:t}function ce(e,t){const r=new Map,s=new Map;for(const e of t)r.set(`${e.method.toUpperCase()} ${e.path}`,e),s.set(e.operationId,e);const n=[],o=[];if(e.steps.forEach((e,t)=>{const a=ie(e),i=r.get(a)??(a?s.get(a):void 0);i?n.push({step:e,op:i,ref:`${i.method.toUpperCase()} ${i.path}`}):o.push(`step ${t} ("${e.name??a}") -> ${a||"<empty ref>"}`)}),o.length)throw new Error(`Scenario references operations not present in the spec: ${o.join("; ")}`);return n}function pe(e,t){if(!e)return;const r=t.toLowerCase();for(const[t,s]of Object.entries(e))if(t.toLowerCase()===r)return s}function le(e){if(null==e)return"";if("string"==typeof e)return e;if("number"==typeof e||"boolean"==typeof e)return String(e);try{return JSON.stringify(e)}catch{return String(e)}}function de(e,t){const r={};if(!e?.length)return r;for(const s of e){let e;e="status"===s.from?t?.status:"header"===s.from?s.key?pe(t?.headers,s.key):void 0:J(t?.body,s.path??"$"),null!=e&&(r[s.name]=le(e))}return r}function ue(e,t,r,s,n){return{index:e,ref:t,name:r.name,operationId:t,path:t.includes(" ")?t.slice(t.indexOf(" ")+1):t,method:t.includes(" ")?t.slice(0,t.indexOf(" ")):"",protocol:"",status:"skipped",durationMs:0,timestamp:(new Date).toISOString(),notRun:!0,notRunReason:s,...n??{}}}async function fe(e,t){const{config:r,onEvent:s,signal:n}=t;A(r);const o=Date.now(),a=new Date(o).toISOString(),i=e=>{try{s?.(e)}catch{}},c=t.spec??await F(r),p=ce(e,V(c,{...r,filter:void 0})),l=(0,ne.createClient)(),d=e.data&&e.data.length?e.data:[],u=Math.trunc(Number(e.iterations)),f=Number.isFinite(u)&&u>0?Math.min(u,ae):1,m=Math.max(f,d.length),h=Array.from({length:m},(e,t)=>d[t]),g=p.map(({step:e})=>e.request?.data&&e.request.data.length?e.request.data.length:1),b={scenarioCount:h.length,stepCounts:g},v=!1!==e.stopOnFailure,y=[];let w="passed",x=!1,$=!1,k=null,S="failed",E={...r.variables??{},...e.variables??{}};i({type:"scenario:start",scenario:e.name,status:w,scenarioIterations:h.length});e:for(let t=0;t<h.length;t++){E={...r.variables??{},...e.variables??{},...h[t]??{}};for(let s=0;s<p.length;s++){const{step:o,op:a,ref:d}=p[s],u=o.request?.data&&o.request.data.length?o.request.data:[void 0],f={scenarioIteration:t,scenarioIterations:h.length,stepIterations:u.length};if(n?.aborted){w="cancelled",$=!0,x=!0,k={s:t,i:s,t:0};break e}if(o.skip)for(let t=0;t<u.length;t++){const r=ue(s,d,o,"Skipped by plan",{...f,stepIteration:t});y.push(r),i({type:"step:skip",scenario:e.name,stepIndex:s,step:r,...f,stepIteration:t})}else for(let p=0;p<u.length;p++){if(n?.aborted){w="cancelled",$=!0,x=!0,k={s:t,i:s,t:p};break e}i({type:"step:start",scenario:e.name,stepIndex:s,...f,stepIteration:p});const m={...E,...u[p]??{}},h=await ee(l,a,{config:r,spec:c,signal:n,variables:m,values:o.request?.values,serverUrl:o.request?.serverUrl,extraAssertions:o.request?.assertions});n?.aborted&&($=!0);const g={};for(const[e,t]of Object.entries(h.env))m[e]!==t&&(g[e]=t);const b=de(o.request?.extract,h.response),O={...g,...b},C=p===u.length-1;C&&Object.assign(E,h.env,b);const j={...h.result,index:s,ref:d,name:o.name,...f,stepIteration:p,extracted:Object.keys(O).length?O:void 0,variablesAfter:C?{...E}:{...E,...u[p]??{},...O}};if(y.push(j),i({type:"step:finish",scenario:e.name,stepIndex:s,step:j,...f,stepIteration:p}),"error"===j.status?w="error":"failed"===j.status&&"error"!==w&&(w="failed"),("failed"===j.status||"error"===j.status)&&v){x=!0,S=j.status,k={s:t,i:s,t:p};break e}}}}if(x&&k){const t=(e,t)=>$?"Scenario cancelled":e===k.s&&t===k.i?`Stopped after ${S} iteration ${k.t+1}`:e===k.s?`Stopped after ${S} step ${k.i+1}`:`Stopped after ${S} run ${k.s+1}`;for(let r=k.s;r<h.length;r++)for(let s=r===k.s?k.i:0;s<p.length;s++){const n=p[s],o=n.step.request?.data&&n.step.request.data.length?n.step.request.data.length:1;for(let a=r===k.s&&s===k.i?k.t+1:0;a<o;a++){const c={scenarioIteration:r,scenarioIterations:h.length,stepIteration:a,stepIterations:o},p=ue(s,n.ref,n.step,t(r,s),c);y.push(p),i({type:"step:skip",scenario:e.name,stepIndex:s,step:p,...c})}}}($||n?.aborted&&"error"!==w&&"failed"!==w)&&(w="cancelled");const O=Date.now()-o,C="cancelled"===w?"scenario:cancel":"scenario:finish",j={name:e.name,description:e.description,status:w,summary:se(y,O),steps:y,variables:E,iterations:b,config:r,startedAt:a,durationMs:O,generatedAt:(new Date).toISOString(),version:oe};return i({type:C,scenario:e.name,status:w,message:x?"halted":void 0}),j}var me=c(require("fs"),1),he=c(require("path"),1);function ge(e,t){const r=he.default.resolve(t);me.default.mkdirSync(r,{recursive:!0});const s=he.default.join(r,"report.json");return me.default.writeFileSync(s,JSON.stringify(e,null,2),"utf-8"),s}var be={reset:"[0m",bold:"[1m",dim:"[2m",green:"[32m",red:"[31m",yellow:"[33m",blue:"[34m",cyan:"[36m",gray:"[90m",bgGreen:"[42m",bgRed:"[41m",bgYellow:"[43m"};function ve(e){if(null==e||!Number.isFinite(e))return`${be.dim}--ms${be.reset}`;return`${e>500?`${be.bold}${be.yellow}`:be.bold}${e}${be.reset}${be.dim}ms${be.reset}`}function ye(e){const t=[],{summary:r,results:s}=e;t.push(""),t.push(`${be.bold}${be.cyan}╔══════════════════════════════════════════════════════╗${be.reset}`),t.push(`${be.bold}${be.cyan}║ Powerduck Test Report ║${be.reset}`),t.push(`${be.bold}${be.cyan}╚══════════════════════════════════════════════════════╝${be.reset}`),t.push("");const n=r.passRate>=80?be.green:r.passRate>=50?be.yellow:be.red;t.push(`${be.bold} Total:${be.reset} ${r.total} ${be.green}Passed:${be.reset} ${r.passed} ${be.red}Failed:${be.reset} ${r.failed} ${be.yellow}Errors:${be.reset} ${r.errors} ${n}Rate:${be.reset} ${r.passRate}% ${be.dim}Time:${be.reset} ${ve(r.durationMs)}`),t.push("");const o=r.total>0?Math.round(r.passed/r.total*50):0,a=r.total>0?Math.round(r.failed/r.total*50):0,i=r.total>0?Math.round(r.errors/r.total*50):0,c=be.green+"█".repeat(o)+be.red+"█".repeat(a)+be.yellow+"█".repeat(i)+be.gray+"░".repeat(Math.max(0,50-o-a-i))+be.reset;t.push(` [${c}]`),t.push("");for(const e of s)t.push(we(e));return t.push(""),r.failed>0||r.errors>0?t.push(`${be.bold}${be.red} ✗ ${r.failed+r.errors} test(s) did not pass${be.reset}`):t.push(`${be.bold}${be.green} ✓ All ${r.total} test(s) passed${be.reset}`),t.push(` ${be.dim}Generated: ${e.generatedAt}${be.reset}`),t.push(""),t.join("\n")}function we(e){const t="passed"===e.status?`${be.green}✓${be.reset}`:"failed"===e.status?`${be.red}✗${be.reset}`:"error"===e.status?`${be.yellow}!${be.reset}`:`${be.gray}-${be.reset}`,r="passed"===e.status?`${be.bgGreen}${be.bold} PASS ${be.reset}`:"failed"===e.status?`${be.bgRed}${be.bold} FAIL ${be.reset}`:"error"===e.status?`${be.bgYellow}${be.bold} ERROR ${be.reset}`:`${be.gray} SKIP ${be.reset}`,s=e.protocol?` ${be.cyan}[${e.protocol}]${be.reset}`:"",n=null!=e.response?.status?` ${be.dim}${e.response.status}${be.reset}`:"";let o=` ${t} ${r} ${be.bold}${e.method}${be.reset} ${e.path}${s}${n} ${ve(e.durationMs)}`;if(e.error&&(o+=`\n ${be.red}${e.error}${be.reset}`),e.assertions?.length)for(const t of e.assertions){o+=`\n ${t.passed?`${be.green} ✓${be.reset}`:`${be.red} ✗${be.reset}`} ${t.name}${t.error?` — ${be.red}${t.error}${be.reset}`:""}`}return o}function xe(e){const t=ye(e);return console.log(t),t}var $e=c(require("fs"),1),ke=c(require("path"),1);function Se(e,t){const r=ke.default.resolve(t);$e.default.mkdirSync(r,{recursive:!0});const s=ke.default.join(r,"report.html");return $e.default.writeFileSync(s,function(e){const{summary:t,results:r}=e,s=JSON.stringify({summary:t,results:r});return`<!DOCTYPE html>\n<html lang="en">\n<head>\n<meta charset="UTF-8">\n<meta name="viewport" content="width=device-width, initial-scale=1.0">\n<title>Test Report — Powerduck</title>\n<style>\n *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}\n :root{\n --bg:#ffffff;--fg:#0f172a;--muted:#64748b;--border:#e2e8f0;\n --bg-subtle:#f8fafc;--bg-hover:#f1f5f9;\n --green:#16a34a;--green-bg:#f0fdf4;--green-border:#bbf7d0;\n --red:#dc2626;--red-bg:#fef2f2;--red-border:#fecaca;\n --amber:#d97706;--amber-bg:#fffbeb;--amber-border:#fde68a;\n --blue:#2563eb;--mono:ui-monospace,SFMono-Regular,'SF Mono',Menlo,Consolas,monospace;\n --sans:ui-sans-serif,system-ui,-apple-system,'Segoe UI',Roboto,Inter,sans-serif;\n }\n @media (prefers-color-scheme:dark){\n :root{\n --bg:#0f172a;--fg:#f1f5f9;--muted:#94a3b8;--border:#1e293b;\n --bg-subtle:#1e293b;--bg-hover:#334155;\n --green:#4ade80;--green-bg:rgba(74,222,128,.1);--green-border:rgba(74,222,128,.25);\n --red:#f87171;--red-bg:rgba(248,113,113,.1);--red-border:rgba(248,113,113,.25);\n --amber:#fbbf24;--amber-bg:rgba(251,191,36,.1);--amber-border:rgba(251,191,36,.25);\n --blue:#60a5fa;\n }\n }\n body{font-family:var(--sans);background:var(--bg);color:var(--fg);line-height:1.5;font-size:14px;-webkit-font-smoothing:antialiased}\n .wrap{max-width:960px;margin:0 auto;padding:32px 24px 64px}\n\n /* Header */\n .hdr{display:flex;align-items:baseline;justify-content:space-between;margin-bottom:24px;padding-bottom:16px;border-bottom:1px solid var(--border)}\n .hdr h1{font-size:18px;font-weight:600;letter-spacing:-.01em}\n .hdr .meta{font-size:12px;color:var(--muted);font-family:var(--mono)}\n\n /* Summary strip */\n .strip{display:grid;grid-template-columns:repeat(6,1fr);gap:0;border:1px solid var(--border);border-radius:8px;overflow:hidden;margin-bottom:24px}\n .stat{padding:14px 16px;border-right:1px solid var(--border);text-align:center}\n .stat:last-child{border-right:none}\n .stat .v{font-size:22px;font-weight:700;letter-spacing:-.02em;line-height:1.2}\n .stat .l{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;margin-top:2px}\n .stat.total .v{color:var(--fg)}\n .stat.pass .v{color:var(--green)}\n .stat.fail .v{color:var(--red)}\n .stat.err .v{color:var(--amber)}\n .stat.rate .v{color:var(--blue)}\n .stat.time .v{font-size:16px;font-weight:600}\n\n /* Progress */\n .bar{height:4px;background:var(--border);border-radius:2px;overflow:hidden;margin-bottom:24px;display:flex}\n .bar>div{height:100%}\n .bar .p{background:var(--green)}\n .bar .f{background:var(--red)}\n .bar .e{background:var(--amber)}\n\n /* Filters */\n .filters{display:flex;gap:4px;margin-bottom:16px}\n .fbtn{padding:4px 12px;border-radius:6px;border:1px solid transparent;background:transparent;color:var(--muted);cursor:pointer;font-size:12px;font-weight:500;transition:all .12s}\n .fbtn:hover{color:var(--fg);background:var(--bg-hover)}\n .fbtn.active{background:var(--bg-subtle);color:var(--fg);border-color:var(--border)}\n .fbtn .n{color:var(--muted);margin-left:4px;font-variant-numeric:tabular-nums}\n\n /* Test rows */\n .row{border:1px solid var(--border);border-radius:8px;margin-bottom:6px;overflow:hidden;transition:border-color .12s}\n .row:hover{border-color:var(--muted)}\n .row-head{display:flex;align-items:center;gap:10px;padding:10px 14px;cursor:pointer;user-select:none}\n .dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}\n .dot.pass{background:var(--green)}\n .dot.fail{background:var(--red)}\n .dot.err{background:var(--amber)}\n .method{font-family:var(--mono);font-size:11px;font-weight:600;color:var(--muted);min-width:40px;text-transform:uppercase}\n .path{font-family:var(--mono);font-size:13px;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n .proto{font-size:10px;text-transform:uppercase;letter-spacing:.05em;color:var(--muted);padding:2px 8px;border:1px solid var(--border);border-radius:4px}\n .dur{font-family:var(--mono);font-size:12px;color:var(--muted);min-width:56px;text-align:right;font-variant-numeric:tabular-nums}\n .chev{color:var(--muted);font-size:10px;transition:transform .15s;flex-shrink:0}\n .row.open .chev{transform:rotate(90deg)}\n\n /* Row body */\n .row-body{display:none;padding:0 14px 12px 32px;border-top:1px solid var(--border)}\n .row.open .row-body{display:block}\n .sect{margin-top:10px}\n .sect-t{font-size:10px;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);font-weight:600;margin-bottom:6px}\n .asrt{display:flex;align-items:center;gap:8px;padding:3px 0;font-size:13px}\n .asrt .ck{width:14px;height:14px;border-radius:3px;display:flex;align-items:center;justify-content:center;font-size:9px;font-weight:700;flex-shrink:0;color:#fff}\n .asrt.pass .ck{background:var(--green)}\n .asrt.fail .ck{background:var(--red)}\n .asrt .an{color:var(--fg)}\n .asrt .ae{color:var(--red);font-size:12px;margin-left:6px;font-family:var(--mono)}\n .rmeta{display:flex;gap:20px;flex-wrap:wrap;font-size:12px}\n .rmeta span{color:var(--muted)}\n .rmeta strong{color:var(--fg);font-weight:600;font-family:var(--mono)}\n .errbox{background:var(--red-bg);border:1px solid var(--red-border);border-radius:6px;padding:8px 12px;color:var(--red);font-size:12px;font-family:var(--mono);margin-top:10px}\n .bprev{background:var(--bg-subtle);border:1px solid var(--border);border-radius:6px;padding:10px 12px;font-family:var(--mono);font-size:11px;color:var(--muted);max-height:180px;overflow:auto;white-space:pre-wrap;word-break:break-all;margin-top:6px}\n\n footer{margin-top:32px;padding-top:16px;border-top:1px solid var(--border);font-size:11px;color:var(--muted);display:flex;justify-content:space-between}\n\n @media(max-width:640px){\n .strip{grid-template-columns:repeat(3,1fr)}\n .stat:nth-child(3){border-right:none}\n .stat:nth-child(4),.stat:nth-child(5){border-top:1px solid var(--border)}\n .path{font-size:11px}\n }\n</style>\n</head>\n<body>\n<div class="wrap">\n <div class="hdr">\n <h1>Test Report</h1>\n <span class="meta">${e.generatedAt} · v${e.version}</span>\n </div>\n\n <div class="strip">\n <div class="stat total"><div class="v">${t.total}</div><div class="l">Total</div></div>\n <div class="stat pass"><div class="v">${t.passed}</div><div class="l">Passed</div></div>\n <div class="stat fail"><div class="v">${t.failed}</div><div class="l">Failed</div></div>\n <div class="stat err"><div class="v">${t.errors}</div><div class="l">Errors</div></div>\n <div class="stat rate"><div class="v">${t.passRate}%</div><div class="l">Pass Rate</div></div>\n <div class="stat time"><div class="v">${(t.durationMs/1e3).toFixed(2)}s</div><div class="l">Duration</div></div>\n </div>\n\n <div class="bar">\n <div class="p" style="width:${t.total?t.passed/t.total*100:0}%"></div>\n <div class="f" style="width:${t.total?t.failed/t.total*100:0}%"></div>\n <div class="e" style="width:${t.total?t.errors/t.total*100:0}%"></div>\n </div>\n\n <div class="filters">\n <button class="fbtn active" data-f="all">All<span class="n">${t.total}</span></button>\n <button class="fbtn" data-f="passed">Passed<span class="n">${t.passed}</span></button>\n <button class="fbtn" data-f="failed">Failed<span class="n">${t.failed}</span></button>\n <button class="fbtn" data-f="error">Errors<span class="n">${t.errors}</span></button>\n </div>\n\n <div id="list"></div>\n\n <footer>\n <span>@powerduck/openapi-cli v${e.version}</span>\n <span>${t.passed}/${t.total} passed</span>\n </footer>\n</div>\n\n<script>\nconst D=${s};\nfunction esc(s){const d=document.createElement('div');d.textContent=String(s??'');return d.innerHTML}\nfunction render(f){\n const list=document.getElementById('list');\n const items=f==='all'?D.results:D.results.filter(r=>r.status===f);\n list.innerHTML=items.map(r=>{\n const as=r.assertions?r.assertions.map(a=>\`\n <div class="asrt \${a.passed?'pass':'fail'}">\n <div class="ck">\${a.passed?'✓':'✗'}</div>\n <span class="an">\${esc(a.name)}</span>\n \${a.error?'<span class="ae">'+esc(a.error)+'</span>':''}\n </div>\`).join(''):'';\n const bp=r.response?.body?'<div class="bprev">'+esc(JSON.stringify(r.response.body,null,2).slice(0,3000))+'</div>':'';\n const eb=r.error?'<div class="errbox">'+esc(r.error)+'</div>':'';\n const rm=r.response?\`\n <div class="rmeta">\n \${r.response.status!=null?'<span>Status <strong>'+r.response.status+(r.response.statusText?' '+r.response.statusText:'')+'</strong></span>':''}\n \${r.response.contentType?'<span>Type <strong>'+esc(r.response.contentType)+'</strong></span>':''}\n \${r.response.sizeBytes!=null?'<span>Size <strong>'+r.response.sizeBytes+'B</strong></span>':''}\n \${r.response.streaming?'<span>Streaming <strong>yes</strong></span>':''}\n </div>\`:'';\n return \`\n <div class="row \${r.status}" data-s="\${r.status}">\n <div class="row-head" onclick="this.parentElement.classList.toggle('open')">\n <div class="dot \${r.status==='passed'?'pass':r.status==='failed'?'fail':'err'}"></div>\n <span class="method">\${r.method}</span>\n <span class="path">\${esc(r.path)}</span>\n \${r.protocol?'<span class="proto">'+r.protocol+'</span>':''}\n <span class="dur">\${r.durationMs}ms</span>\n <span class="chev">▶</span>\n </div>\n <div class="row-body">\n \${eb}\n \${rm}\n \${as?'<div class="sect"><div class="sect-t">Assertions</div>'+as+'</div>':''}\n \${bp?'<div class="sect"><div class="sect-t">Response</div>'+bp+'</div>':''}\n </div>\n </div>\`;\n }).join('');\n}\ndocument.querySelectorAll('.fbtn').forEach(b=>b.addEventListener('click',()=>{\n document.querySelectorAll('.fbtn').forEach(x=>x.classList.remove('active'));\n b.classList.add('active');render(b.dataset.f);\n}));\nrender('all');\n<\/script>\n</body>\n</html>`}(e),"utf-8"),s}
|
package/dist/index.d.cts
CHANGED
|
@@ -63,7 +63,8 @@ type ProtocolName = "http" | "sse" | "websocket" | "graphql" | "grpc" | "mcp";
|
|
|
63
63
|
type ReportFormat = "json" | "cli" | "html";
|
|
64
64
|
type TestStatus = "passed" | "failed" | "skipped" | "error";
|
|
65
65
|
interface AuthConfig {
|
|
66
|
-
type: "bearer" | "basic" | "apikey" | "none";
|
|
66
|
+
type: "bearer" | "basic" | "apikey" | "none" | "digest" | "oauth1" | "oauth2" | "jwt" | "hawk" | "aws4" | "ntlm" | "asap" | "edgegrid";
|
|
67
|
+
parameters?: Record<string, string | number | boolean>;
|
|
67
68
|
token?: string;
|
|
68
69
|
username?: string;
|
|
69
70
|
password?: string;
|
|
@@ -80,6 +81,8 @@ interface TlsConfig {
|
|
|
80
81
|
clientKey?: string;
|
|
81
82
|
/** Skip TLS certificate verification (insecure). */
|
|
82
83
|
strictSSL?: boolean;
|
|
84
|
+
pfx?: string;
|
|
85
|
+
passphrase?: string;
|
|
83
86
|
}
|
|
84
87
|
interface FilterConfig {
|
|
85
88
|
/** Only test these HTTP methods (e.g. ["get","post"]). */
|
|
@@ -92,6 +95,26 @@ interface FilterConfig {
|
|
|
92
95
|
operationIds?: string[];
|
|
93
96
|
}
|
|
94
97
|
interface CliConfig {
|
|
98
|
+
/** Programmatic HTTP runtime certificates matched against the resolved URL. */
|
|
99
|
+
httpRuntime?: {
|
|
100
|
+
certificates?: unknown;
|
|
101
|
+
fileResolver?: unknown;
|
|
102
|
+
};
|
|
103
|
+
signal?: AbortSignal;
|
|
104
|
+
/** Programmatic per-target transport resolution; never serialized over IPC. */
|
|
105
|
+
resolveTransport?: (target: {
|
|
106
|
+
protocol: string;
|
|
107
|
+
url?: string;
|
|
108
|
+
}) => Promise<Pick<CliConfig, "proxy" | "tls" | "httpRuntime">>;
|
|
109
|
+
requester?: {
|
|
110
|
+
followRedirects?: boolean;
|
|
111
|
+
maxRedirects?: number;
|
|
112
|
+
disableCookies?: boolean;
|
|
113
|
+
protocolVersion?: "http1" | "http2" | "auto";
|
|
114
|
+
};
|
|
115
|
+
maxEvents?: number;
|
|
116
|
+
maxStreamMs?: number;
|
|
117
|
+
maxResponseSize?: number;
|
|
95
118
|
/** Path to the OpenAPI 3.2 JSON file. */
|
|
96
119
|
specPath: string;
|
|
97
120
|
/** Override the server URL from the spec. */
|
|
@@ -204,6 +227,12 @@ interface ScenarioExtract {
|
|
|
204
227
|
/** Response header name when from="header". */
|
|
205
228
|
key?: string;
|
|
206
229
|
}
|
|
230
|
+
/**
|
|
231
|
+
* One row of data-driven values. Keys are variable names referenced as
|
|
232
|
+
* {{name}} in step values; values use the same string contract as scenario
|
|
233
|
+
* variables.
|
|
234
|
+
*/
|
|
235
|
+
type ScenarioDataRow = Record<string, string>;
|
|
207
236
|
/** Per-step request overrides and verification rules. */
|
|
208
237
|
interface ScenarioStepRequest {
|
|
209
238
|
/** Request values merged over the operation's sampled defaults. */
|
|
@@ -214,6 +243,12 @@ interface ScenarioStepRequest {
|
|
|
214
243
|
extract?: ScenarioExtract[];
|
|
215
244
|
/** Declarative assertions added on top of the operation's spec assertions. */
|
|
216
245
|
assertions?: DeclarativeAssertion[];
|
|
246
|
+
/**
|
|
247
|
+
* Step-level data rows. The step executes once per row; each row's values
|
|
248
|
+
* layer over the scenario scope only for that execution. Extracts from the
|
|
249
|
+
* last execution are written back into the scenario scope.
|
|
250
|
+
*/
|
|
251
|
+
data?: ScenarioDataRow[];
|
|
217
252
|
}
|
|
218
253
|
/** A single ordered operation invocation inside a scenario. */
|
|
219
254
|
interface ScenarioStep {
|
|
@@ -238,6 +273,19 @@ interface ScenarioDefinition {
|
|
|
238
273
|
variables?: Record<string, string>;
|
|
239
274
|
/** Stop at the first failed/error step. Default: true. */
|
|
240
275
|
stopOnFailure?: boolean;
|
|
276
|
+
/**
|
|
277
|
+
* Scenario-level data rows. The whole step sequence runs once per row, each
|
|
278
|
+
* time with a fresh variable scope seeded from the row; extracts never leak
|
|
279
|
+
* across iterations. An empty/absent array means a single iteration.
|
|
280
|
+
*/
|
|
281
|
+
data?: ScenarioDataRow[];
|
|
282
|
+
/**
|
|
283
|
+
* Declared number of full-sequence repetitions. The effective iteration
|
|
284
|
+
* count is max(iterations, data.length); iterations beyond the data rows
|
|
285
|
+
* run with an empty scope. Useful for "run this flow N times" requests that
|
|
286
|
+
* carry no row-specific values. Defaults to 1 when omitted.
|
|
287
|
+
*/
|
|
288
|
+
iterations?: number;
|
|
241
289
|
}
|
|
242
290
|
type ScenarioStatus = "passed" | "failed" | "error" | "cancelled";
|
|
243
291
|
/** Result of executing one scenario step. */
|
|
@@ -247,6 +295,14 @@ interface ScenarioStepResult extends TestResult {
|
|
|
247
295
|
/** Resolved operation reference ("METHOD /path"). */
|
|
248
296
|
ref: string;
|
|
249
297
|
name?: string;
|
|
298
|
+
/** Zero-based scenario-level iteration (definition.data row index). */
|
|
299
|
+
scenarioIteration?: number;
|
|
300
|
+
/** Total scenario-level iterations for this run. */
|
|
301
|
+
scenarioIterations?: number;
|
|
302
|
+
/** Zero-based step-level iteration (step.request.data row index). */
|
|
303
|
+
stepIteration?: number;
|
|
304
|
+
/** Total step-level iterations for this step (>=1). */
|
|
305
|
+
stepIterations?: number;
|
|
250
306
|
/** Variables written by this step (declarative extraction + scripts). */
|
|
251
307
|
extracted?: Record<string, string>;
|
|
252
308
|
/** Full scenario variable scope after this step completed. */
|
|
@@ -264,6 +320,21 @@ interface ScenarioEvent {
|
|
|
264
320
|
step?: ScenarioStepResult;
|
|
265
321
|
status?: ScenarioStatus;
|
|
266
322
|
message?: string;
|
|
323
|
+
/** Zero-based scenario-level iteration (definition.data row index). */
|
|
324
|
+
scenarioIteration?: number;
|
|
325
|
+
/** Total scenario-level iterations. */
|
|
326
|
+
scenarioIterations?: number;
|
|
327
|
+
/** Zero-based step-level iteration (step.request.data row index). */
|
|
328
|
+
stepIteration?: number;
|
|
329
|
+
/** Total step-level iterations for the current step. */
|
|
330
|
+
stepIterations?: number;
|
|
331
|
+
}
|
|
332
|
+
/** Iteration shape of a run: how many times the sequence and each step run. */
|
|
333
|
+
interface ScenarioIterations {
|
|
334
|
+
/** Number of full sequence runs (definition.data rows, defaults to 1). */
|
|
335
|
+
scenarioCount: number;
|
|
336
|
+
/** Number of executions per step index (step.request.data rows, >=1). */
|
|
337
|
+
stepCounts: number[];
|
|
267
338
|
}
|
|
268
339
|
/** Aggregate result of a full scenario execution. */
|
|
269
340
|
interface ScenarioReport {
|
|
@@ -274,6 +345,8 @@ interface ScenarioReport {
|
|
|
274
345
|
steps: ScenarioStepResult[];
|
|
275
346
|
/** Final shared variable scope. */
|
|
276
347
|
variables: Record<string, string>;
|
|
348
|
+
/** Declared execution grid (iteration counts) for report grouping. */
|
|
349
|
+
iterations?: ScenarioIterations;
|
|
277
350
|
config: CliConfig;
|
|
278
351
|
startedAt: string;
|
|
279
352
|
durationMs: number;
|
|
@@ -330,6 +403,7 @@ declare function buildImplicitAssertions(protocol: string, response: TestResult[
|
|
|
330
403
|
declare function runTests(config: CliConfig): Promise<TestReport>;
|
|
331
404
|
declare function collectOperations(spec: any, config: CliConfig): OperationInfo[];
|
|
332
405
|
interface StepExecutionOptions {
|
|
406
|
+
signal?: AbortSignal;
|
|
333
407
|
config: CliConfig;
|
|
334
408
|
spec: any;
|
|
335
409
|
/** Active variable scope injected as {{name}} values. */
|
|
@@ -369,7 +443,8 @@ declare function resolveSteps(definition: ScenarioDefinition, operations: Operat
|
|
|
369
443
|
/** Read declared extracts out of a normalized response. Missing values are omitted. */
|
|
370
444
|
declare function applyExtracts(extracts: ScenarioExtract[] | undefined, response: TestResult["response"]): Record<string, string>;
|
|
371
445
|
/**
|
|
372
|
-
* Execute a scenario in order with a shared variable scope
|
|
446
|
+
* Execute a scenario in order with a shared variable scope per scenario
|
|
447
|
+
* iteration. See the file header for data-driven iteration semantics.
|
|
373
448
|
*/
|
|
374
449
|
declare function runScenario(definition: ScenarioDefinition, options: RunScenarioOptions): Promise<ScenarioReport>;
|
|
375
450
|
|
|
@@ -403,10 +478,6 @@ interface CliArgs {
|
|
|
403
478
|
mcpArgs?: string;
|
|
404
479
|
mcpCwd?: string;
|
|
405
480
|
}
|
|
406
|
-
/**
|
|
407
|
-
* Resolve the final CliConfig from CLI args, an optional config file,
|
|
408
|
-
* environment variables, and defaults.
|
|
409
|
-
*/
|
|
410
481
|
declare function resolveConfig(args: CliArgs): CliConfig;
|
|
411
482
|
/**
|
|
412
483
|
* Check whether a spec path is a remote URL.
|
package/dist/index.d.ts
CHANGED
|
@@ -63,7 +63,8 @@ type ProtocolName = "http" | "sse" | "websocket" | "graphql" | "grpc" | "mcp";
|
|
|
63
63
|
type ReportFormat = "json" | "cli" | "html";
|
|
64
64
|
type TestStatus = "passed" | "failed" | "skipped" | "error";
|
|
65
65
|
interface AuthConfig {
|
|
66
|
-
type: "bearer" | "basic" | "apikey" | "none";
|
|
66
|
+
type: "bearer" | "basic" | "apikey" | "none" | "digest" | "oauth1" | "oauth2" | "jwt" | "hawk" | "aws4" | "ntlm" | "asap" | "edgegrid";
|
|
67
|
+
parameters?: Record<string, string | number | boolean>;
|
|
67
68
|
token?: string;
|
|
68
69
|
username?: string;
|
|
69
70
|
password?: string;
|
|
@@ -80,6 +81,8 @@ interface TlsConfig {
|
|
|
80
81
|
clientKey?: string;
|
|
81
82
|
/** Skip TLS certificate verification (insecure). */
|
|
82
83
|
strictSSL?: boolean;
|
|
84
|
+
pfx?: string;
|
|
85
|
+
passphrase?: string;
|
|
83
86
|
}
|
|
84
87
|
interface FilterConfig {
|
|
85
88
|
/** Only test these HTTP methods (e.g. ["get","post"]). */
|
|
@@ -92,6 +95,26 @@ interface FilterConfig {
|
|
|
92
95
|
operationIds?: string[];
|
|
93
96
|
}
|
|
94
97
|
interface CliConfig {
|
|
98
|
+
/** Programmatic HTTP runtime certificates matched against the resolved URL. */
|
|
99
|
+
httpRuntime?: {
|
|
100
|
+
certificates?: unknown;
|
|
101
|
+
fileResolver?: unknown;
|
|
102
|
+
};
|
|
103
|
+
signal?: AbortSignal;
|
|
104
|
+
/** Programmatic per-target transport resolution; never serialized over IPC. */
|
|
105
|
+
resolveTransport?: (target: {
|
|
106
|
+
protocol: string;
|
|
107
|
+
url?: string;
|
|
108
|
+
}) => Promise<Pick<CliConfig, "proxy" | "tls" | "httpRuntime">>;
|
|
109
|
+
requester?: {
|
|
110
|
+
followRedirects?: boolean;
|
|
111
|
+
maxRedirects?: number;
|
|
112
|
+
disableCookies?: boolean;
|
|
113
|
+
protocolVersion?: "http1" | "http2" | "auto";
|
|
114
|
+
};
|
|
115
|
+
maxEvents?: number;
|
|
116
|
+
maxStreamMs?: number;
|
|
117
|
+
maxResponseSize?: number;
|
|
95
118
|
/** Path to the OpenAPI 3.2 JSON file. */
|
|
96
119
|
specPath: string;
|
|
97
120
|
/** Override the server URL from the spec. */
|
|
@@ -204,6 +227,12 @@ interface ScenarioExtract {
|
|
|
204
227
|
/** Response header name when from="header". */
|
|
205
228
|
key?: string;
|
|
206
229
|
}
|
|
230
|
+
/**
|
|
231
|
+
* One row of data-driven values. Keys are variable names referenced as
|
|
232
|
+
* {{name}} in step values; values use the same string contract as scenario
|
|
233
|
+
* variables.
|
|
234
|
+
*/
|
|
235
|
+
type ScenarioDataRow = Record<string, string>;
|
|
207
236
|
/** Per-step request overrides and verification rules. */
|
|
208
237
|
interface ScenarioStepRequest {
|
|
209
238
|
/** Request values merged over the operation's sampled defaults. */
|
|
@@ -214,6 +243,12 @@ interface ScenarioStepRequest {
|
|
|
214
243
|
extract?: ScenarioExtract[];
|
|
215
244
|
/** Declarative assertions added on top of the operation's spec assertions. */
|
|
216
245
|
assertions?: DeclarativeAssertion[];
|
|
246
|
+
/**
|
|
247
|
+
* Step-level data rows. The step executes once per row; each row's values
|
|
248
|
+
* layer over the scenario scope only for that execution. Extracts from the
|
|
249
|
+
* last execution are written back into the scenario scope.
|
|
250
|
+
*/
|
|
251
|
+
data?: ScenarioDataRow[];
|
|
217
252
|
}
|
|
218
253
|
/** A single ordered operation invocation inside a scenario. */
|
|
219
254
|
interface ScenarioStep {
|
|
@@ -238,6 +273,19 @@ interface ScenarioDefinition {
|
|
|
238
273
|
variables?: Record<string, string>;
|
|
239
274
|
/** Stop at the first failed/error step. Default: true. */
|
|
240
275
|
stopOnFailure?: boolean;
|
|
276
|
+
/**
|
|
277
|
+
* Scenario-level data rows. The whole step sequence runs once per row, each
|
|
278
|
+
* time with a fresh variable scope seeded from the row; extracts never leak
|
|
279
|
+
* across iterations. An empty/absent array means a single iteration.
|
|
280
|
+
*/
|
|
281
|
+
data?: ScenarioDataRow[];
|
|
282
|
+
/**
|
|
283
|
+
* Declared number of full-sequence repetitions. The effective iteration
|
|
284
|
+
* count is max(iterations, data.length); iterations beyond the data rows
|
|
285
|
+
* run with an empty scope. Useful for "run this flow N times" requests that
|
|
286
|
+
* carry no row-specific values. Defaults to 1 when omitted.
|
|
287
|
+
*/
|
|
288
|
+
iterations?: number;
|
|
241
289
|
}
|
|
242
290
|
type ScenarioStatus = "passed" | "failed" | "error" | "cancelled";
|
|
243
291
|
/** Result of executing one scenario step. */
|
|
@@ -247,6 +295,14 @@ interface ScenarioStepResult extends TestResult {
|
|
|
247
295
|
/** Resolved operation reference ("METHOD /path"). */
|
|
248
296
|
ref: string;
|
|
249
297
|
name?: string;
|
|
298
|
+
/** Zero-based scenario-level iteration (definition.data row index). */
|
|
299
|
+
scenarioIteration?: number;
|
|
300
|
+
/** Total scenario-level iterations for this run. */
|
|
301
|
+
scenarioIterations?: number;
|
|
302
|
+
/** Zero-based step-level iteration (step.request.data row index). */
|
|
303
|
+
stepIteration?: number;
|
|
304
|
+
/** Total step-level iterations for this step (>=1). */
|
|
305
|
+
stepIterations?: number;
|
|
250
306
|
/** Variables written by this step (declarative extraction + scripts). */
|
|
251
307
|
extracted?: Record<string, string>;
|
|
252
308
|
/** Full scenario variable scope after this step completed. */
|
|
@@ -264,6 +320,21 @@ interface ScenarioEvent {
|
|
|
264
320
|
step?: ScenarioStepResult;
|
|
265
321
|
status?: ScenarioStatus;
|
|
266
322
|
message?: string;
|
|
323
|
+
/** Zero-based scenario-level iteration (definition.data row index). */
|
|
324
|
+
scenarioIteration?: number;
|
|
325
|
+
/** Total scenario-level iterations. */
|
|
326
|
+
scenarioIterations?: number;
|
|
327
|
+
/** Zero-based step-level iteration (step.request.data row index). */
|
|
328
|
+
stepIteration?: number;
|
|
329
|
+
/** Total step-level iterations for the current step. */
|
|
330
|
+
stepIterations?: number;
|
|
331
|
+
}
|
|
332
|
+
/** Iteration shape of a run: how many times the sequence and each step run. */
|
|
333
|
+
interface ScenarioIterations {
|
|
334
|
+
/** Number of full sequence runs (definition.data rows, defaults to 1). */
|
|
335
|
+
scenarioCount: number;
|
|
336
|
+
/** Number of executions per step index (step.request.data rows, >=1). */
|
|
337
|
+
stepCounts: number[];
|
|
267
338
|
}
|
|
268
339
|
/** Aggregate result of a full scenario execution. */
|
|
269
340
|
interface ScenarioReport {
|
|
@@ -274,6 +345,8 @@ interface ScenarioReport {
|
|
|
274
345
|
steps: ScenarioStepResult[];
|
|
275
346
|
/** Final shared variable scope. */
|
|
276
347
|
variables: Record<string, string>;
|
|
348
|
+
/** Declared execution grid (iteration counts) for report grouping. */
|
|
349
|
+
iterations?: ScenarioIterations;
|
|
277
350
|
config: CliConfig;
|
|
278
351
|
startedAt: string;
|
|
279
352
|
durationMs: number;
|
|
@@ -330,6 +403,7 @@ declare function buildImplicitAssertions(protocol: string, response: TestResult[
|
|
|
330
403
|
declare function runTests(config: CliConfig): Promise<TestReport>;
|
|
331
404
|
declare function collectOperations(spec: any, config: CliConfig): OperationInfo[];
|
|
332
405
|
interface StepExecutionOptions {
|
|
406
|
+
signal?: AbortSignal;
|
|
333
407
|
config: CliConfig;
|
|
334
408
|
spec: any;
|
|
335
409
|
/** Active variable scope injected as {{name}} values. */
|
|
@@ -369,7 +443,8 @@ declare function resolveSteps(definition: ScenarioDefinition, operations: Operat
|
|
|
369
443
|
/** Read declared extracts out of a normalized response. Missing values are omitted. */
|
|
370
444
|
declare function applyExtracts(extracts: ScenarioExtract[] | undefined, response: TestResult["response"]): Record<string, string>;
|
|
371
445
|
/**
|
|
372
|
-
* Execute a scenario in order with a shared variable scope
|
|
446
|
+
* Execute a scenario in order with a shared variable scope per scenario
|
|
447
|
+
* iteration. See the file header for data-driven iteration semantics.
|
|
373
448
|
*/
|
|
374
449
|
declare function runScenario(definition: ScenarioDefinition, options: RunScenarioOptions): Promise<ScenarioReport>;
|
|
375
450
|
|
|
@@ -403,10 +478,6 @@ interface CliArgs {
|
|
|
403
478
|
mcpArgs?: string;
|
|
404
479
|
mcpCwd?: string;
|
|
405
480
|
}
|
|
406
|
-
/**
|
|
407
|
-
* Resolve the final CliConfig from CLI args, an optional config file,
|
|
408
|
-
* environment variables, and defaults.
|
|
409
|
-
*/
|
|
410
481
|
declare function resolveConfig(args: CliArgs): CliConfig;
|
|
411
482
|
/**
|
|
412
483
|
* Check whether a spec path is a remote URL.
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{DEFAULT_CONFIG as e,buildImplicitAssertions as t,buildSummary as
|
|
1
|
+
import{DEFAULT_CONFIG as e,buildImplicitAssertions as t,buildSummary as n,collectOperations as s,executeStep as r,extractAssertions as a,extractPostmanScripts as o,formatAssertionError as i,generateCliReport as p,generateHtmlReport as c,generateJsonReport as l,isRemoteSpec as u,loadSpec as f,package_default as d,printCliReport as h,resolveConfig as g,resolveJsonPath as m,runDeclarativeAssertions as b,runTests as v,validateRunConfig as I}from"./chunk-LTG44T66.js";import{createClient as y}from"@powerduck/openapi-request";var $=d.version;function S(e){if(e.method&&e.path)return`${e.method.toUpperCase()} ${e.path}`;const t=(e.ref??"").trim(),n=t.indexOf(" ");return n>0?`${t.slice(0,n).toUpperCase()} ${t.slice(n+1).trim()}`:t}function q(e,t){const n=new Map,s=new Map;for(const e of t)n.set(`${e.method.toUpperCase()} ${e.path}`,e),s.set(e.operationId,e);const r=[],a=[];if(e.steps.forEach((e,t)=>{const o=S(e),i=n.get(o)??(o?s.get(o):void 0);i?r.push({step:e,op:i,ref:`${i.method.toUpperCase()} ${i.path}`}):a.push(`step ${t} ("${e.name??o}") -> ${o||"<empty ref>"}`)}),a.length)throw new Error(`Scenario references operations not present in the spec: ${a.join("; ")}`);return r}function w(e,t){if(!e)return;const n=t.toLowerCase();for(const[t,s]of Object.entries(e))if(t.toLowerCase()===n)return s}function x(e){if(null==e)return"";if("string"==typeof e)return e;if("number"==typeof e||"boolean"==typeof e)return String(e);try{return JSON.stringify(e)}catch{return String(e)}}function k(e,t){const n={};if(!e?.length)return n;for(const s of e){let e;e="status"===s.from?t?.status:"header"===s.from?s.key?w(t?.headers,s.key):void 0:m(t?.body,s.path??"$"),null!=e&&(n[s.name]=x(e))}return n}function O(e,t,n,s,r){return{index:e,ref:t,name:n.name,operationId:t,path:t.includes(" ")?t.slice(t.indexOf(" ")+1):t,method:t.includes(" ")?t.slice(0,t.indexOf(" ")):"",protocol:"",status:"skipped",durationMs:0,timestamp:(new Date).toISOString(),notRun:!0,notRunReason:s,...r??{}}}async function C(e,t){const{config:a,onEvent:o,signal:i}=t;I(a);const p=Date.now(),c=new Date(p).toISOString(),l=e=>{try{o?.(e)}catch{}},u=t.spec??await f(a),d=q(e,s(u,{...a,filter:void 0})),h=y(),g=e.data&&e.data.length?e.data:[],m=Math.trunc(Number(e.iterations)),b=Number.isFinite(m)&&m>0?Math.min(m,1e3):1,v=Math.max(b,g.length),S=Array.from({length:v},(e,t)=>g[t]),w=d.map(({step:e})=>e.request?.data&&e.request.data.length?e.request.data.length:1),x={scenarioCount:S.length,stepCounts:w},C=!1!==e.stopOnFailure,M=[];let j="passed",U=!1,A=!1,D=null,E="failed",L={...a.variables??{},...e.variables??{}};l({type:"scenario:start",scenario:e.name,status:j,scenarioIterations:S.length});e:for(let t=0;t<S.length;t++){L={...a.variables??{},...e.variables??{},...S[t]??{}};for(let n=0;n<d.length;n++){const{step:s,op:o,ref:p}=d[n],c=s.request?.data&&s.request.data.length?s.request.data:[void 0],f={scenarioIteration:t,scenarioIterations:S.length,stepIterations:c.length};if(i?.aborted){j="cancelled",A=!0,U=!0,D={s:t,i:n,t:0};break e}if(s.skip)for(let t=0;t<c.length;t++){const r=O(n,p,s,"Skipped by plan",{...f,stepIteration:t});M.push(r),l({type:"step:skip",scenario:e.name,stepIndex:n,step:r,...f,stepIteration:t})}else for(let d=0;d<c.length;d++){if(i?.aborted){j="cancelled",A=!0,U=!0,D={s:t,i:n,t:d};break e}l({type:"step:start",scenario:e.name,stepIndex:n,...f,stepIteration:d});const g={...L,...c[d]??{}},m=await r(h,o,{config:a,spec:u,signal:i,variables:g,values:s.request?.values,serverUrl:s.request?.serverUrl,extraAssertions:s.request?.assertions});i?.aborted&&(A=!0);const b={};for(const[e,t]of Object.entries(m.env))g[e]!==t&&(b[e]=t);const v=k(s.request?.extract,m.response),I={...b,...v},y=d===c.length-1;y&&Object.assign(L,m.env,v);const $={...m.result,index:n,ref:p,name:s.name,...f,stepIteration:d,extracted:Object.keys(I).length?I:void 0,variablesAfter:y?{...L}:{...L,...c[d]??{},...I}};if(M.push($),l({type:"step:finish",scenario:e.name,stepIndex:n,step:$,...f,stepIteration:d}),"error"===$.status?j="error":"failed"===$.status&&"error"!==j&&(j="failed"),("failed"===$.status||"error"===$.status)&&C){U=!0,E=$.status,D={s:t,i:n,t:d};break e}}}}if(U&&D){const t=(e,t)=>A?"Scenario cancelled":e===D.s&&t===D.i?`Stopped after ${E} iteration ${D.t+1}`:e===D.s?`Stopped after ${E} step ${D.i+1}`:`Stopped after ${E} run ${D.s+1}`;for(let n=D.s;n<S.length;n++)for(let s=n===D.s?D.i:0;s<d.length;s++){const r=d[s],a=r.step.request?.data&&r.step.request.data.length?r.step.request.data.length:1;for(let o=n===D.s&&s===D.i?D.t+1:0;o<a;o++){const i={scenarioIteration:n,scenarioIterations:S.length,stepIteration:o,stepIterations:a},p=O(s,r.ref,r.step,t(n,s),i);M.push(p),l({type:"step:skip",scenario:e.name,stepIndex:s,step:p,...i})}}}(A||i?.aborted&&"error"!==j&&"failed"!==j)&&(j="cancelled");const N=Date.now()-p,R="cancelled"===j?"scenario:cancel":"scenario:finish",F={name:e.name,description:e.description,status:j,summary:n(M,N),steps:M,variables:L,iterations:x,config:a,startedAt:c,durationMs:N,generatedAt:(new Date).toISOString(),version:$};return l({type:R,scenario:e.name,status:j,message:U?"halted":void 0}),F}export{e as DEFAULT_CONFIG,k as applyExtracts,t as buildImplicitAssertions,n as buildSummary,s as collectOperations,r as executeStep,a as extractAssertions,o as extractPostmanScripts,i as formatAssertionError,p as generateCliReport,c as generateHtmlReport,l as generateJsonReport,u as isRemoteSpec,f as loadSpec,h as printCliReport,g as resolveConfig,m as resolveJsonPath,q as resolveSteps,b as runDeclarativeAssertions,C as runScenario,v as runTests,S as stepRef};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@powerduck/openapi-cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.13",
|
|
4
4
|
"description": "CI-ready CLI for batch-testing OpenAPI documents across HTTP, SSE, WebSocket, GraphQL, gRPC and MCP",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -51,15 +51,16 @@
|
|
|
51
51
|
"license": "MIT",
|
|
52
52
|
"dependencies": {
|
|
53
53
|
"@powerduck/openapi-parser": "^0.3.5",
|
|
54
|
-
"@powerduck/openapi-request": "^0.2.
|
|
55
|
-
"commander": "^
|
|
54
|
+
"@powerduck/openapi-request": "^0.2.12",
|
|
55
|
+
"commander": "^12.1.0",
|
|
56
|
+
"https-proxy-agent": "^5.0.1"
|
|
56
57
|
},
|
|
57
58
|
"devDependencies": {
|
|
58
59
|
"@types/node": "^20.14.0",
|
|
60
|
+
"@vitest/coverage-v8": "^3.2.7",
|
|
59
61
|
"terser": "^5.51.2",
|
|
60
62
|
"tsup": "^8.0.0",
|
|
61
63
|
"typescript": "^5.5.0",
|
|
62
|
-
"@vitest/coverage-v8": "^3.2.7",
|
|
63
64
|
"vitest": "^3.2.7"
|
|
64
65
|
},
|
|
65
66
|
"repository": {
|
package/dist/chunk-674PEZ5S.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import e from"fs";import t from"path";import r from"http";import s from"https";import{dereference as n}from"@powerduck/openapi-parser";var o={outputDir:"./openapi-cli-report",formats:["json","cli","html"],concurrency:5,timeout:3e4,failOnError:!0,grpcReflection:!0,mcpTransport:"streamable-http"};function a(r){const s=r.config?function(r){const s=t.resolve(r);if(!e.existsSync(s))throw new Error(`Config file not found: ${s}`);const n=e.readFileSync(s,"utf-8");try{return JSON.parse(n)}catch(e){throw new Error(`Invalid config file (must be JSON): ${e.message}`)}}(r.config):{},n={specPath:r.spec??s.specPath??"",serverUrl:r.server??s.serverUrl,outputDir:r.output??s.outputDir??o.outputDir,formats:i(r.format??s.formats),filter:c(r,s),concurrency:r.concurrency??s.concurrency??o.concurrency,timeout:r.timeout??s.timeout??o.timeout,proxy:r.proxy??s.proxy,tls:{caCert:r.ca??s.tls?.caCert,clientCert:r.cert??s.tls?.clientCert,clientKey:r.key??s.tls?.clientKey,strictSSL:void 0!==r.insecure?!r.insecure:s.tls?.strictSSL},headers:p(r.header,s.headers),auth:d(r,s),variables:l(r.variable,s.variables),failOnError:r.failOnError??s.failOnError??o.failOnError,envFile:r.env??s.envFile,grpcReflection:r.grpcReflection??s.grpcReflection??o.grpcReflection,grpcProtoPaths:r.grpcProto??s.grpcProtoPaths,mcpTransport:r.mcpTransport??s.mcpTransport??o.mcpTransport,mcpCommand:r.mcpCommand??s.mcpCommand,mcpArgs:r.mcpArgs?r.mcpArgs.split(/[\s,]+/).filter(Boolean):s.mcpArgs,mcpCwd:r.mcpCwd??s.mcpCwd};if(n.envFile&&function(r){const s=t.resolve(r);if(!e.existsSync(s))return;const n=e.readFileSync(s,"utf-8").split("\n");for(const e of n){const t=e.trim();if(!t||t.startsWith("#"))continue;const r=t.indexOf("=");if(r>0){const e=t.slice(0,r).trim(),s=t.slice(r+1).trim().replace(/^["']|["']$/g,"");e in process.env||(process.env[e]=s)}}}(n.envFile),function(e){const t=e=>"string"!=typeof e?e:e.replace(/\$\{([^}]+)\}/g,(e,t)=>process.env[t]??"");if(e.serverUrl=t(e.serverUrl),e.proxy=t(e.proxy),e.headers)for(const[r,s]of Object.entries(e.headers))e.headers[r]=t(s);e.auth?.token&&(e.auth.token=t(e.auth.token));if(e.variables)for(const[r,s]of Object.entries(e.variables))e.variables[r]=t(s)}(n),!n.specPath)throw new Error("No OpenAPI spec path provided. Use --spec <path> or set specPath in config.");if(n.proxy)try{new URL(n.proxy)}catch{throw new Error(`Invalid proxy URL: ${n.proxy}`)}return n}function i(e){return Array.isArray(e)?e:"string"==typeof e?e.split(",").map(e=>e.trim().toLowerCase()).filter(Boolean):o.formats}function c(e,t){const r=e.method?e.method.split(",").map(e=>e.trim().toLowerCase()):t.filter?.methods,s=e.path?e.path.split(",").map(e=>e.trim()):t.filter?.paths,n=e.tag?e.tag.split(",").map(e=>e.trim()):t.filter?.tags,o=e.operationId?e.operationId.split(",").map(e=>e.trim()):t.filter?.operationIds;if(r||s||n||o)return{methods:r,paths:s,tags:n,operationIds:o}}function p(e,t){const r={...t??{}};for(const t of e??[]){const e=t.indexOf(":");if(e>0){const s=t.slice(0,e).trim(),n=t.slice(e+1).trim();s&&(r[s]=n)}}return Object.keys(r).length?r:void 0}function d(e,t){return e.bearer?{type:"bearer",token:e.bearer}:t.auth}function l(e,t){const r={...t??{}};for(const t of e??[]){const e=t.indexOf("=");e>0&&(r[t.slice(0,e).trim()]=t.slice(e+1).trim())}return Object.keys(r).length?r:void 0}function u(e){return/^https?:\/\//i.test(e.trim())}async function f(a){const i=a.specPath.trim();let c;if(u(i))c=await function(e,t){let n=0;return new Promise((o,a)=>{const i=c=>{const p=("https:"===new URL(c).protocol?s:r).get(c,{timeout:t,headers:{Accept:"application/json, text/plain, */*","User-Agent":"@powerduck/openapi-cli"}},t=>{if(t.statusCode&&t.statusCode>=300&&t.statusCode<400&&t.headers.location){if(++n>5)return t.resume(),void a(new Error(`Too many redirects (>5) while fetching spec: ${e}`));const r=new URL(t.headers.location,c).toString();return t.resume(),void i(r)}if(200!==t.statusCode)return t.resume(),void a(new Error(`Failed to fetch spec from ${c}: HTTP ${t.statusCode} ${t.statusMessage??""}`.trim()));const r=[];t.on("data",e=>r.push(e)),t.on("end",()=>o(Buffer.concat(r).toString("utf-8"))),t.on("error",e=>a(new Error(`Error reading spec response: ${e.message}`)))});p.on("timeout",()=>{p.destroy(new Error(`Timed out after ${t}ms while fetching spec: ${e}`))}),p.on("error",t=>a(new Error(`Network error fetching spec ${e}: ${t.message}`)))};i(e)})}(i,a.timeout??o.timeout);else{const r=t.resolve(i);if(!e.existsSync(r))throw new Error(`OpenAPI spec not found: ${r}`);if(!e.statSync(r).isFile())throw new Error(`OpenAPI spec path is not a file: ${r}`);c=e.readFileSync(r,"utf-8")}const p=function(e,t){let r;try{r=JSON.parse(e)}catch(e){throw new Error(`Spec from ${t} is not valid JSON: ${e.message}`)}if(!r||"object"!=typeof r||Array.isArray(r))throw new Error(`Spec from ${t} is not a JSON object`);if(!r.paths||"object"!=typeof r.paths)throw new Error(`Spec from ${t} does not contain a valid 'paths' object`);return r}(c,i);return async function(e){try{const t=await n(e);return t.errors&&t.errors.length>0?e:t.schema??t.specification??e}catch(t){return e}}(p)}function m(e,t){return e.map(e=>{try{const r=function(e,t){switch(e.assert){case"status":if(void 0===e.value)throw new Error("status assertion requires 'value'");return t.status===e.value;case"header":{if(!e.key)throw new Error("header assertion requires 'key'");const r=function(e,t){const r=t.toLowerCase();for(const[t,s]of Object.entries(e))if(t.toLowerCase()===r)return s;return}(t.headers??{},e.key);return void 0!==r&&(void 0===e.contains||r.toLowerCase().includes(e.contains.toLowerCase()))}case"bodyContains":if(void 0===e.contains)throw new Error("bodyContains assertion requires 'contains'");return(t.bodyText??JSON.stringify(t.body??"")).includes(e.contains);case"bodyEquals":if(void 0===e.body)throw new Error("bodyEquals assertion requires 'body'");return(t.bodyText??JSON.stringify(t.body??""))===e.body;case"jsonPath":{if(!e.path)throw new Error("jsonPath assertion requires 'path'");const r=h(t.body,e.path);return void 0!==e.exists?e.exists?void 0!==r:void 0===r:void 0!==e.equals?v(r,e.equals):void 0!==r}case"responseTime":if(void 0===e.max)throw new Error("responseTime assertion requires 'max'");return t.durationMs<=e.max;default:throw new Error(`Unknown assertion type: ${e.assert}`)}}(e,t);return{name:e.name,passed:r}}catch(t){return{name:e.name,passed:!1,error:t.message}}})}function h(e,t){const r=t.replace(/^\$\.?/,"");if(!r)return e;const s=r.split(/\.|\[(\d+)\]/).filter(e=>void 0!==e&&""!==e);let n=e;for(const e of s){if(null==n)return;n=/^\d+$/.test(e)?n[Number(e)]:n[e]}return n}function v(e,t){if(e===t)return!0;if(typeof e!=typeof t)return!1;if(null===e||null===t)return e===t;if("object"!=typeof e)return e===t;if(Array.isArray(e)!==Array.isArray(t))return!1;if(Array.isArray(e)&&Array.isArray(t))return e.length===t.length&&e.every((e,r)=>v(e,t[r]));const r=Object.keys(e),s=Object.keys(t);return r.length===s.length&&r.every(r=>v(e[r],t[r]))}function g(e,t){const r=e?.["x-tests"],s=t?.["x-tests"];return[...Array.isArray(s)?s:[],...Array.isArray(r)?r:[]].filter(e=>e&&"object"==typeof e&&e.assert&&e.name)}function b(e){const t=e?.["x-postman-scripts"];if(t&&"object"==typeof t&&"string"==typeof t.test)return t.test}import{createClient as y}from"@powerduck/openapi-request";import x from"fs";function w(e){if(!e)return;if("string"==typeof e)return e;if(e instanceof Error)return e.message;const t=e;if("string"==typeof t.message)return t.message;if("string"==typeof t.msg)return t.msg;try{return JSON.stringify(e)}catch{return String(e)}}function $(e,t){const r=[];if("http"===e||"sse"===e){const e=t?.status,s="number"==typeof e&&e>=200&&e<300;r.push({name:"HTTP status is 2xx (implicit)",passed:s,error:s?void 0:`expected 2xx, got ${e??"no status"}`})}else if("graphql"===e){const e=t?.status,s="number"==typeof e&&e>=200&&e<300,n=t?.body,o=Array.isArray(n?.errors)&&n.errors.length>0,a=s&&!o;let i;s?o&&(i=`GraphQL response contains ${n.errors.length} error(s)`):i=`expected HTTP 2xx, got ${e??"no status"}`,r.push({name:"GraphQL success (implicit)",passed:a,error:i})}else if("grpc"===e){const e=t?.statusCode??t?.code,s=t?.statusName??t?.status,n=0===e;r.push({name:"gRPC status is OK (implicit)",passed:n,error:n?void 0:`expected code 0 (OK), got ${e??"unknown"}${s?` (${s})`:""}`})}else if("mcp"===e){const e=t?.body??{},s=void 0!==e.result,n=void 0!==e.error||void 0!==t?.error,o=s&&!n;let a;n?a="MCP JSON-RPC response contains an error":s||(a="MCP JSON-RPC response missing 'result'"),r.push({name:"MCP JSON-RPC success (implicit)",passed:o,error:a})}else if("websocket"===e){const e=t?.events??[],s=e.some(e=>"open"===e.kind||"open"===e.state),n=e.some(e=>"error"===e.kind||"error"===e.state),o=s&&!n;r.push({name:"WebSocket connected (implicit)",passed:o,error:o?void 0:n?"WebSocket connection errored":"WebSocket did not reach open state"})}return r}async function k(e){const t=await f(e),r=y(),s=S(t,e),n=Date.now(),o=[],a=[...s],i=Math.min(e.concurrency??5,a.length),c=[];for(let s=0;s<i;s++)c.push((async()=>{for(;a.length>0;){const s=a.shift();if(!s)break;const n=await E(r,s,e,t);o.push(n)}})());await Promise.all(c);const p=Date.now()-n;return{summary:j(o,p),results:o.sort((e,t)=>e.path.localeCompare(t.path)||e.method.localeCompare(t.method)),config:e,generatedAt:(new Date).toISOString(),version:"0.1.0"}}function S(e,t){const r=[],s=["get","post","put","delete","patch","head","options"];for(const[n,o]of Object.entries(e.paths??{})){const e=o;for(const o of s){const s=e[o];if(!s||"object"!=typeof s)continue;const a={path:n,method:o,operation:s,pathItem:e,operationId:s.operationId??`${o.toUpperCase()} ${n}`,protocol:O(s,e)};C(a,t.filter)&&r.push(a)}}return r}function O(e,t){const r=e["x-protocol"]??t?.["x-protocol"];if(r)return String(r).toLowerCase();const s=e.responses??{};for(const e of Object.values(s)){if((e?.content??{})["text/event-stream"])return"sse"}return e["x-response-stream"]?"sse":"http"}function C(e,t){if(!t)return!0;if(t.methods?.length&&!t.methods.includes(e.method.toLowerCase()))return!1;if(t.operationIds?.length&&!t.operationIds.includes(e.operationId))return!1;if(t.tags?.length){const r=e.operation.tags??[];if(!t.tags.some(e=>r.includes(e)))return!1}if(t.paths?.length){if(!t.paths.some(t=>{try{return new RegExp(t).test(e.path)}catch{return e.path.includes(t)}}))return!1}return!0}async function E(e,t,r,s){return(await T(e,t,{config:r,spec:s,variables:r.variables})).result}async function T(e,t,r){const{config:s,spec:n}=r,o=Date.now(),a={operationId:t.operationId,path:t.path,method:t.method.toUpperCase(),protocol:t.protocol};try{const i=[...g(t.operation,t.pathItem),...r.extraAssertions??[]],c=b(t.operation),p=[];let d,l,u={};switch(t.protocol){case"grpc":{const n=await async function(e,t,r,s){const n=t.operation["x-grpc"]??{},o=n.address??(s??r.serverUrl)?.replace(/^https?:\/\//,"");if(!o)throw new Error("gRPC operation requires x-grpc.address or --server");const a=e.connect({kind:"grpc",address:o,service:n.service,method:n.method??t.operationId,reflection:r.grpcReflection,protoPaths:r.grpcProtoPaths,metadata:r.headers});await a.open();const i=a.kind,c=function(e){const t=e["x-grpc"]??{};if(t.sample&&"object"==typeof t.sample)return t.sample;const r=e.requestBody?.content?.["application/json"]?.schema;if(r?.properties){const e={};for(const[t,s]of Object.entries(r.properties))e[t]=s.default??P(s.type);return e}return{}}(t.operation);"unary"===i||"server_streaming"===i?await a.send(c):(await a.send(c),await a.close());const p=a.events??[],d=p.find(e=>"status"===e.kind),l=p.filter(e=>"data"===e.kind&&"inbound"===e.direction);"unary"!==i&&"server_streaming"!==i||await a.close();const u={status:d?.meta?.code??0,statusText:d?.meta?.statusName,body:1===l.length?l[0].data:l.map(e=>e.data),events:p.map(e=>({kind:e.kind,direction:e.direction,data:e.data,meta:e.meta}))},f=n.method??t.operationId;return{response:u,request:{method:f,target:n.service?`${n.service}/${f}`:f,url:o,...Object.keys(c).length?{body:c}:{},...r.headers?{headers:r.headers}:{}}}}(e,t,s,r.serverUrl);d=n.response,l=n.request;break}case"websocket":{const n=await async function(e,t,r,s){const n=t.operation["x-ws"]??{},o=n.url??(s??r.serverUrl)?.replace(/^http/,"ws");if(!o)throw new Error("WebSocket operation requires x-ws.url or --server");const a=e.connect({kind:"websocket",url:o,subprotocols:n.subprotocols,headers:r.headers});await a.open();const i=n.send??[];for(const e of i.slice(0,3))await a.send(e);await new Promise(e=>setTimeout(e,500)),await a.close();const c=a.events??[],p=c.filter(e=>"in"===e.direction&&"open"!==e.kind);return{response:{status:101,statusText:"Switching Protocols",body:p.map(e=>e.data),events:c.map(e=>({kind:e.kind,direction:e.direction,data:e.data})),streaming:!0},request:{method:"WS",url:o,...i.length?{body:i.slice(0,3)}:{},...r.headers?{headers:r.headers}:{}}}}(e,t,s,r.serverUrl);d=n.response,l=n.request;break}case"mcp":{const n=await async function(e,t,r,s){const n=t.operation["x-mcp"]??{},o=n.endpoint??s??r.serverUrl,a=n.method??"tools/call",i=n.name,c=n.arguments??{};if("stdio"===r.mcpTransport){if(!r.mcpCommand)throw new Error("MCP stdio requires --mcp-command");const t=e.connect({kind:"mcp",transport:"stdio",command:r.mcpCommand,args:r.mcpArgs,cwd:r.mcpCwd,timeoutMs:r.timeout});await t.open();const s=await t.request(a,{name:i,arguments:c});return await t.close(),{response:{status:200,statusText:"OK",body:s},request:{method:a,target:i,body:{name:i,arguments:c}}}}const p={openapi:"3.2.0",info:{title:"mcp",version:"1.0.0"},paths:{"/mcp":{post:{operationId:t.operationId,"x-protocol":"mcp","x-mcp":{endpoint:o,method:a,name:i,arguments:c},responses:{200:{description:"ok"}}}}}},d=await e.send({spec:p,target:{path:"/mcp",method:"post"},timeout:r.timeout,mcp:r.headers?{headers:r.headers}:void 0}),l=d.request;return{response:{status:d.response?.status,statusText:d.response?.statusText,body:d.response?.body},request:{...l?{url:l.url,headers:l.headers}:{},method:a,target:i,body:{name:i,arguments:c}}}}(e,t,s,r.serverUrl);d=n.response,l=n.request;break}default:{const o=await async function(e,t,r,s,n){const o={};if(r.proxy)try{const e=new URL(r.proxy);o.proxies=[{match:"*://*/*",host:e.hostname,port:e.port?Number(e.port):"https:"===e.protocol?443:80,tunnel:!0}]}catch{throw new Error(`Invalid proxy URL: ${r.proxy}`)}const a=r.tls;if(a){const e={...o.requester??{}};!1===a.strictSSL&&(e.strictSSL=!1),a.caCert&&(e.ca=x.readFileSync(a.caCert)),a.clientCert&&(e.cert=x.readFileSync(a.clientCert)),a.clientKey&&(e.key=x.readFileSync(a.clientKey)),Object.keys(e).length&&(o.requester=e)}const i=n.values,c={...r.headers??{},...i?.header??{}},p={...r.headers?{header:r.headers}:{},...i??{},...Object.keys(c).length?{header:c}:{}},d=await e.send({spec:s,target:{path:t.path,method:t.method},serverUrl:n.serverUrl??r.serverUrl,values:Object.keys(p).length?p:void 0,auth:r.auth,variables:n.variables,timeout:r.timeout,runner:Object.keys(o).length?o:void 0,scripts:n.postmanTest?{test:{exec:n.postmanTest},fromSpecExtensions:!1}:void 0,onAssertion:e=>{n.postmanAssertions.push({name:e.name??"assertion",passed:e.passed,error:w(e.error)})},maxStreamMs:r.timeout,maxEvents:50}),l={},u=d.scripts;if(u)for(const e of[...u.prerequest??[],...u.test??[]])e.environment&&Object.assign(l,e.environment);const f=d.request;return{response:{status:d.response?.status,statusText:d.response?.statusText,contentType:d.response?.contentType,sizeBytes:d.response?.sizeBytes,body:d.response?.body,text:d.response?.text,headers:d.response?.headers,events:d.response?.events,streaming:d.response?.streaming},request:f?{method:f.method,url:f.url,headers:f.headers,...void 0!==f.body?{body:f.body}:{}}:void 0,env:l}}(e,t,s,n,{postmanTest:c,postmanAssertions:p,variables:r.variables,values:r.values,serverUrl:r.serverUrl});d=o.response,l=o.request,u=o.env;break}}const f=Date.now()-o,h=i.length>0||p.length>0?[]:$(t.protocol,d),v=i.length?m(i,{status:d?.status,headers:d?d.headers:void 0,body:d?.body,bodyText:d?d.text:void 0,durationMs:f}):[],y=new Set,k=p.filter(e=>{const t=`${e.name}|${e.passed}|${e.error??""}`;return!y.has(t)&&(y.add(t),!0)}),S=[...v,...k,...h],O=S.some(e=>!e.passed);return{result:{...a,status:O?"failed":"passed",durationMs:f,request:l,response:d,assertions:S.length?S:void 0,timestamp:(new Date).toISOString()},response:d,request:l,env:u}}catch(e){const n=Date.now()-o,i=r.values,c={...s.headers??{},...i?.header??{}},p=Object.fromEntries(Object.entries(c).map(([e,t])=>[e,null==t?"":String(t)])),d={method:t.method.toUpperCase(),url:r.serverUrl??s.serverUrl,...Object.keys(p).length?{headers:p}:{},...void 0!==i?.body?{body:i.body}:{}};return{result:{...a,status:"error",durationMs:n,request:d,error:e.message??String(e),timestamp:(new Date).toISOString()},response:void 0,request:d,env:{}}}}function P(e){switch(e){case"string":default:return"";case"number":case"integer":return 0;case"boolean":return!1;case"array":return[];case"object":return{}}}function j(e,t){const r=e.length,s=e.filter(e=>"passed"===e.status).length;return{total:r,passed:s,failed:e.filter(e=>"failed"===e.status).length,errors:e.filter(e=>"error"===e.status).length,skipped:e.filter(e=>"skipped"===e.status).length,durationMs:t,passRate:r>0?Math.round(s/r*1e3)/10:0}}import A from"fs";import q from"path";function R(e,t){const r=q.resolve(t);A.mkdirSync(r,{recursive:!0});const s=q.join(r,"report.json");return A.writeFileSync(s,JSON.stringify(e,null,2),"utf-8"),s}var I="[0m",U="[1m",z="[2m",L="[32m",M="[31m",F="[33m",N="[36m",D="[90m",J="[42m",B="[41m",K="[43m";function H(e){if(null==e||!Number.isFinite(e))return`${z}--ms${I}`;return`${e>500?`${U}${F}`:U}${e}${I}${z}ms${I}`}function W(e){const t=[],{summary:r,results:s}=e;t.push(""),t.push(`${U}${N}╔══════════════════════════════════════════════════════╗${I}`),t.push(`${U}${N}║ Powerduck Test Report ║${I}`),t.push(`${U}${N}╚══════════════════════════════════════════════════════╝${I}`),t.push("");const n=r.passRate>=80?L:r.passRate>=50?F:M;t.push(`${U} Total:${I} ${r.total} ${L}Passed:${I} ${r.passed} ${M}Failed:${I} ${r.failed} ${F}Errors:${I} ${r.errors} ${n}Rate:${I} ${r.passRate}% ${z}Time:${I} ${H(r.durationMs)}`),t.push("");const o=r.total>0?Math.round(r.passed/r.total*50):0,a=r.total>0?Math.round(r.failed/r.total*50):0,i=r.total>0?Math.round(r.errors/r.total*50):0,c=L+"█".repeat(o)+M+"█".repeat(a)+F+"█".repeat(i)+D+"░".repeat(Math.max(0,50-o-a-i))+I;t.push(` [${c}]`),t.push("");for(const e of s)t.push(G(e));return t.push(""),r.failed>0||r.errors>0?t.push(`${U}${M} ✗ ${r.failed+r.errors} test(s) did not pass${I}`):t.push(`${U}${L} ✓ All ${r.total} test(s) passed${I}`),t.push(` ${z}Generated: ${e.generatedAt}${I}`),t.push(""),t.join("\n")}function G(e){const t="passed"===e.status?`${L}✓${I}`:"failed"===e.status?`${M}✗${I}`:"error"===e.status?`${F}!${I}`:`${D}-${I}`,r="passed"===e.status?`${J}${U} PASS ${I}`:"failed"===e.status?`${B}${U} FAIL ${I}`:"error"===e.status?`${K}${U} ERROR ${I}`:`${D} SKIP ${I}`,s=e.protocol?` ${N}[${e.protocol}]${I}`:"",n=null!=e.response?.status?` ${z}${e.response.status}${I}`:"";let o=` ${t} ${r} ${U}${e.method}${I} ${e.path}${s}${n} ${H(e.durationMs)}`;if(e.error&&(o+=`\n ${M}${e.error}${I}`),e.assertions?.length)for(const t of e.assertions){o+=`\n ${t.passed?`${L} ✓${I}`:`${M} ✗${I}`} ${t.name}${t.error?` — ${M}${t.error}${I}`:""}`}return o}function Q(e){const t=W(e);return console.log(t),t}import _ from"fs";import Y from"path";function V(e,t){const r=Y.resolve(t);_.mkdirSync(r,{recursive:!0});const s=Y.join(r,"report.html");return _.writeFileSync(s,function(e){const{summary:t,results:r}=e,s=JSON.stringify({summary:t,results:r});return`<!DOCTYPE html>\n<html lang="en">\n<head>\n<meta charset="UTF-8">\n<meta name="viewport" content="width=device-width, initial-scale=1.0">\n<title>Test Report — Powerduck</title>\n<style>\n *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}\n :root{\n --bg:#ffffff;--fg:#0f172a;--muted:#64748b;--border:#e2e8f0;\n --bg-subtle:#f8fafc;--bg-hover:#f1f5f9;\n --green:#16a34a;--green-bg:#f0fdf4;--green-border:#bbf7d0;\n --red:#dc2626;--red-bg:#fef2f2;--red-border:#fecaca;\n --amber:#d97706;--amber-bg:#fffbeb;--amber-border:#fde68a;\n --blue:#2563eb;--mono:ui-monospace,SFMono-Regular,'SF Mono',Menlo,Consolas,monospace;\n --sans:ui-sans-serif,system-ui,-apple-system,'Segoe UI',Roboto,Inter,sans-serif;\n }\n @media (prefers-color-scheme:dark){\n :root{\n --bg:#0f172a;--fg:#f1f5f9;--muted:#94a3b8;--border:#1e293b;\n --bg-subtle:#1e293b;--bg-hover:#334155;\n --green:#4ade80;--green-bg:rgba(74,222,128,.1);--green-border:rgba(74,222,128,.25);\n --red:#f87171;--red-bg:rgba(248,113,113,.1);--red-border:rgba(248,113,113,.25);\n --amber:#fbbf24;--amber-bg:rgba(251,191,36,.1);--amber-border:rgba(251,191,36,.25);\n --blue:#60a5fa;\n }\n }\n body{font-family:var(--sans);background:var(--bg);color:var(--fg);line-height:1.5;font-size:14px;-webkit-font-smoothing:antialiased}\n .wrap{max-width:960px;margin:0 auto;padding:32px 24px 64px}\n\n /* Header */\n .hdr{display:flex;align-items:baseline;justify-content:space-between;margin-bottom:24px;padding-bottom:16px;border-bottom:1px solid var(--border)}\n .hdr h1{font-size:18px;font-weight:600;letter-spacing:-.01em}\n .hdr .meta{font-size:12px;color:var(--muted);font-family:var(--mono)}\n\n /* Summary strip */\n .strip{display:grid;grid-template-columns:repeat(6,1fr);gap:0;border:1px solid var(--border);border-radius:8px;overflow:hidden;margin-bottom:24px}\n .stat{padding:14px 16px;border-right:1px solid var(--border);text-align:center}\n .stat:last-child{border-right:none}\n .stat .v{font-size:22px;font-weight:700;letter-spacing:-.02em;line-height:1.2}\n .stat .l{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;margin-top:2px}\n .stat.total .v{color:var(--fg)}\n .stat.pass .v{color:var(--green)}\n .stat.fail .v{color:var(--red)}\n .stat.err .v{color:var(--amber)}\n .stat.rate .v{color:var(--blue)}\n .stat.time .v{font-size:16px;font-weight:600}\n\n /* Progress */\n .bar{height:4px;background:var(--border);border-radius:2px;overflow:hidden;margin-bottom:24px;display:flex}\n .bar>div{height:100%}\n .bar .p{background:var(--green)}\n .bar .f{background:var(--red)}\n .bar .e{background:var(--amber)}\n\n /* Filters */\n .filters{display:flex;gap:4px;margin-bottom:16px}\n .fbtn{padding:4px 12px;border-radius:6px;border:1px solid transparent;background:transparent;color:var(--muted);cursor:pointer;font-size:12px;font-weight:500;transition:all .12s}\n .fbtn:hover{color:var(--fg);background:var(--bg-hover)}\n .fbtn.active{background:var(--bg-subtle);color:var(--fg);border-color:var(--border)}\n .fbtn .n{color:var(--muted);margin-left:4px;font-variant-numeric:tabular-nums}\n\n /* Test rows */\n .row{border:1px solid var(--border);border-radius:8px;margin-bottom:6px;overflow:hidden;transition:border-color .12s}\n .row:hover{border-color:var(--muted)}\n .row-head{display:flex;align-items:center;gap:10px;padding:10px 14px;cursor:pointer;user-select:none}\n .dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}\n .dot.pass{background:var(--green)}\n .dot.fail{background:var(--red)}\n .dot.err{background:var(--amber)}\n .method{font-family:var(--mono);font-size:11px;font-weight:600;color:var(--muted);min-width:40px;text-transform:uppercase}\n .path{font-family:var(--mono);font-size:13px;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n .proto{font-size:10px;text-transform:uppercase;letter-spacing:.05em;color:var(--muted);padding:2px 8px;border:1px solid var(--border);border-radius:4px}\n .dur{font-family:var(--mono);font-size:12px;color:var(--muted);min-width:56px;text-align:right;font-variant-numeric:tabular-nums}\n .chev{color:var(--muted);font-size:10px;transition:transform .15s;flex-shrink:0}\n .row.open .chev{transform:rotate(90deg)}\n\n /* Row body */\n .row-body{display:none;padding:0 14px 12px 32px;border-top:1px solid var(--border)}\n .row.open .row-body{display:block}\n .sect{margin-top:10px}\n .sect-t{font-size:10px;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);font-weight:600;margin-bottom:6px}\n .asrt{display:flex;align-items:center;gap:8px;padding:3px 0;font-size:13px}\n .asrt .ck{width:14px;height:14px;border-radius:3px;display:flex;align-items:center;justify-content:center;font-size:9px;font-weight:700;flex-shrink:0;color:#fff}\n .asrt.pass .ck{background:var(--green)}\n .asrt.fail .ck{background:var(--red)}\n .asrt .an{color:var(--fg)}\n .asrt .ae{color:var(--red);font-size:12px;margin-left:6px;font-family:var(--mono)}\n .rmeta{display:flex;gap:20px;flex-wrap:wrap;font-size:12px}\n .rmeta span{color:var(--muted)}\n .rmeta strong{color:var(--fg);font-weight:600;font-family:var(--mono)}\n .errbox{background:var(--red-bg);border:1px solid var(--red-border);border-radius:6px;padding:8px 12px;color:var(--red);font-size:12px;font-family:var(--mono);margin-top:10px}\n .bprev{background:var(--bg-subtle);border:1px solid var(--border);border-radius:6px;padding:10px 12px;font-family:var(--mono);font-size:11px;color:var(--muted);max-height:180px;overflow:auto;white-space:pre-wrap;word-break:break-all;margin-top:6px}\n\n footer{margin-top:32px;padding-top:16px;border-top:1px solid var(--border);font-size:11px;color:var(--muted);display:flex;justify-content:space-between}\n\n @media(max-width:640px){\n .strip{grid-template-columns:repeat(3,1fr)}\n .stat:nth-child(3){border-right:none}\n .stat:nth-child(4),.stat:nth-child(5){border-top:1px solid var(--border)}\n .path{font-size:11px}\n }\n</style>\n</head>\n<body>\n<div class="wrap">\n <div class="hdr">\n <h1>Test Report</h1>\n <span class="meta">${e.generatedAt} · v${e.version}</span>\n </div>\n\n <div class="strip">\n <div class="stat total"><div class="v">${t.total}</div><div class="l">Total</div></div>\n <div class="stat pass"><div class="v">${t.passed}</div><div class="l">Passed</div></div>\n <div class="stat fail"><div class="v">${t.failed}</div><div class="l">Failed</div></div>\n <div class="stat err"><div class="v">${t.errors}</div><div class="l">Errors</div></div>\n <div class="stat rate"><div class="v">${t.passRate}%</div><div class="l">Pass Rate</div></div>\n <div class="stat time"><div class="v">${(t.durationMs/1e3).toFixed(2)}s</div><div class="l">Duration</div></div>\n </div>\n\n <div class="bar">\n <div class="p" style="width:${t.total?t.passed/t.total*100:0}%"></div>\n <div class="f" style="width:${t.total?t.failed/t.total*100:0}%"></div>\n <div class="e" style="width:${t.total?t.errors/t.total*100:0}%"></div>\n </div>\n\n <div class="filters">\n <button class="fbtn active" data-f="all">All<span class="n">${t.total}</span></button>\n <button class="fbtn" data-f="passed">Passed<span class="n">${t.passed}</span></button>\n <button class="fbtn" data-f="failed">Failed<span class="n">${t.failed}</span></button>\n <button class="fbtn" data-f="error">Errors<span class="n">${t.errors}</span></button>\n </div>\n\n <div id="list"></div>\n\n <footer>\n <span>@powerduck/openapi-cli v${e.version}</span>\n <span>${t.passed}/${t.total} passed</span>\n </footer>\n</div>\n\n<script>\nconst D=${s};\nfunction esc(s){const d=document.createElement('div');d.textContent=String(s??'');return d.innerHTML}\nfunction render(f){\n const list=document.getElementById('list');\n const items=f==='all'?D.results:D.results.filter(r=>r.status===f);\n list.innerHTML=items.map(r=>{\n const as=r.assertions?r.assertions.map(a=>\`\n <div class="asrt \${a.passed?'pass':'fail'}">\n <div class="ck">\${a.passed?'✓':'✗'}</div>\n <span class="an">\${esc(a.name)}</span>\n \${a.error?'<span class="ae">'+esc(a.error)+'</span>':''}\n </div>\`).join(''):'';\n const bp=r.response?.body?'<div class="bprev">'+esc(JSON.stringify(r.response.body,null,2).slice(0,3000))+'</div>':'';\n const eb=r.error?'<div class="errbox">'+esc(r.error)+'</div>':'';\n const rm=r.response?\`\n <div class="rmeta">\n \${r.response.status!=null?'<span>Status <strong>'+r.response.status+(r.response.statusText?' '+r.response.statusText:'')+'</strong></span>':''}\n \${r.response.contentType?'<span>Type <strong>'+esc(r.response.contentType)+'</strong></span>':''}\n \${r.response.sizeBytes!=null?'<span>Size <strong>'+r.response.sizeBytes+'B</strong></span>':''}\n \${r.response.streaming?'<span>Streaming <strong>yes</strong></span>':''}\n </div>\`:'';\n return \`\n <div class="row \${r.status}" data-s="\${r.status}">\n <div class="row-head" onclick="this.parentElement.classList.toggle('open')">\n <div class="dot \${r.status==='passed'?'pass':r.status==='failed'?'fail':'err'}"></div>\n <span class="method">\${r.method}</span>\n <span class="path">\${esc(r.path)}</span>\n \${r.protocol?'<span class="proto">'+r.protocol+'</span>':''}\n <span class="dur">\${r.durationMs}ms</span>\n <span class="chev">▶</span>\n </div>\n <div class="row-body">\n \${eb}\n \${rm}\n \${as?'<div class="sect"><div class="sect-t">Assertions</div>'+as+'</div>':''}\n \${bp?'<div class="sect"><div class="sect-t">Response</div>'+bp+'</div>':''}\n </div>\n </div>\`;\n }).join('');\n}\ndocument.querySelectorAll('.fbtn').forEach(b=>b.addEventListener('click',()=>{\n document.querySelectorAll('.fbtn').forEach(x=>x.classList.remove('active'));\n b.classList.add('active');render(b.dataset.f);\n}));\nrender('all');\n<\/script>\n</body>\n</html>`}(e),"utf-8"),s}export{o as DEFAULT_CONFIG,a as resolveConfig,u as isRemoteSpec,f as loadSpec,m as runDeclarativeAssertions,h as resolveJsonPath,g as extractAssertions,b as extractPostmanScripts,w as formatAssertionError,$ as buildImplicitAssertions,k as runTests,S as collectOperations,T as executeStep,j as buildSummary,R as generateJsonReport,W as generateCliReport,Q as printCliReport,V as generateHtmlReport};
|