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
15 changes: 14 additions & 1 deletion .github/workflows/ci-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -544,7 +544,7 @@ jobs:
python -m venv tests/host_smoke/.venv
tests/host_smoke/.venv/bin/python -m pip install --upgrade pip
cd tests/host_smoke
.venv/bin/python -m pip install -r requirements.txt
.venv/bin/python -m pip install -r requirements.txt pytest pytest-cov requests

- name: Exercise OpenAPI routes on a real Functions host
run: |
Expand Down Expand Up @@ -613,6 +613,14 @@ jobs:
--output "$RUNNER_TEMP/swagger.html"
grep -q 'SwaggerUIBundle' "$RUNNER_TEMP/swagger.html"

# Request every route register_openapi_routes registered (JSON, YAML, docs;
# anonymous and protected): indexing alone never proved they respond (#824).
# The status is kept so the host log is still printed and scanned before failing.
pytest_status=0
(cd ../.. && HOST_SMOKE_BASE_URL=http://127.0.0.1:7071 \
tests/host_smoke/.venv/bin/python -m pytest tests/host_smoke -m host_smoke --no-cov -rs) \
|| pytest_status=$?

kill "$host_pid" 2>/dev/null || true
wait "$host_pid" 2>/dev/null || true
cat "$RUNNER_TEMP/host-smoke.log"
Expand All @@ -622,6 +630,11 @@ jobs:
exit 1
fi

if [ "$pytest_status" -ne 0 ]; then
echo "::error::The real-host route tests failed (pytest exit $pytest_status)"
exit "$pytest_status"
fi

# azure-functions 2.x compatibility proof lane (issues #488 / #528),
# following the sibling proof-lane pattern (azure-functions-validation #351 /
# azure-functions-langgraph #350). It installs the package from a WHEEL, then
Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -142,12 +142,13 @@ pretty = true
module = "examples.*"
follow_imports = "skip"
[tool.pytest.ini_options]
addopts = "--cov=src/azure_functions_openapi --cov-report=xml --cov-report=term-missing -ra -q -m 'not e2e and not perf and not browser'"
addopts = "--cov=src/azure_functions_openapi --cov-report=xml --cov-report=term-missing -ra -q -m 'not e2e and not perf and not browser and not host_smoke'"
testpaths = ["tests"]
pythonpath = ["src", "."]
markers = [
"e2e: real Azure end-to-end tests (require E2E_BASE_URL)",
"perf: spec-generation regression budgets (run with make perf)",
"host_smoke: requests a running tests/host_smoke Functions host (set HOST_SMOKE_BASE_URL)",
"browser: offline Chromium script checks (require cached Playwright 1.54.1 and Chromium)",
"browser_cdn: real SRI-enforced Swagger UI CDN checks (skip when CDN network is unavailable)",
]
Expand Down
31 changes: 28 additions & 3 deletions tests/host_smoke/test_fixture_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@
Core Tools that the `host-smoke` CI job spins up. Reverting the fix (dropping
app=app from get_openapi_json() in function_app.py) makes
test_fixture_handler_documents_the_route_it_actually_serves fail.

The tests marked `host_smoke` are the opposite: they request the routes of an
already running host (HOST_SMOKE_BASE_URL) and are deselected from the offline
run by pyproject.toml's addopts. The `host-smoke` CI job selects them with
`-m host_smoke`, and a missing HOST_SMOKE_BASE_URL fails them instead of
skipping them (#824).
"""

from __future__ import annotations
Expand Down Expand Up @@ -107,10 +113,29 @@ def test_protected_docs_fixture_never_echoes_key(fixture_module: ModuleType) ->
assert b'url: "/api/protected/openapi.json"' in response.get_body()


@pytest.mark.skipif(
not os.environ.get("HOST_SMOKE_BASE_URL"),
reason="Set HOST_SMOKE_BASE_URL to a running tests/host_smoke Functions host",
@pytest.mark.host_smoke
@pytest.mark.parametrize(
("route", "needle"),
[
("registered/openapi.json", '"openapi"'),
("registered/openapi.yaml", "openapi:"),
("registered/docs", "SwaggerUIBundle"),
("protected/openapi.json", '"openapi"'),
("protected/openapi.yaml", "openapi:"),
("protected/docs", "SwaggerUIBundle"),
],
)
def test_real_host_serves_every_registered_route(route: str, needle: str) -> None:
# Given: the app indexed by the host (#814 acceptance: request each helper route)
base = os.environ["HOST_SMOKE_BASE_URL"]
# When
response = requests.get(f"{base}/api/{route}", timeout=10, allow_redirects=False)
# Then
assert response.status_code == 200
assert needle in response.text


@pytest.mark.host_smoke
def test_real_host_protected_docs_never_echoes_key() -> None:
# Given: Core Tools does not enforce keys locally; this proves no-echo, NOT key enforcement.
base = os.environ["HOST_SMOKE_BASE_URL"]
Expand Down
9 changes: 9 additions & 0 deletions tests/test_ci_workflow_hardening.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,12 @@ def test_gate_accepts_a_successful_unknown_dependency() -> None:
jobs["future-job"] = {"result": "success", "outputs": {}}

assert run_gate(jobs).returncode == 0


def test_host_smoke_job_runs_the_real_host_tests() -> None:
steps = yaml.safe_load(WORKFLOW.read_text(encoding="utf-8"))["jobs"]["host-smoke"]["steps"]
script = "\n".join(step.get("run", "") for step in steps)

assert "HOST_SMOKE_BASE_URL=" in script
assert "-m host_smoke" in script
assert 'exit "$pytest_status"' in script
Loading