Skip to content

Commit c9d8ba5

Browse files
Update tests
1 parent 07c213a commit c9d8ba5

3 files changed

Lines changed: 154 additions & 99 deletions

File tree

src/aignostics/platform/resources/runs.py

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,9 @@ def get_artifact_download_url(self, artifact_id: str) -> str:
292292
RuntimeError: If the redirect ``Location`` header is missing or the
293293
response status is unexpected.
294294
"""
295+
296+
# Generated client follows redirect automatically which prevents us from getting the presigned URL,
297+
# but we need the presigned URL to get query the metadata of the file (e.g. checksum) before downloading it.
295298
serialize = (
296299
self._api._get_artifact_url_v1_runs_run_id_artifacts_artifact_id_file_get_serialize # noqa: SLF001
297300
)
@@ -303,24 +306,24 @@ def get_artifact_download_url(self, artifact_id: str) -> str:
303306
_headers={"User-Agent": user_agent()},
304307
_host_index=0,
305308
)
306-
response = requests.get(
309+
with requests.get(
307310
url,
308311
headers=dict(header_params),
309312
allow_redirects=False,
310313
timeout=settings().run_timeout,
311-
)
312-
if response.status_code == requests.codes.temporary_redirect:
313-
location = response.headers.get("Location")
314-
if not location:
315-
msg = f"307 redirect received but Location header is absent for artifact {artifact_id!r}"
316-
raise RuntimeError(msg)
317-
return location
318-
response.raise_for_status()
319-
msg = (
320-
f"Unexpected status {response.status_code} from artifact URL endpoint "
321-
f"for artifact {artifact_id!r}; expected 307 redirect"
322-
)
323-
raise RuntimeError(msg)
314+
) as response:
315+
if response.status_code == requests.codes.temporary_redirect:
316+
location = response.headers.get("Location")
317+
if not location:
318+
msg = f"307 redirect received but Location header is absent for artifact {artifact_id!r}"
319+
raise RuntimeError(msg)
320+
return location
321+
response.raise_for_status()
322+
msg = (
323+
f"Unexpected status {response.status_code} from artifact URL endpoint "
324+
f"for artifact {artifact_id!r}; expected 307 redirect"
325+
)
326+
raise RuntimeError(msg)
324327

325328
def download_to_folder( # noqa: C901
326329
self,

tests/aignostics/application/download_test.py

Lines changed: 0 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -403,87 +403,3 @@ def progress_callback(p: DownloadProgress) -> None:
403403

404404
# Verify direct URL was used (no signed URL generation)
405405
mock_get.assert_called_once_with(https_url, stream=True, timeout=60)
406-
407-
408-
# ---------------------------------------------------------------------------
409-
# download_item_artifact tests
410-
# ---------------------------------------------------------------------------
411-
412-
413-
def _make_crc32c_checksum(data: bytes) -> str:
414-
"""Compute a base64-encoded CRC32C checksum matching the SDK's format."""
415-
h = crc32c.CRC32CHash()
416-
h.update(data)
417-
return base64.b64encode(h.digest()).decode("ascii")
418-
419-
420-
@pytest.mark.unit
421-
def test_download_item_artifact_success(tmp_path: Path) -> None:
422-
"""Test that download_item_artifact fetches a URL from the run and downloads the file."""
423-
file_content = b"artifact file content"
424-
presigned_url = "https://storage.example.com/presigned/artifact.tiff"
425-
checksum = _make_crc32c_checksum(file_content)
426-
427-
mock_run = Mock()
428-
mock_run.get_artifact_download_url.return_value = presigned_url
429-
430-
artifact = Mock()
431-
artifact.name = "result"
432-
artifact.output_artifact_id = "artifact-uuid-123"
433-
artifact.metadata = {"checksum_base64_crc32c": checksum, "media_type": "image/tiff"}
434-
435-
progress = DownloadProgress()
436-
437-
with patch("aignostics.application._download.requests.get") as mock_get:
438-
mock_response = Mock()
439-
mock_response.__enter__ = Mock(return_value=mock_response)
440-
mock_response.__exit__ = Mock(return_value=False)
441-
mock_response.raise_for_status = Mock()
442-
mock_response.headers = {"content-length": str(len(file_content))}
443-
mock_response.iter_content = Mock(return_value=[file_content])
444-
mock_get.return_value = mock_response
445-
446-
with patch("aignostics.application._utils.get_file_extension_for_artifact", return_value=".tiff"):
447-
download_item_artifact(progress, mock_run, artifact, tmp_path)
448-
449-
mock_run.get_artifact_download_url.assert_called_once_with("artifact-uuid-123")
450-
assert (tmp_path / "result.tiff").exists()
451-
452-
453-
@pytest.mark.unit
454-
def test_download_item_artifact_no_checksum_raises(tmp_path: Path) -> None:
455-
"""Test that download_item_artifact raises ValueError when no checksum metadata is present."""
456-
mock_run = Mock()
457-
artifact = Mock()
458-
artifact.name = "result"
459-
artifact.metadata = {} # no checksum
460-
461-
progress = DownloadProgress()
462-
463-
with pytest.raises(ValueError, match="No checksum metadata found"):
464-
download_item_artifact(progress, mock_run, artifact, tmp_path)
465-
466-
mock_run.get_artifact_download_url.assert_not_called()
467-
468-
469-
@pytest.mark.unit
470-
def test_download_item_artifact_skips_existing_correct_checksum(tmp_path: Path) -> None:
471-
"""Test that download_item_artifact skips download when file exists with correct checksum."""
472-
file_content = b"existing artifact content"
473-
checksum = _make_crc32c_checksum(file_content)
474-
475-
mock_run = Mock()
476-
artifact = Mock()
477-
artifact.name = "result"
478-
artifact.output_artifact_id = "artifact-uuid-456"
479-
artifact.metadata = {"checksum_base64_crc32c": checksum, "media_type": "image/tiff"}
480-
481-
progress = DownloadProgress()
482-
483-
with patch("aignostics.application._utils.get_file_extension_for_artifact", return_value=".tiff"):
484-
existing_file = tmp_path / "result.tiff"
485-
existing_file.write_bytes(file_content)
486-
487-
download_item_artifact(progress, mock_run, artifact, tmp_path)
488-
489-
mock_run.get_artifact_download_url.assert_not_called()

tests/aignostics/platform/resources/runs_test.py

Lines changed: 137 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44
verifying their functionality for listing, creating, and managing application runs.
55
"""
66

7-
from unittest.mock import Mock
7+
from unittest.mock import MagicMock, Mock, patch
88

99
import pytest
10+
import requests
1011
from aignx.codegen.api.public_api import PublicApi
1112
from aignx.codegen.models import (
1213
InputArtifactCreationRequest,
@@ -703,6 +704,141 @@ def test_run_details_raises_not_found_after_timeout(app_run, mock_api) -> None:
703704
assert mock_api.get_run_v1_runs_run_id_get.call_count > 1
704705

705706

707+
@pytest.fixture
708+
def mock_serialize(mock_api) -> Mock:
709+
"""Configure the serialize method on mock_api to return a valid (method, url, headers) tuple.
710+
711+
Returns:
712+
Mock: The configured serialize mock.
713+
"""
714+
serialize = Mock(return_value=("GET", "https://api.example.com/v1/runs/test-run-id/artifacts/art-1/file", {}, None))
715+
mock_api._get_artifact_url_v1_runs_run_id_artifacts_artifact_id_file_get_serialize = serialize
716+
return serialize
717+
718+
719+
@pytest.mark.unit
720+
def test_get_artifact_download_url_returns_location_on_307(app_run, mock_serialize) -> None:
721+
"""Test that get_artifact_download_url returns the Location header value on a 307 redirect.
722+
723+
Args:
724+
app_run: Run instance with mock API.
725+
mock_serialize: Mock serializer configured on the API.
726+
"""
727+
# Arrange
728+
presigned_url = "https://storage.example.com/artifact?sig=abc123"
729+
mock_response = MagicMock()
730+
mock_response.__enter__ = Mock(return_value=mock_response)
731+
mock_response.__exit__ = Mock(return_value=False)
732+
mock_response.status_code = requests.codes.temporary_redirect
733+
mock_response.headers = {"Location": presigned_url}
734+
735+
with patch("aignostics.platform.resources.runs.requests.get", return_value=mock_response) as mock_get:
736+
# Act
737+
result = app_run.get_artifact_download_url("art-1")
738+
739+
# Assert
740+
assert result == presigned_url
741+
mock_get.assert_called_once_with(
742+
"https://api.example.com/v1/runs/test-run-id/artifacts/art-1/file",
743+
headers={},
744+
allow_redirects=False,
745+
timeout=mock_get.call_args[1]["timeout"],
746+
)
747+
748+
@pytest.mark.parametrize(
749+
("status_code", "expected_message"),
750+
[
751+
(200, "Unexpected status 200 from artifact URL endpoint"),
752+
(307, "307 redirect received but Location header is absent"),
753+
(404, "Unexpected status 404 from artifact URL endpoint for artifact 'art-1'; expected 307 redirect"),
754+
]
755+
)
756+
@pytest.mark.unit
757+
def test_get_artifact_download_url_errors(app_run, mock_serialize, status_code, expected_message) -> None:
758+
"""Test that get_artifact_download_url raises RuntimeError when 307 response has no Location header.
759+
760+
Args:
761+
app_run: Run instance with mock API.
762+
mock_serialize: Mock serializer configured on the API.
763+
"""
764+
# Arrange
765+
mock_response = MagicMock()
766+
mock_response.__enter__ = Mock(return_value=mock_response)
767+
mock_response.__exit__ = Mock(return_value=False)
768+
mock_response.status_code = status_code
769+
mock_response.headers = {} # No Location header
770+
771+
with patch("aignostics.platform.resources.runs.requests.get", return_value=mock_response):
772+
# Act & Assert
773+
with pytest.raises(RuntimeError, match=expected_message):
774+
app_run.get_artifact_download_url("art-1")
775+
776+
777+
@pytest.mark.unit
778+
def test_get_artifact_download_url_passes_correct_artifact_id(app_run, mock_api) -> None:
779+
"""Test that get_artifact_download_url passes the artifact_id to the serializer.
780+
781+
Args:
782+
app_run: Run instance with mock API.
783+
mock_api: Mock ExternalsApi instance.
784+
"""
785+
# Arrange
786+
artifact_id = "specific-artifact-xyz"
787+
serialize = Mock(
788+
return_value=("GET", "https://api.example.com/v1/runs/test-run-id/artifacts/specific-artifact-xyz/file", {}, None)
789+
)
790+
mock_api._get_artifact_url_v1_runs_run_id_artifacts_artifact_id_file_get_serialize = serialize
791+
792+
presigned_url = "https://storage.example.com/file?sig=xyz"
793+
mock_response = MagicMock()
794+
mock_response.__enter__ = Mock(return_value=mock_response)
795+
mock_response.__exit__ = Mock(return_value=False)
796+
mock_response.status_code = requests.codes.temporary_redirect
797+
mock_response.headers = {"Location": presigned_url}
798+
799+
with patch("aignostics.platform.resources.runs.requests.get", return_value=mock_response):
800+
# Act
801+
result = app_run.get_artifact_download_url(artifact_id)
802+
803+
# Assert
804+
assert result == presigned_url
805+
serialize.assert_called_once()
806+
call_kwargs = serialize.call_args[1]
807+
assert call_kwargs["run_id"] == app_run.run_id
808+
assert call_kwargs["artifact_id"] == artifact_id
809+
810+
811+
@pytest.mark.unit
812+
def test_get_artifact_download_url_uses_headers_from_serializer(app_run, mock_api) -> None:
813+
"""Test that get_artifact_download_url passes serializer-provided headers to requests.get.
814+
815+
Args:
816+
app_run: Run instance with mock API.
817+
mock_api: Mock ExternalsApi instance.
818+
"""
819+
# Arrange
820+
auth_headers = [("Authorization", "Bearer token123"), ("X-Custom", "header-value")]
821+
serialize = Mock(
822+
return_value=("GET", "https://api.example.com/v1/runs/test-run-id/artifacts/art-1/file", auth_headers, None)
823+
)
824+
mock_api._get_artifact_url_v1_runs_run_id_artifacts_artifact_id_file_get_serialize = serialize
825+
826+
mock_response = MagicMock()
827+
mock_response.__enter__ = Mock(return_value=mock_response)
828+
mock_response.__exit__ = Mock(return_value=False)
829+
mock_response.status_code = requests.codes.temporary_redirect
830+
mock_response.headers = {"Location": "https://storage.example.com/file"}
831+
832+
with patch("aignostics.platform.resources.runs.requests.get", return_value=mock_response) as mock_get:
833+
# Act
834+
app_run.get_artifact_download_url("art-1")
835+
836+
# Assert
837+
call_kwargs = mock_get.call_args[1]
838+
assert call_kwargs["headers"] == dict(auth_headers)
839+
assert call_kwargs["allow_redirects"] is False
840+
841+
706842
@pytest.mark.unit
707843
def test_run_details_does_not_retry_other_exceptions(app_run, mock_api) -> None:
708844
"""Test that the outer retry does not catch non-NotFoundException errors.

0 commit comments

Comments
 (0)