|
2 | 2 |
|
3 | 3 | from __future__ import annotations |
4 | 4 |
|
| 5 | +import datetime |
| 6 | +import json |
| 7 | +from types import SimpleNamespace |
5 | 8 | from unittest.mock import MagicMock |
6 | 9 |
|
| 10 | +import pytest |
7 | 11 | import requests |
8 | 12 |
|
| 13 | +from src.cli import _run_security_burndown_mode |
9 | 14 | from src.ghas_alert_details import fetch_dependabot_details |
10 | 15 | from src.security_burndown import ( |
11 | 16 | BurndownEntry, |
@@ -739,3 +744,81 @@ def test_full_round_trip(self) -> None: |
739 | 744 | assert "HIGH" in md |
740 | 745 | # medium was filtered out — pip/requests still appears but as the no-ghsa entry |
741 | 746 | assert "pip/requests" in md |
| 747 | + |
| 748 | + |
| 749 | +class TestRunSecurityBurndownMode: |
| 750 | + """CLI dispatch (_run_security_burndown_mode) — markdown + JSON sidecar.""" |
| 751 | + |
| 752 | + def _write_ghas(self, output_dir, username: str) -> None: |
| 753 | + today = datetime.date.today().isoformat() |
| 754 | + ghas = { |
| 755 | + "RepoA": { |
| 756 | + "dependabot_details": [ |
| 757 | + { |
| 758 | + "package": "lodash", |
| 759 | + "ecosystem": "npm", |
| 760 | + "scope": "runtime", |
| 761 | + "severity": "critical", |
| 762 | + "ghsa_id": "GHSA-aaaa-0001", |
| 763 | + "first_patched_version": "4.17.21", |
| 764 | + } |
| 765 | + ] |
| 766 | + }, |
| 767 | + "RepoB": { |
| 768 | + "dependabot_details": [ |
| 769 | + { |
| 770 | + "package": "lodash", |
| 771 | + "ecosystem": "npm", |
| 772 | + "scope": "runtime", |
| 773 | + "severity": "critical", |
| 774 | + "ghsa_id": "GHSA-aaaa-0001", |
| 775 | + "first_patched_version": "4.17.21", |
| 776 | + } |
| 777 | + ] |
| 778 | + }, |
| 779 | + } |
| 780 | + (output_dir / f"ghas-alerts-{username}-{today}.json").write_text( |
| 781 | + json.dumps(ghas), encoding="utf-8" |
| 782 | + ) |
| 783 | + |
| 784 | + def test_writes_markdown_and_json_sidecar(self, tmp_path, capsys) -> None: |
| 785 | + self._write_ghas(tmp_path, "octocat") |
| 786 | + args = SimpleNamespace(output_dir=str(tmp_path), username="octocat") |
| 787 | + |
| 788 | + _run_security_burndown_mode(args) |
| 789 | + |
| 790 | + today = datetime.date.today().isoformat() |
| 791 | + md_path = tmp_path / f"security-burndown-octocat-{today}.md" |
| 792 | + json_path = tmp_path / f"security-burndown-octocat-{today}.json" |
| 793 | + assert md_path.exists(), "markdown artifact should be written" |
| 794 | + assert json_path.exists(), "JSON sidecar should be written" |
| 795 | + |
| 796 | + payload = json.loads(json_path.read_text(encoding="utf-8")) |
| 797 | + # one advisory (GHSA-aaaa-0001) spanning two repos |
| 798 | + assert payload["distinct_advisories"] == 1 |
| 799 | + assert payload["repos_touched"] == 2 |
| 800 | + assert payload["total_repo_instances"] == 2 |
| 801 | + entry = payload["entries"][0] |
| 802 | + assert entry["package"] == "lodash" |
| 803 | + assert entry["severity"] == "critical" |
| 804 | + assert entry["affected_repo_count"] == 2 |
| 805 | + assert sorted(entry["affected_repos"]) == ["RepoA", "RepoB"] |
| 806 | + |
| 807 | + def test_no_ghas_file_exits_nonzero(self, tmp_path) -> None: |
| 808 | + args = SimpleNamespace(output_dir=str(tmp_path), username="nobody") |
| 809 | + with pytest.raises(SystemExit) as exc: |
| 810 | + _run_security_burndown_mode(args) |
| 811 | + assert exc.value.code == 1 |
| 812 | + |
| 813 | + def test_counts_only_ghas_exits_without_writing(self, tmp_path) -> None: |
| 814 | + """A counts-only ghas file (no dependabot_details) exits 0, writes nothing.""" |
| 815 | + today = datetime.date.today().isoformat() |
| 816 | + ghas = {"RepoA": {"dependabot": {"critical": 1, "available": True}}} |
| 817 | + (tmp_path / f"ghas-alerts-octocat-{today}.json").write_text( |
| 818 | + json.dumps(ghas), encoding="utf-8" |
| 819 | + ) |
| 820 | + args = SimpleNamespace(output_dir=str(tmp_path), username="octocat") |
| 821 | + with pytest.raises(SystemExit) as exc: |
| 822 | + _run_security_burndown_mode(args) |
| 823 | + assert exc.value.code == 0 |
| 824 | + assert not list(tmp_path.glob("security-burndown-*")) |
0 commit comments