fix(drone): ADV-drone-01 — no-follow redirect pattern in SCM test
Some checks failed
continuous-integration/drone/push Build is failing
Some checks failed
continuous-integration/drone/push Build is failing
test_scm_configured.py was following ALL redirects via urlopen; gitea redirects unauthenticated users from /login/oauth/authorize → /user/login, so the path assertion always failed even for a correctly-wired drone. Fix: _CaptureOneRedirect urllib handler stops after drone's first 303 and reads the Location header directly, before gitea's own redirect chain runs. - Consume BUILDER-INBOX.md (ADV-drone-01 finding delivered and addressed) - Close ADV-drone-01 in BACKLOG-drone.md - Update test_gitea_dep.py terminology: "location_url" not "final_url" - All 10 unit tests pass Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@ -6,30 +6,47 @@ The negative control: a drone deployed WITHOUT DRONE_GITEA_CLIENT_ID + DRONE_GIT
|
||||
authorize endpoint — it would error or redirect elsewhere. This test is therefore falsified
|
||||
by a misconfigured drone.
|
||||
|
||||
Test: GET https://<drone>/login (following redirects) must land at the per-run gitea dep's
|
||||
/login/oauth/authorize URL, and the client_id query param must match the OAuth2 app the
|
||||
harness created in the gitea dep (recorded in deps["gitea"]["client_id"]).
|
||||
Test: GET https://<drone>/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 the Adversary's pre-probe (REVIEW-drone.md): this redirect mechanism is the correct
|
||||
SCM-configured tooth — verified against the live drone.ci.commoninternet.net instance.
|
||||
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 redirect to the per-run gitea dep's OAuth2 authorize endpoint.
|
||||
"""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 matches the app the harness created in the dep gitea instance; (c) the
|
||||
redirect targets the TEST-RUN gitea, not any hardcoded external provider.
|
||||
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. "
|
||||
@ -43,19 +60,41 @@ def test_login_redirects_to_gitea_dep(live_app, deps):
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = ssl.CERT_NONE
|
||||
|
||||
# Follow all redirects from /login — the final URL must be gitea's OAuth2 authorize page.
|
||||
req = urllib.request.Request(f"https://{live_app}/login", method="GET")
|
||||
with urllib.request.urlopen(req, timeout=30, context=ctx) as resp:
|
||||
final_url = resp.geturl()
|
||||
opener = urllib.request.build_opener(
|
||||
_CaptureOneRedirect(),
|
||||
urllib.request.HTTPSHandler(context=ctx),
|
||||
)
|
||||
|
||||
parsed = urllib.parse.urlparse(final_url)
|
||||
assert parsed.scheme == "https", f"Unexpected scheme in final URL: {final_url!r}"
|
||||
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, (
|
||||
f"Drone /login returned {e.code} but Location header is empty — "
|
||||
f"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"final URL: {final_url!r} — check GITEA_DOMAIN + COMPOSE_FILE in drone's .env"
|
||||
f"Location: {redirect_url!r} — check GITEA_DOMAIN + COMPOSE_FILE in drone's .env"
|
||||
)
|
||||
assert parsed.path == "/login/oauth/authorize", (
|
||||
f"Final URL path is {parsed.path!r}, expected /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)
|
||||
|
||||
Reference in New Issue
Block a user