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
34 changes: 34 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
[run]
branch = True
source = src/stac_auth_proxy
omit =
*/tests/*
*/test_*
*/__pycache__/*
*/venv/*
*/build/*
*/dist/*
*/htmlcov/*
*/lambda.py
*/__main__.py

[report]
exclude_lines =
pragma: no cover
def __repr__
if self.debug:
if settings.DEBUG
raise AssertionError
raise NotImplementedError
if 0:
if __name__ == .__main__.:
class .*\bProtocol\):
@(abc\.)?abstractmethod
# Have to re-enable the standard pragma
pragma: no cover

[html]
directory = htmlcov

[xml]
output = coverage.xml
18 changes: 17 additions & 1 deletion .github/workflows/cicd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,20 @@ jobs:
- uses: astral-sh/setup-uv@v4
with:
enable-cache: true
- run: uv run pytest -n auto
- name: Run tests with coverage
run: |
uv run pytest -n auto --cov=src/stac_auth_proxy --cov-report=xml --cov-report=html --cov-report=term-missing --cov-fail-under=85
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v5
with:
files: ./coverage.xml
flags: unittests
fail_ci_if_error: false
- name: Archive coverage reports
uses: actions/upload-artifact@v4
if: always()
with:
name: coverage-reports
path: |
htmlcov/
coverage.xml
17 changes: 6 additions & 11 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,29 +1,24 @@
repos:
- repo: https://github.com/abravalheri/validate-pyproject
rev: v0.23
rev: v0.24.1
hooks:
- id: validate-pyproject

- repo: https://github.com/psf/black
rev: 22.12.0
hooks:
- id: black
language_version: python

- repo: https://github.com/PyCQA/isort
rev: 5.12.0
rev: 6.0.1
hooks:
- id: isort
language_version: python

- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.238
rev: v0.12.11
hooks:
- id: ruff
- id: ruff-check
args: ["--fix"]
- id: ruff-format

- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.3.0
rev: v1.17.1
hooks:
- id: mypy
language_version: python
Expand Down
72 changes: 72 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
.PHONY: help test test-coverage test-fast lint format clean install dev-install

help: ## Show this help message
@echo "Available commands:"
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'

install: ## Install the package
uv sync

dev-install: ## Install development dependencies
uv sync --group dev

test: ## Run tests
uv run pytest

test-fast: ## Run tests in parallel
uv run pytest -n auto

test-coverage: ## Run tests with coverage
@echo "🧪 Running tests with coverage..."
uv run pytest \
--cov=src/stac_auth_proxy \
--cov-report=term-missing \
--cov-report=html \
--cov-report=xml \
--cov-fail-under=85 \
-v
@echo "✅ Coverage report generated!"
@echo "📊 HTML report available at: htmlcov/index.html"
@echo "📄 XML report available at: coverage.xml"
@if [ "$(CI)" = "true" ]; then \
echo "🚀 Running in CI environment"; \
else \
echo "💻 Running locally - opening HTML report..."; \
if command -v open >/dev/null 2>&1; then \
open htmlcov/index.html; \
elif command -v xdg-open >/dev/null 2>&1; then \
xdg-open htmlcov/index.html; \
else \
echo "Please open htmlcov/index.html in your browser to view the coverage report"; \
fi; \
fi

lint: ## Run linting
uv run pre-commit run ruff-check --all-files
uv run pre-commit run mypy --all-files

format: ## Format code
uv run pre-commit run ruff-format --all-files

clean: ## Clean up generated files
rm -rf htmlcov/
rm -rf .coverage
rm -rf coverage.xml
rm -rf .pytest_cache/
rm -rf build/
rm -rf dist/
rm -rf *.egg-info/
find . -type d -name __pycache__ -delete
find . -type f -name "*.pyc" -delete

ci: ## Run CI checks locally
uv run pre-commit run --all-files
@echo "🧪 Running tests with coverage..."
uv run pytest \
--cov=src/stac_auth_proxy \
--cov-report=term-missing \
--cov-report=html \
--cov-report=xml \
--cov-fail-under=85 \
-v
@echo "✅ CI checks completed!"
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
[![PyPI - Version][pypi-version-badge]][pypi-link]
[![GHCR - Version][ghcr-version-badge]][ghcr-link]
[![GHCR - Size][ghcr-size-badge]][ghcr-link]
[![codecov][codecov-badge]][codecov-link]
[![Tests][tests-badge]][tests-link]

STAC Auth Proxy is a proxy API that mediates between the client and your internally accessible STAC API to provide flexible authentication, authorization, and content-filtering mechanisms.

Expand Down Expand Up @@ -37,3 +39,7 @@ Head to [Getting Started](https://developmentseed.org/stac-auth-proxy/user-guide
[ghcr-version-badge]: https://ghcr-badge.egpl.dev/developmentseed/stac-auth-proxy/latest_tag?color=%2344cc11&ignore=latest&label=image+version&trim=
[ghcr-size-badge]: https://ghcr-badge.egpl.dev/developmentseed/stac-auth-proxy/size?color=%2344cc11&tag=latest&label=image+size&trim=
[ghcr-link]: https://github.com/developmentseed/stac-auth-proxy/pkgs/container/stac-auth-proxy
[codecov-badge]: https://codecov.io/gh/developmentseed/stac-auth-proxy/branch/main/graph/badge.svg
[codecov-link]: https://codecov.io/gh/developmentseed/stac-auth-proxy
[tests-badge]: https://github.com/developmentseed/stac-auth-proxy/actions/workflows/cicd.yaml/badge.svg
[tests-link]: https://github.com/developmentseed/stac-auth-proxy/actions/workflows/cicd.yaml
57 changes: 56 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,43 @@ lambda = [

[tool.coverage.run]
branch = true
source = ["src/stac_auth_proxy"]
omit = [
"*/tests/*",
"*/test_*",
"*/__pycache__/*",
"*/venv/*",
"*/build/*",
"*/dist/*",
"*/htmlcov/*",
"*/lambda.py", # Lambda entry point not tested in unit tests
]

[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"def __repr__",
"if self.debug:",
"if settings.DEBUG",
"raise AssertionError",
"raise NotImplementedError",
"if 0:",
"if __name__ == .__main__.:",
"class .*\\bProtocol\\):",
"@(abc\\.)?abstractmethod",
]

[tool.coverage.html]
directory = "htmlcov"

[tool.coverage.xml]
output = "coverage.xml"

[tool.isort]
known_first_party = ["stac_auth_proxy"]
profile = "black"

[tool.ruff]
[tool.ruff.lint]
ignore = ["E501", "D203", "D205", "D212"]
select = ["D", "E", "F"]

Expand All @@ -58,14 +89,38 @@ requires = ["hatchling>=1.12.0"]
[dependency-groups]
dev = [
"jwcrypto>=1.5.6",
"mypy>=1.3.0",
"pre-commit>=3.5.0",
"pytest-asyncio>=0.25.1",
"pytest-cov>=5.0.0",
"pytest-xdist>=3.6.1",
"pytest>=8.3.3",
"ruff>=0.0.238",
"starlette-cramjam>=0.4.0",
"types-simplejson",
"types-attrs",
]

[tool.pytest.ini_options]
asyncio_default_fixture_loop_scope = "function"
asyncio_mode = "auto"
testpaths = ["tests"]
python_files = ["test_*.py", "*_test.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
addopts = [
"--strict-markers",
"--strict-config",
"--verbose",
"--tb=short",
"--cov=src/stac_auth_proxy",
"--cov-report=term-missing",
"--cov-report=html",
"--cov-report=xml",
"--cov-fail-under=85",
]
markers = [
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
"integration: marks tests as integration tests",
"unit: marks tests as unit tests",
]
1 change: 1 addition & 0 deletions src/stac_auth_proxy/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ def configure_app(
**settings_kwargs : Any
Keyword arguments used to configure the health and conformance checks if
``settings`` is not provided.

"""
settings = settings or Settings(**settings_kwargs)

Expand Down
2 changes: 1 addition & 1 deletion src/stac_auth_proxy/lifespan.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,6 @@ async def check_conformance(
"""Check if the upstream API supports a given conformance class."""
required_conformances: dict[str, list[str]] = {}
for middleware in middleware_classes:

for conformance in getattr(middleware.cls, attr_name, []):
required_conformances.setdefault(conformance, []).append(
middleware.cls.__name__
Expand Down Expand Up @@ -127,6 +126,7 @@ def build_lifespan(settings: Settings | None = None, **settings_kwargs: Any):
-------
Callable[[FastAPI], AsyncContextManager[Any]]
A callable suitable for the ``lifespan`` parameter of ``FastAPI``.
"""
if settings is None:
settings = Settings(**settings_kwargs)
Expand Down
3 changes: 2 additions & 1 deletion src/stac_auth_proxy/utils/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,10 @@ def get_value_by_path(obj: dict, path: str, default: Any = None) -> Any:
path: The dot notation path (e.g. "payload.sub")
default: Default value to return if path doesn't exist

Returns
Returns:
-------
The value at the specified path or default if path doesn't exist

"""
try:
for key in path.split("."):
Expand Down
5 changes: 4 additions & 1 deletion src/stac_auth_proxy/utils/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ def should_transform_response(
Returns
-------
bool: True if the response should be transformed

"""
...

Expand All @@ -39,10 +40,12 @@ def transform_json(self, data: Any, request: Request) -> Any:

Args:
data: The parsed JSON data
request: The HTTP request object

Returns
Returns:
-------
The transformed JSON data

"""
...

Expand Down
18 changes: 9 additions & 9 deletions tests/test_authn.py
Original file line number Diff line number Diff line change
Expand Up @@ -312,14 +312,14 @@ def test_with_invalid_tokens_fails(invalid_token, expected_status, source_api_se
)
client = TestClient(test_app)
response = client.get("/collections", headers={"Authorization": invalid_token})
assert (
response.status_code == expected_status
), f"GET request should fail with token: {invalid_token}"
assert response.status_code == expected_status, (
f"GET request should fail with token: {invalid_token}"
)

response = client.options("/collections", headers={"Authorization": invalid_token})
assert (
response.status_code == 200
), f"OPTIONS request should succeed with token: {invalid_token}"
assert response.status_code == 200, (
f"OPTIONS request should succeed with token: {invalid_token}"
)


def test_options_requests_with_cors_headers(source_api_server):
Expand All @@ -339,9 +339,9 @@ def test_options_requests_with_cors_headers(source_api_server):
}

response = client.options("/collections", headers=cors_headers)
assert (
response.status_code == 200
), "OPTIONS request with CORS headers should succeed"
assert response.status_code == 200, (
"OPTIONS request with CORS headers should succeed"
)


@pytest.mark.parametrize(
Expand Down
18 changes: 9 additions & 9 deletions tests/test_filters_jinja2.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,9 +188,9 @@ async def test_search_post(
"filter-lang": "cql2-json",
}

assert (
proxied_body == expected_output
), "POST query should combine filter expressions."
assert proxied_body == expected_output, (
"POST query should combine filter expressions."
)


@pytest.mark.parametrize(
Expand Down Expand Up @@ -241,9 +241,9 @@ async def test_search_get(
),
"filter-lang": filter_lang,
}
assert (
proxied_request.query_params == expected_output
), "GET query should combine filter expressions."
assert proxied_request.query_params == expected_output, (
"GET query should combine filter expressions."
)


@pytest.mark.parametrize(
Expand Down Expand Up @@ -444,9 +444,9 @@ async def test_collections_list(
),
"filter-lang": filter_lang,
}
assert (
proxied_request.query_params == expected_output
), "Collections query should combine filter expressions."
assert proxied_request.query_params == expected_output, (
"Collections query should combine filter expressions."
)


@pytest.mark.parametrize(
Expand Down
Loading
Loading