Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ description = "Monorepo for Satellite Tasking API (STAPI) Specification Python p
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"pystapi-validator",
"stapi-pydantic",
"stapi-fastapi",
"stapi-fastapi"
]

[dependency-groups]
Expand All @@ -30,9 +31,10 @@ docs = [
default-groups = ["dev", "docs"]

[tool.uv.workspace]
members = ["stapi-pydantic", "stapi-fastapi"]
members = ["pystapi-validator", "stapi-pydantic", "stapi-fastapi"]

[tool.uv.sources]
pystapi-validator.workspace = true
stapi-pydantic.workspace = true
stapi-fastapi.workspace = true

Expand All @@ -58,6 +60,7 @@ max-complexity = 8 # default 10
[tool.mypy]
strict = true
files = [
"pystapi-validator/src/pystapi_validator/**/*.py",
"stapi-pydantic/src/stapi_pydantic/**/*.py",
"stapi-fastapi/src/stapi_fastapi/**/*.py"
]
Expand Down
27 changes: 27 additions & 0 deletions pystapi-validator/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# pystapi-validator

This project provides API validation for STAPI FastAPI implementations using Schemathesis against the latest STAPI OpenAPI
specification.

## Configuration

- The STAPI OpenAPI specification is fetched from:
[STAPI OpenAPI Spec](https://raw.githubusercontent.com/stapi-spec/stapi-spec/refs/heads/main/openapi.yaml)
- The base URL for the API being tested is set to `http://localhost:8000`. Update the `BASE_URL` in `tests/validate_api.py`
if your API is hosted elsewhere.

## Setup

1. Install dependencies:

```bash
uv sync
```

1. Run tests and generate report:

```bash
uv run pytest tests/validate_api.py --html=report.html --self-contained-html
```

1. Open `report.html` in your browser to view the detailed test report.
25 changes: 25 additions & 0 deletions pystapi-validator/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
[project]
name = "pystapi-validator"
version = "0.1.0"
description = "API validation for STAPI FastAPI using Schemathesis and OpenAPI YAML with reporting"
authors = [
{ name = "Brian Terry", email = "[email protected]" }
]
license = "MIT"
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"schemathesis>=3.37.0",
"pytest>=8.3.3",
"requests>=2.32.3",
"pyyaml>=6.0.2",
"pytest-html>=4.1.1",
"pytest-metadata>=3.1.1",
]

[project.scripts]
pystapi-validator = "pystapi_validator:main"

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
11 changes: 11 additions & 0 deletions pystapi-validator/src/pystapi_validator/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import sys

import pytest


def main() -> None:
sys.exit(pytest.main(["pystapi-validator/tests/validate_api.py"]))


if __name__ == "__main__":
main()
Empty file.
61 changes: 61 additions & 0 deletions pystapi-validator/tests/validate_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import json

import pytest
import schemathesis
from schemathesis.checks import (
content_type_conformance,
negative_data_rejection,
not_a_server_error,
response_headers_conformance,
response_schema_conformance,
status_code_conformance,
)

schemathesis.experimental.OPEN_API_3_1.enable()

SCHEMA_URL = "https://raw.githubusercontent.com/stapi-spec/stapi-spec/refs/heads/main/openapi.yaml"
schema = schemathesis.from_uri(SCHEMA_URL)

BASE_URL = "http://localhost:8000"


@schema.parametrize()
def test_api(case):
response = case.call_and_validate(base_url=BASE_URL)
case.validate_response(response)

not_a_server_error(response, case)
status_code_conformance(response, case)
content_type_conformance(response, case)
response_schema_conformance(response, case)
response_headers_conformance(response, case)
negative_data_rejection(response, case)


def test_openapi_specification():
assert schema.validate()


@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call):
outcome = yield
rep = outcome.get_result()
if rep.when == "call":
item.session.results = getattr(item.session, "results", {})
item.session.results[item.nodeid] = rep


def pytest_sessionfinish(session, exitstatus):
if hasattr(session, "results"):
with open("test_results.json", "w") as f:
json.dump(
{
nodeid: {
"outcome": rep.outcome,
"longrepr": str(rep.longrepr) if rep.longrepr else None,
}
for nodeid, rep in session.results.items()
},
f,
indent=2,
)
Loading