feat: opt-in Docker-based Gitea integration test suite (#66)

Adds tests/integration/: an optional real-Gitea suite, skipped by default,
enabled only by GITEA_INTEGRATION=1.

- docker-compose.yml pins gitea/gitea:1.22.6 (disposable container, port 3003)
- gitea-integration helper: up (wait-ready) / token (test-only admin, token to
  stdout only) / down (removes container + volume)
- conftest.py: session fixtures; unique disposable seed repo (inttest-<hex>)
  created via API and deleted on teardown
- test_gitea_live.py (6 tests, via shared api_request/api_get_all client):
  issue pagination multi-page walk + overall limit, PR listing, targeted label
  add/remove leaves other labels intact, bad-token 401 fails closed without
  echoing the credential, real 404 payload surfaces as safe redacted error
- README + developer-testing-guidelines section 8 updated (planned -> real)

Default pytest tests/ -q: 355 passed, 6 skipped (unit suite unchanged, no
network). Live verification: 6 passed against the pinned container. No
production credentials; token never logged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-02 16:11:48 -04:00
parent 02c0c2023b
commit 22347bd549
7 changed files with 327 additions and 8 deletions
+65
View File
@@ -0,0 +1,65 @@
"""Fixtures for the opt-in Docker/local Gitea integration suite (#66).
Everything here is inert unless GITEA_INTEGRATION=1 — the test modules carry
a module-level skipif, so a default ``pytest tests/ -q`` run never touches the
network and never needs Docker.
Required environment (see tests/integration/README.md):
- GITEA_INTEGRATION=1 opt-in switch
- GITEA_INTEGRATION_URL base URL (default http://localhost:3003)
- GITEA_INTEGRATION_TOKEN API token for the *local test* instance
The token is a throwaway credential for the disposable container. It is never
printed, logged, or asserted on. Production credentials must never be used.
"""
import os
import sys
import uuid
from pathlib import Path
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent))
from gitea_auth import api_request # noqa: E402
ENABLED = os.environ.get("GITEA_INTEGRATION") == "1"
def _base_url():
return os.environ.get("GITEA_INTEGRATION_URL", "http://localhost:3003").rstrip("/")
@pytest.fixture(scope="session")
def gitea():
"""Session facts: base URL, auth header string, authenticated login."""
token = os.environ.get("GITEA_INTEGRATION_TOKEN")
if not token:
pytest.fail(
"GITEA_INTEGRATION=1 but GITEA_INTEGRATION_TOKEN is unset; "
"run tests/integration/gitea-integration token"
)
base = _base_url()
auth = f"token {token}"
me = api_request("GET", f"{base}/api/v1/user", auth)
return {"base": base, "auth": auth, "login": me["login"]}
@pytest.fixture(scope="session")
def seed_repo(gitea):
"""Create a disposable, uniquely-named repo; delete it on teardown."""
name = f"inttest-{uuid.uuid4().hex[:8]}"
repo = api_request(
"POST", f"{gitea['base']}/api/v1/user/repos", gitea["auth"],
payload={"name": name, "auto_init": True,
"description": "gitea-tools #66 integration seed (disposable)"},
)
owner = repo["owner"]["login"]
yield {"owner": owner, "name": name,
"api": f"{gitea['base']}/api/v1/repos/{owner}/{name}"}
# Teardown: best-effort delete; a leaked repo is visible and disposable.
try:
api_request("DELETE", f"{gitea['base']}/api/v1/repos/{owner}/{name}",
gitea["auth"])
except RuntimeError:
pass