blob: a589ad5da74ea6b17b788f78216df46c528b6ede [file]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""CLI tests — `gh` is faked via a subprocess.run patch."""
from __future__ import annotations
import json
import subprocess
from collections.abc import Iterator
from dataclasses import dataclass
import pytest
from github_rollup import cli
MARKER_LINE = (
"<!-- airflow-s status rollup v1 — all bot-authored status updates fold into this single comment. -->"
)
ROLLUP_BODY = (
f"{MARKER_LINE}\n"
"<details><summary>2026-05-28 · @potiuk · Import</summary>\n\n"
"Imported from security@.\n\n"
"</details>"
)
@dataclass
class FakeResult:
returncode: int
stdout: str = ""
stderr: str = ""
class FakeGh:
"""Records every `gh` call and returns canned output for each
pattern. Tests assert on the recorded calls."""
def __init__(self, has_rollup: bool = True) -> None:
self.calls: list[tuple[list[str], str | None]] = []
self.has_rollup = has_rollup
# Track the last patched body so tests can verify what we
# actually wrote.
self.patched_body: str | None = None
self.posted_body: str | None = None
def __call__(
self,
cmd: list[str],
*,
capture_output: bool = False,
text: bool = False,
input: str | None = None,
check: bool = False,
**_: object,
) -> FakeResult:
self.calls.append((cmd, input))
joined = " ".join(cmd)
if "issue view" in joined and "--json comments" in joined:
comments = []
if self.has_rollup:
comments.append(
{
"id": "IC_NODEID",
"body": ROLLUP_BODY,
"url": "https://github.com/o/r/issues/1#issuecomment-1",
}
)
return FakeResult(returncode=0, stdout=json.dumps({"comments": comments}))
if "api user" in joined:
return FakeResult(returncode=0, stdout="potiuk\n")
if "repo view" in joined:
return FakeResult(returncode=0, stdout="o/r\n")
if "issues/comments?per_page=100" in joined and "node_id" in joined:
# REST-id resolution stub.
return FakeResult(returncode=0, stdout="98765\n")
if "-X" in cmd and "PATCH" in cmd and "issues/comments" in joined:
# Extract the body from -f body=<value>.
for i, tok in enumerate(cmd):
if tok == "-f" and i + 1 < len(cmd) and cmd[i + 1].startswith("body="):
self.patched_body = cmd[i + 1][len("body=") :]
break
return FakeResult(returncode=0)
if "issue comment" in joined and "--body-file" in cmd and "-" in cmd:
self.posted_body = input
return FakeResult(returncode=0)
raise AssertionError(f"unexpected gh invocation: {cmd!r}")
@pytest.fixture
def fake_gh(monkeypatch: pytest.MonkeyPatch) -> Iterator[FakeGh]:
f = FakeGh()
monkeypatch.setattr(subprocess, "run", f)
yield f
@pytest.fixture
def fake_gh_no_rollup(monkeypatch: pytest.MonkeyPatch) -> Iterator[FakeGh]:
f = FakeGh(has_rollup=False)
monkeypatch.setattr(subprocess, "run", f)
yield f
# ---------------------------------------------------------------------------
# append — append path
# ---------------------------------------------------------------------------
def test_append_to_existing_rollup_patches_with_new_entry(
fake_gh: FakeGh, capsys: pytest.CaptureFixture[str]
) -> None:
rc = cli.main(
[
"--repo",
"o/r",
"append",
"1",
"--action",
"CVE allocated",
"--entry-body",
"Allocated CVE-2026-12345.",
"--now",
"2026-05-30T12:00:00Z",
]
)
assert rc == 0
assert fake_gh.patched_body is not None
# The PATCH body contains BOTH the existing Import entry and the
# new CVE allocated entry, separated by the canonical ruler.
assert "Import" in fake_gh.patched_body
assert "CVE allocated" in fake_gh.patched_body
assert "</details>\n\n---\n\n<details>" in fake_gh.patched_body
err = capsys.readouterr().err
assert "appended to rollup on o/r#1" in err
def test_append_uses_gh_auth_user_when_user_flag_omitted(
fake_gh: FakeGh, capsys: pytest.CaptureFixture[str]
) -> None:
cli.main(
[
"--repo",
"o/r",
"append",
"1",
"--action",
"Sync",
"--entry-body",
"X",
"--now",
"2026-05-30T12:00:00Z",
]
)
assert fake_gh.patched_body is not None
# FakeGh returns `potiuk` from `gh api user`.
assert "· @potiuk ·" in fake_gh.patched_body
def test_append_explicit_user_overrides_auth(fake_gh: FakeGh, capsys: pytest.CaptureFixture[str]) -> None:
cli.main(
[
"--repo",
"o/r",
"append",
"1",
"--action",
"Sync",
"--entry-body",
"X",
"--user",
"@other-user",
"--now",
"2026-05-30T12:00:00Z",
]
)
assert fake_gh.patched_body is not None
assert "· @other-user ·" in fake_gh.patched_body
def test_append_dry_run_skips_writes(fake_gh: FakeGh, capsys: pytest.CaptureFixture[str]) -> None:
rc = cli.main(
[
"--repo",
"o/r",
"append",
"1",
"--action",
"Sync",
"--entry-body",
"X",
"--now",
"2026-05-30T12:00:00Z",
"--dry-run",
]
)
assert rc == 0
assert fake_gh.patched_body is None
assert fake_gh.posted_body is None
err = capsys.readouterr().err
assert "dry-run" in err
assert "APPEND" in err
# ---------------------------------------------------------------------------
# append — create path
# ---------------------------------------------------------------------------
def test_append_creates_new_rollup_when_none_exists(
fake_gh_no_rollup: FakeGh, capsys: pytest.CaptureFixture[str]
) -> None:
rc = cli.main(
[
"--repo",
"o/r",
"append",
"1",
"--action",
"Import",
"--entry-body",
"Imported from security@.",
"--now",
"2026-05-30T12:00:00Z",
]
)
assert rc == 0
body = fake_gh_no_rollup.posted_body
assert body is not None
assert body.startswith("<!-- airflow-s status rollup v1 — all bot-authored status updates fold")
assert "· @potiuk · Import" in body
assert "Imported from security@" in body
err = capsys.readouterr().err
assert "created rollup on o/r#1" in err
def test_append_dry_run_signals_create_path(
fake_gh_no_rollup: FakeGh, capsys: pytest.CaptureFixture[str]
) -> None:
rc = cli.main(
[
"--repo",
"o/r",
"append",
"1",
"--action",
"Import",
"--entry-body",
"X",
"--now",
"2026-05-30T12:00:00Z",
"--dry-run",
]
)
assert rc == 0
err = capsys.readouterr().err
assert "CREATE" in err
# ---------------------------------------------------------------------------
# append — argument validation
# ---------------------------------------------------------------------------
def test_append_rejects_both_entry_body_flags(fake_gh: FakeGh, capsys: pytest.CaptureFixture[str]) -> None:
with pytest.raises(SystemExit) as exc:
cli.main(
[
"--repo",
"o/r",
"append",
"1",
"--action",
"Sync",
"--entry-body",
"x",
"--entry-body-file",
"/tmp/x",
]
)
assert exc.value.code == 2
err = capsys.readouterr().err
assert "not both" in err
def test_append_requires_an_entry_body_source(fake_gh: FakeGh, capsys: pytest.CaptureFixture[str]) -> None:
with pytest.raises(SystemExit) as exc:
cli.main(["--repo", "o/r", "append", "1", "--action", "Sync"])
assert exc.value.code == 2
err = capsys.readouterr().err
assert "required" in err
# ---------------------------------------------------------------------------
# list and latest
# ---------------------------------------------------------------------------
def test_list_prints_one_summary_per_line(fake_gh: FakeGh, capsys: pytest.CaptureFixture[str]) -> None:
rc = cli.main(["--repo", "o/r", "list", "1"])
assert rc == 0
out = capsys.readouterr().out
assert "2026-05-28 · @potiuk · Import" in out
def test_list_json(fake_gh: FakeGh, capsys: pytest.CaptureFixture[str]) -> None:
rc = cli.main(["--repo", "o/r", "list", "1", "--json"])
assert rc == 0
out = capsys.readouterr().out
parsed = json.loads(out)
assert parsed == [{"date": "2026-05-28", "user": "@potiuk", "action": "Import"}]
def test_list_exits_3_when_no_rollup(fake_gh_no_rollup: FakeGh, capsys: pytest.CaptureFixture[str]) -> None:
rc = cli.main(["--repo", "o/r", "list", "1"])
assert rc == 3
err = capsys.readouterr().err
assert "no rollup" in err
def test_latest_prints_last_entry_body(fake_gh: FakeGh, capsys: pytest.CaptureFixture[str]) -> None:
rc = cli.main(["--repo", "o/r", "latest", "1"])
assert rc == 0
out = capsys.readouterr().out
assert "Imported from security@" in out
def test_latest_exits_3_when_no_rollup(fake_gh_no_rollup: FakeGh, capsys: pytest.CaptureFixture[str]) -> None:
rc = cli.main(["--repo", "o/r", "latest", "1"])
assert rc == 3