{"name":"restk-testserver","routes":[{"path":"/http/echo","group":"HTTP","purpose":"JSON describing the incoming request"},{"path":"/http/echo-headers","group":"HTTP","purpose":"JSON map of request headers"},{"path":"/http/echo-body","group":"HTTP","purpose":"verbatim copy of request body"},{"path":"/http/status/{code}","group":"HTTP","purpose":"returns the given HTTP status"},{"path":"/http/delay?ms=N","group":"HTTP","purpose":"sleeps N ms before responding"},{"path":"/http/json/{shape}","group":"HTTP","purpose":"user|list|empty canned JSON"},{"path":"/http/redirect/{n}?status=N","group":"HTTP","purpose":"chain of n redirects (?status=301|302|307|308) ending at /http/echo"},{"path":"/http/cookies","group":"HTTP","purpose":"echo request cookies as JSON"},{"path":"/http/cookies/set?name=...\u0026value=...","group":"HTTP","purpose":"sets a Set-Cookie response header"},{"path":"/http/gzip","group":"HTTP","purpose":"gzip-compressed JSON response (Content-Encoding: gzip)"},{"path":"/http/etag","group":"HTTP","purpose":"stable ETag + 304 Not Modified on If-None-Match"},{"path":"/http/large-body?bytes=N","group":"HTTP","purpose":"N-byte JSON body (default 1MB, cap 32MB)"},{"path":"/http/slow-stream?bytes=N\u0026intervalMs=M","group":"HTTP","purpose":"stream N bytes in 64-byte chunks every M ms"},{"path":"/http/multipart","group":"HTTP","purpose":"echo multipart/form-data parts as JSON"},{"path":"/http/form-urlencoded","group":"HTTP","purpose":"echo application/x-www-form-urlencoded body as JSON"},{"path":"/http/html","group":"HTTP","purpose":"small HTML document (text/html)"},{"path":"/http/xml","group":"HTTP","purpose":"small XML document (application/xml; not SOAP)"},{"path":"/http/cache-control?maxAge=N","group":"HTTP","purpose":"emits Cache-Control: public, max-age=N"},{"path":"/http/binary-upload","group":"HTTP","purpose":"accept raw body (any Content-Type); echo size+contentType+hex-preview as JSON (target for Restk's Binary body type)"},{"path":"/http/method/{verb}","group":"HTTP","purpose":"only the named verb succeeds; others → 405 + Allow header"},{"path":"/http/head","group":"HTTP","purpose":"HEAD with Content-Length but no body"},{"path":"/http/options","group":"HTTP","purpose":"OPTIONS echoing Allow + CORS headers"},{"path":"/http/anything","group":"HTTP","purpose":"echo method/headers/args(query)/form(body)/json/data (httpbin-style)"},{"path":"/http/require-content-type/{kind}","group":"HTTP","purpose":"415 unless Content-Type matches (json|xml|form|text)"},{"path":"/http/upload/limit?max=N","group":"HTTP","purpose":"413 if body exceeds N bytes"},{"path":"/http/headers/one/{name}","group":"HTTP","purpose":"return a single named request header"},{"path":"/http/response-headers?k=v","group":"HTTP","purpose":"reflect query params as response headers"},{"path":"/http/host","group":"HTTP","purpose":"echo the Host header the server saw"},{"path":"/http/status/empty/{code}","group":"HTTP","purpose":"given status with an empty body"},{"path":"/http/redirect-to?url=...\u0026status=N","group":"HTTP","purpose":"redirect to an arbitrary URL (default 302)"},{"path":"/http/redirect-code/{code}","group":"HTTP","purpose":"single redirect with the given 3xx code"},{"path":"/http/redirect-preserve","group":"HTTP","purpose":"307 — method + body preserved"},{"path":"/http/redirect-downgrade","group":"HTTP","purpose":"302 — POST downgrades to GET on follow"},{"path":"/http/deflate","group":"HTTP","purpose":"deflate-compressed JSON (Content-Encoding: deflate)"},{"path":"/http/brotli","group":"HTTP","purpose":"brotli-compressed JSON (Content-Encoding: br)"},{"path":"/http/cookies/set-attributes?...","group":"HTTP","purpose":"Set-Cookie with Path/Max-Age/HttpOnly/SameSite/Secure"},{"path":"/http/cookies/delete?name=...","group":"HTTP","purpose":"expire a cookie (Max-Age=0)"},{"path":"/http/drip?bytes=N\u0026durationMs=M","group":"HTTP","purpose":"trickle N bytes over M ms (slow-response test)"},{"path":"/http/connection/reset","group":"HTTP","purpose":"hijack + close TCP with no HTTP response (transport error)"},{"path":"/http/bytes/{n}","group":"HTTP","purpose":"n random bytes (application/octet-stream, cap 64MB)"},{"path":"/http/stream/{n}","group":"HTTP","purpose":"n newline-delimited JSON lines, chunked"},{"path":"/http/download/{name}","group":"HTTP","purpose":"Content-Disposition attachment (filename)"},{"path":"/http/plain | /http/css | /http/javascript","group":"HTTP","purpose":"text/plain, text/css, application/javascript bodies"},{"path":"/http/content-type-mismatch","group":"HTTP","purpose":"JSON body sent with Content-Type: text/plain"},{"path":"/http/image/{png|jpeg|gif|svg}","group":"HTTP","purpose":"a small image of the given type"},{"path":"/shop/login","group":"Shop","purpose":"POST {username:qa, password:shop-pass} → bearer token"},{"path":"/shop/search?q=\u0026category=\u0026maxPrice=\u0026sort=\u0026page=","group":"Shop","purpose":"product search with filters + pagination"},{"path":"/shop/categories","group":"Shop","purpose":"list product categories"},{"path":"/shop/products/{id}","group":"Shop","purpose":"product detail (p-1001..p-1008)"},{"path":"/shop/cart[/{id}/items[/{itemId}]]","group":"Shop","purpose":"create cart; add/update/remove items (bearer required)"},{"path":"/shop/checkout","group":"Shop","purpose":"POST cartId → order (computes totals)"},{"path":"/shop/payment","group":"Shop","purpose":"POST — card ...0000 → 402 declined; paying twice → 409 (idempotency)"},{"path":"/shop/order/{id}","group":"Shop","purpose":"order status"},{"path":"/shop/reset","group":"Shop","purpose":"POST — wipe carts/orders back to seed"},{"path":"/auth/bearer?expected=...","group":"Auth","purpose":"200 if Bearer token matches, 401 otherwise"},{"path":"/auth/basic?user=...\u0026pass=...","group":"Auth","purpose":"200 if Basic credentials match, 401 otherwise"},{"path":"/auth/apikey?header=...\u0026expected=...","group":"Auth","purpose":"200 if header value matches, 401 otherwise"},{"path":"/auth/digest?user=\u0026pass=\u0026algorithm=\u0026qop=","group":"Auth","purpose":"RFC 7616/2069 Digest challenge/response; algorithm=MD5|MD5-sess|SHA-256|SHA-256-sess (default MD5), qop=auth|auth-int|none (default auth); recomputes + verifies, echoes expected/received on mismatch"},{"path":"/auth/oauth2/token","group":"Auth","purpose":"POST grant_type=client_credentials → access_token (Basic or form body for client_id/secret)"},{"path":"/auth/oauth2/protected","group":"Auth","purpose":"200 if Bearer matches a token issued by /auth/oauth2/token"},{"path":"/auth/awssigv4","group":"Auth","purpose":"AWS Signature v4 (access AKIDEXAMPLE / secret wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY); 200 if signature matches, 403 + canonicalRequest/stringToSign on mismatch"},{"path":"/auth/oauth1[/plaintext]","group":"Auth","purpose":"OAuth 1.0a (consumer oauth1-key/oauth1-secret, token oauth1-token/oauth1-token-secret); HMAC-SHA1|HMAC-SHA256|PLAINTEXT; 200 or 401 + baseString"},{"path":"/auth/hawk[/sha1]","group":"Auth","purpose":"Hawk (id hawk-id / key hawk-key; sha256 default, /sha1 selects sha1); 200 or 401 + normalized string; optional payload-hash check"},{"path":"/auth/jwt","group":"Auth","purpose":"verify Authorization: Bearer \u003cjwt\u003e (or ?token=); HS/RS/PS/ES/EdDSA against fixed keys; validates exp/nbf; rejects alg=none; 200 + decoded claims, else 401 + decoded header/claims + reason"},{"path":"/auth/jwt/keys","group":"Auth","purpose":"fixed signing material (HMAC secret + RSA/EC/Ed25519 private-key PEMs) so QA can sign tokens /auth/jwt accepts — TEST keys, not secret"},{"path":"/script/matchers","group":"Script","purpose":"one JSON object with a value of every JS type + nested/array/empty shapes — exercises EVERY nova.expect matcher (toBe/toEqual/toBeType/toContain/toHaveLength/toHaveProperty/toBeCloseTo/toMatch/toBeOneOf/toBeEmpty/toBeInstanceOf/.not)"},{"path":"/script/text","group":"Script","purpose":"fixed text/plain body — nova.response.text()/body, contentType, isJSON=false, toContain/toMatch"},{"path":"/script/headers","group":"Script","purpose":"fixed X-Request-Id/X-Rate-Limit/X-Custom-Header response headers — nova.response.getHeader(name)/getHeaders()"},{"path":"/script/status/{code}","group":"Script","purpose":"returns {code} with body {code,reason} — nova.response.status/code/reason/isSuccess"},{"path":"/script/delay?ms=N","group":"Script","purpose":"sleeps N ms (default 300, cap 5000) — nova.response.responseTime"},{"path":"/script/size?bytes=N","group":"Script","purpose":"N-byte ASCII body (default 512) so nova.response.size == text().length — nova.response.size"},{"path":"/soap/fault","group":"SOAP","purpose":"returns a SOAP 1.1 Fault envelope (HTTP 500)"},{"path":"/soap/ok","group":"SOAP","purpose":"returns a SOAP 1.1 success envelope (HTTP 200)"},{"path":"/soap/wss-verify?user=...\u0026pass=...","group":"SOAP","purpose":"validates incoming wsse:UsernameToken; 200 if valid else SOAP Fault"},{"path":"/soap/ok12","group":"SOAP","purpose":"SOAP 1.2 success envelope (application/soap+xml)"},{"path":"/soap/fault12","group":"SOAP","purpose":"SOAP 1.2 fault (Code/Value + Reason/Text), HTTP 500"},{"path":"/soap/fault-codeonly","group":"SOAP","purpose":"1.1 fault with \u003cfaultcode\u003e but no \u003cfaultstring\u003e"},{"path":"/soap/fault-stringonly","group":"SOAP","purpose":"1.1 fault with \u003cfaultstring\u003e but no \u003cfaultcode\u003e"},{"path":"/soap/fault12-reasononly","group":"SOAP","purpose":"1.2 fault with \u003cReason\u003e but no \u003cCode\u003e"},{"path":"/soap/echo","group":"SOAP","purpose":"reflects request operation name, body child elements, and SOAPAction back"},{"path":"/soap/action-required?action=...","group":"SOAP","purpose":"200 only if SOAPAction header matches (default urn:DoThing), else fault"},{"path":"/soap/malformed","group":"SOAP","purpose":"non-XML/truncated body so nova.soap.parse() yields null"},{"path":"/soap/large?count=...","group":"SOAP","purpose":"valid envelope with N repeated \u003citem\u003e rows (default 500, cap 5000)"},{"path":"/soap/wss-timestamp","group":"SOAP","purpose":"200 only if request carries wsu:Timestamp/Created"},{"path":"/soap/fault-detail","group":"SOAP","purpose":"1.1 fault with \u003cfaultactor\u003e + \u003cdetail\u003e app-error payload"},{"path":"/soap/fault12-full","group":"SOAP","purpose":"1.2 fault with Code/Value + Subcode + Reason + Role + Detail"},{"path":"/soap/echo-with-header","group":"SOAP","purpose":"reflects request operation + SOAP \u003cHeader\u003e child names back"},{"path":"/soap/ok-prefixed","group":"SOAP","purpose":"1.1 success using env: envelope prefix (namespace-agnostic check)"},{"path":"/soap/fault-prefixed","group":"SOAP","purpose":"1.1 fault using env: envelope prefix"},{"path":"/wsdl/echo.wsdl","group":"WSDL","purpose":"SOAP 1.1 EchoService: Echo (string part) + Ping operations → /soap/echo"},{"path":"/wsdl/ping12.wsdl","group":"WSDL","purpose":"SOAP 1.2 PingService → /soap/ok12 (1.2 version detection)"},{"path":"/wsdl/multi.wsdl","group":"WSDL","purpose":"SOAP 1.1 two services (Echo → /soap/echo, User → /soap/ok) — exercises service/port pickers"},{"path":"/ws/echo","group":"WebSocket","purpose":"echo any text/binary frame back"},{"path":"/ws/echo-once","group":"WebSocket","purpose":"echo first frame then close(1000, 'echoed-once')"},{"path":"/ws/close-with-code/{code}?reason=...","group":"WebSocket","purpose":"accept then close with given code"},{"path":"/ws/reject-handshake/{status}","group":"WebSocket","purpose":"refuse the WS upgrade with HTTP status"},{"path":"/ws/require-subprotocol?expected=NAME","group":"WebSocket","purpose":"only accept if Sec-WebSocket-Protocol matches"},{"path":"/ws/require-auth?bearer=TOKEN","group":"WebSocket","purpose":"401 if Authorization Bearer mismatches"},{"path":"/ws/burst?count=N\u0026intervalMs=M","group":"WebSocket","purpose":"send N text frames at M ms intervals"},{"path":"/ws/slow-first?delayMs=N","group":"WebSocket","purpose":"wait N ms then send 'delayed'"},{"path":"/ws/large-message?bytes=N","group":"WebSocket","purpose":"send one ~N-byte text frame"},{"path":"/ws/no-pong","group":"WebSocket","purpose":"ignore client pings (test ping-timeout UX)"},{"path":"/ws/binary-echo","group":"WebSocket","purpose":"echo binary frames; close 1003 on text"},{"path":"/ws/server-push?intervalMs=N\u0026count=M","group":"WebSocket","purpose":"server pushes M frames at N-ms intervals; no client send required"},{"path":"/ws/ping-from-server?intervalMs=N\u0026count=M","group":"WebSocket","purpose":"server sends WS PING control frames; tests client auto-pong"},{"path":"/ws/abrupt-drop?sendFirst=1","group":"WebSocket","purpose":"accepts handshake then closes TCP without a WS close frame → fires onError on the client (vs onClose for /ws/close-with-code/*)"},{"path":"/sse/events?count=N\u0026intervalMs=M","group":"SSE","purpose":"emit N data: events"},{"path":"/sse/typed-events?types=a,b,c","group":"SSE","purpose":"emit one event per type with id"},{"path":"/sse/heartbeat?count=N\u0026intervalMs=M","group":"SSE","purpose":"emit only `:` heartbeats"},{"path":"/sse/reconnect-with-id?total=N","group":"SSE","purpose":"honors Last-Event-ID for resume"},{"path":"/sse/retry-directive?retryMs=N","group":"SSE","purpose":"send `retry: N` directive"},{"path":"/sse/require-auth?bearer=TOKEN","group":"SSE","purpose":"401 if Authorization mismatches"},{"path":"/sse/large-event?bytes=N","group":"SSE","purpose":"emit one ~N-byte data: event"},{"path":"/sse/multi-line-data","group":"SSE","purpose":"emit event with 3 data: lines"},{"path":"/sse/drop-after?count=N","group":"SSE","purpose":"emit N events then forcibly drop socket"},{"path":"/sse/slow-events?count=N\u0026intervalMs=M","group":"SSE","purpose":"emit N events spaced M ms apart (default 10 / 1000ms — AI-stream-style)"},{"path":"/sse/cors","group":"SSE","purpose":"emits 3 events with CORS headers (Access-Control-Allow-Origin: *); supports preflight OPTIONS"},{"path":"/api/users","group":"REST","purpose":"GET list (?page\u0026limit\u0026status), POST create (201)"},{"path":"/api/users/{id}","group":"REST","purpose":"GET (ETag/304), PUT (If-Match/412), DELETE (204)"},{"path":"/api/reset","group":"REST","purpose":"POST — wipe + reseed user store"},{"path":"/graphql","group":"GraphQL","purpose":"POST {query,variables,operationName} or GET ?query=...; supports introspection"},{"path":"/graphql/always-errors","group":"GraphQL","purpose":"always returns an errors array"},{"path":"/graphql/partial-errors","group":"GraphQL","purpose":"returns BOTH data and errors (partial-success)"},{"path":"/graphql/shop","group":"GraphQL","purpose":"real executable schema (graphql-go): product/products/search/categories queries, addReview mutation, nested reviews, enum, nullable discount, aliases/fragments/variables/directives/introspection, boom (top-level error), flaky (partial error)"},{"path":"/ws/graphql","group":"GraphQL","purpose":"graphql-transport-ws subprotocol: subscribe to {tick}, {ticks(count,intervalMs)}, {errorAfter(count)}"},{"path":"/ws/stomp","group":"STOMP","purpose":"STOMP 1.2 broker (subprotocol v12.stomp); CONNECT/SUBSCRIBE/SEND/UNSUBSCRIBE/DISCONNECT; per-connection echo"},{"path":"/ws/mqtt","group":"MQTT","purpose":"MQTT 3.1.1 broker (subprotocol mqtt); CONNECT/SUBSCRIBE/PUBLISH/PINGREQ/DISCONNECT; QoS 0 only, per-connection echo"}],"version":"0.1.0"}
