Skip to content

Commit 82d3c3f

Browse files
Update tests
1 parent 07c213a commit 82d3c3f

4 files changed

Lines changed: 161 additions & 104 deletions

File tree

CHANGELOG.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1469,5 +1469,3 @@
14691469
* @idelsink made their first contribution
14701470
* @dependabot[bot] made their first contribution
14711471
* @ari-nz made their first contribution
1472-
1473-

src/aignostics/platform/resources/runs.py

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,8 @@ 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+
# Generated client follows redirect automatically which prevents us from getting the presigned URL,
296+
# but we need the presigned URL to get query the metadata of the file (e.g. checksum) before downloading it.
295297
serialize = (
296298
self._api._get_artifact_url_v1_runs_run_id_artifacts_artifact_id_file_get_serialize # noqa: SLF001
297299
)
@@ -303,24 +305,23 @@ def get_artifact_download_url(self, artifact_id: str) -> str:
303305
_headers={"User-Agent": user_agent()},
304306
_host_index=0,
305307
)
306-
response = requests.get(
308+
with requests.get(
307309
url,
308310
headers=dict(header_params),
309311
allow_redirects=False,
310312
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)
313+
) as response:
314+
if response.status_code == requests.codes.temporary_redirect:
315+
location = response.headers.get("Location")
316+
if not location:
317+
msg = f"307 redirect received but Location header is absent for artifact {artifact_id!r}"
318+
raise RuntimeError(msg)
319+
return location
320+
msg = (
321+
f"Unexpected status {response.status_code} from artifact URL endpoint "
322+
f"for artifact {artifact_id!r}; expected 307 redirect"
323+
)
324+
raise RuntimeError(msg)
324325

325326
def download_to_folder( # noqa: C901
326327
self,

tests/aignostics/application/download_test.py

Lines changed: 0 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
11
"""Tests for download utility functions in the application module."""
22

3-
import base64
43
from pathlib import Path
54
from unittest.mock import Mock, patch
65

7-
import crc32c
86
import pytest
97
import requests
108

119
from aignostics.application._download import (
12-
download_item_artifact,
1310
download_url_to_file_with_progress,
1411
extract_filename_from_url,
1512
)
@@ -403,87 +400,3 @@ def progress_callback(p: DownloadProgress) -> None:
403400

404401
# Verify direct URL was used (no signed URL generation)
405402
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: 146 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,150 @@ 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+
749+
@pytest.mark.parametrize(
750+
("status_code", "expected_message"),
751+
[
752+
(200, "Unexpected status 200 from artifact URL endpoint"),
753+
(307, "307 redirect received but Location header is absent"),
754+
(404, "Unexpected status 404 from artifact URL endpoint for artifact 'art-1'; expected 307 redirect"),
755+
],
756+
)
757+
@pytest.mark.unit
758+
def test_get_artifact_download_url_errors(app_run, mock_serialize, status_code, expected_message) -> None:
759+
"""Test that get_artifact_download_url raises RuntimeError after unexpected result.
760+
761+
Args:
762+
app_run: Run instance with mock API.
763+
mock_serialize: Mock serializer configured on the API.
764+
status_code: The HTTP status code to simulate in the response.
765+
expected_message: The expected error message to be included in the RuntimeError.
766+
"""
767+
# Arrange
768+
mock_response = MagicMock()
769+
mock_response.__enter__ = Mock(return_value=mock_response)
770+
mock_response.__exit__ = Mock(return_value=False)
771+
mock_response.status_code = status_code
772+
mock_response.headers = {} # No Location header
773+
774+
with (
775+
patch("aignostics.platform.resources.runs.requests.get", return_value=mock_response),
776+
pytest.raises(RuntimeError, match=expected_message),
777+
):
778+
app_run.get_artifact_download_url("art-1")
779+
780+
781+
@pytest.mark.unit
782+
def test_get_artifact_download_url_passes_correct_artifact_id(app_run, mock_api) -> None:
783+
"""Test that get_artifact_download_url passes the artifact_id to the serializer.
784+
785+
Args:
786+
app_run: Run instance with mock API.
787+
mock_api: Mock ExternalsApi instance.
788+
"""
789+
# Arrange
790+
artifact_id = "specific-artifact-xyz"
791+
serialize = Mock(
792+
return_value=(
793+
"GET",
794+
"https://api.example.com/v1/runs/test-run-id/artifacts/specific-artifact-xyz/file",
795+
{},
796+
None,
797+
)
798+
)
799+
mock_api._get_artifact_url_v1_runs_run_id_artifacts_artifact_id_file_get_serialize = serialize
800+
801+
presigned_url = "https://storage.example.com/file?sig=xyz"
802+
mock_response = MagicMock()
803+
mock_response.__enter__ = Mock(return_value=mock_response)
804+
mock_response.__exit__ = Mock(return_value=False)
805+
mock_response.status_code = requests.codes.temporary_redirect
806+
mock_response.headers = {"Location": presigned_url}
807+
808+
with patch("aignostics.platform.resources.runs.requests.get", return_value=mock_response):
809+
# Act
810+
result = app_run.get_artifact_download_url(artifact_id)
811+
812+
# Assert
813+
assert result == presigned_url
814+
serialize.assert_called_once()
815+
call_kwargs = serialize.call_args[1]
816+
assert call_kwargs["run_id"] == app_run.run_id
817+
assert call_kwargs["artifact_id"] == artifact_id
818+
819+
820+
@pytest.mark.unit
821+
def test_get_artifact_download_url_uses_headers_from_serializer(app_run, mock_api) -> None:
822+
"""Test that get_artifact_download_url passes serializer-provided headers to requests.get.
823+
824+
Args:
825+
app_run: Run instance with mock API.
826+
mock_api: Mock ExternalsApi instance.
827+
"""
828+
# Arrange
829+
auth_headers = [("Authorization", "Bearer token123"), ("X-Custom", "header-value")]
830+
serialize = Mock(
831+
return_value=("GET", "https://api.example.com/v1/runs/test-run-id/artifacts/art-1/file", auth_headers, None)
832+
)
833+
mock_api._get_artifact_url_v1_runs_run_id_artifacts_artifact_id_file_get_serialize = serialize
834+
835+
mock_response = MagicMock()
836+
mock_response.__enter__ = Mock(return_value=mock_response)
837+
mock_response.__exit__ = Mock(return_value=False)
838+
mock_response.status_code = requests.codes.temporary_redirect
839+
mock_response.headers = {"Location": "https://storage.example.com/file"}
840+
841+
with patch("aignostics.platform.resources.runs.requests.get", return_value=mock_response) as mock_get:
842+
# Act
843+
app_run.get_artifact_download_url("art-1")
844+
845+
# Assert
846+
call_kwargs = mock_get.call_args[1]
847+
assert call_kwargs["headers"] == dict(auth_headers)
848+
assert call_kwargs["allow_redirects"] is False
849+
850+
706851
@pytest.mark.unit
707852
def test_run_details_does_not_retry_other_exceptions(app_run, mock_api) -> None:
708853
"""Test that the outer retry does not catch non-NotFoundException errors.

0 commit comments

Comments
 (0)