fix(web): Cloudflare UA block, Gitea path@ref, attempt 0
Three bugs the real API surfaced, none visible against a stubbed payload: - Gitea here is proxied by Cloudflare, which answers urllib's default `Python-urllib/3.x` agent with 403 + "error code: 1010" before the request reaches Gitea — every endpoint 403s, including unauthenticated `/version`, so it reads exactly like a token problem. Send an explicit User-Agent. - Gitea reports `path` as either `.gitea/workflows/lint.yml` or the `lint.yml@refs/heads/main` form; splitting on "/" alone rendered the latter as the workflow name "main". Drop the ref first. - Gitea reports `run_attempt: 0` for a never-retried run while the job id the workflow builds uses github.run_attempt, which starts at 1 — so 0 must read as 1 or no VM would ever correlate to its run. (The existing `or 1` already did this; it now has a test naming why, against the real payload shape.) HTTP failures now carry the server's own message (Gitea names the missing scope; a Cloudflare interception says something else entirely) instead of a bare status code that sent this investigation the wrong way once already.
This commit is contained in:
@@ -100,9 +100,13 @@ class _FakeResponse:
|
||||
return None
|
||||
|
||||
|
||||
def _http_error(code: int) -> urllib.error.HTTPError:
|
||||
def _http_error(code: int, body: str = "") -> urllib.error.HTTPError:
|
||||
return urllib.error.HTTPError(
|
||||
url="https://gitea.example", code=code, msg="nope", hdrs=None, fp=None
|
||||
url="https://gitea.example",
|
||||
code=code,
|
||||
msg="nope",
|
||||
hdrs=None, # type: ignore[arg-type]
|
||||
fp=BytesIO(body.encode("utf-8")) if body else None,
|
||||
)
|
||||
|
||||
|
||||
@@ -192,6 +196,95 @@ def test_admin_payload_parsed_into_executions(
|
||||
]
|
||||
|
||||
|
||||
def test_request_sends_an_explicit_user_agent(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""Cloudflare in front of Gitea 403s urllib's default agent (error 1010)."""
|
||||
agents: list[str | None] = []
|
||||
|
||||
def _fake_urlopen(request: Any, timeout: float | None = None) -> Any:
|
||||
agents.append(request.get_header("User-agent"))
|
||||
return _FakeResponse({"workflow_runs": []})
|
||||
|
||||
monkeypatch.setattr(ex.urllib.request, "urlopen", _fake_urlopen)
|
||||
|
||||
list_executions(_make_config(tmp_path), [], token="tok", now=_NOW)
|
||||
|
||||
assert agents and agents[0]
|
||||
assert "python-urllib" not in str(agents[0]).lower()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("path", "expected"),
|
||||
[
|
||||
(".gitea/workflows/build-ns7zip.yml", "build-ns7zip.yml"),
|
||||
# Gitea's <file>@<ref> form: splitting on "/" alone yields "main".
|
||||
("lint.yml@refs/heads/main", "lint.yml"),
|
||||
(".gitea/workflows/ci.yml@refs/heads/feat/x", "ci.yml"),
|
||||
("", ""),
|
||||
],
|
||||
)
|
||||
def test_workflow_name_drops_the_ref(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, path: str, expected: str
|
||||
) -> None:
|
||||
_install_urlopen(monkeypatch, _admin_ok([_run(path=path)]))
|
||||
|
||||
view = list_executions(_make_config(tmp_path), [], token="tok", now=_NOW)
|
||||
|
||||
assert view.runs[0].workflow == expected
|
||||
|
||||
|
||||
def test_zero_attempt_reads_as_one(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""Gitea sends ``run_attempt: 0`` for a first run; job ids start at 1."""
|
||||
_install_urlopen(monkeypatch, _admin_ok([_run(run_id=384, attempt=0)]))
|
||||
vm = _vm("Clone_384-1_20260725_115000_linux")
|
||||
|
||||
view = list_executions(_make_config(tmp_path), [vm], token="tok", now=_NOW)
|
||||
|
||||
assert view.runs[0].attempt == 1
|
||||
assert view.runs[0].vms == [vm]
|
||||
|
||||
|
||||
def test_http_error_carries_the_server_message(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""Gitea names the missing scope in its body -- surface it, don't guess."""
|
||||
body = json.dumps(
|
||||
{"message": "token does not have at least one of required scope(s)"}
|
||||
)
|
||||
_install_urlopen(monkeypatch, lambda _url: _http_error(500, body))
|
||||
|
||||
view = list_executions(_make_config(tmp_path), [], token="tok", now=_NOW)
|
||||
|
||||
assert view.error is not None
|
||||
assert "HTTP 500" in view.error
|
||||
assert "required scope(s)" in view.error
|
||||
|
||||
|
||||
def test_non_json_error_body_is_included_verbatim(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""A Cloudflare interception is HTML, not Gitea JSON -- still show it."""
|
||||
_install_urlopen(monkeypatch, lambda _url: _http_error(500, "<html>error code: 1010</html>"))
|
||||
|
||||
view = list_executions(_make_config(tmp_path), [], token="tok", now=_NOW)
|
||||
|
||||
assert view.error is not None
|
||||
assert "1010" in view.error
|
||||
|
||||
|
||||
def test_empty_error_body_leaves_a_bare_status(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
_install_urlopen(monkeypatch, lambda _url: _http_error(500))
|
||||
|
||||
view = list_executions(_make_config(tmp_path), [], token="tok", now=_NOW)
|
||||
|
||||
assert view.error == "Gitea API returned HTTP 500."
|
||||
|
||||
|
||||
def test_limit_argument_overrides_config(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
|
||||
Reference in New Issue
Block a user