This repository has been archived on 2026-11-08. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
platform-ci/tests/test_cli_integration.py
forge-recoveryandClaude Sonnet 5.5 fd4689fd19
KofTwentyTwo/ci-policy: ci / fork-guard (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / go-0 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / go-nix-1 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / gradle-2 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / gradle-3 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / java-docker-4 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / java-maven-5 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / manifests-6 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / nix-7 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / node-8 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / python-9 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / rust-10 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / rust-11 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / rust-12 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / source-config-13 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / source-config-14 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / image-build-0 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / image-build-1 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / image-build-2 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / image-build-3 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / image-build-4 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / image-build-5 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / image-build-6 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / image-build-7 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / not-enrolled (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / content-validate-0 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / standards-0 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / node-matrix-0 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / source-config-15 (pull_request) Blocked by required conditions
KofTwentyTwo/ci-policy: Shared source and config CI / verify (pull_request) Blocked by required conditions
KofTwentyTwo/ci-policy: Shared source and config CI / secrets (pull_request) Blocked by required conditions
KofTwentyTwo/ci-policy: Shared source and config CI / gate (pull_request) Waiting for needed jobs
KofTwentyTwo/ci-policy: ci / ci-result (pull_request) Waiting for needed jobs
KofTwentyTwo/ci-policy: native / fork-deny (pull_request) Skipped
KofTwentyTwo/ci-policy: native / build-macos-0 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / build-macos-1 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / build-macos-2 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / build-macos-3 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / build-windows-4 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / build-windows-5 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / build-windows-6 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / build-windows-7 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / dotnet-8 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / dotnet-9 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / dotnet-10 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / dotnet-11 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / swift-12 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / swift-13 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / swift-14 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / swift-15 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / swift-16 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / swift-17 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / swift-18 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / swift-19 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / native-result (pull_request) Canceled after 3s
feat(ci): reuse verified builds before merge verification
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-05 03:54:30 +00:00

216 lines
11 KiB
Python

"""CLI integration: verify judges existing JUnit reports without rerunning, scan writes a redacted manifest, report needs all three jobs.
No test starts an application build, a scanner or the network: the native runner and the scan runner are replaced.
"""
import json
import shutil
import tempfile
import unittest
from functools import partial
from pathlib import Path
from typing import override
from unittest import mock
from platform_ci.__main__ import main
from platform_ci.packaging import manifest_digest
from platform_ci.plan import Target, to_compact_json
from platform_ci.secret_scan import scan_history
from tests.support import PLATFORM_REVISION, SOURCE_REVISION, SOURCE_TREE, event, example_configuration, first_target, plan_for
from tests.test_secret_scan import FakeGit
PASSING = '<testsuite tests="2" skipped="1" failures="0" errors="0"><testcase name="a"/></testsuite>'
SENTINEL = "SENTINEL-SECRET-BODY"
FINDING = json.dumps([{"RuleID": "generic", "File": "a.txt", "Secret": SENTINEL}])
class CliTestCase(unittest.TestCase):
"""A temporary source checkout and a maven target that declares optional JUnit and required file reports."""
@override
def setUp(self) -> None:
"""Create the checkout."""
directory = tempfile.TemporaryDirectory()
self.addCleanup(directory.cleanup)
self.root = Path(directory.name).resolve()
self.source = self.root / "source"
self.source.mkdir()
self.target: Target = first_target("maven-reports.ci.yaml")
def write(self, relative: str, content: str) -> None:
"""Write a file under the source checkout."""
path = self.source / relative
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content, encoding="utf-8")
class VerifyCommandTest(CliTestCase):
"""Native commands run once; afterwards each declared JUnit report is judged as data."""
def _verify(self, native_succeeds: bool = True) -> tuple[int, mock.Mock]:
runner = mock.Mock(return_value=native_succeeds)
environment = {"CI_TARGET": to_compact_json(self.target)}
with mock.patch("platform_ci.__main__.run_target_commands", runner):
code = main(["verify", "--source", str(self.source)], environment)
return code, runner
def test_passing_and_empty_reports_pass_after_one_native_run(self) -> None:
"""Zero reports, zero tests and all-skipped stay allowed, as in the legacy gate."""
self.write("target/surefire-reports/TEST-a.xml", PASSING)
self.write("target/surefire-reports/TEST-b.xml", '<testsuite tests="1" skipped="1"><testcase name="b"><skipped/></testcase></testsuite>')
code, runner = self._verify()
self.assertEqual(code, 0)
runner.assert_called_once()
def test_no_matching_report_is_not_judged_by_verify(self) -> None:
"""Whether a required report exists is packaging's decision, so verify passes with none."""
self.assertEqual(self._verify()[0], 0)
def test_failing_malformed_and_declaring_reports_fail_the_build_result(self) -> None:
"""Failures, errors, malformed XML and DTDs fail without a second native run."""
bad_reports = {
"failure": '<testsuite tests="1" failures="1"><testcase name="a"><failure/></testcase></testsuite>',
"error": '<testsuite tests="1" errors="1"/>',
"malformed": "<testsuite",
"doctype": '<!DOCTYPE testsuite [<!ENTITY x "y">]><testsuite tests="0"/>',
}
for name, content in bad_reports.items():
with self.subTest(report=name):
self.write("target/surefire-reports/TEST-a.xml", PASSING)
self.write("target/failsafe-reports/TEST-bad.xml", content)
code, runner = self._verify()
self.assertEqual(code, 1)
runner.assert_called_once()
def test_failed_native_run_fails_without_reading_reports(self) -> None:
"""A failed build result is final; reports are left for packaging to retain."""
self.write("target/surefire-reports/TEST-a.xml", PASSING)
with mock.patch("platform_ci.__main__.check_junit_reports") as judged:
self.assertEqual(self._verify(native_succeeds=False)[0], 1)
judged.assert_not_called()
def test_file_reports_and_excluded_directories_are_not_judged(self) -> None:
"""Only `junit` reports are parsed, and matches inside `.git` or `.platform-ci` are skipped like packaging does."""
self.write("target/site/jacoco/jacoco.xml", "not xml at all <")
self.write(".platform-ci/target/surefire-reports/TEST-x.xml", "<broken")
self.write(".git/target/surefire-reports/TEST-y.xml", "<broken")
self.assertEqual(self._verify()[0], 0)
def test_a_report_symlinked_outside_the_workspace_fails(self) -> None:
"""A link to a file outside the checkout is an error, never silently read or skipped."""
outside = self.root / "outside.xml"
outside.write_text(PASSING, encoding="utf-8")
link = self.source / "target" / "surefire-reports" / "TEST-link.xml"
link.parent.mkdir(parents=True)
try:
link.symlink_to(outside)
except OSError:
self.skipTest("symbolic links are unavailable")
self.assertEqual(self._verify()[0], 1)
def test_missing_required_output_still_fails_packaging(self) -> None:
"""A required file report that was never produced fails packaging after a successful build."""
environment = {
"CI_TARGET": to_compact_json(self.target),
"CI_BUILD_OUTCOME": "success",
"CI_SOURCE_REVISION": SOURCE_REVISION,
"CI_SOURCE_TREE": SOURCE_TREE,
"CI_PLATFORM_REVISION": PLATFORM_REVISION,
"CI_RUN_ID": "1",
"CI_RUN_ATTEMPT": "1",
}
with self.assertLogs("platform_ci", level="ERROR"):
self.assertEqual(main(["package", "--source", str(self.source), "--output", str(self.root / "out")], environment), 1)
class ScanCommandTest(CliTestCase):
"""The scan command writes a source, platform and run bound manifest that never contains findings or output."""
def _scan(self, fake: FakeGit, environment: dict[str, str] | None = None, output: Path | None = None) -> int:
full = {"CI_SOURCE_REVISION": SOURCE_REVISION, "CI_PLATFORM_REVISION": PLATFORM_REVISION, "CI_RUN_ID": "77", "CI_RUN_ATTEMPT": "2"}
full.update(environment or {})
scan = partial(scan_history, run_command=fake, parent_environment={"PATH": "/usr/bin"})
arguments = ["scan", "--source", str(self.source), "--report", ".platform-security/gitleaks.json", "--output", str(output or self.root / "security-output")]
with mock.patch("platform_ci.__main__.scan_history", scan), self.assertLogs("platform_ci", level="INFO"):
return main(arguments, full)
def _manifest(self) -> dict[str, object]:
loaded: dict[str, object] = json.loads((self.root / "security-output" / "manifest.json").read_text(encoding="utf-8"))
return loaded
def test_clean_scan_writes_a_bound_manifest_and_exits_zero(self) -> None:
"""The manifest binds the exact source, platform and run, and its digest verifies."""
self.assertEqual(self._scan(FakeGit()), 0)
manifest = self._manifest()
self.assertEqual(
{key: manifest[key] for key in ("kind", "source_revision", "platform_revision", "run_id", "run_attempt", "passed", "findings", "failure")},
{"kind": "secret-scan", "source_revision": SOURCE_REVISION, "platform_revision": PLATFORM_REVISION, "run_id": "77", "run_attempt": "2", "passed": True, "findings": 0, "failure": None},
)
self.assertEqual(manifest["digest"], manifest_digest(manifest))
self.assertEqual(sorted(path.name for path in (self.root / "security-output").iterdir()), ["manifest.json"])
def test_findings_fail_and_never_reach_the_manifest_or_logs(self) -> None:
"""A finding exits 1; only counts and the raw report's digest are recorded, never its body or tool output."""
fake = FakeGit(scan_exit_code=2, report=FINDING)
self.assertEqual(self._scan(fake), 1)
manifest = self._manifest()
self.assertEqual((manifest["passed"], manifest["findings"]), (False, 1))
text = (self.root / "security-output" / "manifest.json").read_text(encoding="utf-8")
for forbidden in (SENTINEL, "RAW-MATCHED-SECRET-IN-OUTPUT", "generic", "a.txt"):
self.assertNotIn(forbidden, text)
self.assertEqual(sorted(cast_report(manifest)), ["sha256", "size"])
def test_tool_problem_fails_closed_with_a_manifest(self) -> None:
"""An unpinned scanner version or a missing report is a failed manifest, not a pass."""
for fake in (FakeGit(version="9.9.9"), FakeGit(report=None), FakeGit(shallow="true"), FakeGit(head="c" * 40)):
with self.subTest(fake=vars(fake)):
shutil.rmtree(self.root / "security-output", ignore_errors=True)
self.assertEqual(self._scan(fake), 1)
self.assertFalse(self._manifest()["passed"])
def test_missing_run_facts_and_existing_output_fail(self) -> None:
"""Absent identity facts or a reused output directory exit 1 without scanning."""
fake = FakeGit()
with self.assertLogs("platform_ci", level="ERROR"):
self.assertEqual(
main(["scan", "--source", str(self.source), "--report", "r.json", "--output", str(self.root / "o")], {"CI_SOURCE_REVISION": SOURCE_REVISION}),
1,
)
(self.root / "security-output").mkdir()
self.assertEqual(self._scan(fake), 1)
self.assertEqual(fake.calls, [])
def cast_report(manifest: dict[str, object]) -> dict[str, object]:
"""Return the manifest's report metadata mapping."""
report = manifest["report"]
assert isinstance(report, dict)
return {str(key): value for key, value in report.items()} # pyright: ignore[reportUnknownVariableType, reportUnknownArgumentType]
class ReportCommandTest(unittest.TestCase):
"""The report command requires the plan and all three job results."""
def _report(self, event_args: tuple[str, str], **results: str) -> int:
plan = plan_for(example_configuration("maven.ci.yaml"), event(*event_args))
environment = {"CI_PLAN": to_compact_json(plan)}
environment.update({f"CI_{name.upper()}_RESULT": value for name, value in results.items()})
return main(["report"], environment)
def test_requires_every_result(self) -> None:
"""An absent secrets result fails closed even when verify succeeded."""
with self.assertLogs("platform_ci", level="ERROR"):
self.assertEqual(self._report(("pull_request", "refs/pull/1/merge"), prepare="success", verify="success"), 1)
def test_outcomes(self) -> None:
"""Two successes pass a requested run; two skips pass an unrequested push as not verified; mixes fail."""
requested = ("pull_request", "refs/pull/1/merge")
unrequested = ("push", "refs/heads/feature-x")
self.assertEqual(self._report(requested, prepare="success", verify="success", secrets="success"), 0)
self.assertEqual(self._report(requested, prepare="success", verify="success", secrets="skipped"), 1)
self.assertEqual(self._report(requested, prepare="success", verify="success", secrets="failure"), 1)
self.assertEqual(self._report(unrequested, prepare="success", verify="skipped", secrets="skipped"), 0)
self.assertEqual(self._report(unrequested, prepare="success", verify="skipped", secrets="success"), 1)
with self.assertLogs("platform_ci", level="ERROR"):
self.assertEqual(self._report(requested, prepare="failure", verify="skipped", secrets="skipped"), 1)