|
4 | 4 | verifying their functionality for listing, creating, and managing application runs. |
5 | 5 | """ |
6 | 6 |
|
7 | | -from unittest.mock import Mock |
| 7 | +from unittest.mock import MagicMock, Mock, patch |
8 | 8 |
|
9 | 9 | import pytest |
| 10 | +import requests |
10 | 11 | from aignx.codegen.api.public_api import PublicApi |
11 | 12 | from aignx.codegen.models import ( |
12 | 13 | InputArtifactCreationRequest, |
@@ -703,6 +704,150 @@ def test_run_details_raises_not_found_after_timeout(app_run, mock_api) -> None: |
703 | 704 | assert mock_api.get_run_v1_runs_run_id_get.call_count > 1 |
704 | 705 |
|
705 | 706 |
|
| 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 | + |
706 | 851 | @pytest.mark.unit |
707 | 852 | def test_run_details_does_not_retry_other_exceptions(app_run, mock_api) -> None: |
708 | 853 | """Test that the outer retry does not catch non-NotFoundException errors. |
|
0 commit comments