specforge-api 0.1.2__tar.gz → 0.1.4__tar.gz
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.
- {specforge_api-0.1.2 → specforge_api-0.1.4}/PKG-INFO +7 -2
- {specforge_api-0.1.2 → specforge_api-0.1.4}/README.md +1 -0
- {specforge_api-0.1.2 → specforge_api-0.1.4}/pyproject.toml +7 -2
- {specforge_api-0.1.2 → specforge_api-0.1.4}/src/specforge/__init__.py +1 -1
- {specforge_api-0.1.2 → specforge_api-0.1.4}/src/specforge/assets/index.html +91 -7
- {specforge_api-0.1.2 → specforge_api-0.1.4}/.gitignore +0 -0
- {specforge_api-0.1.2 → specforge_api-0.1.4}/LICENSE +0 -0
- {specforge_api-0.1.2 → specforge_api-0.1.4}/src/specforge/adapters/fastapi.py +0 -0
- {specforge_api-0.1.2 → specforge_api-0.1.4}/src/specforge/adapters/flask.py +0 -0
- {specforge_api-0.1.2 → specforge_api-0.1.4}/src/specforge/cli/main.py +0 -0
- {specforge_api-0.1.2 → specforge_api-0.1.4}/src/specforge/config.py +0 -0
- {specforge_api-0.1.2 → specforge_api-0.1.4}/src/specforge/parser/models.py +0 -0
- {specforge_api-0.1.2 → specforge_api-0.1.4}/src/specforge/parser/openapi3.py +0 -0
- {specforge_api-0.1.2 → specforge_api-0.1.4}/src/specforge/parser/resolver.py +0 -0
- {specforge_api-0.1.2 → specforge_api-0.1.4}/src/specforge/testing/exporters/pytest_export.py +0 -0
- {specforge_api-0.1.2 → specforge_api-0.1.4}/src/specforge/testing/fuzzers/array_fuzzer.py +0 -0
- {specforge_api-0.1.2 → specforge_api-0.1.4}/src/specforge/testing/fuzzers/boolean_fuzzer.py +0 -0
- {specforge_api-0.1.2 → specforge_api-0.1.4}/src/specforge/testing/fuzzers/numeric_fuzzer.py +0 -0
- {specforge_api-0.1.2 → specforge_api-0.1.4}/src/specforge/testing/fuzzers/object_fuzzer.py +0 -0
- {specforge_api-0.1.2 → specforge_api-0.1.4}/src/specforge/testing/fuzzers/string_fuzzer.py +0 -0
- {specforge_api-0.1.2 → specforge_api-0.1.4}/src/specforge/testing/invariants.py +0 -0
- {specforge_api-0.1.2 → specforge_api-0.1.4}/src/specforge/testing/planner.py +0 -0
- {specforge_api-0.1.2 → specforge_api-0.1.4}/src/specforge/utils/html_injector.py +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: specforge-api
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: The interactive API client and automated contract testing engine for Python.
|
|
5
5
|
Project-URL: Homepage, https://github.com/specforge-api/specforge
|
|
6
|
-
Project-URL: Documentation, https://github.
|
|
6
|
+
Project-URL: Documentation, https://specforge-api.github.io/specforge/
|
|
7
7
|
Project-URL: Repository, https://github.com/specforge-api/specforge
|
|
8
8
|
Project-URL: Issues, https://github.com/specforge-api/specforge/issues
|
|
9
9
|
Author: SpecForge Team
|
|
@@ -59,6 +59,10 @@ Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
|
|
|
59
59
|
Requires-Dist: pytest>=7.4.0; extra == 'dev'
|
|
60
60
|
Requires-Dist: ruff>=0.1.0; extra == 'dev'
|
|
61
61
|
Requires-Dist: types-pyyaml; extra == 'dev'
|
|
62
|
+
Provides-Extra: docs
|
|
63
|
+
Requires-Dist: mkdocs-material>=9.5.0; extra == 'docs'
|
|
64
|
+
Requires-Dist: mkdocs>=1.5.0; extra == 'docs'
|
|
65
|
+
Requires-Dist: pymdown-extensions>=10.7; extra == 'docs'
|
|
62
66
|
Provides-Extra: fastapi
|
|
63
67
|
Requires-Dist: fastapi>=0.100.0; extra == 'fastapi'
|
|
64
68
|
Provides-Extra: flask
|
|
@@ -69,6 +73,7 @@ Description-Content-Type: text/markdown
|
|
|
69
73
|
|
|
70
74
|
[](https://pypi.org/project/specforge-api/)
|
|
71
75
|
[](https://pypi.org/project/specforge-api/)
|
|
76
|
+
[](https://specforge-api.github.io/specforge/)
|
|
72
77
|
[](https://github.com/specforge-api/specforge/actions/workflows/ci.yml)
|
|
73
78
|
[](https://opensource.org/licenses/MIT)
|
|
74
79
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://pypi.org/project/specforge-api/)
|
|
4
4
|
[](https://pypi.org/project/specforge-api/)
|
|
5
|
+
[](https://specforge-api.github.io/specforge/)
|
|
5
6
|
[](https://github.com/specforge-api/specforge/actions/workflows/ci.yml)
|
|
6
7
|
[](https://opensource.org/licenses/MIT)
|
|
7
8
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "specforge-api"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.4"
|
|
8
8
|
description = "The interactive API client and automated contract testing engine for Python."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { file = "LICENSE" }
|
|
@@ -58,13 +58,18 @@ dev = [
|
|
|
58
58
|
"mypy>=1.5.0",
|
|
59
59
|
"types-pyyaml",
|
|
60
60
|
]
|
|
61
|
+
docs = [
|
|
62
|
+
"mkdocs>=1.5.0",
|
|
63
|
+
"mkdocs-material>=9.5.0",
|
|
64
|
+
"pymdown-extensions>=10.7",
|
|
65
|
+
]
|
|
61
66
|
|
|
62
67
|
[project.scripts]
|
|
63
68
|
specforge = "specforge.cli.main:cli_entrypoint"
|
|
64
69
|
|
|
65
70
|
[project.urls]
|
|
66
71
|
Homepage = "https://github.com/specforge-api/specforge"
|
|
67
|
-
Documentation = "https://github.
|
|
72
|
+
Documentation = "https://specforge-api.github.io/specforge/"
|
|
68
73
|
Repository = "https://github.com/specforge-api/specforge"
|
|
69
74
|
Issues = "https://github.com/specforge-api/specforge/issues"
|
|
70
75
|
|
|
@@ -5,7 +5,7 @@ from specforge.parser.openapi3 import OpenAPIParser
|
|
|
5
5
|
from specforge.testing.exporters.pytest_export import PytestExporter
|
|
6
6
|
from specforge.testing.planner import TestPlanner
|
|
7
7
|
|
|
8
|
-
__version__ = "0.1.
|
|
8
|
+
__version__ = "0.1.4"
|
|
9
9
|
|
|
10
10
|
# Conditionally import FastAPI adapter if available
|
|
11
11
|
try:
|
|
@@ -1522,8 +1522,8 @@
|
|
|
1522
1522
|
}
|
|
1523
1523
|
parseOperations();
|
|
1524
1524
|
renderEndpointList();
|
|
1525
|
+
await synthesizeTestPlan();
|
|
1525
1526
|
selectOperation(0);
|
|
1526
|
-
synthesizeTestPlan();
|
|
1527
1527
|
} catch (err) {
|
|
1528
1528
|
console.error("Failed to load OpenAPI spec:", err);
|
|
1529
1529
|
}
|
|
@@ -1916,6 +1916,38 @@
|
|
|
1916
1916
|
document.getElementById("request-body-input").value = JSON.stringify(baseline, null, 2);
|
|
1917
1917
|
}
|
|
1918
1918
|
|
|
1919
|
+
function resolveRef(schema, visited = new Set()) {
|
|
1920
|
+
if (!schema || typeof schema !== 'object') return schema;
|
|
1921
|
+
if (schema.$ref && typeof schema.$ref === 'string') {
|
|
1922
|
+
if (visited.has(schema.$ref)) return schema;
|
|
1923
|
+
visited.add(schema.$ref);
|
|
1924
|
+
const refPath = schema.$ref.replace(/^#\//, '').split('/');
|
|
1925
|
+
let target = activeSpec;
|
|
1926
|
+
for (const seg of refPath) {
|
|
1927
|
+
if (!target) break;
|
|
1928
|
+
target = target[seg];
|
|
1929
|
+
}
|
|
1930
|
+
if (target) {
|
|
1931
|
+
const resolvedTarget = resolveRef(target, visited);
|
|
1932
|
+
return { ...resolvedTarget, ...schema, $ref: undefined };
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
if (Array.isArray(schema.allOf)) {
|
|
1936
|
+
let merged = {};
|
|
1937
|
+
for (const sub of schema.allOf) {
|
|
1938
|
+
const resolvedSub = resolveRef(sub, visited);
|
|
1939
|
+
merged = {
|
|
1940
|
+
...merged,
|
|
1941
|
+
...resolvedSub,
|
|
1942
|
+
properties: { ...(merged.properties || {}), ...(resolvedSub.properties || {}) },
|
|
1943
|
+
required: [...(merged.required || []), ...(resolvedSub.required || [])]
|
|
1944
|
+
};
|
|
1945
|
+
}
|
|
1946
|
+
return { ...merged, ...schema, allOf: undefined };
|
|
1947
|
+
}
|
|
1948
|
+
return schema;
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1919
1951
|
function generateMockFromOp(op) {
|
|
1920
1952
|
const schema = op.raw?.requestBody?.content?.['application/json']?.schema;
|
|
1921
1953
|
if (!schema) return { message: "sample_payload" };
|
|
@@ -1923,7 +1955,11 @@
|
|
|
1923
1955
|
}
|
|
1924
1956
|
|
|
1925
1957
|
function generateMockFromSchema(schema) {
|
|
1926
|
-
if (schema
|
|
1958
|
+
if (!schema) return "value";
|
|
1959
|
+
schema = resolveRef(schema);
|
|
1960
|
+
if (schema.example !== undefined) return schema.example;
|
|
1961
|
+
if (schema.default !== undefined) return schema.default;
|
|
1962
|
+
if (schema.enum && schema.enum.length > 0) return schema.enum[0];
|
|
1927
1963
|
if (schema.type === 'object' || schema.properties) {
|
|
1928
1964
|
const obj = {};
|
|
1929
1965
|
for (const [k, v] of Object.entries(schema.properties || {})) {
|
|
@@ -1933,11 +1969,19 @@
|
|
|
1933
1969
|
}
|
|
1934
1970
|
if (schema.type === 'string') {
|
|
1935
1971
|
if (schema.format === 'email') return "user@example.com";
|
|
1972
|
+
if (schema.format === 'uuid') return "123e4567-e89b-12d3-a456-426614174000";
|
|
1973
|
+
if (schema.format === 'date-time') return new Date().toISOString();
|
|
1974
|
+
if (schema.format === 'date') return "2026-10-28";
|
|
1936
1975
|
return "sample_string";
|
|
1937
1976
|
}
|
|
1938
|
-
if (schema.type === 'integer' || schema.type === 'number') return schema.minimum
|
|
1977
|
+
if (schema.type === 'integer' || schema.type === 'number') return schema.minimum !== undefined ? schema.minimum : 25;
|
|
1939
1978
|
if (schema.type === 'boolean') return true;
|
|
1940
|
-
if (schema.type === 'array')
|
|
1979
|
+
if (schema.type === 'array') {
|
|
1980
|
+
if (schema.items) {
|
|
1981
|
+
return [generateMockFromSchema(schema.items)];
|
|
1982
|
+
}
|
|
1983
|
+
return [];
|
|
1984
|
+
}
|
|
1941
1985
|
return "value";
|
|
1942
1986
|
}
|
|
1943
1987
|
|
|
@@ -2013,10 +2057,47 @@
|
|
|
2013
2057
|
}
|
|
2014
2058
|
|
|
2015
2059
|
// Fuzzing & Testing Generator
|
|
2016
|
-
function synthesizeTestPlan() {
|
|
2060
|
+
async function synthesizeTestPlan() {
|
|
2061
|
+
// 1. Try to fetch rich server-synthesized test plan if available
|
|
2062
|
+
try {
|
|
2063
|
+
const res = await fetch("/specforge/api/plan");
|
|
2064
|
+
if (res.ok) {
|
|
2065
|
+
const data = await res.json();
|
|
2066
|
+
if (data && data.plans && data.plans.length > 0) {
|
|
2067
|
+
testCases = [];
|
|
2068
|
+
data.plans.forEach(plan => {
|
|
2069
|
+
(plan.test_cases || []).forEach(tc => {
|
|
2070
|
+
testCases.push({
|
|
2071
|
+
opMethod: plan.method.toLowerCase(),
|
|
2072
|
+
opPath: plan.endpoint,
|
|
2073
|
+
endpoint: plan.endpoint,
|
|
2074
|
+
method: plan.method.toUpperCase(),
|
|
2075
|
+
field: tc.field_name || tc.target_field || "payload",
|
|
2076
|
+
category: tc.invariant_type || tc.category || "Boundary",
|
|
2077
|
+
desc: tc.description || tc.name,
|
|
2078
|
+
payload: tc.payload,
|
|
2079
|
+
headers: tc.headers || {},
|
|
2080
|
+
queryParams: tc.query_params || {},
|
|
2081
|
+
expected: tc.expected_status || 422,
|
|
2082
|
+
isNegative: tc.is_negative_test !== false
|
|
2083
|
+
});
|
|
2084
|
+
});
|
|
2085
|
+
});
|
|
2086
|
+
mergeCustomProbesFromStorage();
|
|
2087
|
+
populateTestScopeDropdown();
|
|
2088
|
+
renderEndpointPlannedTests();
|
|
2089
|
+
renderPlannedTestTable();
|
|
2090
|
+
return;
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
} catch (e) {
|
|
2094
|
+
// Fall back to client-side synthesis
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2017
2097
|
testCases = [];
|
|
2018
2098
|
operations.forEach(op => {
|
|
2019
|
-
const
|
|
2099
|
+
const rawSchema = op.raw?.requestBody?.content?.['application/json']?.schema;
|
|
2100
|
+
const schema = resolveRef(rawSchema);
|
|
2020
2101
|
const rawParams = op.raw?.parameters || [];
|
|
2021
2102
|
const opMethod = op.method.toLowerCase();
|
|
2022
2103
|
const opPath = op.path;
|
|
@@ -2025,7 +2106,8 @@
|
|
|
2025
2106
|
if (schema && schema.properties) {
|
|
2026
2107
|
const baseline = generateMockFromSchema(schema);
|
|
2027
2108
|
|
|
2028
|
-
for (const [propName,
|
|
2109
|
+
for (const [propName, rawPropSchema] of Object.entries(schema.properties)) {
|
|
2110
|
+
const propSchema = resolveRef(rawPropSchema);
|
|
2029
2111
|
if (propSchema.type === 'string') {
|
|
2030
2112
|
testCases.push({
|
|
2031
2113
|
opMethod,
|
|
@@ -2720,6 +2802,8 @@
|
|
|
2720
2802
|
const tbody = document.getElementById("test-table-body");
|
|
2721
2803
|
const activeCases = getActiveTestCases();
|
|
2722
2804
|
window.__activeTestCases = activeCases;
|
|
2805
|
+
const statTotalEl = document.getElementById("stat-total");
|
|
2806
|
+
if (statTotalEl) statTotalEl.textContent = activeCases.length;
|
|
2723
2807
|
tbody.innerHTML = "";
|
|
2724
2808
|
|
|
2725
2809
|
if (activeCases.length === 0) {
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{specforge_api-0.1.2 → specforge_api-0.1.4}/src/specforge/testing/exporters/pytest_export.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|