specforge-api 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,170 @@
1
+ """Test invariants, status evaluations, and result data structures."""
2
+
3
+ from dataclasses import dataclass, field
4
+ from enum import Enum
5
+ from typing import Any
6
+
7
+
8
+ class TestStatus(str, Enum):
9
+ PASS = "PASS" # Server cleanly rejected invalid input (400/422) or accepted valid input
10
+ WARN_LEAK = "WARN_LEAK" # Server returned 200 OK for an invalid/boundary payload (Schema Leak)
11
+ CRASH_500 = "CRASH_500" # Server threw an unhandled 500 error on client input (Critical Crash)
12
+ FAIL_STATUS = "FAIL_STATUS" # Unexpected status code (e.g. 404 instead of 422)
13
+
14
+
15
+ @dataclass
16
+ class TestCase:
17
+ """Represents an executable boundary or negative test case against an API endpoint."""
18
+
19
+ id: str
20
+ endpoint: str
21
+ method: str
22
+ field_name: str
23
+ mutation_category: str
24
+ mutation_name: str
25
+ description: str
26
+ payload: Any
27
+ headers: dict[str, str] = field(default_factory=dict)
28
+ query_params: dict[str, Any] = field(default_factory=dict)
29
+ expected_status: int = 422
30
+ is_negative_test: bool = True
31
+
32
+ def to_dict(self) -> dict[str, Any]:
33
+ """Convert test case to dictionary for frontend execution or serialization."""
34
+ return {
35
+ "id": self.id,
36
+ "endpoint": self.endpoint,
37
+ "method": self.method,
38
+ "field_name": self.field_name,
39
+ "mutation_category": self.mutation_category,
40
+ "mutation_name": self.mutation_name,
41
+ "description": self.description,
42
+ "payload": self.payload,
43
+ "headers": self.headers,
44
+ "query_params": self.query_params,
45
+ "expected_status": self.expected_status,
46
+ "is_negative_test": self.is_negative_test,
47
+ }
48
+
49
+
50
+ @dataclass
51
+ class TestResult:
52
+ """Represents the execution outcome of a TestCase."""
53
+
54
+ test_case: TestCase
55
+ actual_status: int
56
+ latency_ms: float
57
+ response_body: str
58
+ status: TestStatus
59
+ message: str
60
+ curl_command: str = ""
61
+
62
+ def to_dict(self) -> dict[str, Any]:
63
+ """Convert result to dictionary for frontend inspection."""
64
+ return {
65
+ "test_case": self.test_case.to_dict(),
66
+ "actual_status": self.actual_status,
67
+ "latency_ms": self.latency_ms,
68
+ "response_body": self.response_body,
69
+ "status": self.status.value,
70
+ "message": self.message,
71
+ "curl_command": self.curl_command,
72
+ }
73
+
74
+
75
+ def evaluate_test_result(
76
+ test_case: TestCase,
77
+ status_code: int,
78
+ response_body: str,
79
+ latency_ms: float,
80
+ ) -> TestResult:
81
+ """Evaluate HTTP execution against contract invariants."""
82
+ curl_cmd = build_curl_command(test_case)
83
+
84
+ # Invariant 1: An HTTP 500 on ANY input is an unhandled server crash bug
85
+ if status_code >= 500:
86
+ return TestResult(
87
+ test_case=test_case,
88
+ actual_status=status_code,
89
+ latency_ms=latency_ms,
90
+ response_body=response_body,
91
+ status=TestStatus.CRASH_500,
92
+ message=f"CRITICAL: Server crashed with {status_code} on {test_case.mutation_name}!",
93
+ curl_command=curl_cmd,
94
+ )
95
+
96
+ # Invariant 2: Negative test expects a rejection (400 or 422). If 200/201 was returned, it's a schema leak
97
+ if test_case.is_negative_test:
98
+ if 200 <= status_code < 300:
99
+ return TestResult(
100
+ test_case=test_case,
101
+ actual_status=status_code,
102
+ latency_ms=latency_ms,
103
+ response_body=response_body,
104
+ status=TestStatus.WARN_LEAK,
105
+ message=(
106
+ f"SCHEMA LEAK: Server accepted invalid payload ({test_case.mutation_name}) "
107
+ f"with HTTP {status_code}. Expected 422 or 400."
108
+ ),
109
+ curl_command=curl_cmd,
110
+ )
111
+ elif status_code in (400, 422):
112
+ return TestResult(
113
+ test_case=test_case,
114
+ actual_status=status_code,
115
+ latency_ms=latency_ms,
116
+ response_body=response_body,
117
+ status=TestStatus.PASS,
118
+ message=f"PASS: Server cleanly rejected {test_case.mutation_name} with HTTP {status_code}.",
119
+ curl_command=curl_cmd,
120
+ )
121
+ else:
122
+ return TestResult(
123
+ test_case=test_case,
124
+ actual_status=status_code,
125
+ latency_ms=latency_ms,
126
+ response_body=response_body,
127
+ status=TestStatus.FAIL_STATUS,
128
+ message=f"UNEXPECTED: Got HTTP {status_code}, expected {test_case.expected_status}.",
129
+ curl_command=curl_cmd,
130
+ )
131
+ else:
132
+ # Happy-path test
133
+ if 200 <= status_code < 300:
134
+ return TestResult(
135
+ test_case=test_case,
136
+ actual_status=status_code,
137
+ latency_ms=latency_ms,
138
+ response_body=response_body,
139
+ status=TestStatus.PASS,
140
+ message=f"PASS: Endpoint succeeded with HTTP {status_code}.",
141
+ curl_command=curl_cmd,
142
+ )
143
+ else:
144
+ return TestResult(
145
+ test_case=test_case,
146
+ actual_status=status_code,
147
+ latency_ms=latency_ms,
148
+ response_body=response_body,
149
+ status=TestStatus.FAIL_STATUS,
150
+ message=f"FAIL: Happy path failed with HTTP {status_code}.",
151
+ curl_command=curl_cmd,
152
+ )
153
+
154
+
155
+ def build_curl_command(test_case: TestCase, base_url: str = "http://localhost:8000") -> str:
156
+ """Generate a clean cURL reproduction command for a test case."""
157
+ import json
158
+
159
+ url = f"{base_url.rstrip('/')}{test_case.endpoint}"
160
+ cmd_parts = [f"curl -X {test_case.method.upper()} '{url}'"]
161
+
162
+ for h_name, h_val in test_case.headers.items():
163
+ cmd_parts.append(f"-H '{h_name}: {h_val}'")
164
+
165
+ if test_case.payload is not None:
166
+ cmd_parts.append("-H 'Content-Type: application/json'")
167
+ payload_str = json.dumps(test_case.payload).replace("'", "'\\''")
168
+ cmd_parts.append(f"-d '{payload_str}'")
169
+
170
+ return " \\\n ".join(cmd_parts)
@@ -0,0 +1,155 @@
1
+ """Test Plan orchestrator that assembles boundary test suites for operations."""
2
+
3
+ import copy
4
+ import uuid
5
+ from dataclasses import dataclass, field
6
+ from typing import Any
7
+
8
+ from specforge.parser.models import Operation
9
+ from specforge.testing.fuzzers.array_fuzzer import ArrayFuzzer
10
+ from specforge.testing.fuzzers.boolean_fuzzer import BooleanFuzzer
11
+ from specforge.testing.fuzzers.numeric_fuzzer import NumericFuzzer
12
+ from specforge.testing.fuzzers.object_fuzzer import ObjectFuzzer
13
+ from specforge.testing.fuzzers.string_fuzzer import StringFuzzer
14
+ from specforge.testing.invariants import TestCase
15
+
16
+
17
+ @dataclass
18
+ class TestPlan:
19
+ """Represents the complete test suite planned for an operation or API."""
20
+
21
+ operation_key: str
22
+ endpoint: str
23
+ method: str
24
+ test_cases: list[TestCase] = field(default_factory=list)
25
+
26
+ def to_dict(self) -> dict[str, Any]:
27
+ """Convert test plan to dictionary."""
28
+ return {
29
+ "operation_key": self.operation_key,
30
+ "endpoint": self.endpoint,
31
+ "method": self.method,
32
+ "total_tests": len(self.test_cases),
33
+ "test_cases": [tc.to_dict() for tc in self.test_cases],
34
+ }
35
+
36
+
37
+ class TestPlanner:
38
+ """Assembles combinatorial test cases by coordinating type fuzzers."""
39
+
40
+ @classmethod
41
+ def plan_operation_tests(cls, operation: Operation) -> TestPlan:
42
+ """Construct full boundary and negative test suite for a single operation."""
43
+ test_cases: list[TestCase] = []
44
+ op_key = operation.key
45
+ endpoint = operation.path
46
+ method = operation.method.lower()
47
+
48
+ # 1. Request Body Fuzzing
49
+ if operation.request_body and operation.request_body.schema:
50
+ body_schema = operation.request_body.schema
51
+ baseline = ObjectFuzzer.generate_valid_baseline(body_schema)
52
+
53
+ # Test 1.0: Baseline Happy Path
54
+ test_cases.append(
55
+ TestCase(
56
+ id=str(uuid.uuid4())[:8],
57
+ endpoint=endpoint,
58
+ method=method,
59
+ field_name="[body_root]",
60
+ mutation_category="Baseline",
61
+ mutation_name="Happy Path: Valid Schema Payload",
62
+ description="Verify endpoint succeeds with schema-compliant baseline data",
63
+ payload=baseline,
64
+ expected_status=200,
65
+ is_negative_test=False,
66
+ )
67
+ )
68
+
69
+ # Test 1.1: Object-level mutations (missing keys, extra keys, primitive inversion)
70
+ object_mutations = ObjectFuzzer.generate_object_mutations(body_schema, baseline)
71
+ for mut_name, mut_desc, mut_payload, mut_cat in object_mutations:
72
+ test_cases.append(
73
+ TestCase(
74
+ id=str(uuid.uuid4())[:8],
75
+ endpoint=endpoint,
76
+ method=method,
77
+ field_name="[body_root]",
78
+ mutation_category=f"Object:{mut_cat}",
79
+ mutation_name=mut_name,
80
+ description=mut_desc,
81
+ payload=mut_payload,
82
+ expected_status=422,
83
+ is_negative_test=True,
84
+ )
85
+ )
86
+
87
+ # Test 1.2: Field-level property mutations
88
+ for prop_name, prop_schema in body_schema.properties.items():
89
+ field_mutations = []
90
+ schema_type = prop_schema.type
91
+
92
+ if schema_type == "string":
93
+ field_mutations = StringFuzzer.generate_mutations(prop_name, prop_schema)
94
+ elif schema_type in ("integer", "number"):
95
+ field_mutations = NumericFuzzer.generate_mutations(prop_name, prop_schema)
96
+ elif schema_type == "boolean":
97
+ field_mutations = BooleanFuzzer.generate_mutations(prop_name, prop_schema)
98
+ elif schema_type == "array":
99
+ field_mutations = ArrayFuzzer.generate_mutations(prop_name, prop_schema)
100
+
101
+ # Inject each mutated field value into a fresh copy of the baseline payload
102
+ for mut_name, mut_desc, mut_val, mut_cat in field_mutations:
103
+ mutated_payload = copy.deepcopy(baseline)
104
+ mutated_payload[prop_name] = mut_val
105
+
106
+ test_cases.append(
107
+ TestCase(
108
+ id=str(uuid.uuid4())[:8],
109
+ endpoint=endpoint,
110
+ method=method,
111
+ field_name=prop_name,
112
+ mutation_category=f"{str(schema_type or 'field').capitalize()}:{mut_cat}",
113
+ mutation_name=mut_name,
114
+ description=mut_desc,
115
+ payload=mutated_payload,
116
+ expected_status=422,
117
+ is_negative_test=True,
118
+ )
119
+ )
120
+
121
+ # 2. Query Parameter Fuzzing
122
+ for param in operation.parameters:
123
+ if param.location.value == "query" and param.schema:
124
+ param_name = param.name
125
+ param_type = param.schema.type
126
+
127
+ param_mutations = []
128
+ if param_type == "string":
129
+ param_mutations = StringFuzzer.generate_mutations(param_name, param.schema)
130
+ elif param_type in ("integer", "number"):
131
+ param_mutations = NumericFuzzer.generate_mutations(param_name, param.schema)
132
+
133
+ for mut_name, mut_desc, mut_val, mut_cat in param_mutations:
134
+ test_cases.append(
135
+ TestCase(
136
+ id=str(uuid.uuid4())[:8],
137
+ endpoint=endpoint,
138
+ method=method,
139
+ field_name=param_name,
140
+ mutation_category=f"QueryParam:{mut_cat}",
141
+ mutation_name=mut_name,
142
+ description=mut_desc,
143
+ payload=None,
144
+ query_params={param_name: mut_val},
145
+ expected_status=422,
146
+ is_negative_test=True,
147
+ )
148
+ )
149
+
150
+ return TestPlan(
151
+ operation_key=op_key,
152
+ endpoint=endpoint,
153
+ method=method,
154
+ test_cases=test_cases,
155
+ )
@@ -0,0 +1,43 @@
1
+ """Injects configuration and specifications into the pre-compiled HTML asset."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from typing import Any, Optional
6
+
7
+ from specforge.config import SpecForgeConfig
8
+
9
+
10
+ def get_asset_html_path() -> Path:
11
+ """Return the filesystem path to the bundled index.html asset."""
12
+ return Path(__file__).parent.parent / "assets" / "index.html"
13
+
14
+
15
+ def render_html(
16
+ config: SpecForgeConfig,
17
+ spec_dict: Optional[dict[str, Any]] = None,
18
+ ) -> str:
19
+ """Read the inlined HTML template and inject the runtime configuration and spec."""
20
+ asset_path = get_asset_html_path()
21
+
22
+ if not asset_path.exists():
23
+ raise FileNotFoundError(
24
+ f"SpecForge frontend asset not found at {asset_path}. "
25
+ "Ensure the package was installed with assets bundled."
26
+ )
27
+
28
+ html_template = asset_path.read_text(encoding="utf-8")
29
+
30
+ config_data = config.to_dict()
31
+ if spec_dict is not None:
32
+ config_data["embedded_spec"] = spec_dict
33
+
34
+ config_json = json.dumps(config_data)
35
+ replacement = f"<script>window.__SPECFORGE_CONFIG__ = {config_json};</script>"
36
+
37
+ # Replace placeholder or insert into <head>
38
+ if "<!-- __SPECFORGE_CONFIG_INJECTION__ -->" in html_template:
39
+ return html_template.replace("<!-- __SPECFORGE_CONFIG_INJECTION__ -->", replacement)
40
+ elif "</head>" in html_template:
41
+ return html_template.replace("</head>", f"{replacement}\n</head>")
42
+ else:
43
+ return f"{replacement}\n{html_template}"
@@ -0,0 +1,260 @@
1
+ Metadata-Version: 2.5
2
+ Name: specforge-api
3
+ Version: 0.1.0
4
+ Summary: The interactive API client and automated contract testing engine for Python.
5
+ Project-URL: Homepage, https://github.com/specforge-api/specforge
6
+ Project-URL: Documentation, https://github.com/specforge-api/specforge#readme
7
+ Project-URL: Repository, https://github.com/specforge-api/specforge
8
+ Project-URL: Issues, https://github.com/specforge-api/specforge/issues
9
+ Author: SpecForge Team
10
+ License: MIT License
11
+
12
+ Copyright (c) 2026 SpecForge Contributors
13
+
14
+ Permission is hereby granted, free of charge, to any person obtaining a copy
15
+ of this software and associated documentation files (the "Software"), to deal
16
+ in the Software without restriction, including without limitation the rights
17
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
18
+ copies of the Software, and to permit persons to whom the Software is
19
+ furnished to do so, subject to the following conditions:
20
+
21
+ The above copyright notice and this permission notice shall be included in all
22
+ copies or substantial portions of the Software.
23
+
24
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
25
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
26
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
27
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
28
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
29
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
30
+ SOFTWARE.
31
+ License-File: LICENSE
32
+ Keywords: api-client,contract-testing,fastapi,fuzzing,openapi,pytest,swagger,testing
33
+ Classifier: Development Status :: 3 - Alpha
34
+ Classifier: Intended Audience :: Developers
35
+ Classifier: License :: OSI Approved :: MIT License
36
+ Classifier: Programming Language :: Python :: 3
37
+ Classifier: Programming Language :: Python :: 3.9
38
+ Classifier: Programming Language :: Python :: 3.10
39
+ Classifier: Programming Language :: Python :: 3.11
40
+ Classifier: Programming Language :: Python :: 3.12
41
+ Classifier: Programming Language :: Python :: 3.13
42
+ Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
43
+ Classifier: Topic :: Software Development :: Testing
44
+ Requires-Python: >=3.9
45
+ Requires-Dist: httpx>=0.24.0
46
+ Requires-Dist: pydantic>=2.0.0
47
+ Requires-Dist: pyyaml>=6.0
48
+ Provides-Extra: all
49
+ Requires-Dist: fastapi>=0.100.0; extra == 'all'
50
+ Requires-Dist: flask>=2.0.0; extra == 'all'
51
+ Requires-Dist: rich>=13.0.0; extra == 'all'
52
+ Requires-Dist: typer>=0.9.0; extra == 'all'
53
+ Provides-Extra: cli
54
+ Requires-Dist: rich>=13.0.0; extra == 'cli'
55
+ Requires-Dist: typer>=0.9.0; extra == 'cli'
56
+ Provides-Extra: dev
57
+ Requires-Dist: mypy>=1.5.0; extra == 'dev'
58
+ Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
59
+ Requires-Dist: pytest>=7.4.0; extra == 'dev'
60
+ Requires-Dist: ruff>=0.1.0; extra == 'dev'
61
+ Requires-Dist: types-pyyaml; extra == 'dev'
62
+ Provides-Extra: fastapi
63
+ Requires-Dist: fastapi>=0.100.0; extra == 'fastapi'
64
+ Provides-Extra: flask
65
+ Requires-Dist: flask>=2.0.0; extra == 'flask'
66
+ Description-Content-Type: text/markdown
67
+
68
+ # SpecForge ⚡
69
+
70
+ [![PyPI version](https://img.shields.io/pypi/v/specforge-api.svg)](https://pypi.org/project/specforge-api/)
71
+ [![Python versions](https://img.shields.io/pypi/pyversions/specforge-api.svg)](https://pypi.org/project/specforge-api/)
72
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
73
+
74
+ **The Interactive API Client & Automated Contract Testing Engine for Python.**
75
+
76
+ A modern, drop-in Python library that replaces Swagger UI with a beautiful, responsive API workspace **and** automatically discovers boundary vulnerabilities, missing whitespace stripping, and unhandled 500 server crashes directly against your endpoints.
77
+
78
+ ---
79
+
80
+ ## 🚀 Why SpecForge?
81
+
82
+ 1. **Active Quality Gate, Not Just Passive Docs**: Traditional tools like Swagger UI only show the "happy path". SpecForge algorithmically analyzes your schema constraints and runs targeted boundary fuzzing right in your browser.
83
+ 2. **"Zero 500s" Invariant**: Verifies that edge-case inputs (e.g. `" "`, empty strings, off-by-one numbers, malformed emails) return clean client errors (`422/400`), never unhandled server exceptions (`500`).
84
+ 3. **One-Click Pytest Exporter**: Discovered edge cases can be instantly exported into commit-ready `test_api_contracts.py` files with `@pytest.mark.parametrize`.
85
+ 4. **Zero Frontend Build Dependencies**: The interactive workspace is fully compiled and inlined into the Python package. No Node.js or npm needed on your machine.
86
+
87
+ ---
88
+
89
+ ## 📦 Installation
90
+
91
+ ```bash
92
+ pip install specforge-api
93
+ ```
94
+
95
+ With framework extras:
96
+ ```bash
97
+ pip install "specforge-api[fastapi]" # FastAPI
98
+ pip install "specforge-api[flask]" # Flask
99
+ pip install "specforge-api[all]" # Full CLI & all framework adapters
100
+ ```
101
+
102
+ ---
103
+
104
+ ## ⚡ Quickstart
105
+
106
+ ### FastAPI
107
+
108
+ ```python
109
+ from fastapi import FastAPI
110
+ from specforge import SpecForge
111
+
112
+ app = FastAPI(title="My API", version="1.0.0")
113
+
114
+ # Mount SpecForge (auto-detects /openapi.json)
115
+ SpecForge(app)
116
+
117
+ @app.post("/users")
118
+ def create_user(username: str, age: int):
119
+ return {"status": "created", "username": username}
120
+ ```
121
+
122
+ Now open `http://localhost:8000/docs` in your browser. You get:
123
+ - A modern, interactive dark/light mode API client (Try-It-Out, request history, multi-language code snippets).
124
+ - An integrated **Boundary & Contract Testing Tab** to run automated fuzzing on all endpoints with one click.
125
+
126
+ ---
127
+
128
+ ### Flask
129
+
130
+ ```python
131
+ from flask import Flask
132
+ from specforge.flask import SpecForge
133
+
134
+ app = Flask(__name__)
135
+
136
+ # Register Blueprint
137
+ SpecForge(app, spec_url="/api/spec.json", path="/docs")
138
+ ```
139
+
140
+ ---
141
+
142
+ ## 🧪 Schema-Aware Boundary Fuzzing
143
+
144
+ SpecForge generates targeted test cases according to field types:
145
+
146
+ | Field Type | Automated Test Coverage |
147
+ |---|---|
148
+ | **String** | Rejecting whitespace-only (`" "`), empty strings (`""`), min/max bounds, Unicode 4-byte emojis, null bytes (`\x00`), format violations (email, uuid, datetime), and injection probes. |
149
+ | **Number / Integer** | Boundary checks (`min - 1`, `max + 1`), negative values on unsigned fields, zero checks, float-into-integer pollution, and integer overflow. |
150
+ | **Boolean** | Strict type testing vs loose coercion (`"true"`, `"yes"`, `1`, `0`, `null`). |
151
+ | **Array** | Empty lists (`[]`), `minItems` / `maxItems` boundaries, and `uniqueItems: true` duplicates. |
152
+ | **Object** | Combinatorial required field omission, extra unexpected keys (`additionalProperties: false`), and empty bodies. |
153
+
154
+ ---
155
+
156
+ ## 📁 Integrating Existing Project Tests
157
+
158
+ If your repository already contains test suites (written in `pytest` or `unittest`), SpecForge can automatically index and display them in a dedicated **Project Tests** tab and link them directly to their corresponding API routes.
159
+
160
+ ### Quick Setup
161
+
162
+ Pass `test_paths` when mounting SpecForge in your application:
163
+
164
+ ```python
165
+ from fastapi import FastAPI
166
+ from specforge import SpecForge
167
+
168
+ app = FastAPI()
169
+
170
+ # Mount SpecForge with existing project tests
171
+ SpecForge(
172
+ app,
173
+ test_paths=["tests"], # Can be directories or individual test files
174
+ )
175
+ ```
176
+
177
+ Now open `/docs` to see:
178
+ - **📁 Project Tests Tab**: Browse all repository tests, file paths, line numbers, docstrings, and parameter matrices.
179
+ - **Route Coverage Linking**: View your existing tests directly inside the API Explorer alongside SpecForge's automated boundary fuzzers.
180
+
181
+ ### Linking Tests to API Routes
182
+
183
+ SpecForge associates existing tests with OpenAPI routes using two mechanisms:
184
+
185
+ #### 1. Automatic HTTP Client Detection (Zero Configuration)
186
+ If your test uses `TestClient`, `httpx`, or `requests` to call an API path, SpecForge automatically recognizes the route:
187
+ ```python
188
+ def test_user_creation_underage(client):
189
+ """Rejects underage registrations."""
190
+ response = client.post("/api/v1/users", json={"username": "bob", "age": 16})
191
+ assert response.status_code == 422
192
+ ```
193
+
194
+ #### 2. Explicit Pytest Marker (Recommended for Complex Tests)
195
+ For tests with dynamic URLs or mocks, declare the endpoint explicitly:
196
+ ```python
197
+ import pytest
198
+
199
+ @pytest.mark.endpoint("POST", "/api/v1/users")
200
+ def test_user_signup_pipeline():
201
+ """End-to-end user registration and event dispatch."""
202
+ ...
203
+ ```
204
+
205
+ ### 🔒 Security & Zero-Execution Guarantee
206
+
207
+ SpecForge uses **Python Static Abstract Syntax Tree (AST) parsing** to discover tests:
208
+ - **Zero Code Execution**: SpecForge **never** imports or executes your test files. Tests are analyzed purely as static syntax trees.
209
+ - **No Side Effects**: Discovering tests will never trigger database operations, background tasks, or network calls.
210
+ - **Path Sandboxing**: Scanned paths are strictly verified to remain within the project root directory, preventing any path traversal.
211
+
212
+ ---
213
+
214
+ ## 🛠️ CLI & CI/CD Pipeline
215
+
216
+ Run headless contract fuzzing in your continuous integration pipeline:
217
+
218
+ ```bash
219
+ specforge test http://localhost:8000/openapi.json --fail-on-500
220
+ ```
221
+
222
+ ---
223
+
224
+ ## 💻 Local Development & Testing Guide
225
+
226
+ ### 1. Run the Interactive Live Demo (Zero Dependencies)
227
+ You can launch and explore SpecForge immediately using Python's standard library:
228
+ ```bash
229
+ python examples/standalone_demo.py
230
+ ```
231
+ Open **`http://localhost:8000/docs`** in your browser to:
232
+ - Test API endpoints and inspect formatted JSON responses.
233
+ - Click **"⚡ Run Boundary Test Suite"** to watch automated fuzzing of whitespace strings, negative numbers, and missing fields.
234
+ - Click **"🐍 Export to Pytest"** to view and copy auto-generated test code.
235
+
236
+ ### 2. Run the Automated Unit Test Suite
237
+ ```bash
238
+ pytest -v
239
+ ```
240
+
241
+ ### 3. Install in Another Project Locally (Editable Mode)
242
+ To use this local version inside any other Python app on your machine without publishing to PyPI:
243
+ ```bash
244
+ # In your other project's directory and virtualenv:
245
+ pip install -e c:\Research-projects\specforge
246
+ ```
247
+ *Any change you make in SpecForge source code will instantly reflect in your other application without reinstalling!*
248
+
249
+ ### 4. Build a Distribution Wheel (.whl)
250
+ ```bash
251
+ pip install build
252
+ python -m build
253
+ # Outputs wheel file in dist/specforge-0.1.0-py3-none-any.whl
254
+ ```
255
+
256
+ ---
257
+
258
+ ## 📄 License
259
+
260
+ SpecForge is licensed under the [MIT License](LICENSE).
@@ -0,0 +1,23 @@
1
+ specforge/__init__.py,sha256=ASOKokzk1SCxc30JpuDLT_6oUN2ByHR-gz-E3kkbK24,636
2
+ specforge/config.py,sha256=Br3CP2P1sLOO3YZJTYfjYWy7HNXZC_8eGpaouS3Ke6U,1499
3
+ specforge/adapters/fastapi.py,sha256=X8N8sAyjMByzwintgzuPi-DX44uzh8sGFfem939zLlo,3603
4
+ specforge/adapters/flask.py,sha256=ZMvji15SdjwF02_Bp-uoRGQlcgeEiEj28Q2jFkG6WXE,2353
5
+ specforge/assets/index.html,sha256=7NVQW2LJFgMiaeHhvwTquzinUlPyD5NN3aJe_16vO9I,114194
6
+ specforge/cli/main.py,sha256=HMMEwJItVA2yTiTbdy8Rzx7lpzsRYvNDiCqV5kK-P7o,5933
7
+ specforge/parser/models.py,sha256=XJF9cYfYYVyE-jlylCvOP0AkO7Sh3IPLtGQr10aSWJU,3814
8
+ specforge/parser/openapi3.py,sha256=6kQnH6RF0GiitUfVTBtTfO3zEVbgCi5zN9kBd43xJO4,10735
9
+ specforge/parser/resolver.py,sha256=VPPQ-Oe3rf3-nAklJjw9W8p8x5qVisoUF0-Rev3ZKSI,4052
10
+ specforge/testing/invariants.py,sha256=XuWT3jfIfEhQW16cJfpy9AJsLeE6gTYD9u7RLWHIA8I,6182
11
+ specforge/testing/planner.py,sha256=LEmW6V779McAjvd1A-1uAxEjam_NdIMK4z_C9HN0e18,6563
12
+ specforge/testing/exporters/pytest_export.py,sha256=c5v-hJ2PSSta_rATGZ_BejPKfIHPFYLR9ucADcxxdXw,2901
13
+ specforge/testing/fuzzers/array_fuzzer.py,sha256=CsuochFAPHuTJ4ojEJu04FCyuqDlzUB1Mv4cUPim2e4,1850
14
+ specforge/testing/fuzzers/boolean_fuzzer.py,sha256=GTEiSDyaKuCCMTvui33ExIY_ze2nHeRC5WO4oYXqaPM,1119
15
+ specforge/testing/fuzzers/numeric_fuzzer.py,sha256=9GQXzxU4ri7kNLaTvgxSXK-k2ys7omtxnV644M2BokE,3120
16
+ specforge/testing/fuzzers/object_fuzzer.py,sha256=WHY2tGea58Ng8rCuOwcA2YK11HGO75HEBBwBnNHaFhc,4131
17
+ specforge/testing/fuzzers/string_fuzzer.py,sha256=2xNP2NpKLTrhempy8UqmGv22i5kfHUkNyt1bfUchWms,5406
18
+ specforge/utils/html_injector.py,sha256=xYo-AHX2J4dVYRWk3ilWxfloDf7uX3HG7sMgi0T7yf8,1497
19
+ specforge_api-0.1.0.dist-info/METADATA,sha256=kF1XBRgAfYFp2kYAw7L8GF3lddS7zi5dFHHxiQ2H3GE,10304
20
+ specforge_api-0.1.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
21
+ specforge_api-0.1.0.dist-info/entry_points.txt,sha256=_RzXTgdjdZrMPLWQD0tMSDiQXFnitiFMgCvN8XIpxG0,64
22
+ specforge_api-0.1.0.dist-info/licenses/LICENSE,sha256=O_09tG0cQIwYwDQbHFG6WptgedhxMpOUQI-A-EX8QFs,1079
23
+ specforge_api-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ specforge = specforge.cli.main:cli_entrypoint
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 SpecForge Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.