{ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://raw.githubusercontent.com/NazarKalytiuk/tarn/main/schemas/v1/testfile.json", "title": "Tarn Test File", "description": "Schema for .tarn.yaml API test files", "type": "object", "required": ["name"], "properties": { "version": { "type": "string", "description": "Schema version", "enum": ["1"], "default": "1" }, "name": { "type": "string", "description": "Human-readable name for this test file", "minLength": 1 }, "description": { "type": "string", "description": "Optional description of what this test file covers" }, "tags": { "type": "array", "description": "Tags for filtering with --tag", "items": { "type": "string" }, "default": [] }, "openapi_operation_ids": { "type": "array", "description": "Optional list of OpenAPI operation ids this test file exercises. Consumed by `tarn impact` to map changed operations to tests. Adopting the field is additive and never required.", "items": { "type": "string", "minLength": 1 } }, "env": { "type": "object", "description": "Inline environment variables (lowest priority in resolution chain)", "additionalProperties": { "type": "string" } }, "cookies": { "type": "string", "description": "Cookie handling mode: 'auto' (default), 'off' (no cookie jar), or 'per-test' (clear the default jar between named tests; named jars are preserved). The CLI flag --cookie-jar-per-test overrides this, except when the value is 'off'.", "enum": ["auto", "off", "per-test"], "default": "auto" }, "serial_only": { "type": "boolean", "description": "When true, this file must never run concurrently with other files under --parallel. The scheduler pins serial_only files onto a single worker that runs after every parallel-safe bucket completes. Use this for tests that share mutable state (DB fixtures, singletons).", "default": false }, "group": { "type": "string", "description": "Optional resource group name. Files sharing the same `group` land in the same parallel bucket and run sequentially within the group, while different groups run in parallel. Useful for 'all the postgres tests' or 'all the S3 tests' where an external resource forces per-resource serialization.", "minLength": 1 }, "redaction": { "$ref": "#/$defs/RedactionConfig", "description": "Report-time redaction policy for sensitive headers" }, "defaults": { "$ref": "#/$defs/Defaults" }, "setup": { "type": "array", "description": "Steps run once before all tests. Supports include directives.", "items": { "$ref": "#/$defs/StepOrInclude" } }, "teardown": { "type": "array", "description": "Steps run once after all tests, even on failure. Supports include directives.", "items": { "$ref": "#/$defs/StepOrInclude" } }, "tests": { "type": "object", "description": "Named test groups (full format)", "additionalProperties": { "$ref": "#/$defs/TestGroup" } }, "steps": { "type": "array", "description": "Flat steps (simple format). Supports include directives.", "items": { "$ref": "#/$defs/StepOrInclude" } } }, "anyOf": [ { "required": ["steps"] }, { "required": ["tests"] } ], "$defs": { "Defaults": { "type": "object", "description": "Default settings applied to every request in this file", "properties": { "headers": { "type": "object", "description": "Headers added to every request", "additionalProperties": { "type": "string" } }, "auth": { "$ref": "#/$defs/AuthConfig", "description": "Optional default auth helper. Explicit Authorization headers still win." }, "timeout": { "type": "integer", "description": "Default timeout in milliseconds", "minimum": 0 }, "connect_timeout": { "type": "integer", "description": "Default connect timeout in milliseconds", "minimum": 0 }, "follow_redirects": { "type": "boolean", "description": "Whether requests should follow redirects by default" }, "max_redirs": { "type": "integer", "description": "Default maximum redirects to follow", "minimum": 0 }, "retries": { "type": "integer", "description": "Default retry count for all steps", "minimum": 0 }, "delay": { "type": "string", "description": "Default delay before each request (e.g., '100ms', '2s')", "pattern": "^\\d+(ms|s)$", "examples": ["100ms", "1s"] } }, "additionalProperties": false }, "RedactionConfig": { "type": "object", "description": "Configurable header redaction policy for JSON report output", "properties": { "headers": { "type": "array", "description": "Case-insensitive header names to redact", "items": { "type": "string", "minLength": 1 }, "default": ["authorization", "cookie", "set-cookie", "x-api-key", "x-auth-token"] }, "replacement": { "type": "string", "description": "Replacement string used for redacted header values", "default": "***" }, "env": { "type": "array", "description": "Environment variable names whose interpolated values should be redacted in reports", "items": { "type": "string", "minLength": 1 }, "default": [] }, "captures": { "type": "array", "description": "Capture names whose extracted/interpolated values should be redacted in reports", "items": { "type": "string", "minLength": 1 }, "default": [] } }, "additionalProperties": false }, "TestGroup": { "type": "object", "description": "A named group of test steps", "properties": { "description": { "type": "string", "description": "Description of this test group" }, "tags": { "type": "array", "description": "Tags for filtering", "items": { "type": "string" } }, "steps": { "type": "array", "description": "Steps in this test group. Supports include directives.", "items": { "$ref": "#/$defs/StepOrInclude" }, "minItems": 1 }, "serial_only": { "type": "boolean", "description": "When true, this test must never run concurrently with other work under --parallel. Because Tarn's parallelism unit is the file, this escalates the whole file to the serial bucket to preserve per-file isolation (setup, teardown, cookie jars, captures).", "default": false } }, "required": ["steps"] }, "StepOrInclude": { "oneOf": [ { "$ref": "#/$defs/Step" }, { "$ref": "#/$defs/IncludeDirective" } ] }, "IncludeDirective": { "type": "object", "description": "Include steps from another .tarn.yaml file", "required": ["include"], "properties": { "include": { "type": "string", "description": "Relative path to .tarn.yaml file to include", "examples": ["./shared/auth-setup.tarn.yaml"] }, "with": { "type": "object", "description": "Parameter values injected into the included file via {{ params.name }} placeholders" }, "override": { "type": "object", "description": "Deep override merged into every imported step from the included file" } }, "additionalProperties": false }, "Step": { "type": "object", "description": "A single test step: one HTTP request with optional capture and assertions", "required": ["name", "request"], "properties": { "name": { "type": "string", "description": "Human-readable step name", "minLength": 1 }, "description": { "type": "string", "description": "Optional human-readable description of this step. Supports multi-line YAML block scalars (`|`, `>`)." }, "request": { "$ref": "#/$defs/Request" }, "capture": { "type": "object", "description": "Capture values from the response. Values can be JSONPath strings or extended capture objects with header/cookie/jsonpath/body/status/url/regex fields.", "additionalProperties": { "oneOf": [ { "type": "string", "description": "JSONPath expression (e.g., '$.id', '$.user.token')" }, { "$ref": "#/$defs/ExtendedCapture" } ] }, "examples": [ { "user_id": "$.id", "token": "$.auth.token" }, { "session": { "header": "set-cookie", "regex": "session=([^;]+)" } }, { "session_cookie": { "cookie": "session" } }, { "status_code": { "status": true } }, { "final_url": { "url": true } }, { "body_word": { "body": true, "regex": "plain (text)" } } ] }, "assert": { "$ref": "#/$defs/Assertion" }, "retries": { "type": "integer", "description": "Number of retries on failure (0 = no retries)", "minimum": 0 }, "timeout": { "type": "integer", "description": "Step-level timeout in milliseconds (overrides defaults)", "minimum": 0 }, "connect_timeout": { "type": "integer", "description": "Step-level connect timeout in milliseconds (overrides defaults.connect_timeout)", "minimum": 0 }, "follow_redirects": { "type": "boolean", "description": "Whether this step should follow redirects (overrides defaults.follow_redirects)" }, "max_redirs": { "type": "integer", "description": "Maximum redirects to follow for this step (overrides defaults.max_redirs)", "minimum": 0 }, "delay": { "type": "string", "description": "Delay before executing this step (overrides defaults.delay)", "pattern": "^\\d+(ms|s)$", "examples": ["500ms", "2s"] }, "poll": { "$ref": "#/$defs/PollConfig" }, "script": { "type": "string", "description": "Lua script to run after HTTP response for custom validation" }, "cookies": { "description": "Per-step cookie control. `false` disables cookies. `true` uses the default jar. A string names a specific jar for multi-user scenarios (e.g., 'admin', 'user').", "oneOf": [ { "type": "boolean" }, { "type": "string", "minLength": 1 } ], "default": true }, "if": { "type": "string", "description": "Conditionally run this step only when the interpolated expression is truthy. Empty / unset / 'false' / '0' / 'null' are falsy; any other non-empty value is truthy. Mutually exclusive with 'unless'.", "examples": ["{{ capture.request_uuid }}", "{{ env.run_cleanup }}"] }, "unless": { "type": "string", "description": "Conditionally run this step only when the interpolated expression is falsy (inverse of 'if'). Mutually exclusive with 'if'.", "examples": ["{{ capture.already_provisioned }}"] }, "debug": { "type": "boolean", "description": "When true, record request/response details for this step in the report even when it passes. Equivalent to running `tarn run --verbose-responses` for just this step. Useful for keeping one hot debugging step loud without enabling verbose capture across the whole file.", "default": false } }, "additionalProperties": false }, "ExtendedCapture": { "type": "object", "description": "Extended capture specification supporting cookies, status, final URL, headers, body JSONPath, whole-body capture, and optional regex extraction", "properties": { "header": { "type": "string", "description": "Capture from a response header (case-insensitive)" }, "cookie": { "type": "string", "description": "Capture from a response cookie by cookie name" }, "jsonpath": { "type": "string", "description": "Capture from body via JSONPath (explicit form)" }, "body": { "type": "boolean", "description": "Capture from the whole response body string. Use `true` to enable this source." }, "status": { "type": "boolean", "description": "Capture from the HTTP response status code. Use `true` to enable this source." }, "url": { "type": "boolean", "description": "Capture from the final response URL after redirects. Use `true` to enable this source." }, "regex": { "type": "string", "description": "Regex to extract a sub-match (capture group 1 is used)" }, "where": { "type": "object", "description": "Predicate filter applied to a JSONPath array result. Keeps only elements whose fields match the { field: value } pairs here. Combine with the `first` transform to capture an object by stable identifier instead of `$[0]`.", "additionalProperties": true }, "optional": { "type": "boolean", "description": "When true, a missing source (path no-match, header absent, regex no-match, etc.) leaves the capture unset instead of failing the step. Downstream {{ capture.X }} references where X was optional and unset produce a distinct 'declared optional and not set' error instead of the generic unresolved-template error." }, "default": { "description": "Value used when the source yields no match. Implies optional: missing values never fail the step when a default is supplied. Any JSON-compatible YAML value (string, number, boolean, null, array, object) is accepted." }, "when": { "$ref": "#/$defs/CaptureWhen", "description": "Only attempt this capture when the response matches the predicate. When present and unmet, the capture is skipped (variable unset, no error) the same way optional/default captures are when their source is missing." } }, "oneOf": [ { "required": ["header"] }, { "required": ["cookie"] }, { "required": ["jsonpath"] }, { "required": ["body"] }, { "required": ["status"] }, { "required": ["url"] } ], "additionalProperties": false }, "CaptureWhen": { "type": "object", "description": "Response-shape gate for a capture. Currently supports gating on status code using the same grammar as assert.status (exact value, shorthand range '2xx', or { in / gte / gt / lte / lt } spec).", "properties": { "status": { "description": "Status matcher: exact code, shorthand range ('2xx'), or complex spec.", "oneOf": [ { "type": "integer", "minimum": 100, "maximum": 599 }, { "type": "string", "pattern": "^[1-5]xx$" }, { "type": "object", "properties": { "in": { "type": "array", "items": { "type": "integer", "minimum": 100, "maximum": 599 } }, "gte": { "type": "integer" }, "gt": { "type": "integer" }, "lte": { "type": "integer" }, "lt": { "type": "integer" } }, "additionalProperties": false } ] } }, "additionalProperties": false }, "PollConfig": { "type": "object", "description": "Polling configuration: re-execute until condition is met", "required": ["until", "interval", "max_attempts"], "properties": { "until": { "$ref": "#/$defs/Assertion", "description": "Assertions that must pass for polling to stop" }, "interval": { "type": "string", "description": "Time between attempts", "pattern": "^\\d+(ms|s)$", "examples": ["2s", "500ms"] }, "max_attempts": { "type": "integer", "description": "Maximum number of polling attempts", "minimum": 1 } }, "additionalProperties": false }, "Request": { "type": "object", "description": "HTTP request definition", "required": ["method", "url"], "properties": { "method": { "type": "string", "description": "HTTP method", "minLength": 1, "examples": ["GET", "POST", "PROPFIND", "PURGE"] }, "url": { "type": "string", "description": "Request URL. Supports interpolation: {{ env.base_url }}/path" }, "headers": { "type": "object", "description": "Request headers. Supports interpolation: {{ capture.token }}", "additionalProperties": { "type": "string" } }, "auth": { "$ref": "#/$defs/AuthConfig", "description": "Optional auth helper for bearer/basic flows. Explicit Authorization headers still win." }, "body": { "description": "Request body sent as JSON. Can be any valid JSON value (object, array, string, number, boolean, null)" }, "form": { "$ref": "#/$defs/FormBody", "description": "URL-encoded form body sent as application/x-www-form-urlencoded" }, "graphql": { "$ref": "#/$defs/GraphqlRequest" }, "multipart": { "$ref": "#/$defs/MultipartBody" } }, "additionalProperties": false }, "AuthConfig": { "type": "object", "properties": { "bearer": { "type": "string", "description": "Bearer token value without the 'Bearer ' prefix" }, "basic": { "$ref": "#/$defs/BasicAuthConfig" } }, "oneOf": [ { "required": ["bearer"] }, { "required": ["basic"] } ], "additionalProperties": false }, "BasicAuthConfig": { "type": "object", "required": ["username", "password"], "properties": { "username": { "type": "string" }, "password": { "type": "string" } }, "additionalProperties": false }, "FormBody": { "type": "object", "description": "URL-encoded form body", "additionalProperties": { "type": "string" }, "default": {} }, "GraphqlRequest": { "type": "object", "description": "GraphQL query definition", "required": ["query"], "properties": { "query": { "type": "string", "description": "GraphQL query or mutation string" }, "variables": { "description": "Variables to pass to the query" }, "operation_name": { "type": "string", "description": "Operation name when query contains multiple operations" } }, "additionalProperties": false }, "MultipartBody": { "type": "object", "description": "Multipart form data body for file uploads", "properties": { "fields": { "type": "array", "description": "Text form fields", "items": { "type": "object", "required": ["name", "value"], "properties": { "name": { "type": "string", "description": "Field name" }, "value": { "type": "string", "description": "Field value" } }, "additionalProperties": false } }, "files": { "type": "array", "description": "File upload fields", "items": { "type": "object", "required": ["name", "path"], "properties": { "name": { "type": "string", "description": "Form field name" }, "path": { "type": "string", "description": "Path to file (relative to test file)" }, "content_type": { "type": "string", "description": "MIME content type" }, "filename": { "type": "string", "description": "Override filename in form" } }, "additionalProperties": false } } }, "additionalProperties": false }, "Assertion": { "type": "object", "description": "Assertions on the HTTP response", "properties": { "status": { "description": "Expected HTTP status code. Supports exact (200), shorthand ('2xx'), set ({ in: [200, 201] }), or range ({ gte: 400, lt: 500 })", "oneOf": [ { "type": "integer", "description": "Exact status code", "minimum": 100, "maximum": 599 }, { "type": "string", "description": "Shorthand range (e.g., '2xx', '4xx', '5xx')", "pattern": "^[1-5]xx$" }, { "type": "object", "description": "Complex status assertion", "properties": { "in": { "type": "array", "items": { "type": "integer", "minimum": 100, "maximum": 599 }, "description": "Set of allowed status codes" }, "gte": { "type": "integer", "description": "Greater than or equal" }, "gt": { "type": "integer", "description": "Greater than" }, "lte": { "type": "integer", "description": "Less than or equal" }, "lt": { "type": "integer", "description": "Less than" } }, "additionalProperties": false } ] }, "duration": { "type": "string", "description": "Response time assertion", "pattern": "^[<>]=?\\s*\\d+(ms|s)?$", "examples": ["< 500ms", "<= 1s", "> 100ms", ">= 200ms"] }, "redirect": { "$ref": "#/$defs/RedirectAssertion" }, "headers": { "type": "object", "description": "Header assertions. Values can be exact match, 'contains \"substring\"', or 'matches \"regex\"'. Header names are case-insensitive.", "additionalProperties": { "type": "string" }, "examples": [{ "content-type": "application/json", "x-request-id": "matches \"^[a-f0-9-]{36}$\"", "x-custom": "contains \"value\"" }] }, "body": { "type": "object", "description": "Body assertions using JSONPath keys. Values can be literal (exact match) or operator objects.", "additionalProperties": { "oneOf": [ { "type": "string", "description": "Exact string match" }, { "type": "number", "description": "Exact number match" }, { "type": "boolean", "description": "Exact boolean match" }, { "type": "null", "description": "Assert field is null" }, { "$ref": "#/$defs/BodyAssertionOperators" } ] }, "examples": [{ "$.id": { "type": "string", "not_empty": true }, "$.name": "Jane Doe", "$.age": { "gt": 18, "lt": 100 }, "$.tags": { "type": "array", "length": 2, "contains": "admin" }, "$.email": { "matches": "^[^@]+@[^@]+$" }, "$.notes": { "empty": true }, "$": { "bytes": 15 }, "$.payload": { "sha256": "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824" }, "$.legacy": { "md5": "5d41402abc4b2a76b9719d911017c592" }, "$.requestId": { "is_uuid": true }, "$.createdAt": { "is_date": true }, "$.clientIp": { "is_ipv4": true }, "$.serverIp": { "is_ipv6": true }, "$.deletedAt": null }] } }, "additionalProperties": false }, "RedirectAssertion": { "type": "object", "description": "Assertions against the final redirect target and the number of followed redirects", "properties": { "url": { "type": "string", "description": "Expected final response URL after following redirects" }, "count": { "type": "integer", "minimum": 0, "description": "Expected number of followed redirects" } }, "minProperties": 1, "additionalProperties": false, "examples": [{ "url": "https://api.example.com/health", "count": 2 }] }, "BodyAssertionOperators": { "type": "object", "description": "Assertion operators for body field checks. Multiple operators on the same field use AND logic.", "properties": { "eq": { "description": "Exact equality (same as literal value)" }, "not_eq": { "description": "Not equal to value" }, "type": { "type": "string", "description": "Assert the JSON type of the value", "enum": ["string", "number", "boolean", "array", "object", "null"] }, "contains": { "description": "For strings: substring match. For arrays: element exists in array.", "oneOf": [ { "type": "string" }, { "type": "number" }, { "type": "boolean" } ] }, "not_contains": { "description": "Inverse of contains", "oneOf": [ { "type": "string" }, { "type": "number" }, { "type": "boolean" } ] }, "starts_with": { "type": "string", "description": "Assert string starts with prefix" }, "ends_with": { "type": "string", "description": "Assert string ends with suffix" }, "matches": { "type": "string", "description": "Assert string matches regex pattern" }, "is_uuid": { "type": "boolean", "description": "Assert value is a valid UUID (any version)", "const": true }, "is_uuid_v4": { "type": "boolean", "description": "Assert value is a valid UUID v4 (random)", "const": true }, "is_uuid_v7": { "type": "boolean", "description": "Assert value is a valid UUID v7 (time-ordered)", "const": true }, "is_date": { "type": "boolean", "description": "Assert value is a valid date or datetime string", "const": true }, "is_ipv4": { "type": "boolean", "description": "Assert value is a valid IPv4 address", "const": true }, "is_ipv6": { "type": "boolean", "description": "Assert value is a valid IPv6 address", "const": true }, "empty": { "type": "boolean", "description": "Assert value is empty (strings, arrays, objects, or null)", "const": true }, "is_empty": { "type": "boolean", "description": "Alias for empty; assert value is empty (strings, arrays, objects, or null)", "const": true }, "not_empty": { "type": "boolean", "description": "Assert value is not empty (strings, arrays, objects)", "const": true }, "bytes": { "type": "integer", "description": "Assert byte length of the matched value. For `$`, uses the raw response body bytes.", "minimum": 0 }, "sha256": { "type": "string", "description": "Assert SHA-256 hex digest of the matched value. For `$`, uses the raw response body bytes." }, "md5": { "type": "string", "description": "Assert MD5 hex digest of the matched value. For `$`, uses the raw response body bytes." }, "exists": { "type": "boolean", "description": "Assert field exists (true) or does not exist (false)" }, "length": { "type": "integer", "description": "Exact length of string or array", "minimum": 0 }, "length_gt": { "type": "integer", "description": "Length greater than", "minimum": 0 }, "length_gte": { "type": "integer", "description": "Length greater than or equal", "minimum": 0 }, "length_lte": { "type": "integer", "description": "Length less than or equal", "minimum": 0 }, "gt": { "type": "number", "description": "Greater than (numeric comparison)" }, "gte": { "type": "number", "description": "Greater than or equal (numeric comparison)" }, "lt": { "type": "number", "description": "Less than (numeric comparison)" }, "lte": { "type": "number", "description": "Less than or equal (numeric comparison)" }, "exists_where": { "type": "object", "description": "Assert the target array contains at least one object matching the given { field: value } predicate. Use this instead of `$[N]` lookups and exact `length:` to assert identity-based membership on shared list endpoints.", "additionalProperties": true }, "not_exists_where": { "type": "object", "description": "Assert the target array contains no object matching the given { field: value } predicate.", "additionalProperties": true }, "contains_object": { "type": "object", "description": "Alias for exists_where — assert the target array contains an object with the given fields.", "additionalProperties": true } }, "additionalProperties": false } } }