@kirrosh/zond 0.21.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +758 -3
- package/README.md +78 -15
- package/package.json +17 -10
- package/src/cli/argv.ts +122 -0
- package/src/cli/commands/add-api.ts +134 -0
- package/src/cli/commands/api/annotate/idempotency.ts +59 -0
- package/src/cli/commands/api/annotate/index.ts +525 -0
- package/src/cli/commands/api/annotate/lifecycle.ts +74 -0
- package/src/cli/commands/api/annotate/overlay.ts +206 -0
- package/src/cli/commands/api/annotate/pagination.ts +60 -0
- package/src/cli/commands/api/annotate/prompts.ts +183 -0
- package/src/cli/commands/api/annotate/readback.ts +58 -0
- package/src/cli/commands/api/annotate/resources.ts +91 -0
- package/src/cli/commands/api/annotate/seed-bodies.ts +61 -0
- package/src/cli/commands/audit.ts +480 -0
- package/src/cli/commands/bootstrap.ts +710 -0
- package/src/cli/commands/catalog.ts +35 -0
- package/src/cli/commands/check.ts +348 -0
- package/src/cli/commands/checks.ts +756 -0
- package/src/cli/commands/ci-init.ts +55 -6
- package/src/cli/commands/clean.ts +212 -0
- package/src/cli/commands/cleanup.ts +262 -0
- package/src/cli/commands/completions.ts +192 -0
- package/src/cli/commands/coverage.ts +605 -132
- package/src/cli/commands/db.ts +180 -8
- package/src/cli/commands/describe.ts +37 -2
- package/src/cli/commands/discover.ts +1236 -0
- package/src/cli/commands/doctor.ts +607 -0
- package/src/cli/commands/fixtures.ts +402 -0
- package/src/cli/commands/generate.ts +420 -47
- package/src/cli/commands/init/agents-md.ts +61 -0
- package/src/cli/commands/init/bootstrap.ts +108 -0
- package/src/cli/commands/init/index.ts +244 -0
- package/src/cli/commands/init/skills.ts +98 -0
- package/src/cli/commands/init/templates/agents.md +77 -0
- package/src/cli/commands/init/templates/markdown.d.ts +4 -0
- package/src/cli/commands/init/templates/skills/zond-checks.md +397 -0
- package/src/cli/commands/init/templates/skills/zond-triage.md +210 -0
- package/src/cli/commands/init/templates/skills/zond.md +651 -0
- package/src/cli/commands/init/templates/zond-config.yml +14 -0
- package/src/cli/commands/prepare-fixtures.ts +135 -0
- package/src/cli/commands/probe/mass-assignment.ts +503 -0
- package/src/cli/commands/probe/security.ts +454 -0
- package/src/cli/commands/probe/static.ts +255 -0
- package/src/cli/commands/probe/webhooks.ts +161 -0
- package/src/cli/commands/probe.ts +459 -0
- package/src/cli/commands/reference.ts +87 -0
- package/src/cli/commands/refresh-api.ts +169 -0
- package/src/cli/commands/remove-api.ts +150 -0
- package/src/cli/commands/report-bundle.ts +318 -0
- package/src/cli/commands/report.ts +241 -0
- package/src/cli/commands/request.ts +379 -4
- package/src/cli/commands/run.ts +911 -33
- package/src/cli/commands/session.ts +244 -0
- package/src/cli/commands/use.ts +74 -0
- package/src/cli/index.ts +36 -607
- package/src/cli/json-envelope.ts +112 -3
- package/src/cli/json-schemas.ts +263 -0
- package/src/cli/program.ts +218 -0
- package/src/cli/resolve.ts +105 -0
- package/src/cli/status-filter.ts +124 -0
- package/src/cli/util/api-context.ts +85 -0
- package/src/cli/version.ts +8 -0
- package/src/core/anti-fp/bootstrap.ts +34 -0
- package/src/core/anti-fp/index.ts +33 -0
- package/src/core/anti-fp/registry.ts +44 -0
- package/src/core/anti-fp/rules/baseline-echo.ts +74 -0
- package/src/core/anti-fp/rules/schemathesis/body_negation_becomes_valid.ts +52 -0
- package/src/core/anti-fp/rules/schemathesis/coverage_phase_boundary_positive.ts +38 -0
- package/src/core/anti-fp/rules/schemathesis/has_unverifiable_mutations.ts +35 -0
- package/src/core/anti-fp/rules/schemathesis/index.ts +24 -0
- package/src/core/anti-fp/rules/schemathesis/string_type_mutation_becomes_valid.ts +53 -0
- package/src/core/anti-fp/rules/subscription-gated/index.ts +11 -0
- package/src/core/anti-fp/rules/subscription-gated/paid-plan-403.ts +75 -0
- package/src/core/anti-fp/types.ts +68 -0
- package/src/core/checks/checks/_crud-helpers.ts +133 -0
- package/src/core/checks/checks/_negative_mutator.ts +133 -0
- package/src/core/checks/checks/_readback-helpers.ts +133 -0
- package/src/core/checks/checks/content_type_conformance.ts +39 -0
- package/src/core/checks/checks/cross_call_references.ts +134 -0
- package/src/core/checks/checks/ensure_resource_availability.ts +62 -0
- package/src/core/checks/checks/idempotency_replay.ts +246 -0
- package/src/core/checks/checks/ignored_auth.ts +211 -0
- package/src/core/checks/checks/index.ts +65 -0
- package/src/core/checks/checks/lifecycle_transitions.ts +273 -0
- package/src/core/checks/checks/missing_required_header.ts +40 -0
- package/src/core/checks/checks/negative_data_rejection.ts +45 -0
- package/src/core/checks/checks/not_a_server_error.ts +27 -0
- package/src/core/checks/checks/open_cors_on_sensitive.ts +131 -0
- package/src/core/checks/checks/pagination_invariants.ts +238 -0
- package/src/core/checks/checks/positive_data_acceptance.ts +36 -0
- package/src/core/checks/checks/rate_limit_headers_absent.ts +77 -0
- package/src/core/checks/checks/response_headers_conformance.ts +74 -0
- package/src/core/checks/checks/response_schema_conformance.ts +30 -0
- package/src/core/checks/checks/status_code_conformance.ts +61 -0
- package/src/core/checks/checks/unsupported_method.ts +63 -0
- package/src/core/checks/checks/use_after_free.ts +78 -0
- package/src/core/checks/index.ts +30 -0
- package/src/core/checks/mode.ts +79 -0
- package/src/core/checks/recommended-action.ts +64 -0
- package/src/core/checks/registry.ts +78 -0
- package/src/core/checks/runner.ts +874 -0
- package/src/core/checks/sarif.ts +230 -0
- package/src/core/checks/stateful.ts +121 -0
- package/src/core/checks/types.ts +189 -0
- package/src/core/classifier/recommended-action.ts +222 -0
- package/src/core/context/current.ts +51 -0
- package/src/core/context/session.ts +78 -0
- package/src/core/coverage/loader.ts +185 -0
- package/src/core/coverage/reasons.ts +300 -0
- package/src/core/diagnostics/db-analysis.ts +161 -12
- package/src/core/diagnostics/failure-class.ts +120 -0
- package/src/core/diagnostics/failure-hints.ts +212 -9
- package/src/core/diagnostics/spec-pointer.ts +99 -0
- package/src/core/diagnostics/suggested-fixes.ts +156 -0
- package/src/core/exporter/case-study/index.ts +270 -0
- package/src/core/exporter/curl.ts +40 -0
- package/src/core/exporter/exporter.ts +48 -0
- package/src/core/exporter/html-report/escape.ts +24 -0
- package/src/core/exporter/html-report/index.ts +479 -0
- package/src/core/exporter/html-report/script.ts +100 -0
- package/src/core/exporter/html-report/styles.ts +408 -0
- package/src/core/generator/chunker.ts +53 -15
- package/src/core/generator/coverage-phase.ts +0 -0
- package/src/core/generator/create-body.ts +89 -0
- package/src/core/generator/data-factory.ts +490 -33
- package/src/core/generator/describe.ts +1 -1
- package/src/core/generator/fixtures-builder.ts +325 -0
- package/src/core/generator/index.ts +7 -5
- package/src/core/generator/openapi-reader.ts +55 -3
- package/src/core/generator/path-param-disambig.ts +114 -0
- package/src/core/generator/resources-builder.ts +648 -0
- package/src/core/generator/schema-utils.ts +11 -3
- package/src/core/generator/serializer.ts +114 -15
- package/src/core/generator/suite-generator.ts +484 -77
- package/src/core/generator/types.ts +8 -0
- package/src/core/identity/identity-file.ts +129 -0
- package/src/core/lint/affects.ts +28 -0
- package/src/core/lint/config.ts +96 -0
- package/src/core/lint/format.ts +42 -0
- package/src/core/lint/index.ts +94 -0
- package/src/core/lint/reporter.ts +128 -0
- package/src/core/lint/rules/consistency.ts +158 -0
- package/src/core/lint/rules/heuristics.ts +97 -0
- package/src/core/lint/rules/strictness.ts +109 -0
- package/src/core/lint/types.ts +96 -0
- package/src/core/lint/walker.ts +248 -0
- package/src/core/meta/meta-store.ts +6 -73
- package/src/core/output/README.md +91 -0
- package/src/core/output/index.ts +13 -0
- package/src/core/output/run.ts +126 -0
- package/src/core/output/types.ts +129 -0
- package/src/core/parser/env-interpolation.ts +104 -0
- package/src/core/parser/filter.ts +57 -0
- package/src/core/parser/schema.ts +132 -5
- package/src/core/parser/types.ts +29 -2
- package/src/core/parser/variables.ts +0 -0
- package/src/core/parser/yaml-parser.ts +108 -13
- package/src/core/probe/bootstrap.ts +34 -0
- package/src/core/probe/dry-run-envelope.ts +57 -0
- package/src/core/probe/mass-assignment-probe-class.ts +198 -0
- package/src/core/probe/mass-assignment-probe.ts +1122 -0
- package/src/core/probe/mass-assignment-template.ts +212 -0
- package/src/core/probe/method-probe.ts +164 -0
- package/src/core/probe/method-shared.ts +69 -0
- package/src/core/probe/negative-probe.ts +691 -0
- package/src/core/probe/orphan-tracker.ts +188 -0
- package/src/core/probe/path-discovery.ts +440 -0
- package/src/core/probe/probe-harness.ts +120 -0
- package/src/core/probe/registry.ts +89 -0
- package/src/core/probe/runner.ts +136 -0
- package/src/core/probe/security-probe-class.ts +201 -0
- package/src/core/probe/security-probe.ts +1453 -0
- package/src/core/probe/shared.ts +505 -0
- package/src/core/probe/static-probe-class.ts +125 -0
- package/src/core/probe/types.ts +165 -0
- package/src/core/probe/verdict-aggregator.ts +33 -0
- package/src/core/probe/webhooks-probe.ts +284 -0
- package/src/core/reporter/console.ts +69 -4
- package/src/core/reporter/index.ts +2 -3
- package/src/core/reporter/json.ts +15 -2
- package/src/core/reporter/junit.ts +27 -12
- package/src/core/reporter/ndjson.ts +37 -0
- package/src/core/reporter/types.ts +3 -0
- package/src/core/runner/assertions.ts +62 -2
- package/src/core/runner/async-pool.ts +108 -0
- package/src/core/runner/auth-path.ts +8 -0
- package/src/core/runner/ci-context.ts +72 -0
- package/src/core/runner/executor.ts +391 -52
- package/src/core/runner/form-encode.ts +51 -0
- package/src/core/runner/http-client.ts +115 -7
- package/src/core/runner/learn-drift.ts +293 -0
- package/src/core/runner/preflight-vars.ts +149 -0
- package/src/core/runner/progress-tracker.ts +73 -0
- package/src/core/runner/rate-limiter.ts +203 -0
- package/src/core/runner/run-kind.ts +39 -0
- package/src/core/runner/schema-validator.ts +312 -0
- package/src/core/runner/send-request.ts +153 -20
- package/src/core/runner/types.ts +38 -0
- package/src/core/secrets/registry.ts +164 -0
- package/src/core/secrets/secrets-file.ts +115 -0
- package/src/core/selectors/operation-filter.ts +144 -0
- package/src/core/setup-api.ts +419 -17
- package/src/core/severity/category.ts +94 -0
- package/src/core/severity/index.ts +121 -0
- package/src/core/spec/layers.ts +154 -0
- package/src/core/util/format-eta.ts +21 -0
- package/src/core/utils.ts +5 -1
- package/src/core/workspace/config.ts +129 -0
- package/src/core/workspace/manifest.ts +283 -0
- package/src/core/workspace/output-rotation.ts +62 -0
- package/src/core/workspace/root.ts +94 -0
- package/src/core/workspace/triage-path.ts +87 -0
- package/src/db/lint-runs.ts +47 -0
- package/src/db/migrate.ts +126 -0
- package/src/db/migrations/0001_run_kind.sql +25 -0
- package/src/db/migrations/sql.d.ts +4 -0
- package/src/db/queries/collections.ts +133 -0
- package/src/db/queries/coverage.ts +9 -0
- package/src/db/queries/dashboard.ts +59 -0
- package/src/db/queries/results.ts +128 -0
- package/src/db/queries/runs.ts +235 -0
- package/src/db/queries/sessions.ts +42 -0
- package/src/db/queries/settings.ts +28 -0
- package/src/db/queries/types.ts +172 -0
- package/src/db/queries.ts +72 -802
- package/src/db/schema.ts +179 -48
- package/src/cli/commands/export.ts +0 -144
- package/src/cli/commands/guide.ts +0 -127
- package/src/cli/commands/init.ts +0 -57
- package/src/cli/commands/serve.ts +0 -81
- package/src/cli/commands/sync.ts +0 -269
- package/src/cli/commands/update.ts +0 -189
- package/src/cli/commands/validate.ts +0 -34
- package/src/core/exporter/postman.ts +0 -963
- package/src/core/generator/guide-builder.ts +0 -253
- package/src/core/meta/types.ts +0 -21
- package/src/core/parser/index.ts +0 -21
- package/src/core/runner/execute-run.ts +0 -132
- package/src/core/runner/index.ts +0 -12
- package/src/core/sync/spec-differ.ts +0 -38
- package/src/web/data/collection-state.ts +0 -362
- package/src/web/routes/api.ts +0 -314
- package/src/web/routes/dashboard.ts +0 -350
- package/src/web/routes/runs.ts +0 -64
- package/src/web/schemas.ts +0 -121
- package/src/web/server.ts +0 -134
- package/src/web/static/htmx.min.cjs +0 -1
- package/src/web/static/style.css +0 -1148
- package/src/web/views/endpoints-tab.ts +0 -174
- package/src/web/views/explorer-tab.ts +0 -402
- package/src/web/views/health-strip.ts +0 -92
- package/src/web/views/layout.ts +0 -48
- package/src/web/views/results.ts +0 -210
- package/src/web/views/runs-tab.ts +0 -126
- package/src/web/views/suites-tab.ts +0 -181
|
@@ -3,13 +3,47 @@ import type { OpenAPIV3 } from "openapi-types";
|
|
|
3
3
|
/**
|
|
4
4
|
* Recursively generates test data from an OpenAPI schema.
|
|
5
5
|
* Uses heuristic placeholders ({{$...}} generators) where possible.
|
|
6
|
+
*
|
|
7
|
+
* `forRequest` (default true) toggles request-body filters that strip
|
|
8
|
+
* server-assigned fields the client must not send: properties marked
|
|
9
|
+
* `readOnly: true`, and the literal field name `id` at any object level
|
|
10
|
+
* (universally server-assigned in REST). Pass `forRequest: false` to
|
|
11
|
+
* preserve full schema shape for response-side fixtures.
|
|
6
12
|
*/
|
|
7
13
|
export function generateFromSchema(
|
|
8
14
|
schema: OpenAPIV3.SchemaObject,
|
|
9
15
|
propertyName?: string,
|
|
10
|
-
_depth =
|
|
16
|
+
opts: { _depth?: number; forRequest?: boolean } = {},
|
|
11
17
|
): unknown {
|
|
12
|
-
|
|
18
|
+
const _depth = opts._depth ?? 0;
|
|
19
|
+
const forRequest = opts.forRequest ?? true;
|
|
20
|
+
const recurse = (s: OpenAPIV3.SchemaObject, name?: string) =>
|
|
21
|
+
generateFromSchema(s, name, { _depth: _depth + 1, forRequest });
|
|
22
|
+
|
|
23
|
+
if (_depth > 7) {
|
|
24
|
+
return depthLimitDefault(schema, propertyName);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Highest-priority signal: explicit example from spec.
|
|
28
|
+
// Beats enum, format, heuristics — the spec author told us what to send.
|
|
29
|
+
// Two exceptions:
|
|
30
|
+
// 1. `null` examples are noise (often nullable: true with no real example) —
|
|
31
|
+
// skip so we fall through to type/format defaults instead of emitting null.
|
|
32
|
+
// 2. UUID-shaped examples on FK-context fields (name ends with `_id` or
|
|
33
|
+
// schema.format === "uuid") are usually copy-pasted from another tenant's
|
|
34
|
+
// spec. Honoring them leaks foreign IDs and guarantees 422 on real APIs;
|
|
35
|
+
// `{{$uuid}}` is at least an honest test placeholder.
|
|
36
|
+
//
|
|
37
|
+
// OpenAPI 3.1 / JSON Schema also allows `examples: [...]` (plural array). When
|
|
38
|
+
// both are present `example` wins; otherwise pick the first non-null entry
|
|
39
|
+
// from `examples` and apply the same FK-UUID guard. `example` (singular) is
|
|
40
|
+
// still the OpenAPI 3.0 form and remains supported.
|
|
41
|
+
const exampleValue = pickExampleValue(schema);
|
|
42
|
+
if (exampleValue !== undefined) {
|
|
43
|
+
if (!isLikelyForeignFKExample(schema, propertyName, exampleValue)) {
|
|
44
|
+
return exampleValue;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
13
47
|
|
|
14
48
|
// allOf: merge all schemas
|
|
15
49
|
if (schema.allOf) {
|
|
@@ -20,26 +54,68 @@ export function generateFromSchema(
|
|
|
20
54
|
merged.properties = { ...merged.properties, ...s.properties };
|
|
21
55
|
}
|
|
22
56
|
}
|
|
23
|
-
return
|
|
57
|
+
return recurse(merged, propertyName);
|
|
24
58
|
}
|
|
25
59
|
|
|
26
|
-
// oneOf / anyOf:
|
|
60
|
+
// oneOf / anyOf: pick the most informative variant. Prefer objects with
|
|
61
|
+
// properties over loose primitives — APIs that accept `Array<{id}>|Array<string>`
|
|
62
|
+
// need the object variant, not a string that 422s. Falls back to first
|
|
63
|
+
// non-null entry.
|
|
64
|
+
//
|
|
65
|
+
// ARV-78 (feedback round-04 / F25): when the parent schema declares a
|
|
66
|
+
// `discriminator: { propertyName, mapping? }` (typical OpenAPI 3 polymorphism —
|
|
67
|
+
// /automations.steps with type=trigger|action), pick the variant whose
|
|
68
|
+
// discriminator property carries a const/enum-single value and stamp that
|
|
69
|
+
// value into the result. Without this, generator emits a random variant and
|
|
70
|
+
// the API 422s with "Missing <required-by-other-variant>".
|
|
27
71
|
if (schema.oneOf) {
|
|
28
|
-
|
|
72
|
+
const variants = schema.oneOf as OpenAPIV3.SchemaObject[];
|
|
73
|
+
const picked = pickDiscriminatorVariant(variants, schema.discriminator?.propertyName)
|
|
74
|
+
?? pickPreferredVariant(variants);
|
|
75
|
+
const result = recurse(picked, propertyName);
|
|
76
|
+
return stampDiscriminator(result, picked, schema.discriminator?.propertyName);
|
|
29
77
|
}
|
|
30
78
|
if (schema.anyOf) {
|
|
31
|
-
|
|
79
|
+
const variants = schema.anyOf as OpenAPIV3.SchemaObject[];
|
|
80
|
+
const picked = pickDiscriminatorVariant(variants, schema.discriminator?.propertyName)
|
|
81
|
+
?? pickPreferredVariant(variants);
|
|
82
|
+
const result = recurse(picked, propertyName);
|
|
83
|
+
return stampDiscriminator(result, picked, schema.discriminator?.propertyName);
|
|
32
84
|
}
|
|
33
85
|
|
|
34
|
-
// enum: first value
|
|
86
|
+
// enum: first value (always valid for the API contract)
|
|
35
87
|
if (schema.enum && schema.enum.length > 0) {
|
|
36
88
|
return schema.enum[0];
|
|
37
89
|
}
|
|
38
90
|
|
|
39
|
-
//
|
|
40
|
-
|
|
91
|
+
// Format-based placeholders override type resolution. Schemas in the wild
|
|
92
|
+
// commonly carry `format` without an explicit `type` (loosely-defined specs)
|
|
93
|
+
// or with `type: ["string", "null"]` (OpenAPI 3.1 nullable). Falling through
|
|
94
|
+
// to the type switch in those cases dropped us into the default branch and
|
|
95
|
+
// produced `{{$randomString}}` for `format: email` — TASK-86 regression.
|
|
96
|
+
const formatPlaceholder = formatToPlaceholder(schema.format);
|
|
97
|
+
if (formatPlaceholder !== undefined) return formatPlaceholder;
|
|
41
98
|
|
|
42
|
-
|
|
99
|
+
// OpenAPI 3.1: type can be `["string", "null"]`. Collapse to the first
|
|
100
|
+
// non-null entry so the switch below routes correctly.
|
|
101
|
+
let effectiveType = Array.isArray(schema.type)
|
|
102
|
+
? (schema.type as string[]).find(t => t !== "null") as OpenAPIV3.SchemaObject["type"] | undefined
|
|
103
|
+
: schema.type;
|
|
104
|
+
|
|
105
|
+
// ARV-67 (feedback round-01 / F7): schemas in the wild routinely omit
|
|
106
|
+
// `type` on nested-object fields and rely on `properties` / `required`
|
|
107
|
+
// / `items` to convey shape. Without the salvage below, the default
|
|
108
|
+
// branch returns "{{$randomString}}" for a missing-type field — which
|
|
109
|
+
// is what made `prepare-fixtures --seed` send a string for nested
|
|
110
|
+
// objects like `automations.config` / `automations.steps` and earn
|
|
111
|
+
// "Expected object, received string" 422s. Infer the type
|
|
112
|
+
// from structural hints when nothing else gives one.
|
|
113
|
+
if (effectiveType === undefined) {
|
|
114
|
+
if ((schema as { items?: unknown }).items !== undefined) effectiveType = "array";
|
|
115
|
+
else if (schema.properties || Array.isArray(schema.required)) effectiveType = "object";
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
switch (effectiveType) {
|
|
43
119
|
case "string":
|
|
44
120
|
return guessStringPlaceholder(schema, propertyName);
|
|
45
121
|
|
|
@@ -53,8 +129,9 @@ export function generateFromSchema(
|
|
|
53
129
|
return true;
|
|
54
130
|
|
|
55
131
|
case "array": {
|
|
56
|
-
|
|
57
|
-
|
|
132
|
+
const arr = schema as OpenAPIV3.ArraySchemaObject;
|
|
133
|
+
if (arr.items) {
|
|
134
|
+
const item = recurse(arr.items as OpenAPIV3.SchemaObject, undefined);
|
|
58
135
|
return [item];
|
|
59
136
|
}
|
|
60
137
|
return [];
|
|
@@ -66,20 +143,27 @@ export function generateFromSchema(
|
|
|
66
143
|
if (schema.properties) {
|
|
67
144
|
const obj: Record<string, unknown> = {};
|
|
68
145
|
for (const [key, propSchema] of Object.entries(schema.properties)) {
|
|
69
|
-
|
|
146
|
+
const ps = propSchema as OpenAPIV3.SchemaObject;
|
|
147
|
+
if (forRequest && shouldSkipForRequest(key, ps)) continue;
|
|
148
|
+
obj[key] = recurse(ps, key);
|
|
70
149
|
}
|
|
71
150
|
return obj;
|
|
72
151
|
}
|
|
73
|
-
// Record type
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
152
|
+
// Record type (additionalProperties only). The historical behavior was
|
|
153
|
+
// to materialize fake `key1`/`key2` entries to make the shape visible.
|
|
154
|
+
// Real APIs reject those — the keys are always domain-specific
|
|
155
|
+
// (filter names, label keys). Emit an empty `{}` instead: it preserves
|
|
156
|
+
// the object type (so type-validators pass) without injecting payloads
|
|
157
|
+
// the server didn't ask for. Callers who need realistic record content
|
|
158
|
+
// should override via fixture-pack/.env.yaml.
|
|
159
|
+
if (
|
|
160
|
+
(schema.additionalProperties && typeof schema.additionalProperties === "object") ||
|
|
161
|
+
schema.additionalProperties === true
|
|
162
|
+
) {
|
|
163
|
+
return {};
|
|
80
164
|
}
|
|
81
165
|
// Bare object with no properties
|
|
82
|
-
if (
|
|
166
|
+
if (effectiveType === "object") {
|
|
83
167
|
return {};
|
|
84
168
|
}
|
|
85
169
|
return "{{$randomString}}";
|
|
@@ -87,6 +171,306 @@ export function generateFromSchema(
|
|
|
87
171
|
}
|
|
88
172
|
}
|
|
89
173
|
|
|
174
|
+
/** Fields the client must not send in a request body: explicit `readOnly: true`,
|
|
175
|
+
* or the literal name `id`. The latter is a heuristic for under-specified specs
|
|
176
|
+
* (common in in-house APIs) that don't mark the server-assigned id readOnly
|
|
177
|
+
* but still 4xx on it being present. */
|
|
178
|
+
function shouldSkipForRequest(name: string, schema: OpenAPIV3.SchemaObject): boolean {
|
|
179
|
+
if (schema.readOnly === true) return true;
|
|
180
|
+
if (name === "id") return true;
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** When recursion hits the depth cap, return a type-appropriate placeholder
|
|
185
|
+
* rather than `{}` — `{}` for `array<string>` produces `[{}]` which 422s on
|
|
186
|
+
* every realistic API. */
|
|
187
|
+
function depthLimitDefault(schema: OpenAPIV3.SchemaObject, name?: string): unknown {
|
|
188
|
+
const t = Array.isArray(schema.type)
|
|
189
|
+
? (schema.type as string[]).find(x => x !== "null")
|
|
190
|
+
: schema.type;
|
|
191
|
+
switch (t) {
|
|
192
|
+
case "string": return formatToPlaceholder(schema.format) ?? guessStringPlaceholder(schema, name);
|
|
193
|
+
case "integer": return 1;
|
|
194
|
+
case "number": return 1;
|
|
195
|
+
case "boolean": return true;
|
|
196
|
+
case "array": return [];
|
|
197
|
+
case "object":
|
|
198
|
+
default: return {};
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** ARV-78 (F25): when a parent oneOf/anyOf carries `discriminator.propertyName`,
|
|
203
|
+
* pick the variant whose discriminator property has a single-value enum or
|
|
204
|
+
* const so its identity is unambiguous. Returns undefined when nothing
|
|
205
|
+
* qualifies — caller falls back to pickPreferredVariant. */
|
|
206
|
+
function pickDiscriminatorVariant(
|
|
207
|
+
variants: OpenAPIV3.SchemaObject[],
|
|
208
|
+
propertyName: string | undefined,
|
|
209
|
+
): OpenAPIV3.SchemaObject | undefined {
|
|
210
|
+
if (!propertyName) return undefined;
|
|
211
|
+
for (const v of variants) {
|
|
212
|
+
const prop = v.properties?.[propertyName] as OpenAPIV3.SchemaObject | undefined;
|
|
213
|
+
if (!prop) continue;
|
|
214
|
+
const en = (prop as { enum?: unknown[] }).enum;
|
|
215
|
+
const cn = (prop as { const?: unknown }).const;
|
|
216
|
+
if (Array.isArray(en) && en.length === 1) return v;
|
|
217
|
+
if (cn !== undefined && cn !== null) return v;
|
|
218
|
+
}
|
|
219
|
+
return undefined;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Stamp the discriminator key onto a generated object. Without this the
|
|
223
|
+
* variant choice is "anonymous" from the body's point of view — APIs that
|
|
224
|
+
* switch on `type` reject the request even when every other field is
|
|
225
|
+
* perfect. No-op when the propertyName is missing or the variant lacks an
|
|
226
|
+
* enum/const for that property. */
|
|
227
|
+
function stampDiscriminator(
|
|
228
|
+
result: unknown,
|
|
229
|
+
variant: OpenAPIV3.SchemaObject,
|
|
230
|
+
propertyName: string | undefined,
|
|
231
|
+
): unknown {
|
|
232
|
+
if (!propertyName) return result;
|
|
233
|
+
if (!result || typeof result !== "object" || Array.isArray(result)) return result;
|
|
234
|
+
const prop = variant.properties?.[propertyName] as OpenAPIV3.SchemaObject | undefined;
|
|
235
|
+
if (!prop) return result;
|
|
236
|
+
const en = (prop as { enum?: unknown[] }).enum;
|
|
237
|
+
const cn = (prop as { const?: unknown }).const;
|
|
238
|
+
let stamp: unknown;
|
|
239
|
+
if (Array.isArray(en) && en.length === 1) stamp = en[0];
|
|
240
|
+
else if (cn !== undefined && cn !== null) stamp = cn;
|
|
241
|
+
else return result;
|
|
242
|
+
(result as Record<string, unknown>)[propertyName] = stamp;
|
|
243
|
+
return result;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** Prefer the most data-shape-informative variant from a oneOf/anyOf list:
|
|
247
|
+
* object-with-properties > non-null > first. Skips `type: "null"` entries
|
|
248
|
+
* introduced by 3.1 nullable shorthand. */
|
|
249
|
+
function pickPreferredVariant(variants: OpenAPIV3.SchemaObject[]): OpenAPIV3.SchemaObject {
|
|
250
|
+
const isNull = (s: OpenAPIV3.SchemaObject) =>
|
|
251
|
+
(s as { type?: unknown }).type === "null";
|
|
252
|
+
const nonNull = variants.filter(v => !isNull(v));
|
|
253
|
+
const pool = nonNull.length > 0 ? nonNull : variants;
|
|
254
|
+
|
|
255
|
+
const objectWithProps = pool.find(
|
|
256
|
+
v => v.type === "object" && v.properties && Object.keys(v.properties).length > 0,
|
|
257
|
+
);
|
|
258
|
+
if (objectWithProps) return objectWithProps;
|
|
259
|
+
|
|
260
|
+
return pool[0]!;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
264
|
+
|
|
265
|
+
/** Names that strongly imply an email field. Kept in sync with the email
|
|
266
|
+
* branch of `guessStringPlaceholder`/`classifyFieldSource`. Used to gate the
|
|
267
|
+
* description-based domain heuristic so phrases like "verified sending
|
|
268
|
+
* domain" in the description of a `from`/`to` field don't override the
|
|
269
|
+
* email mapping. */
|
|
270
|
+
function isEmailContextName(name?: string): boolean {
|
|
271
|
+
if (!name) return false;
|
|
272
|
+
const lower = name.toLowerCase();
|
|
273
|
+
return (
|
|
274
|
+
lower === "email" ||
|
|
275
|
+
lower === "from" ||
|
|
276
|
+
lower === "to" ||
|
|
277
|
+
lower === "cc" ||
|
|
278
|
+
lower === "bcc" ||
|
|
279
|
+
lower === "sender" ||
|
|
280
|
+
lower === "recipient" ||
|
|
281
|
+
lower === "reply_to" ||
|
|
282
|
+
lower === "replyto" ||
|
|
283
|
+
lower.endsWith("_email") ||
|
|
284
|
+
lower.endsWith("Email") ||
|
|
285
|
+
lower.endsWith("_reply_to") ||
|
|
286
|
+
lower.endsWith("_from") ||
|
|
287
|
+
lower.endsWith("_to") ||
|
|
288
|
+
lower.endsWith("_cc") ||
|
|
289
|
+
lower.endsWith("_bcc")
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** A schema `pattern` that explicitly allows lowercase but not uppercase
|
|
294
|
+
* letters (typical slug regex like `^[a-z0-9_\-]+$`). Used to switch from
|
|
295
|
+
* mixed-case `{{$randomString}}` to a slug-shaped generator. */
|
|
296
|
+
function isLowercaseOnlyPattern(pattern: string | undefined): boolean {
|
|
297
|
+
if (!pattern) return false;
|
|
298
|
+
return pattern.includes("a-z") && !pattern.includes("A-Z");
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** A string example shaped like a UUID, on a field that looks like a foreign
|
|
302
|
+
* key (name ends with `_id` or schema declares `format: uuid`), is almost
|
|
303
|
+
* always a tenant-specific value the spec author left in `example:`. Sending
|
|
304
|
+
* it verbatim guarantees 422 on a fresh account and leaks foreign IDs. */
|
|
305
|
+
function isLikelyForeignFKExample(
|
|
306
|
+
schema: OpenAPIV3.SchemaObject,
|
|
307
|
+
name?: string,
|
|
308
|
+
value?: unknown,
|
|
309
|
+
): boolean {
|
|
310
|
+
const ex = value !== undefined ? value : schema.example;
|
|
311
|
+
if (typeof ex !== "string") return false;
|
|
312
|
+
if (!UUID_RE.test(ex)) return false;
|
|
313
|
+
const fkByName = !!name && name.toLowerCase().endsWith("_id");
|
|
314
|
+
const fkByFormat = schema.format === "uuid";
|
|
315
|
+
return fkByName || fkByFormat;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** Resolve the effective example value from a schema, supporting both
|
|
319
|
+
* OpenAPI 3.0 `example` (singular) and OpenAPI 3.1 / JSON Schema `examples`
|
|
320
|
+
* (plural array). `example` wins when both are set — it's the more
|
|
321
|
+
* intentional, single-source signal. `null` is treated as "no example"
|
|
322
|
+
* (see TASK-221). For `examples`, we pick the first non-null entry. */
|
|
323
|
+
function pickExampleValue(schema: OpenAPIV3.SchemaObject): unknown {
|
|
324
|
+
if (schema.example !== undefined && schema.example !== null) {
|
|
325
|
+
return schema.example;
|
|
326
|
+
}
|
|
327
|
+
const examples = (schema as { examples?: unknown }).examples;
|
|
328
|
+
if (Array.isArray(examples)) {
|
|
329
|
+
for (const ex of examples) {
|
|
330
|
+
if (ex !== null && ex !== undefined) return ex;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return undefined;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* TASK-269 — per-field provenance for `zond generate --explain`.
|
|
338
|
+
*
|
|
339
|
+
* Returns a label describing *why* `generateFromSchema` would emit the
|
|
340
|
+
* value it does for a given (schema, propertyName). Mirrors the dispatch
|
|
341
|
+
* priority in `generateFromSchema` without producing the value, so
|
|
342
|
+
* `--explain` can show "name → {{$randomName}} [heuristic:name]" without
|
|
343
|
+
* re-executing generation.
|
|
344
|
+
*
|
|
345
|
+
* Kept as a parallel function instead of refactoring `generateFromSchema`
|
|
346
|
+
* to record sources — the recursion path-tracking complexity would
|
|
347
|
+
* outweigh the value for what is currently a debug-only surface. The
|
|
348
|
+
* heuristic order here MUST stay in lockstep with the function above; a
|
|
349
|
+
* unit test (data-factory.test.ts) pins the labels for each branch.
|
|
350
|
+
*/
|
|
351
|
+
export type FieldSource =
|
|
352
|
+
| "example"
|
|
353
|
+
| "examples"
|
|
354
|
+
| "enum"
|
|
355
|
+
| "format"
|
|
356
|
+
| "pattern"
|
|
357
|
+
| "min"
|
|
358
|
+
| "max"
|
|
359
|
+
| "random"
|
|
360
|
+
| "default"
|
|
361
|
+
| `heuristic:${string}`;
|
|
362
|
+
|
|
363
|
+
export function classifyFieldSource(
|
|
364
|
+
schema: OpenAPIV3.SchemaObject,
|
|
365
|
+
propertyName?: string,
|
|
366
|
+
): FieldSource {
|
|
367
|
+
// example > examples (3.1) — same FK-UUID guard as generateFromSchema.
|
|
368
|
+
if (schema.example !== undefined && schema.example !== null) {
|
|
369
|
+
if (!isLikelyForeignFKExample(schema, propertyName, schema.example)) {
|
|
370
|
+
return "example";
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
const examples = (schema as { examples?: unknown }).examples;
|
|
374
|
+
if (Array.isArray(examples)) {
|
|
375
|
+
for (const ex of examples) {
|
|
376
|
+
if (ex === null || ex === undefined) continue;
|
|
377
|
+
if (!isLikelyForeignFKExample(schema, propertyName, ex)) return "examples";
|
|
378
|
+
break;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
if (schema.enum && schema.enum.length > 0) return "enum";
|
|
382
|
+
if (formatToPlaceholder(schema.format) !== undefined) return "format";
|
|
383
|
+
|
|
384
|
+
const t = Array.isArray(schema.type)
|
|
385
|
+
? (schema.type as string[]).find(x => x !== "null")
|
|
386
|
+
: schema.type;
|
|
387
|
+
|
|
388
|
+
if (t === "string") {
|
|
389
|
+
// ARV-38: keep --explain in sync with guessStringPlaceholder — when a
|
|
390
|
+
// default is consumed, label the source as "default", not "random".
|
|
391
|
+
if (typeof schema.default === "string" && schema.default.length > 0) return "default";
|
|
392
|
+
if (isLowercaseOnlyPattern(schema.pattern)) return "pattern";
|
|
393
|
+
if (
|
|
394
|
+
schema.description &&
|
|
395
|
+
/\b(domain|hostname|fqdn)\b/i.test(schema.description) &&
|
|
396
|
+
!isEmailContextName(propertyName)
|
|
397
|
+
) {
|
|
398
|
+
return "heuristic:domain-from-description";
|
|
399
|
+
}
|
|
400
|
+
if (propertyName) {
|
|
401
|
+
const lower = propertyName.toLowerCase();
|
|
402
|
+
if (lower === "slug" || lower.endsWith("_slug")) return "heuristic:slug";
|
|
403
|
+
if (lower === "domain" || lower === "hostname" || lower === "fqdn" || lower.endsWith("_domain")) return "heuristic:domain";
|
|
404
|
+
if (lower === "platform") return "heuristic:platform";
|
|
405
|
+
if (lower === "language" || lower === "lang" || lower === "locale") return "heuristic:locale";
|
|
406
|
+
if (lower === "country" || lower === "country_code" || lower.endsWith("_country") || lower.endsWith("_country_code")) return "heuristic:country";
|
|
407
|
+
if (lower === "timezone" || lower === "time_zone" || lower === "tz") return "heuristic:timezone";
|
|
408
|
+
if (lower === "currency" || lower === "currency_code" || lower.endsWith("_currency") || lower.endsWith("_currency_code")) return "heuristic:currency";
|
|
409
|
+
if (lower === "mcc" || lower.endsWith("_mcc") || lower === "merchant_category_code") return "heuristic:mcc";
|
|
410
|
+
if (lower === "color" || lower.endsWith("_color") || lower === "background_color" || lower === "hex" || lower.endsWith("_hex_color")) return "heuristic:color";
|
|
411
|
+
if (lower === "ip" || lower === "ip_address" || lower.endsWith("_ip") || lower.endsWith("_ip_address")) return "heuristic:ip";
|
|
412
|
+
if (
|
|
413
|
+
lower === "email" || lower === "from" || lower === "to" || lower === "cc" ||
|
|
414
|
+
lower === "bcc" || lower === "sender" || lower === "recipient" ||
|
|
415
|
+
lower === "reply_to" || lower === "replyto" ||
|
|
416
|
+
lower.endsWith("_email") || lower.endsWith("Email") ||
|
|
417
|
+
lower.endsWith("_reply_to") || lower.endsWith("_from") ||
|
|
418
|
+
lower.endsWith("_to") || lower.endsWith("_cc") || lower.endsWith("_bcc")
|
|
419
|
+
) return "heuristic:email";
|
|
420
|
+
if (lower === "id" || lower === "uuid" || lower.endsWith("_id") || lower.endsWith("id")) return "heuristic:id";
|
|
421
|
+
if (lower === "name" || lower.endsWith("_name") || lower.endsWith("Name")) return "heuristic:name";
|
|
422
|
+
if (lower === "url" || lower.endsWith("_url") || lower === "uri" || lower === "href" || lower === "website") return "heuristic:url";
|
|
423
|
+
if (lower === "password" || lower.endsWith("_password")) return "heuristic:password";
|
|
424
|
+
if (lower === "phone" || lower === "telephone" || lower.endsWith("_phone")) return "heuristic:phone";
|
|
425
|
+
}
|
|
426
|
+
return "random";
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (t === "integer") {
|
|
430
|
+
if (schema.maximum !== undefined) return "max";
|
|
431
|
+
if (schema.minimum !== undefined && schema.minimum > 0) return "min";
|
|
432
|
+
return "random";
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (t === "number" || t === "boolean") return "default";
|
|
436
|
+
return "default";
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Map an OpenAPI `format` value to a zond generator placeholder. Returns
|
|
441
|
+
* undefined when the format is unknown or absent so callers can fall back
|
|
442
|
+
* to type / property-name heuristics. Exported for tests.
|
|
443
|
+
*/
|
|
444
|
+
export function formatToPlaceholder(format: string | undefined): string | undefined {
|
|
445
|
+
switch (format) {
|
|
446
|
+
case "email": return "{{$randomEmail}}";
|
|
447
|
+
case "uuid": return "{{$uuid}}";
|
|
448
|
+
case "date-time": return "{{$randomIsoDate}}";
|
|
449
|
+
case "date": return "{{$randomDate}}";
|
|
450
|
+
case "uri":
|
|
451
|
+
case "url": return "{{$randomUrl}}";
|
|
452
|
+
case "hostname": return "{{$randomFqdn}}";
|
|
453
|
+
case "ipv4": return "{{$randomIpv4}}";
|
|
454
|
+
case "ipv6": return "::1";
|
|
455
|
+
case "password": return "TestPass123!";
|
|
456
|
+
// ARV-165: format-aware helpers. None of these are standard OpenAPI 3.x
|
|
457
|
+
// formats, but Stripe/GitHub/Shopify/Twilio specs frequently carry them
|
|
458
|
+
// as ad-hoc `format:` tags. Falling through to {{$randomString}} guarantees
|
|
459
|
+
// 400 from format-validated APIs (R09 finding: 199 hit-but-fail Stripe steps).
|
|
460
|
+
case "iso-country-code":
|
|
461
|
+
case "country-code":
|
|
462
|
+
case "country": return "{{$randomCountryCode}}";
|
|
463
|
+
case "iso-currency-code":
|
|
464
|
+
case "currency-code":
|
|
465
|
+
case "currency": return "{{$randomCurrencyCode}}";
|
|
466
|
+
case "mcc": return "{{$randomMCC}}";
|
|
467
|
+
case "color":
|
|
468
|
+
case "hex-color":
|
|
469
|
+
case "rgb-hex": return "{{$randomColorHex}}";
|
|
470
|
+
default: return undefined;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
90
474
|
/**
|
|
91
475
|
* Generate a multipart body object from an OpenAPI multipart/form-data schema.
|
|
92
476
|
* Binary fields (format: binary/byte) become file upload objects; all others become strings.
|
|
@@ -100,6 +484,7 @@ export function generateMultipartFromSchema(
|
|
|
100
484
|
|
|
101
485
|
for (const [key, propSchema] of Object.entries(schema.properties)) {
|
|
102
486
|
const s = propSchema as OpenAPIV3.SchemaObject;
|
|
487
|
+
if (shouldSkipForRequest(key, s)) continue;
|
|
103
488
|
if (s.format === "binary" || s.format === "byte") {
|
|
104
489
|
result[key] = { file: `./fixtures/${key}.bin`, content_type: "application/octet-stream" };
|
|
105
490
|
} else {
|
|
@@ -112,21 +497,93 @@ export function generateMultipartFromSchema(
|
|
|
112
497
|
}
|
|
113
498
|
|
|
114
499
|
function guessStringPlaceholder(schema: OpenAPIV3.SchemaObject, name?: string): string {
|
|
115
|
-
// Format-based
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
if (schema.
|
|
124
|
-
|
|
500
|
+
// Format-based dispatch already happened earlier in generateFromSchema;
|
|
501
|
+
// this branch only sees strings whose format is empty or unrecognised.
|
|
502
|
+
|
|
503
|
+
// ARV-38: when the spec declares a JSON-Schema `default` for a string-typed
|
|
504
|
+
// field with no enum, prefer it over heuristics. PATCH endpoints in
|
|
505
|
+
// particular rely on this — e.g. a `PATCH /domains/{id}` with
|
|
506
|
+
// `tls: { type: string, default: "opportunistic" }` would otherwise get
|
|
507
|
+
// a random fallback and a guaranteed 422 every run.
|
|
508
|
+
if (typeof schema.default === "string" && schema.default.length > 0) {
|
|
509
|
+
return schema.default;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Pattern-aware: many specs constrain slugs via regex like
|
|
513
|
+
// `^(?![0-9]+$)[a-z0-9_\-]+$` without setting `format`. Default
|
|
514
|
+
// `{{$randomString}}` mixes upper+lower → 400 from the validator.
|
|
515
|
+
// Heuristic: pattern allows `a-z` but forbids `A-Z` → emit a slug.
|
|
516
|
+
if (isLowercaseOnlyPattern(schema.pattern)) {
|
|
517
|
+
return "{{$randomSlug}}";
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Description-aware: when the schema describes a domain/hostname (e.g.
|
|
521
|
+
// a `POST /domains/`-style endpoint or DNS-zone create route) but the
|
|
522
|
+
// field is generically named `name`, the default `{{$randomName}}`
|
|
523
|
+
// returns "Bob Wilson" and the server rejects it. TASK-224.
|
|
524
|
+
// Skip when the field name is clearly in email vocabulary — email-API
|
|
525
|
+
// specs often describe `from`/`to`/etc. with phrases like "verified
|
|
526
|
+
// sending domain" or "Name <user@domain>", which trips the regex but
|
|
527
|
+
// the field is an email, not a domain. Email vocab > domain-from-description.
|
|
528
|
+
if (
|
|
529
|
+
schema.description &&
|
|
530
|
+
/\b(domain|hostname|fqdn)\b/i.test(schema.description) &&
|
|
531
|
+
!isEmailContextName(name)
|
|
532
|
+
) {
|
|
533
|
+
return "{{$randomDomain}}";
|
|
534
|
+
}
|
|
125
535
|
|
|
126
536
|
// Name-based heuristics
|
|
127
537
|
if (name) {
|
|
128
538
|
const lower = name.toLowerCase();
|
|
129
|
-
if (lower === "
|
|
539
|
+
if (lower === "slug" || lower.endsWith("_slug")) {
|
|
540
|
+
return "{{$randomSlug}}";
|
|
541
|
+
}
|
|
542
|
+
if (lower === "domain" || lower === "hostname" || lower === "fqdn" || lower.endsWith("_domain")) {
|
|
543
|
+
return "{{$randomDomain}}";
|
|
544
|
+
}
|
|
545
|
+
// Closed-vocabulary fields where servers validate against an internal
|
|
546
|
+
// dictionary even when the spec lacks `enum:`. Random strings → 400.
|
|
547
|
+
// Pick the most universally-accepted value per dictionary.
|
|
548
|
+
if (lower === "platform") return "python";
|
|
549
|
+
if (lower === "language" || lower === "lang" || lower === "locale") return "en";
|
|
550
|
+
// ARV-165: country/currency literals (US/USD) were universally accepted
|
|
551
|
+
// but offered zero variety — added endsWith() patterns so nested fields
|
|
552
|
+
// like `bank_account.country`, `payout.currency_code`, `from_country`
|
|
553
|
+
// also resolve. Still emit a literal — picking from the random helper
|
|
554
|
+
// would weaken the "always-valid" property for downstream assertions
|
|
555
|
+
// that pin on the first value.
|
|
556
|
+
if (lower === "country" || lower === "country_code" || lower.endsWith("_country") || lower.endsWith("_country_code")) return "US";
|
|
557
|
+
if (lower === "timezone" || lower === "time_zone" || lower === "tz") return "UTC";
|
|
558
|
+
if (lower === "currency" || lower === "currency_code" || lower.endsWith("_currency") || lower.endsWith("_currency_code")) return "USD";
|
|
559
|
+
// ARV-165: MCC (merchant category code) — Stripe/Square/issuing APIs.
|
|
560
|
+
// Random {{$randomString}} → 400 because it's not a 4-digit code.
|
|
561
|
+
if (lower === "mcc" || lower.endsWith("_mcc") || lower === "merchant_category_code") return "{{$randomMCC}}";
|
|
562
|
+
// ARV-165: hex color — Stripe brand settings, Slack themes, GitHub labels.
|
|
563
|
+
if (lower === "color" || lower.endsWith("_color") || lower === "background_color" || lower === "hex" || lower.endsWith("_hex_color")) return "{{$randomColorHex}}";
|
|
564
|
+
// ARV-165: IP addresses — Stripe tos_acceptance.ip, audit logs, fraud APIs.
|
|
565
|
+
if (lower === "ip" || lower === "ip_address" || lower.endsWith("_ip") || lower.endsWith("_ip_address")) return "{{$randomIpv4}}";
|
|
566
|
+
// Email-context fields. Email-API specs often
|
|
567
|
+
// omit `format: email` on `from`/`to`/`reply_to`/`cc`/`bcc` — the field
|
|
568
|
+
// name is the only clue, and `{{$randomString}}` guarantees a 422.
|
|
569
|
+
if (
|
|
570
|
+
lower === "email" ||
|
|
571
|
+
lower === "from" ||
|
|
572
|
+
lower === "to" ||
|
|
573
|
+
lower === "cc" ||
|
|
574
|
+
lower === "bcc" ||
|
|
575
|
+
lower === "sender" ||
|
|
576
|
+
lower === "recipient" ||
|
|
577
|
+
lower === "reply_to" ||
|
|
578
|
+
lower === "replyto" ||
|
|
579
|
+
lower.endsWith("_email") ||
|
|
580
|
+
lower.endsWith("Email") ||
|
|
581
|
+
lower.endsWith("_reply_to") ||
|
|
582
|
+
lower.endsWith("_from") ||
|
|
583
|
+
lower.endsWith("_to") ||
|
|
584
|
+
lower.endsWith("_cc") ||
|
|
585
|
+
lower.endsWith("_bcc")
|
|
586
|
+
) {
|
|
130
587
|
return "{{$randomEmail}}";
|
|
131
588
|
}
|
|
132
589
|
if (lower === "id" || lower === "uuid" || lower.endsWith("_id") || lower.endsWith("id")) {
|
|
@@ -136,7 +593,7 @@ function guessStringPlaceholder(schema: OpenAPIV3.SchemaObject, name?: string):
|
|
|
136
593
|
return "{{$randomName}}";
|
|
137
594
|
}
|
|
138
595
|
if (lower === "url" || lower.endsWith("_url") || lower === "uri" || lower === "href" || lower === "website") {
|
|
139
|
-
return "
|
|
596
|
+
return "{{$randomUrl}}";
|
|
140
597
|
}
|
|
141
598
|
if (lower === "password" || lower.endsWith("_password")) {
|
|
142
599
|
return "TestPass123!";
|