specforge-api 0.1.3__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.
Files changed (23) hide show
  1. {specforge_api-0.1.3 → specforge_api-0.1.4}/PKG-INFO +1 -1
  2. {specforge_api-0.1.3 → specforge_api-0.1.4}/pyproject.toml +1 -1
  3. {specforge_api-0.1.3 → specforge_api-0.1.4}/src/specforge/__init__.py +1 -1
  4. {specforge_api-0.1.3 → specforge_api-0.1.4}/src/specforge/assets/index.html +91 -7
  5. {specforge_api-0.1.3 → specforge_api-0.1.4}/.gitignore +0 -0
  6. {specforge_api-0.1.3 → specforge_api-0.1.4}/LICENSE +0 -0
  7. {specforge_api-0.1.3 → specforge_api-0.1.4}/README.md +0 -0
  8. {specforge_api-0.1.3 → specforge_api-0.1.4}/src/specforge/adapters/fastapi.py +0 -0
  9. {specforge_api-0.1.3 → specforge_api-0.1.4}/src/specforge/adapters/flask.py +0 -0
  10. {specforge_api-0.1.3 → specforge_api-0.1.4}/src/specforge/cli/main.py +0 -0
  11. {specforge_api-0.1.3 → specforge_api-0.1.4}/src/specforge/config.py +0 -0
  12. {specforge_api-0.1.3 → specforge_api-0.1.4}/src/specforge/parser/models.py +0 -0
  13. {specforge_api-0.1.3 → specforge_api-0.1.4}/src/specforge/parser/openapi3.py +0 -0
  14. {specforge_api-0.1.3 → specforge_api-0.1.4}/src/specforge/parser/resolver.py +0 -0
  15. {specforge_api-0.1.3 → specforge_api-0.1.4}/src/specforge/testing/exporters/pytest_export.py +0 -0
  16. {specforge_api-0.1.3 → specforge_api-0.1.4}/src/specforge/testing/fuzzers/array_fuzzer.py +0 -0
  17. {specforge_api-0.1.3 → specforge_api-0.1.4}/src/specforge/testing/fuzzers/boolean_fuzzer.py +0 -0
  18. {specforge_api-0.1.3 → specforge_api-0.1.4}/src/specforge/testing/fuzzers/numeric_fuzzer.py +0 -0
  19. {specforge_api-0.1.3 → specforge_api-0.1.4}/src/specforge/testing/fuzzers/object_fuzzer.py +0 -0
  20. {specforge_api-0.1.3 → specforge_api-0.1.4}/src/specforge/testing/fuzzers/string_fuzzer.py +0 -0
  21. {specforge_api-0.1.3 → specforge_api-0.1.4}/src/specforge/testing/invariants.py +0 -0
  22. {specforge_api-0.1.3 → specforge_api-0.1.4}/src/specforge/testing/planner.py +0 -0
  23. {specforge_api-0.1.3 → specforge_api-0.1.4}/src/specforge/utils/html_injector.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: specforge-api
3
- Version: 0.1.3
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
6
  Project-URL: Documentation, https://specforge-api.github.io/specforge/
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "specforge-api"
7
- version = "0.1.3"
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" }
@@ -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.3"
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.example) return schema.example;
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 || 25;
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') return [];
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 schema = op.raw?.requestBody?.content?.['application/json']?.schema;
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, propSchema] of Object.entries(schema.properties)) {
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