feat: add controlled source ingestion and replay
All checks were successful
Build and Publish Container Image / build-and-push (push) Successful in 38s
All checks were successful
Build and Publish Container Image / build-and-push (push) Successful in 38s
Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a02b22-9638-76d2-bbff-b7ea1770b118
This commit is contained in:
parent
b95fba9a9f
commit
879012c776
16 changed files with 1156 additions and 154 deletions
|
|
@ -1,11 +1,14 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from contextlib import contextmanager
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from sbom_nexus import api
|
||||
from sbom_nexus.api import create_app
|
||||
from sbom_nexus.source_fetch import ControlledSource
|
||||
|
||||
|
||||
def client_for(tmp_path: Path) -> TestClient:
|
||||
|
|
@ -19,6 +22,7 @@ def register(
|
|||
checkout_path: str | None = None,
|
||||
last_sbom_at: datetime | None = None,
|
||||
active: bool = True,
|
||||
source_ref: dict | None = None,
|
||||
) -> None:
|
||||
response = client.put(
|
||||
f"/repositories/{slug}",
|
||||
|
|
@ -26,6 +30,7 @@ def register(
|
|||
"checkout_path": checkout_path,
|
||||
"last_sbom_at": last_sbom_at.isoformat() if last_sbom_at else None,
|
||||
"active": active,
|
||||
"source_ref": source_ref,
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
|
@ -135,6 +140,96 @@ def test_checkout_scan_records_provenance_and_success(tmp_path: Path) -> None:
|
|||
assert detail["entries"][0]["snapshot_at"] == detail["snapshot_at"]
|
||||
|
||||
|
||||
def test_ingest_operation_key_replays_without_a_second_snapshot(tmp_path: Path) -> None:
|
||||
repo = tmp_path / "idempotent"
|
||||
repo.mkdir()
|
||||
(repo / "requirements.txt").write_text("fastapi==0.136.1\n", encoding="utf-8")
|
||||
client = client_for(tmp_path)
|
||||
register(client, "idempotent", checkout_path=str(repo))
|
||||
headers = {
|
||||
"Idempotency-Key": "activity-operation-1",
|
||||
"X-Activity-Core-Operation-ID": "activity-operation-1",
|
||||
}
|
||||
|
||||
first = client.post("/sbom/idempotent/ingest", headers=headers)
|
||||
second = client.post("/sbom/idempotent/ingest", headers=headers)
|
||||
|
||||
assert first.status_code == 200
|
||||
assert second.json() == first.json()
|
||||
assert len(client.get("/sbom/snapshots/?repo_slug=idempotent").json()) == 1
|
||||
conflict = client.post(
|
||||
"/sbom/idempotent/skip",
|
||||
headers=headers,
|
||||
json={"reason": "no-checkout"},
|
||||
)
|
||||
assert conflict.status_code == 409
|
||||
|
||||
|
||||
def test_skip_operation_key_replays_original_outcome(tmp_path: Path) -> None:
|
||||
client = client_for(tmp_path)
|
||||
register(client, "skip-replay")
|
||||
headers = {"Idempotency-Key": "skip-operation-1"}
|
||||
|
||||
first = client.post(
|
||||
"/sbom/skip-replay/skip",
|
||||
headers=headers,
|
||||
json={"reason": "source-unavailable", "detail": "not projected"},
|
||||
)
|
||||
second = client.post(
|
||||
"/sbom/skip-replay/skip",
|
||||
headers=headers,
|
||||
json={"reason": "source-unavailable", "detail": "not projected"},
|
||||
)
|
||||
|
||||
assert first.status_code == 200
|
||||
assert second.json() == first.json()
|
||||
assert len(client.get("/sbom/snapshots/?repo_slug=skip-replay").json()) == 1
|
||||
|
||||
|
||||
def test_controlled_source_records_explicit_revision_and_archive_provenance(
|
||||
tmp_path: Path, monkeypatch
|
||||
) -> None:
|
||||
extracted = tmp_path / "extracted"
|
||||
extracted.mkdir()
|
||||
(extracted / "requirements.txt").write_text(
|
||||
"fastapi==0.136.1\n", encoding="utf-8"
|
||||
)
|
||||
revision = "a" * 40
|
||||
source_ref = {
|
||||
"kind": "forgejo-archive-v1",
|
||||
"repository": "coulomb/controlled",
|
||||
"revision": revision,
|
||||
"observed_ref": "refs/heads/main",
|
||||
"observed_at": "2026-08-22T20:00:00Z",
|
||||
}
|
||||
|
||||
@contextmanager
|
||||
def fake_fetch(repo_slug: str, selected: dict):
|
||||
assert repo_slug == "controlled"
|
||||
assert selected["revision"] == revision
|
||||
yield ControlledSource(
|
||||
root=extracted,
|
||||
provenance={**selected, "archive_sha256": "b" * 64, "archive_bytes": 42},
|
||||
)
|
||||
|
||||
monkeypatch.setattr(api, "fetch_controlled_source", fake_fetch)
|
||||
client = client_for(tmp_path)
|
||||
register(client, "controlled", source_ref=source_ref)
|
||||
|
||||
result = client.post(
|
||||
"/sbom/controlled/ingest",
|
||||
json={"source_ref": source_ref},
|
||||
headers={"Idempotency-Key": "controlled-operation-1"},
|
||||
)
|
||||
|
||||
assert result.status_code == 200
|
||||
assert result.json()["source_revision"] == revision
|
||||
detail = client.get(f"/sbom/snapshots/{result.json()['snapshot_id']}").json()
|
||||
assert detail["source"] == "forgejo-archive-v1"
|
||||
assert detail["source_provenance"]["archive_sha256"] == "b" * 64
|
||||
assert detail["sources"][0]["path"] == "requirements.txt"
|
||||
|
||||
|
||||
def test_historical_import_is_idempotent(tmp_path: Path) -> None:
|
||||
client = client_for(tmp_path)
|
||||
payload = {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue