22347bd549
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>
116 lines
4.6 KiB
Python
116 lines
4.6 KiB
Python
"""Opt-in integration tests against a real (containerized) Gitea (#66).
|
|
|
|
Skipped by default. Enable with GITEA_INTEGRATION=1 plus a local instance and
|
|
token — see tests/integration/README.md. These prove real Gitea behavior that
|
|
the mocked unit suite can only simulate: pagination, targeted label edits,
|
|
permission fail-closed, and error payload surfacing — all through the shared
|
|
client (``gitea_auth.api_request`` / ``api_get_all``, #65/#67 shape).
|
|
|
|
No production credentials; tokens never appear in output or assertions.
|
|
"""
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent))
|
|
|
|
from gitea_auth import api_request, api_get_all # noqa: E402
|
|
|
|
pytestmark = pytest.mark.skipif(
|
|
os.environ.get("GITEA_INTEGRATION") != "1",
|
|
reason="integration tests are opt-in: set GITEA_INTEGRATION=1 (see tests/integration/README.md)",
|
|
)
|
|
|
|
|
|
# --- issue listing + pagination --------------------------------------------
|
|
|
|
N_ISSUES = 7 # > page_size below, forcing a real multi-page walk
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def issues(gitea, seed_repo):
|
|
created = []
|
|
for i in range(N_ISSUES):
|
|
created.append(api_request(
|
|
"POST", f"{seed_repo['api']}/issues", gitea["auth"],
|
|
payload={"title": f"pagination seed {i}"}))
|
|
return created
|
|
|
|
|
|
def test_issue_pagination_walks_all_pages(gitea, seed_repo, issues):
|
|
got = api_get_all(f"{seed_repo['api']}/issues?state=open", gitea["auth"],
|
|
page_size=3)
|
|
titles = {i["title"] for i in got}
|
|
assert {f"pagination seed {i}" for i in range(N_ISSUES)} <= titles
|
|
assert len(got) >= N_ISSUES
|
|
|
|
|
|
def test_issue_pagination_honors_overall_limit(gitea, seed_repo, issues):
|
|
got = api_get_all(f"{seed_repo['api']}/issues?state=open", gitea["auth"],
|
|
page_size=3, limit=4)
|
|
assert len(got) == 4
|
|
|
|
|
|
# --- PR listing --------------------------------------------------------------
|
|
|
|
def test_pr_listing_via_shared_client(gitea, seed_repo, issues):
|
|
# Create a branch (via the contents API) and a real PR, then list.
|
|
api_request(
|
|
"POST", f"{seed_repo['api']}/contents/inttest.txt", gitea["auth"],
|
|
payload={"content": "aW50ZWdyYXRpb24gc2VlZAo=", # "integration seed"
|
|
"message": "seed PR branch", "new_branch": "inttest-pr"})
|
|
pr = api_request(
|
|
"POST", f"{seed_repo['api']}/pulls", gitea["auth"],
|
|
payload={"title": "integration seed PR", "head": "inttest-pr",
|
|
"base": "main"})
|
|
got = api_get_all(f"{seed_repo['api']}/pulls?state=open", gitea["auth"],
|
|
page_size=1)
|
|
assert any(p["number"] == pr["number"] for p in got)
|
|
|
|
|
|
# --- targeted label add/remove ----------------------------------------------
|
|
|
|
def test_targeted_label_add_and_remove(gitea, seed_repo, issues):
|
|
labels = {}
|
|
for name, color in (("keep-me", "#00ff00"), ("drop-me", "#ff0000")):
|
|
labels[name] = api_request(
|
|
"POST", f"{seed_repo['api']}/labels", gitea["auth"],
|
|
payload={"name": name, "color": color})
|
|
issue_no = issues[0]["number"]
|
|
issue_labels_url = f"{seed_repo['api']}/issues/{issue_no}/labels"
|
|
api_request("POST", issue_labels_url, gitea["auth"],
|
|
payload={"labels": [labels["keep-me"]["id"],
|
|
labels["drop-me"]["id"]]})
|
|
# Targeted removal of one label must not disturb the other.
|
|
api_request("DELETE", f"{issue_labels_url}/{labels['drop-me']['id']}",
|
|
gitea["auth"])
|
|
remaining = {l["name"] for l in api_request("GET", issue_labels_url,
|
|
gitea["auth"])}
|
|
assert "keep-me" in remaining
|
|
assert "drop-me" not in remaining
|
|
|
|
|
|
# --- permission denial / fail-closed -----------------------------------------
|
|
|
|
def test_bad_token_fails_closed_without_leaking_it(gitea):
|
|
bogus = "token 0123456789abcdef0123456789abcdef01234567"
|
|
with pytest.raises(RuntimeError) as exc:
|
|
api_request("GET", f"{gitea['base']}/api/v1/user", bogus)
|
|
msg = str(exc.value)
|
|
assert "401" in msg
|
|
assert "0123456789abcdef" not in msg # credential never echoed
|
|
|
|
|
|
def test_real_error_payload_surfaces_safely(gitea, seed_repo):
|
|
# A genuinely missing resource: Gitea's real 404 error payload must become
|
|
# a clear RuntimeError, not a stack trace, and must not echo the token.
|
|
with pytest.raises(RuntimeError) as exc:
|
|
api_request("GET", f"{seed_repo['api']}/issues/999999", gitea["auth"])
|
|
msg = str(exc.value)
|
|
assert "404" in msg
|
|
token = os.environ.get("GITEA_INTEGRATION_TOKEN", "")
|
|
if token:
|
|
assert token not in msg
|