import hashlib import pytest import requests from pytest_mock import MockerFixture from dangerzone.updater.registry import ( Image, _get_auth_header, _url, get_manifest, get_manifest_digest, list_tags, parse_image_location, ) def test_parse_image_location_no_tag(): """Test that parse_image_location correctly handles an image location without a tag.""" image_str = "ghcr.io/freedomofpress/dangerzone" image = parse_image_location(image_str) assert isinstance(image, Image) assert image.registry == "ghcr.io" assert image.namespace == "freedomofpress" assert image.image_name == "dangerzone" assert image.tag == "latest" # Default tag should be "latest" assert image.digest is None def test_parse_image_location_with_tag(): """Test that parse_image_location correctly handles an image location with a tag.""" image_str = "ghcr.io/freedomofpress/dangerzone:v0.4.2" image = parse_image_location(image_str) assert isinstance(image, Image) assert image.registry == "ghcr.io" assert image.namespace == "freedomofpress" assert image.image_name == "dangerzone" assert image.tag == "v0.4.2" def test_parse_image_location_tag_plus_digest(): """Test that parse_image_location handles an image location with a tag that includes a digest.""" image_str = ( "ghcr.io/freedomofpress/dangerzone" ":20250205-0.8.0-148-ge67fbc1" "@sha256:19e8eacd75879d05f6621c2ea8dd955e68ee3e07b41b9d53f4c8cc9929a68a67" ) image = parse_image_location(image_str) assert isinstance(image, Image) assert image.registry == "ghcr.io" assert image.namespace == "freedomofpress" assert image.image_name == "dangerzone" assert image.tag == "20250205-0.8.0-148-ge67fbc1" assert ( image.digest == "sha256:19e8eacd75879d05f6621c2ea8dd955e68ee3e07b41b9d53f4c8cc9929a68a67" ) def test_parse_invalid_image_location(): """Test that parse_image_location raises an error for invalid image locations.""" invalid_image_locations = [ "ghcr.io/dangerzone", # Missing namespace "ghcr.io/freedomofpress/dangerzone:", # Empty tag "freedomofpress/dangerzone", # Missing registry "ghcr.io:freedomofpress/dangerzone", # Invalid format "", # Empty string ] for invalid_image in invalid_image_locations: with pytest.raises(ValueError, match="Malformed image location"): parse_image_location(invalid_image) def test_list_tags(mocker: MockerFixture): """Test that list_tags correctly retrieves tags from the registry.""" # Mock the authentication response image_str = "ghcr.io/freedomofpress/dangerzone" # Mock requests.get to return appropriate values for both calls mock_response_auth = mocker.Mock() mock_response_auth.json.return_value = {"token": "dummy_token"} mock_response_auth.raise_for_status.return_value = None mock_response_tags = mocker.Mock() mock_response_tags.json.return_value = { "tags": ["v0.4.0", "v0.4.1", "v0.4.2", "latest"] } mock_response_tags.raise_for_status.return_value = None # Setup the mock to return different responses for each URL def mock_get(url, **kwargs): if "token" in url: return mock_response_auth else: return mock_response_tags mocker.patch("requests.get", side_effect=mock_get) # Call the function tags = list_tags(image_str) # Verify the result assert tags == ["v0.4.0", "v0.4.1", "v0.4.2", "latest"] def test_list_tags_auth_error(mocker: MockerFixture): """Test that list_tags handles authentication errors correctly.""" image_str = "ghcr.io/freedomofpress/dangerzone" # Mock requests.get to raise an HTTPError mock_response = mocker.Mock() mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError( "401 Client Error: Unauthorized" ) mocker.patch("requests.get", return_value=mock_response) # Call the function and expect an error with pytest.raises(requests.exceptions.HTTPError): list_tags(image_str) def test_list_tags_registry_error(mocker: MockerFixture): """Test that list_tags handles registry errors correctly.""" image_str = "ghcr.io/freedomofpress/dangerzone" # Mock requests.get to return success for auth but error for tags mock_response_auth = mocker.Mock() mock_response_auth.json.return_value = {"token": "dummy_token"} mock_response_auth.raise_for_status.return_value = None mock_response_tags = mocker.Mock() mock_response_tags.raise_for_status.side_effect = requests.exceptions.HTTPError( "404 Client Error: Not Found" ) # Setup the mock to return different responses for each URL def mock_get(url, **kwargs): if "token" in url: return mock_response_auth else: return mock_response_tags mocker.patch("requests.get", side_effect=mock_get) # Call the function and expect an error with pytest.raises(requests.exceptions.HTTPError): list_tags(image_str) def test_get_manifest(mocker: MockerFixture): """Test that get_manifest correctly retrieves manifests from the registry.""" image_str = "ghcr.io/freedomofpress/dangerzone:v0.4.2" # Mock the responses manifest_content = { "schemaVersion": 2, "mediaType": "application/vnd.docker.distribution.manifest.v2+json", "config": { "mediaType": "application/vnd.docker.container.image.v1+json", "size": 1234, "digest": "sha256:abc123def456", }, "layers": [ { "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", "size": 12345, "digest": "sha256:layer1", } ], } mock_response_auth = mocker.Mock() mock_response_auth.json.return_value = {"token": "dummy_token"} mock_response_auth.raise_for_status.return_value = None mock_response_manifest = mocker.Mock() mock_response_manifest.json.return_value = manifest_content mock_response_manifest.status_code = 200 mock_response_manifest.raise_for_status.return_value = None # Setup the mock to return different responses for each URL def mock_get(url, **kwargs): if "token" in url: return mock_response_auth else: return mock_response_manifest mocker.patch("requests.get", side_effect=mock_get) # Call the function response = get_manifest(image_str) # Verify the result assert response.status_code == 200 assert response.json() == manifest_content def test_get_manifest_digest(): """Test that get_manifest_digest correctly calculates the manifest digest.""" # Create a sample manifest content manifest_content = b'{"schemaVersion":2,"mediaType":"application/vnd.docker.distribution.manifest.v2+json"}' # Calculate the expected digest manually import hashlib expected_digest = hashlib.sha256(manifest_content).hexdigest() # Call the function with the content directly digest = get_manifest_digest("unused_image_str", manifest_content) # Verify the result assert digest == expected_digest def test_get_manifest_digest_from_registry(mocker: MockerFixture): """Test that get_manifest_digest correctly retrieves and calculates digests from the registry.""" image_str = "ghcr.io/freedomofpress/dangerzone:v0.4.2" # Sample manifest content manifest_content = b'{"schemaVersion":2,"mediaType":"application/vnd.docker.distribution.manifest.v2+json"}' expected_digest = hashlib.sha256(manifest_content).hexdigest() # Mock get_manifest mock_response = mocker.Mock() mock_response.content = manifest_content mocker.patch("dangerzone.updater.registry.get_manifest", return_value=mock_response) # Call the function digest = get_manifest_digest(image_str) # Verify the result assert digest == expected_digest