"""drone — SCM-configured functional test (phase drone). Proves that drone is wired to the per-run gitea dep, not just healthy. The negative control: a drone deployed WITHOUT DRONE_GITEA_CLIENT_ID + DRONE_GITEA_SERVER (i.e., without compose.gitea.yml) would NOT redirect /login to the gitea dep's OAuth authorize endpoint — it would error or redirect elsewhere. This test is therefore falsified by a misconfigured drone. Test: GET https:///login must issue a 303 redirect whose Location header points to the per-run gitea dep's /login/oauth/authorize URL. We capture ONLY drone's first redirect (not gitea's subsequent redirect to /user/login for unauthenticated users). Per ADV-drone-01: following all redirects causes the assertion to land on gitea's /user/login (200 OK after gitea redirects unauthenticated users away from /login/oauth/authorize), which means the path assertion always fails. The fix is a no-follow handler that captures the Location header from drone's 303 directly. """ from __future__ import annotations import ssl import urllib.error import urllib.parse import urllib.request import pytest class _CaptureOneRedirect(urllib.request.HTTPRedirectHandler): """Stop redirect-following after the FIRST hop; raise HTTPError so the caller can inspect the Location header from drone's 303 without following gitea's subsequent redirects.""" def http_error_302(self, req, fp, code, msg, headers): raise urllib.error.HTTPError(req.full_url, code, msg, headers, fp) http_error_303 = http_error_302 @pytest.mark.requires_deps def test_login_redirects_to_gitea_dep(live_app, deps): """Drone's /login must issue a 303 redirect to the per-run gitea dep's OAuth2 authorize endpoint. Proves: (a) gitea is the SCM backend (not github or unconfigured); (b) the OAuth2 client_id in the Location header matches the app the harness created in the dep gitea instance; (c) the redirect targets the TEST-RUN gitea, not any hardcoded external provider. ADV-drone-01 fix: only drone's first 303 is captured; gitea's own redirects (unauthenticated user → /user/login) are not followed, so the path assertion is against the correct URL. """ assert "gitea" in deps, ( f"gitea dep not in deps — dep provisioning should have populated this. " f"Got keys: {list(deps.keys())}" ) gitea = deps["gitea"] gitea_domain: str = gitea["domain"] expected_client_id: str = gitea["client_id"] ctx = ssl.create_default_context() ctx.check_hostname = False ctx.verify_mode = ssl.CERT_NONE opener = urllib.request.build_opener( _CaptureOneRedirect(), urllib.request.HTTPSHandler(context=ctx), ) redirect_url = None try: opener.open(f"https://{live_app}/login", timeout=30) pytest.fail( f"Expected a 302/303 redirect from https://{live_app}/login but got 200 OK — " f"drone may not have gitea SCM configured (check COMPOSE_FILE + GITEA_DOMAIN)" ) except urllib.error.HTTPError as e: if e.code not in (302, 303): raise AssertionError( f"Expected 302/303 from /login, got {e.code} — " f"drone may not have gitea SCM configured" ) from e redirect_url = e.headers.get("Location") or e.headers.get("location", "") assert redirect_url, ( "Drone /login returned a redirect but Location header is empty — " "check drone gitea SCM configuration" ) parsed = urllib.parse.urlparse(redirect_url) assert parsed.scheme == "https", f"Redirect Location has unexpected scheme: {redirect_url!r}" assert parsed.netloc == gitea_domain, ( f"Drone /login did not redirect to the gitea dep ({gitea_domain!r}); " f"Location: {redirect_url!r} — check GITEA_DOMAIN + COMPOSE_FILE in drone's .env" ) assert parsed.path == "/login/oauth/authorize", ( f"Redirect path is {parsed.path!r}, expected /login/oauth/authorize — " f"drone may not have gitea SCM configured" ) params = urllib.parse.parse_qs(parsed.query) actual_client_id = params.get("client_id", [None])[0] assert actual_client_id == expected_client_id, ( f"OAuth2 client_id mismatch: drone is using {actual_client_id!r} but the harness " f"created app {expected_client_id!r} in the dep gitea — check install_steps.sh" )