diff --git a/.gitea/workflows/test-backoffice.yml b/.gitea/workflows/test-backoffice.yml new file mode 100644 index 0000000..934b2cb --- /dev/null +++ b/.gitea/workflows/test-backoffice.yml @@ -0,0 +1,65 @@ +name: Test backoffice (smb-crm) + +# Automatic, unlike deploy-backoffice.yml: this workflow never touches the +# live host, only spins up a throwaway postgres:16-alpine container via the +# test suite itself (see backoffice/app/tests/conftest.py), so it's safe to +# run on every push/PR. Scoped to the paths that can actually affect these +# tests so unrelated changes elsewhere (n8n/, deploy/, docs/, etc.) don't +# trigger a run. +on: + push: + paths: + - "backoffice/app/**" + - "backoffice/db/**" + pull_request: + paths: + - "backoffice/app/**" + - "backoffice/db/**" + +jobs: + test: + runs-on: [self-hosted, homelab] + steps: + # Plain git + fetch-by-sha instead of actions/checkout@v4 (Node.js + # based; this runner deliberately has no Node toolchain) and instead + # of deploy-backoffice.yml's branch-name clone (that workflow is + # workflow_dispatch-only with a branch/tag input; push/pull_request + # give us a commit SHA via github.sha, which needs a fetch-by-SHA, not + # a branch checkout, to be exact for PRs). + - name: Checkout + env: + DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }} + GIT_SHA: ${{ github.sha }} + run: | + set -euo pipefail + find . -mindepth 1 -delete + git init -q + git remote add origin "http://${DEPLOY_TOKEN}@172.24.0.2:3000/BPPP/smb-online.git" + git fetch --depth 1 origin "$GIT_SHA" + git checkout -q FETCH_HEAD + git remote set-url origin http://172.24.0.2:3000/BPPP/smb-online.git + + # This runner is bare-metal (no guaranteed python3.12 binary), but it + # already has docker access (see deploy-backoffice.yml), so the venv is + # built inside a python:3.12-slim container — the same base image + # backoffice/app/Dockerfile ships on, so "3.12 venv" here means exactly + # what runs in production, with zero assumptions about the host's own + # Python. --network host is required: tests/conftest.py itself runs + # `docker run -p 127.0.0.1::5432 postgres:16-alpine` as a sibling + # container (via the mounted docker socket), and the test process must + # see that published port on the same network namespace it lands in. + - name: Install deps and run backoffice test suite (python:3.12-slim) + run: | + set -euo pipefail + docker run --rm --network host \ + -v "$PWD":/repo \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -w /repo \ + -e HOME=/tmp \ + python:3.12-slim bash -euxc ' + apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y -qq docker.io >/dev/null + python3 -m venv .venv + .venv/bin/pip install -q --upgrade pip + .venv/bin/pip install -q -r backoffice/app/requirements-dev.txt + .venv/bin/python -m pytest backoffice/app/tests -q + ' diff --git a/AGENTS.md b/AGENTS.md index 0ca585f..91b8ad1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,3 +20,10 @@ with `/mattpocock-skills:triage`. See `docs/agents/triage-labels.md`. Single-context: one `CONTEXT.md` + `docs/adr/` at the repo root (created lazily when terms or decisions get resolved). See `docs/agents/domain.md`. + +### Testing + +Backoffice tests need only Docker (a throwaway Postgres container is started for you) and a +Python 3.12 venv — the `psycopg[binary]` pin has version-specific wheel availability, so use +3.12, not whatever `python3` defaults to. Runs automatically in Gitea Actions on push/PR. See +`docs/agents/testing.md`. diff --git a/backoffice/app/requirements.txt b/backoffice/app/requirements.txt index d96bfba..4d9c685 100644 --- a/backoffice/app/requirements.txt +++ b/backoffice/app/requirements.txt @@ -1,4 +1,10 @@ Flask==3.0.3 +# psycopg-binary 3.2.1 only publishes prebuilt wheels up to cp312 (see +# https://pypi.org/project/psycopg-binary/3.2.1/#files) — installing this +# under Python 3.13+ falls back to a source build (needs libpq headers) or +# just fails to resolve. Match the python:3.12-slim runtime everywhere +# (venv, CI, prod image) and this pin installs from wheel with no build +# toolchain needed. See docs/agents/testing.md. psycopg[binary]==3.2.1 waitress==3.0.0 PyJWT[crypto]==2.9.0 diff --git a/docs/agents/testing.md b/docs/agents/testing.md new file mode 100644 index 0000000..cbb3584 --- /dev/null +++ b/docs/agents/testing.md @@ -0,0 +1,61 @@ +# Testing: backoffice (smb-crm) + +## Running the suite locally + +```bash +python3.12 -m venv .venv # must be 3.12 — see "psycopg pin" below +.venv/bin/pip install -r backoffice/app/requirements-dev.txt +cd backoffice/app && ../../.venv/bin/python -m pytest tests/ -q +``` + +No `python3.12` on your machine? Run it in a container instead — this is what +CI does (see `.gitea/workflows/test-backoffice.yml`): + +```bash +docker run --rm --network host -v "$PWD":/repo -v /var/run/docker.sock:/var/run/docker.sock \ + -w /repo -e HOME=/tmp python:3.12-slim bash -c ' + apt-get update -qq && apt-get install -y -qq docker.io >/dev/null + python3 -m venv .venv && .venv/bin/pip install -q -r backoffice/app/requirements-dev.txt + .venv/bin/python -m pytest backoffice/app/tests -q' +``` + +`--network host` matters: `tests/conftest.py` runs `docker run -p 127.0.0.1::5432` +as a sibling container via the mounted socket, and the test process needs to see that +published port in the same network namespace. + +## Docker is the only prerequisite + +`backoffice/app/tests/conftest.py` starts its own **throwaway `postgres:16-alpine` +container** at collection time (a `docker run` in module scope, bound to a random free +local port), applies `backoffice/db/init.sql` against it, and stops it via `atexit` when +the process exits. There is: + +- no `DATABASE_URL` to set by hand — the fixture sets `os.environ["DATABASE_URL"]` itself +- no shared/persistent test database to seed, migrate, or worry about polluting between runs +- nothing to tear down manually — a crashed run leaves an orphaned `smb-booking-test-db-*` + container behind (`docker ps -a | grep smb-booking-test-db` to find and `docker rm -f` it), + but the container name is randomized per run so it never collides with a live one. + +Per module docstring, this is deliberate (issue #14's testing decision): real Postgres, no +mocking, so the booking `EXCLUDE` constraint and tenancy filters get exercised for real. + +## The `psycopg[binary]==3.2.1` pin: why it can fail to install, and the fix + +`backoffice/app/requirements.txt` pins `psycopg[binary]==3.2.1`. If `pip install` reports +no matching distribution, **check your Python version first** — this is not a stale pin. + +`psycopg-binary` 3.2.1 only ships prebuilt wheels for CPython 3.8–3.12 +(confirmed against ). Under Python +3.13+ there is no wheel, so pip either falls back to a from-source build (needs libpq dev +headers you probably don't have) or fails outright with "No matching distribution." + +The project's production image is `python:3.12-slim` (see `backoffice/app/Dockerfile` and +the `tzdata` comment in `requirements.txt`), so the fix is to **match that Python version +everywhere** — local venv, CI, prod — rather than bump the pin. Use `python3.12`, or the +`python:3.12-slim` container recipe above if your machine doesn't have 3.12 installed. + +## CI + +This suite runs automatically on every push/PR touching `backoffice/app/**` or +`backoffice/db/**`, via `.gitea/workflows/test-backoffice.yml`. A local run before +pushing is a fast pre-check, not the only gate.