# Copyright Kevin Deldycke <[email protected]> and contributors.
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
"""Tests for the attestation bundle naming rule."""
from __future__ import annotations
import base64
import json
from pathlib import Path
import pytest
from repomatic.release.attestation import (
ATTESTATION_SUFFIX,
bundle_filename,
bundle_subjects,
pack_attestation,
)
[docs]
def write_bundle(path: Path, *subjects: str) -> Path:
"""Write a sigstore bundle attesting *subjects*.
Mirrors the shape `actions/attest` emits: a DSSE envelope whose base64
payload is an in-toto statement listing one entry per attested file.
"""
statement = {
"_type": "https://in-toto.io/Statement/v1",
"predicateType": "https://slsa.dev/provenance/v1",
"subject": [
{"name": name, "digest": {"sha256": f"{index:064x}"}}
for index, name in enumerate(subjects)
],
}
payload = base64.b64encode(json.dumps(statement).encode("UTF-8")).decode("ascii")
path.write_text(
json.dumps({
"mediaType": "application/vnd.dev.sigstore.bundle.v0.3+json",
"dsseEnvelope": {
"payload": payload,
"payloadType": "application/vnd.in-toto+json",
},
}),
encoding="UTF-8",
)
return path
[docs]
def test_bundle_subjects_reads_every_name(tmp_path):
"""Subjects come back in the order the statement lists them."""
bundle = write_bundle(tmp_path / "b.json", "papaya.tar.gz", "mango.zip")
assert bundle_subjects(bundle) == ("papaya.tar.gz", "mango.zip")
[docs]
def test_bundle_subjects_rejects_empty_subject_list(tmp_path):
"""A bundle attesting nothing has no name to give."""
bundle = write_bundle(tmp_path / "b.json")
with pytest.raises(ValueError, match="attests no subject"):
bundle_subjects(bundle)
[docs]
@pytest.mark.parametrize("name", ("../papaya.zip", "nested/papaya.zip"))
def test_bundle_subjects_rejects_path_subject(tmp_path, name):
"""A subject naming a path could escape the asset directory."""
bundle = write_bundle(tmp_path / "b.json", name)
with pytest.raises(ValueError, match="not a bare filename"):
bundle_subjects(bundle)
[docs]
@pytest.mark.parametrize(
("subjects", "set_name", "expected"),
(
# A lone subject lends the bundle its whole filename, extension kept,
# so the two sort together on an alphabetically listed release page.
(("papaya.tar.gz",), None, "papaya.tar.gz.attestation.json"),
(
("papaya-1.2.3-linux-arm64.bin",),
None,
"papaya-1.2.3-linux-arm64.bin.attestation.json",
),
# A set name is ignored when there is a subject to name it after.
(("papaya.zip",), "mango-set", "papaya.zip.attestation.json"),
# Several subjects share one bundle, which takes the set name.
(
("papaya.zip", "mango.zip"),
"fruit-extra-assets",
"fruit-extra-assets.attestation.json",
),
),
)
def test_bundle_filename(subjects, set_name, expected):
"""One subject names the bundle; several fall back to the set name."""
assert bundle_filename(subjects, set_name) == expected
[docs]
def test_bundle_filename_needs_a_set_name_for_several_subjects():
"""No single filename can claim a bundle covering several assets."""
with pytest.raises(ValueError, match="pass --name"):
bundle_filename(("papaya.zip", "mango.zip"))
[docs]
def test_bundle_filename_rejects_no_subject():
"""Naming a bundle that attests nothing is a programming error."""
with pytest.raises(ValueError, match="attests no subject"):
bundle_filename(())
[docs]
def test_pack_attestation_names_bundle_and_lists_uploads(tmp_path):
"""The bundle lands beside its subject, and both are listed for upload."""
(tmp_path / "papaya.tar.gz").write_bytes(b"tarball")
bundle = write_bundle(tmp_path / "raw.json", "papaya.tar.gz")
uploads = pack_attestation(bundle, tmp_path)
assert [path.name for path in uploads] == [
"papaya.tar.gz",
"papaya.tar.gz.attestation.json",
]
named = tmp_path / f"papaya.tar.gz{ATTESTATION_SUFFIX}"
assert named.read_bytes() == bundle.read_bytes()
# Idempotent: a second run copies the same bytes over the same name.
assert pack_attestation(bundle, tmp_path) == uploads
[docs]
def test_pack_attestation_covers_a_whole_set(tmp_path):
"""Several subjects yield one set-named bundle and every asset."""
(tmp_path / "papaya.zip").write_bytes(b"one")
(tmp_path / "mango.zip").write_bytes(b"two")
bundle = write_bundle(tmp_path / "raw.json", "papaya.zip", "mango.zip")
uploads = pack_attestation(bundle, tmp_path, "fruit-extra-assets")
assert [path.name for path in uploads] == [
"fruit-extra-assets.attestation.json",
"mango.zip",
"papaya.zip",
]
[docs]
def test_pack_attestation_rejects_a_missing_subject(tmp_path):
"""A bundle covering a file that is not there attested another tree."""
(tmp_path / "papaya.zip").write_bytes(b"one")
bundle = write_bundle(tmp_path / "raw.json", "papaya.zip", "mango.zip")
with pytest.raises(ValueError, match=r"mango\.zip, missing from"):
pack_attestation(bundle, tmp_path, "fruit-extra-assets")