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
70 changes: 69 additions & 1 deletion .github/workflows/student-2-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ jobs:
ruff check student-2
ruff format --check student-2
- name: Run tests
run: pytest student-2/tests -q
# tests/endpoint needs the live stack and runs in the compose job.
run: pytest student-2/tests/database student-2/tests/backend student-2/tests/frontend student-2/tests/e2e -q
- name: Build database service image
run: docker build -f student-2/database/Dockerfile -t student-2-database student-2
- name: Health check database service
Expand Down Expand Up @@ -110,3 +111,70 @@ jobs:
docker logs student-2-frontend
exit 1
fi

compose:
name: Student 2 Post-Commit Endpoint Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Start the Student 2 slice
# MCP, RAG and multi-agent stay off: no host service exists in CI.
env:
STUDENT2_BACKEND_AI_MODE_URL: ""
STUDENT2_BACKEND_RAG_URL: ""
STUDENT2_BACKEND_MULTI_AGENT_ENABLED: "false"
run: |
docker compose up -d --build --no-deps \
shared-database shared-backend \
student-2-database student-2-backend
- name: Wait for the backend
run: python scripts/ci/wait_for_http.py --label "student 2 backend" http://127.0.0.1:9000/health
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install Student 2 endpoint test dependencies
working-directory: student-2
run: |
python -m pip install --upgrade pip
python -m pip install -e .[dev]
# POST /accommodation and GET /accommodation/{id} over HTTP against the
# stack started above; the backend is published on 9000.
- name: Post-commit endpoint tests
working-directory: student-2
env:
STUDENT2_ENDPOINT_BASE_URL: http://127.0.0.1:9000
run: python -m pytest tests/endpoint --junitxml=student-2-endpoint-tests.xml -v
- name: Endpoint test summary
if: always()
working-directory: student-2
run: |
python - <<'EOF' | tee student-2-endpoint-tests.md >> "$GITHUB_STEP_SUMMARY"
import os
import xml.etree.ElementTree as ET
print("## Student 2 post-commit endpoint tests\n")
if not os.path.exists("student-2-endpoint-tests.xml"):
print("The endpoint tests did not run.")
raise SystemExit
print("| Test | Result |\n| --- | --- |")
for case in ET.parse("student-2-endpoint-tests.xml").iter("testcase"):
outcome = next(
(child.tag for child in case if child.tag in {"failure", "error", "skipped"}),
"passed",
)
mark = "PASS" if outcome == "passed" else outcome.upper()
print(f"| `{case.get('name')}` | {mark} |")
EOF
- name: Upload endpoint test evidence
if: always()
uses: actions/upload-artifact@v4
with:
name: student-2-endpoint-test-evidence
path: |
student-2/student-2-endpoint-tests.xml
student-2/student-2-endpoint-tests.md
- name: Compose logs on failure
if: failure()
run: |
docker compose logs --no-color \
shared-backend student-2-database student-2-backend
2 changes: 1 addition & 1 deletion student-2/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ frontend_service = [
]

[tool.pytest.ini_options]
testpaths = ["tests"]
testpaths = ["tests/database", "tests/backend", "tests/frontend", "tests/e2e"]
addopts = "-q"
filterwarnings = [
'ignore:Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead\.',
Expand Down
106 changes: 106 additions & 0 deletions student-2/tests/endpoint/test_post_commit_endpoints.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
"""Post-commit endpoint tests against the live Student 2 Compose stack.

student-2-ci.yml's compose job runs these over HTTP once the stack is ready.
An unreachable or unhealthy backend fails the suite. Ordinary pytest runs
exclude this directory through testpaths in pyproject.toml.
"""

from __future__ import annotations

import os
import uuid
from typing import TYPE_CHECKING

import httpx
import pytest

if TYPE_CHECKING:
from collections.abc import Iterator

BASE_URL = os.environ.get("STUDENT2_ENDPOINT_BASE_URL", "http://127.0.0.1:9000")


@pytest.fixture(scope="module")
def client() -> Iterator[httpx.Client]:
with httpx.Client(base_url=BASE_URL, timeout=10) as http:
http.get("/health").raise_for_status()
yield http


def accommodation_payload(**overrides: object) -> dict[str, object]:
return {
"name": f"CI Endpoint Stay {uuid.uuid4().hex[:12]}",
"type": "hotel",
"description": "Created by the post-commit endpoint tests",
"price_per_night": 189.5,
"availability_status": "available",
"location_details": {"country": "australia", "city": "sydney"},
} | overrides


@pytest.fixture
def accommodation(client: httpx.Client) -> Iterator[dict[str, object]]:
sent = accommodation_payload()
response = client.post("/accommodation", json=sent)
assert response.status_code == 201, response.text
created = response.json() | {"sent": sent}
yield created
# Deleting the row keeps reruns against a persistent volume clean.
client.delete(f"/accommodation/{created['id']}")


# Endpoint 1: POST /accommodation


def test_create_accommodation_returns_201_and_reads_back(client, accommodation):
sent = accommodation["sent"]
assert accommodation["name"] == sent["name"]

fetched = client.get(f"/accommodation/{accommodation['id']}")
assert fetched.status_code == 200
body = fetched.json()
assert body["name"] == sent["name"]
assert body["location_details"]["city"] == "sydney"


def test_created_accommodation_is_listed(client, accommodation):
response = client.get("/accommodation", params={"limit": 100})
assert response.status_code == 200
body = response.json()
assert body["total"] >= 1
assert accommodation["id"] in {row["id"] for row in body["accommodations"]}


def test_create_accommodation_rejects_missing_name(client):
payload = accommodation_payload()
del payload["name"]
assert client.post("/accommodation", json=payload).status_code in {400, 422}


def test_create_accommodation_rejects_unknown_city(client):
payload = accommodation_payload(
location_details={"country": "australia", "city": "narnia"}
)
assert client.post("/accommodation", json=payload).status_code == 400


# Endpoint 2: GET /accommodation/{id}


def test_get_accommodation_returns_full_message(client, accommodation):
response = client.get(f"/accommodation/{accommodation['id']}")
assert response.status_code == 200
body = response.json()
assert body["id"] == accommodation["id"]
assert body["price_per_night"] == accommodation["sent"]["price_per_night"]
assert body["availability_status"] == "available"


def test_get_unknown_accommodation_returns_404(client):
assert client.get(f"/accommodation/{uuid.uuid4()}").status_code == 404


def test_deleted_accommodation_is_gone(client):
created = client.post("/accommodation", json=accommodation_payload()).json()
assert client.delete(f"/accommodation/{created['id']}").status_code == 204
assert client.get(f"/accommodation/{created['id']}").status_code == 404
Loading