Skip to content

Commit cab4c0e

Browse files
authored
fix: update types for url config parameters to be compatible with str (#127)
1 parent d4c7d34 commit cab4c0e

File tree

3 files changed

+187
-18
lines changed

3 files changed

+187
-18
lines changed

lib/ingestor-api/runtime/src/config.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
11
import os
22
from getpass import getuser
3-
from typing import Optional
3+
from typing import Annotated, Optional
44

5-
from pydantic import AnyHttpUrl, Field, constr
5+
from pydantic import (
6+
AfterValidator,
7+
AnyHttpUrl,
8+
Field,
9+
constr,
10+
)
611
from pydantic_settings import BaseSettings
712
from pydantic_ssm_settings.settings import SsmBaseSettings
813

14+
HttpUrlString = Annotated[AnyHttpUrl, AfterValidator(str)]
15+
916
AwsArn = constr(pattern=r"^arn:aws:iam::\d{12}:role/.+")
1017

1118

@@ -14,11 +21,11 @@ class Settings(BaseSettings):
1421

1522
root_path: Optional[str] = Field(description="Path from where to serve this URL.")
1623

17-
jwks_url: Optional[AnyHttpUrl] = Field(
24+
jwks_url: Optional[HttpUrlString] = Field(
1825
description="URL of JWKS, e.g. https://cognito-idp.{region}.amazonaws.com/{userpool_id}/.well-known/jwks.json" # noqa
1926
)
2027

21-
stac_url: AnyHttpUrl = Field(description="URL of STAC API")
28+
stac_url: HttpUrlString = Field(description="URL of STAC API")
2229

2330
data_access_role: AwsArn = Field(
2431
description="ARN of AWS Role used to validate access to S3 data"

lib/ingestor-api/runtime/tests/conftest.py

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import os
2-
31
import boto3
42
import pytest
53
from fastapi.testclient import TestClient
@@ -8,20 +6,20 @@
86

97

108
@pytest.fixture
11-
def test_environ():
12-
# Mocked AWS Credentials for moto (best practice recommendation from moto)
13-
os.environ["AWS_ACCESS_KEY_ID"] = "testing"
14-
os.environ["AWS_SECRET_ACCESS_KEY"] = "testing"
15-
os.environ["AWS_SECURITY_TOKEN"] = "testing"
16-
os.environ["AWS_SESSION_TOKEN"] = "testing"
9+
def test_environ(monkeypatch):
10+
# Mocked AWS Credentials for moto
11+
monkeypatch.setenv("AWS_ACCESS_KEY_ID", "testing")
12+
monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "testing")
13+
monkeypatch.setenv("AWS_SECURITY_TOKEN", "testing")
14+
monkeypatch.setenv("AWS_SESSION_TOKEN", "testing")
1715

1816
# Config mocks
19-
os.environ["DYNAMODB_TABLE"] = "test_table"
20-
os.environ["JWKS_URL"] = "https://test-jwks.url"
21-
os.environ["STAC_URL"] = "https://test-stac.url"
22-
os.environ["DATA_ACCESS_ROLE"] = "arn:aws:iam::123456789012:role/test-role"
23-
os.environ["DB_SECRET_ARN"] = "testing"
24-
os.environ["ROOT_PATH"] = "testing"
17+
monkeypatch.setenv("DYNAMODB_TABLE", "test_table")
18+
monkeypatch.setenv("JWKS_URL", "https://test-jwks.url")
19+
monkeypatch.setenv("STAC_URL", "https://test-stac.url")
20+
monkeypatch.setenv("DATA_ACCESS_ROLE", "arn:aws:iam::123456789012:role/test-role")
21+
monkeypatch.setenv("DB_SECRET_ARN", "testing")
22+
monkeypatch.setenv("ROOT_PATH", "testing")
2523

2624

2725
@pytest.fixture
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
from unittest.mock import MagicMock, patch
2+
3+
import pytest
4+
import requests
5+
from pydantic import AnyHttpUrl
6+
from src import validators
7+
8+
9+
@pytest.fixture
10+
def mock_settings():
11+
"""Fixture to instantiate and patch settings with proper types."""
12+
from src.config import Settings
13+
14+
mock_settings = Settings(
15+
dynamodb_table="test-table",
16+
stac_url=AnyHttpUrl("https://test-stac.url"),
17+
data_access_role="arn:aws:iam::123456789012:role/test-role",
18+
requester_pays=False,
19+
jwks_url=AnyHttpUrl("https://test-jwks.url"),
20+
root_path="testing",
21+
)
22+
23+
with patch("src.config.settings", mock_settings):
24+
yield mock_settings
25+
26+
27+
@pytest.fixture
28+
def mock_requests():
29+
"""Fixture to mock requests library."""
30+
with patch("src.validators.requests") as mock_requests:
31+
mock_response = MagicMock()
32+
mock_response.ok = True
33+
mock_response.raise_for_status.return_value = None
34+
mock_requests.get.return_value = mock_response
35+
mock_requests.head.return_value = mock_response
36+
37+
mock_requests.exceptions = requests.exceptions
38+
39+
yield mock_requests
40+
41+
42+
@pytest.fixture
43+
def mock_boto3():
44+
"""Fixture to mock boto3 library."""
45+
with patch("src.validators.boto3") as mock_boto3:
46+
mock_client = MagicMock()
47+
mock_client.exceptions.ClientError = Exception
48+
49+
mock_sts_client = MagicMock()
50+
mock_sts_client.assume_role.return_value = {
51+
"Credentials": {
52+
"AccessKeyId": "test_access_key",
53+
"SecretAccessKey": "test_secret_key",
54+
"SessionToken": "test_session_token",
55+
}
56+
}
57+
58+
def mock_client_factory(service_name, **kwargs):
59+
if service_name == "sts":
60+
return mock_sts_client
61+
return mock_client
62+
63+
mock_boto3.client.side_effect = mock_client_factory
64+
65+
yield mock_boto3, mock_client
66+
67+
68+
class TestValidators:
69+
def test_collection_exists_success(self, mock_settings, mock_requests):
70+
"""Test collection_exists when the collection exists."""
71+
validators.collection_exists.cache_clear()
72+
73+
result = validators.collection_exists("test-collection")
74+
75+
assert result
76+
77+
expected_url = f"{mock_settings.stac_url}collections/test-collection"
78+
mock_requests.get.assert_called_once_with(expected_url)
79+
80+
def test_collection_exists_failure(self, mock_settings, mock_requests):
81+
"""Test collection_exists when the collection doesn't exist."""
82+
validators.collection_exists.cache_clear()
83+
84+
mock_response = MagicMock()
85+
mock_response.ok = False
86+
mock_response.status_code = 404
87+
mock_requests.get.return_value = mock_response
88+
89+
with pytest.raises(ValueError) as excinfo:
90+
validators.collection_exists("nonexistent-collection")
91+
92+
assert "Invalid collection 'nonexistent-collection'" in str(excinfo.value)
93+
assert "404 response code" in str(excinfo.value)
94+
95+
def test_url_is_accessible_success(self, mock_requests):
96+
"""Test url_is_accessible when the URL is accessible."""
97+
validators.url_is_accessible("https://example.com/asset.tif")
98+
99+
mock_requests.head.assert_called_once_with("https://example.com/asset.tif")
100+
101+
def test_url_is_accessible_failure(self, mock_requests):
102+
"""Test url_is_accessible when the URL is not accessible."""
103+
mock_response = MagicMock()
104+
mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError(
105+
response=MagicMock(status_code=403, reason="Forbidden")
106+
)
107+
mock_requests.head.return_value = mock_response
108+
109+
with pytest.raises(ValueError) as excinfo:
110+
validators.url_is_accessible("https://example.com/private.tif")
111+
112+
assert "Asset not accessible" in str(excinfo.value)
113+
114+
def test_s3_object_is_accessible_success(self, mock_settings, mock_boto3):
115+
"""Test s3_object_is_accessible when the object is accessible."""
116+
_, mock_s3_client = mock_boto3
117+
118+
validators.s3_object_is_accessible("test-bucket", "test-key")
119+
120+
mock_s3_client.head_object.assert_called_once_with(
121+
Bucket="test-bucket", Key="test-key"
122+
)
123+
124+
def test_s3_object_is_accessible_with_requester_pays(self, mock_settings, mock_boto3):
125+
"""Test s3_object_is_accessible with requester pays enabled."""
126+
_, mock_s3_client = mock_boto3
127+
128+
mock_settings.requester_pays = True
129+
130+
validators.s3_object_is_accessible("test-bucket", "test-key")
131+
132+
mock_s3_client.head_object.assert_called_once_with(
133+
Bucket="test-bucket", Key="test-key", RequestPayer="requester"
134+
)
135+
136+
def test_s3_object_is_accessible_failure(self, mock_settings, mock_boto3):
137+
"""Test s3_object_is_accessible when the object is not accessible."""
138+
_, mock_s3_client = mock_boto3
139+
140+
_ = {"Error": {"Message": "Access Denied"}}
141+
mock_s3_client.head_object.side_effect = Exception()
142+
mock_s3_client.head_object.side_effect.__dict__["response"] = {
143+
"Error": {"Message": "Access Denied"}
144+
}
145+
146+
with pytest.raises(ValueError) as excinfo:
147+
validators.s3_object_is_accessible("test-bucket", "private-key")
148+
149+
assert "Asset not accessible" in str(excinfo.value)
150+
151+
def test_get_s3_credentials(self, mock_boto3):
152+
"""Test get_s3_credentials returns the expected credentials."""
153+
_, _ = mock_boto3
154+
155+
validators.get_s3_credentials.cache_clear()
156+
157+
credentials = validators.get_s3_credentials()
158+
159+
expected_credentials = {
160+
"aws_access_key_id": "test_access_key",
161+
"aws_secret_access_key": "test_secret_key",
162+
"aws_session_token": "test_session_token",
163+
}
164+
assert credentials == expected_credentials

0 commit comments

Comments
 (0)