From c9c0ff96fc77b7eb6496aa212538976b77b8edf1 Mon Sep 17 00:00:00 2001 From: Lars Date: Sat, 3 Oct 2026 11:20:30 +0200 Subject: [PATCH] Ergaenze die Kansho-analoge Test-Suite gegen buyingagent_test. Co-authored-by: Cursor --- .gitea/workflows/test.yml | 48 ++++++++++++++++++++++++++++++++- Dockerfile | 2 +- README.md | 4 ++- docker-compose.dev-env.yml | 20 ++++++++++++++ docker-compose.test.yml | 7 ++++- docs/DEPLOYMENT.md | 4 ++- pyproject.toml | 4 +++ tests/integration/test_ready.py | 40 +++++++++++++++++++++++++++ tests/unit/test_settings.py | 14 ++++++++++ 9 files changed, 138 insertions(+), 5 deletions(-) create mode 100644 tests/integration/test_ready.py create mode 100644 tests/unit/test_settings.py diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml index 3dc4f68..22f771e 100644 --- a/.gitea/workflows/test.yml +++ b/.gitea/workflows/test.yml @@ -9,6 +9,52 @@ on: workflow_dispatch: jobs: + pytest-backend: + if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }} + runs-on: ubuntu-latest + steps: + - name: pytest against Dev Postgres (buyingagent_test) + run: | + set -e + APP_DIR="$HOME/docker/buyingagent-dev" + COMPOSE_FILE="docker-compose.dev-env.yml" + echo "pytest uses Dev Postgres (${APP_DIR}); event=${{ github.event_name }}" + cd "$APP_DIR" + for i in $(seq 1 60); do + if [ -f "$APP_DIR/pyproject.toml" ] && docker compose -f "$COMPOSE_FILE" exec -T api true 2>/dev/null; then + echo "api and pyproject.toml ready (attempt $i)" + break + fi + if [ "$i" -eq 60 ]; then + echo "timeout waiting for Dev checkout/api" + docker compose -f "$COMPOSE_FILE" ps || true + ls -la "$APP_DIR" || true + exit 1 + fi + sleep 5 + done + PGUSER="$(docker compose -f "$COMPOSE_FILE" exec -T postgres printenv POSTGRES_USER | tr -d '\r')" + echo "test database buyingagent_test on the existing Dev Postgres (owner ${PGUSER})" + docker compose -f "$COMPOSE_FILE" exec -T postgres \ + psql -U "$PGUSER" -d postgres -v ON_ERROR_STOP=1 \ + -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = 'buyingagent_test' AND pid <> pg_backend_pid();" + docker compose -f "$COMPOSE_FILE" exec -T postgres \ + psql -U "$PGUSER" -d postgres -v ON_ERROR_STOP=1 \ + -c "DROP DATABASE IF EXISTS buyingagent_test;" + docker compose -f "$COMPOSE_FILE" exec -T postgres \ + psql -U "$PGUSER" -d postgres -v ON_ERROR_STOP=1 \ + -c "CREATE DATABASE buyingagent_test OWNER ${PGUSER};" + docker compose -f "$COMPOSE_FILE" --profile test build --pull=false api-test + docker compose -f "$COMPOSE_FILE" --profile test run --rm --no-deps \ + -v "${APP_DIR}:/src:ro" \ + -w /src \ + -e PYTHONPATH=/src \ + -e DB_NAME=buyingagent_test \ + -e ENVIRONMENT=test \ + -e ENGINE_BUILD=test \ + api-test \ + sh -lc 'set -e; alembic upgrade head; pytest tests -m "not slow" -ra --tb=short' + smoke-dev: if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }} runs-on: ubuntu-latest @@ -16,7 +62,7 @@ jobs: - name: Non-destructive smoke against the running Dev API run: | set -e - echo "wait for live Dev API (buyingagent-dev)" + echo "live Dev API (buyingagent-dev), read-only health" for i in $(seq 1 60); do if curl -sf http://localhost:8095/health/live >/dev/null; then echo "DEV /health/live ready (attempt $i)" diff --git a/Dockerfile b/Dockerfile index a6793b8..a9b991e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -37,4 +37,4 @@ RUN uv sync --frozen --extra test \ && chown -R app:app /app USER app -CMD ["pytest", "tests", "-ra"] +CMD ["pytest", "tests", "-m", "not slow", "-ra", "--tb=short"] diff --git a/README.md b/README.md index a8dd51b..fee6518 100644 --- a/README.md +++ b/README.md @@ -26,11 +26,13 @@ Ready: `GET /health/ready` (PostgreSQL erreichbar und Alembic auf Head) ## Tests +Lokal, isolierter Stack (tmpfs, kein Dev-/Prod-Volume): + ```sh docker compose -f docker-compose.test.yml up --build --abort-on-container-exit --exit-code-from api ``` -Die Testdatenbank liegt auf tmpfs und leert kein normales Datenvolume. +Auf dem Server nach Dev-Deploy (Gitea `test.yml`, analog Kanshō): pytest gegen `buyingagent_test` auf dem Dev-Postgres plus read-only Smoke gegen `http://192.168.2.49:8095/health/ready`. ## Server diff --git a/docker-compose.dev-env.yml b/docker-compose.dev-env.yml index b34b823..d185c0e 100644 --- a/docker-compose.dev-env.yml +++ b/docker-compose.dev-env.yml @@ -53,6 +53,26 @@ services: networks: - dev-buyingagent-network + api-test: + profiles: ["test"] + build: + context: . + dockerfile: Dockerfile + target: test + environment: + DB_HOST: postgres + DB_PORT: "5432" + DB_NAME: "${DB_NAME:-buyingagent_dev}" + DB_USER: "${DB_USER:-buyingagent_dev}" + DB_PASSWORD: "${DB_PASSWORD:-dev_password_change_me}" + ENVIRONMENT: test + ENGINE_BUILD: test + depends_on: + postgres: + condition: service_healthy + networks: + - dev-buyingagent-network + volumes: dev-buyingagent-db-data: diff --git a/docker-compose.test.yml b/docker-compose.test.yml index 22aee50..6897a3b 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -36,7 +36,12 @@ services: depends_on: postgres: condition: service_healthy - command: ["pytest", "tests", "-ra"] + command: + [ + "sh", + "-lc", + "alembic upgrade head && pytest tests -m 'not slow' -ra --tb=short", + ] networks: - buyingagent-test-network diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index d5fdd71..44b9a5e 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -64,10 +64,12 @@ Watchtower bleibt aus. |----------|---------|--------| | `deploy-dev.yml` | Push `develop` | Deploy nach `~/docker/buyingagent-dev`, Health `localhost:8095` | | `deploy-prod.yml` | Push `main` | Deploy nach `~/docker/buyingagent`, Health `localhost:8006` | -| `test.yml` | nach Dev-Deploy bzw. Push `develop` | nicht-schreibender Smoke gegen `/health/live` und `/health/ready` | +| `test.yml` | nach Dev-Deploy bzw. Push `develop` | pytest auf `buyingagent_test` (Dev-Postgres, nicht Live-DB) und read-only Smoke gegen `/health/live` und `/health/ready` | Der API-Build nutzt `ghcr.io` (nicht Docker Hub). `docker compose build --pull=false` verwendet vorhandene Basis-Images und vermeidet IPv6-Timeouts gegen `auth.docker.io`. +`test.yml` folgt Kanshō: pytest läuft in einem Einweg-Container (`api-test`, Compose-Profil `test`) gegen die wegwerfbare Datenbank `buyingagent_test` auf dem bereits laufenden Dev-Postgres. Die Live-Datenbank bleibt unangetastet. Es gibt kein Frontend, daher keinen Frontend-Build-Job. + --- ## Manuelles Deploy diff --git a/pyproject.toml b/pyproject.toml index 8d560c0..f745736 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,3 +30,7 @@ include = ["app*"] [tool.pytest.ini_options] testpaths = ["tests"] addopts = "-ra" +markers = [ + "slow: long-running tests skipped in the default CI suite", + "integration: needs PostgreSQL 17", +] diff --git a/tests/integration/test_ready.py b/tests/integration/test_ready.py new file mode 100644 index 0000000..b5501dc --- /dev/null +++ b/tests/integration/test_ready.py @@ -0,0 +1,40 @@ +import pytest +from fastapi.testclient import TestClient +from sqlalchemy import text + +from alembic.config import Config +from alembic.runtime.migration import MigrationContext +from alembic.script import ScriptDirectory + +from app.main import app +from app.persistence.db import engine + +pytestmark = pytest.mark.integration + +client = TestClient(app) + + +@pytest.fixture(scope="module", autouse=True) +def require_postgres() -> None: + try: + with engine.connect() as connection: + connection.execute(text("SELECT 1")) + except Exception as exc: + pytest.skip(f"PostgreSQL not available: {exc}") + + +def test_health_ready_when_migrated() -> None: + response = client.get("/health/ready") + assert response.status_code == 200 + body = response.json() + assert body["status"] == "ready" + assert "environment" in body + assert "engine_build" in body + + +def test_alembic_is_at_head() -> None: + config = Config("alembic.ini") + script = ScriptDirectory.from_config(config) + with engine.connect() as connection: + context = MigrationContext.configure(connection) + assert set(context.get_current_heads()) == set(script.get_heads()) diff --git a/tests/unit/test_settings.py b/tests/unit/test_settings.py new file mode 100644 index 0000000..a64eb3c --- /dev/null +++ b/tests/unit/test_settings.py @@ -0,0 +1,14 @@ +from app.settings import Settings + + +def test_database_url_uses_psycopg_and_explicit_fields() -> None: + settings = Settings( + db_host="postgres", + db_port=5432, + db_name="buyingagent_test", + db_user="buyingagent", + db_password="secret", + ) + assert settings.database_url == ( + "postgresql+psycopg://buyingagent:secret@postgres:5432/buyingagent_test" + )