test_artifact_sign.py
1 """Tests for artifact attestation signing and publishing.""" 2 3 from unittest.mock import MagicMock 4 5 import pytest 6 7 from auths import ArtifactPublishResult, ArtifactSigningResult 8 from auths.artifact import ArtifactPublishResult as ArtifactPublishFromModule 9 from auths.artifact import ArtifactSigningResult as ArtifactFromModule 10 11 12 class TestArtifactSigningResult: 13 14 def test_fields(self): 15 r = ArtifactSigningResult( 16 attestation_json='{"rid":"sha256:abc"}', 17 rid="sha256:abc123def456", 18 digest="abc123def456", 19 file_size=1024, 20 ) 21 assert r.attestation_json == '{"rid":"sha256:abc"}' 22 assert r.rid == "sha256:abc123def456" 23 assert r.digest == "abc123def456" 24 assert r.file_size == 1024 25 26 def test_repr_shows_size(self): 27 r = ArtifactSigningResult( 28 attestation_json="{}", 29 rid="sha256:a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4", 30 digest="a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4", 31 file_size=2_500_000, 32 ) 33 s = repr(r) 34 assert "ArtifactSigningResult" in s 35 assert "MB" in s 36 37 def test_repr_small_file(self): 38 r = ArtifactSigningResult( 39 attestation_json="{}", 40 rid="sha256:short", 41 digest="short", 42 file_size=512, 43 ) 44 s = repr(r) 45 assert "512 B" in s 46 47 def test_repr_kb(self): 48 r = ArtifactSigningResult( 49 attestation_json="{}", 50 rid="sha256:x", 51 digest="x", 52 file_size=15_360, 53 ) 54 s = repr(r) 55 assert "KB" in s 56 57 58 class TestImports: 59 60 def test_importable_from_top_level(self): 61 from auths import ArtifactSigningResult 62 assert ArtifactSigningResult is not None 63 64 def test_importable_from_module(self): 65 from auths.artifact import ArtifactSigningResult 66 assert ArtifactSigningResult is not None 67 68 def test_ffi_functions_importable(self): 69 from auths._native import sign_artifact, sign_artifact_bytes 70 assert sign_artifact is not None 71 assert sign_artifact_bytes is not None 72 73 def test_sign_artifact_nonexistent_file(self): 74 from auths._native import sign_artifact 75 with pytest.raises(FileNotFoundError, match="not found"): 76 sign_artifact("/nonexistent/path/file.bin", "main", "/tmp", None, None, None) 77 78 79 class TestArtifactPublishResult: 80 81 def test_fields(self): 82 r = ArtifactPublishResult( 83 attestation_rid="rid-abc", 84 package_name="npm:react@18.3.0", 85 signer_did="did:keri:abc", 86 ) 87 assert r.attestation_rid == "rid-abc" 88 assert r.package_name == "npm:react@18.3.0" 89 assert r.signer_did == "did:keri:abc" 90 91 def test_package_name_none(self): 92 r = ArtifactPublishResult(attestation_rid="x", package_name=None, signer_did="y") 93 assert r.package_name is None 94 95 def test_repr_truncates_long_rid(self): 96 long_rid = "a" * 60 97 r = ArtifactPublishResult(attestation_rid=long_rid, package_name=None, signer_did="did:keri:z") 98 assert len(repr(r)) < len(long_rid) + 40 99 assert "..." in repr(r) 100 101 def test_repr_with_package(self): 102 r = ArtifactPublishResult(attestation_rid="rid", package_name="npm:x", signer_did="did:keri:abc") 103 assert "npm:x" in repr(r) 104 105 def test_repr_without_package(self): 106 r = ArtifactPublishResult(attestation_rid="rid", package_name=None, signer_did="did:keri:abc") 107 assert "pkg" not in repr(r) 108 109 def test_top_level_export(self): 110 assert ArtifactPublishResult is ArtifactPublishFromModule 111 112 113 class TestPublishArtifactNative: 114 115 def test_import(self): 116 from auths._native import publish_artifact # noqa: F401 117 118 def test_invalid_json_raises(self): 119 from auths._native import publish_artifact 120 with pytest.raises((ValueError, RuntimeError)): 121 publish_artifact("not-json", "http://localhost", None) 122 123 def test_unreachable_host_raises(self): 124 from auths._native import publish_artifact 125 with pytest.raises((RuntimeError, OSError, ConnectionError)): 126 publish_artifact('{"attestation":"x"}', "http://127.0.0.1:1", None) 127 128 129 class TestPublishArtifactClient: 130 131 def test_method_exists(self): 132 from auths import Auths 133 assert hasattr(Auths, "publish_artifact") 134 135 def test_returns_result_type(self, monkeypatch): 136 import auths._native as native 137 mock_raw = MagicMock() 138 mock_raw.attestation_rid = "rid-1" 139 mock_raw.package_name = "npm:foo" 140 mock_raw.signer_did = "did:keri:abc" 141 monkeypatch.setattr(native, "publish_artifact", lambda *_: mock_raw) 142 from auths import Auths 143 result = Auths().publish_artifact('{"a":1}', registry_url="http://x") 144 assert isinstance(result, ArtifactPublishResult) 145 assert result.attestation_rid == "rid-1" 146 147 def test_duplicate_raises_storage_error(self, monkeypatch): 148 import auths._native as native 149 150 def _raise(*_): 151 raise RuntimeError("duplicate_attestation: artifact attestation already published (duplicate RID)") 152 153 monkeypatch.setattr(native, "publish_artifact", _raise) 154 from auths import Auths 155 from auths._errors import StorageError 156 with pytest.raises(StorageError) as exc_info: 157 Auths().publish_artifact('{"a":1}', registry_url="http://x") 158 assert exc_info.value.code == "duplicate_attestation" 159 160 def test_verification_failed_raises_verification_error(self, monkeypatch): 161 import auths._native as native 162 163 def _raise(*_): 164 raise RuntimeError("verification_failed: signature rejected by registry") 165 166 monkeypatch.setattr(native, "publish_artifact", _raise) 167 from auths import Auths 168 from auths._errors import VerificationError 169 with pytest.raises(VerificationError) as exc_info: 170 Auths().publish_artifact('{"a":1}', registry_url="http://x") 171 assert exc_info.value.code == "verification_failed" 172 173 def test_network_error_raises_network_error(self, monkeypatch): 174 import auths._native as native 175 176 def _raise(*_): 177 raise RuntimeError("registry unreachable: connection refused") 178 179 monkeypatch.setattr(native, "publish_artifact", _raise) 180 from auths import Auths 181 from auths._errors import NetworkError 182 with pytest.raises(NetworkError): 183 Auths().publish_artifact('{"a":1}', registry_url="http://x")