22
33from __future__ import annotations
44
5+ import json
6+ from unittest import mock
7+
58import image_ref
69import pytest
10+ import requests
711
812
913def test_pyxis_url_for_pull_spec_with_tag_and_registry_rewrite () -> None :
@@ -28,3 +32,103 @@ def test_pyxis_url_for_pull_spec_invalid() -> None:
2832 """Invalid pull specs raise `ValueError`."""
2933 with pytest .raises (ValueError , match = "invalid pull spec" ):
3034 image_ref .pyxis_url_for_pull_spec ("https://pyxis/v1" , "not/a-pullspec" )
35+
36+
37+ def test_resolve_quay_digest_skips_non_quay () -> None :
38+ """Non-quay.io images return `None` without calling the Quay API."""
39+ assert (
40+ image_ref .resolve_quay_digest_to_git_sha (
41+ "sha256:abc" ,
42+ "registry.io/org/repo@sha256:abc" ,
43+ )
44+ is None
45+ )
46+
47+
48+ def test_resolve_quay_digest_finds_sha_tag () -> None :
49+ """A 40-char hex tag matching the digest is returned from the first API page."""
50+ digest = "sha256:" + "a" * 64
51+ sha = "b" * 40
52+ payload = json .dumps (
53+ {
54+ "tags" : [{"name" : sha , "manifest_digest" : digest }],
55+ "has_additional" : False ,
56+ }
57+ )
58+ with mock .patch ("image_ref.http_client.get_text" , return_value = payload ):
59+ out = image_ref .resolve_quay_digest_to_git_sha (
60+ digest ,
61+ f"quay.io/org/repo@{ digest } " ,
62+ )
63+ assert out == sha
64+
65+
66+ def test_resolve_quay_digest_non_200_response () -> None :
67+ """Quay API errors return `None` instead of raising."""
68+ digest = "sha256:" + "a" * 64
69+ response = mock .MagicMock (status_code = 503 )
70+ with mock .patch (
71+ "image_ref.http_client.get_text" ,
72+ side_effect = requests .HTTPError (response = response ),
73+ ):
74+ out = image_ref .resolve_quay_digest_to_git_sha (
75+ digest ,
76+ f"quay.io/org/repo@{ digest } " ,
77+ )
78+ assert out is None
79+
80+
81+ def test_resolve_quay_digest_paginates () -> None :
82+ """Resolution follows `has_additional` across multiple tag-list pages."""
83+ digest = "sha256:" + "a" * 64
84+ sha = "c" * 40
85+ page_one = json .dumps ({"tags" : [], "has_additional" : True })
86+ page_two = json .dumps (
87+ {
88+ "tags" : [{"name" : sha , "manifest_digest" : digest }],
89+ "has_additional" : False ,
90+ }
91+ )
92+ with mock .patch (
93+ "image_ref.http_client.get_text" ,
94+ side_effect = [page_one , page_two ],
95+ ) as get_text :
96+ out = image_ref .resolve_quay_digest_to_git_sha (
97+ digest ,
98+ f"quay.io/org/repo@{ digest } " ,
99+ )
100+ assert out == sha
101+ assert get_text .call_count == 2
102+
103+
104+ def test_resolve_quay_digest_no_matching_tag () -> None :
105+ """Return `None` when no tag has both the digest and a 40-char hex name."""
106+ digest = "sha256:" + "a" * 64
107+ payload = json .dumps (
108+ {
109+ "tags" : [
110+ {"name" : "not-a-sha" , "manifest_digest" : digest },
111+ {"name" : "b" * 40 , "manifest_digest" : "sha256:other" },
112+ ],
113+ "has_additional" : False ,
114+ }
115+ )
116+ with mock .patch ("image_ref.http_client.get_text" , return_value = payload ):
117+ out = image_ref .resolve_quay_digest_to_git_sha (
118+ digest ,
119+ f"quay.io/org/repo@{ digest } " ,
120+ )
121+ assert out is None
122+
123+
124+ def test_resolve_quay_digest_handles_exception () -> None :
125+ """Unexpected failures are swallowed and return `None`."""
126+ with mock .patch (
127+ "image_ref.http_client.get_text" ,
128+ side_effect = RuntimeError ("network down" ),
129+ ):
130+ out = image_ref .resolve_quay_digest_to_git_sha (
131+ "sha256:abc" ,
132+ "quay.io/org/repo@sha256:abc" ,
133+ )
134+ assert out is None
0 commit comments