diff --git a/app/platforms/implementations/ogc_api_process.py b/app/platforms/implementations/ogc_api_process.py index d751b7f..f9cca3d 100644 --- a/app/platforms/implementations/ogc_api_process.py +++ b/app/platforms/implementations/ogc_api_process.py @@ -319,26 +319,60 @@ def _extract_download_link_from_asset(self, asset: dict) -> str | None: return href return None - def _generate_signed_url(self, href: str, user_token: str) -> str: + def _generate_signed_url(self, href: str, user_token: str) -> str | None: """ Generate a signed URL for the given href using the provided user token. - This is a placeholder implementation and should be replaced with - actual logic to generate signed URLs. + The endpoint is expected to answer with a redirect and a `Location` + header that points to the signed resource. Args: href (str): The original href. user_token (str): The user token to be used for signing. + + Returns: + str | None: The signed URL if it can be extracted, otherwise None. """ - # TODO - Add implementation logger.debug(f"Generating signed URL for href: {href} with user token.") - response = requests.get( - href, - headers={"Authorization": f"Bearer {user_token}"}, - allow_redirects=False, + try: + response = requests.get( + href, + headers={"Authorization": f"Bearer {user_token}"}, + allow_redirects=False, + timeout=20, + ) + response.raise_for_status() + except requests.RequestException as exc: + logger.warning( + "Could not generate signed URL due to HTTP/network error " + f"for href '{href}': {exc}" + ) + return None + + location_header = response.headers.get("location") or response.headers.get( + "Location" + ) + if location_header: + logger.debug(f"Signed URL generated for href '{href}'.") + return location_header + + # Some providers may return 200 with a direct link instead of redirecting. + if response.url and response.url != href: + logger.warning( + "Missing Location header while generating signed URL for " + f"href '{href}'. Falling back to response URL '{response.url}'." + ) + return response.url + + response_content_type = response.headers.get("content-type", "unknown") + response_body_preview = (response.text or "")[:250].replace("\n", " ") + logger.warning( + "Missing Location header while generating signed URL for " + f"href '{href}'. Status={response.status_code}, " + f"content-type='{response_content_type}', " + f"headers={dict(response.headers)}, " + f"body-preview='{response_body_preview}'." ) - signed_url = response.headers["location"] - logger.debug(f"Signed URL: {signed_url}") - return signed_url + return None def _update_assets_hrefs(self, assets: dict, user_token: str) -> dict: """ @@ -358,8 +392,14 @@ def _update_assets_hrefs(self, assets: dict, user_token: str) -> dict: "Skipping asset..." ) else: - href = self._generate_signed_url(href, user_token) - updated_asset["href"] = href + signed_url = self._generate_signed_url(href, user_token) + if not signed_url: + logger.warning( + f"Could not sign asset href for '{asset_name}'. Keeping " + "original HTTPS href." + ) + signed_url = href + updated_asset["href"] = signed_url updated_assets[asset_name] = updated_asset return updated_assets diff --git a/tests/platforms/test_ogc_api_process_platform.py b/tests/platforms/test_ogc_api_process_platform.py index 75ba9fc..9d9cbea 100644 --- a/tests/platforms/test_ogc_api_process_platform.py +++ b/tests/platforms/test_ogc_api_process_platform.py @@ -4,6 +4,7 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest +import requests def _install_ogc_api_processes_client_stub(): @@ -258,6 +259,86 @@ def test_transform_bbox_parameter_invalid_value(platform, value): platform._transform_bbox_parameter("bbox", value) +@patch("app.platforms.implementations.ogc_api_process.requests.get") +def test_generate_signed_url_returns_location_header(mock_requests_get, platform): + mock_response = MagicMock() + mock_response.headers = {"location": "https://signed.example.com/file.tif"} + mock_response.url = "https://download.example.com/resource" + mock_response.status_code = 302 + mock_response.raise_for_status.return_value = None + mock_requests_get.return_value = mock_response + + result = platform._generate_signed_url( + "https://download.example.com/resource", "token" + ) + + assert result == "https://signed.example.com/file.tif" + + +@patch("app.platforms.implementations.ogc_api_process.requests.get") +def test_generate_signed_url_falls_back_to_response_url(mock_requests_get, platform): + mock_response = MagicMock() + mock_response.headers = {} + mock_response.url = "https://signed.example.com/direct-link.tif" + mock_response.status_code = 200 + mock_response.raise_for_status.return_value = None + mock_requests_get.return_value = mock_response + + result = platform._generate_signed_url( + "https://download.example.com/resource", "token" + ) + + assert result == "https://signed.example.com/direct-link.tif" + + +@patch("app.platforms.implementations.ogc_api_process.requests.get") +def test_generate_signed_url_returns_none_when_location_is_missing( + mock_requests_get, platform +): + mock_response = MagicMock() + mock_response.headers = {} + mock_response.url = "https://download.example.com/resource" + mock_response.status_code = 200 + mock_response.text = '{"detail":"missing redirect"}' + mock_response.raise_for_status.return_value = None + mock_requests_get.return_value = mock_response + + result = platform._generate_signed_url( + "https://download.example.com/resource", "token" + ) + + assert result is None + + +@patch("app.platforms.implementations.ogc_api_process.requests.get") +def test_generate_signed_url_returns_none_on_request_exception( + mock_requests_get, platform +): + mock_requests_get.side_effect = requests.RequestException("connection error") + + result = platform._generate_signed_url( + "https://download.example.com/resource", "token" + ) + + assert result is None + + +def test_update_assets_hrefs_keeps_original_href_when_signing_fails(platform): + assets = { + "result": { + "href": "s3://bucket/result.tif", + "alternate": { + "https": {"href": "https://download.example.com/resource"} + }, + } + } + + with patch.object(platform, "_generate_signed_url", return_value=None): + updated = platform._update_assets_hrefs(assets, "token") + + assert updated["result"]["href"] == "https://download.example.com/resource" + + @pytest.mark.parametrize( ("job_id", "expected"), [