Skip to content

Commit e9abd15

Browse files
committed
feat: osm user agent added as env variable and test cases added for osm response
1 parent fdfed33 commit e9abd15

File tree

5 files changed

+57
-18
lines changed

5 files changed

+57
-18
lines changed

backend/config.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,10 @@ def assemble_db_connection(
280280
"OHSOME_STATS_API_URL", "https://stats.now.ohsome.org/api"
281281
)
282282
OHSOME_STATS_TOPICS: str = os.getenv("OHSOME_STATS_TOPICS", None)
283+
OSM_USER_AGENT: str = os.getenv(
284+
"OSM_USER_AGENT",
285+
"HOT-TaskingManager-API/5.0 (https://tasking-manager-production-api.hotosm.org)",
286+
)
283287

284288

285289
class TestEnvironmentConfig(Settings):
@@ -297,6 +301,10 @@ class TestEnvironmentConfig(Settings):
297301
)
298302

299303
LOG_LEVEL: str = "DEBUG"
304+
OSM_USER_AGENT: str = os.getenv(
305+
"OSM_USER_AGENT",
306+
"HOT-TaskingManager-API/5.0 (https://tasking-manager-production-api.hotosm.org)",
307+
)
300308

301309

302310
@lru_cache

backend/services/users/user_service.py

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
from sqlalchemy import and_, desc, distinct, func, insert, select
88
from httpx import AsyncClient
99

10-
from backend.config import Settings
1110
from backend.exceptions import NotFound
1211
from backend.models.dtos.interests_dto import InterestDTO, InterestsListDTO
1312
from backend.models.dtos.project_dto import ProjectFavoritesDTO, ProjectSearchResultsDTO
@@ -50,9 +49,7 @@
5049
from backend.services.users.osm_service import OSMService
5150
from backend.services.mapping_levels import MappingLevelService
5251
from fastapi import HTTPException
53-
54-
55-
settings = Settings()
52+
from backend.config import settings
5653

5754

5855
class UserServiceError(Exception):
@@ -181,9 +178,7 @@ async def get_and_save_stats(user_id: int, db: Database) -> dict:
181178
osm_user_details_url = f"{settings.OSM_SERVER_URL}/api/0.6/user/{user_id}.json"
182179

183180
oh_some_headers = {"Authorization": f"Basic {settings.OHSOME_STATS_TOKEN}"}
184-
osm_headers = {
185-
"User-Agent": "HOT-TaskingManager-API/2.0 (https://tasking-manager-production-api.hotosm.org)"
186-
}
181+
osm_headers = {"User-Agent": settings.OSM_USER_AGENT}
187182

188183
async with AsyncClient(timeout=10.0) as client:
189184
oh_some_response = await client.get(oh_some_url, headers=oh_some_headers)
@@ -192,6 +187,7 @@ async def get_and_save_stats(user_id: int, db: Database) -> dict:
192187
)
193188

194189
if oh_some_response.status_code != 200:
190+
195191
error_msg = (
196192
"External-Error in Ohsome API: url=%s status_code=%s response=%s"
197193
% (

example.env

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ OSM_SERVER_URL=${OSM_SERVER_URL:-https://www.openstreetmap.org}
3838
OSM_SERVER_API_URL=${OSM_SERVER_API_URL:-https://api.openstreetmap.org}
3939
OSM_NOMINATIM_SERVER_URL=${OSM_NOMINATIM_SERVER_URL:-https://nominatim.openstreetmap.org}
4040
OSM_REGISTER_URL=${OSM_REGISTER_URL:-https://www.openstreetmap.org/user/new}
41+
OSM_USER_AGENT=${OSM_USER_AGENT:-HOT-TaskingManager}
4142

4243
# Information about the Editor URLs. Those are the default values on the frontend.
4344
# You only need to modify it in case you want to direct users to map on a different OSM instance.

tests/api/integration/services/users/test_user_service.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
return_canned_user,
1313
create_mapping_levels,
1414
)
15+
from httpx import AsyncClient
16+
from backend.config import test_settings as settings
1517

1618

1719
@pytest.mark.anyio
@@ -143,3 +145,14 @@ async def test_register_user_creates_new_user(self):
143145
user = await UserService.get_user_by_id(canned.id, db=self.db)
144146
assert user.username == canned.username
145147
assert user.mapping_level == 1
148+
149+
async def test_osm_user_endpoint_not_rate_limited(self):
150+
url = "https://www.openstreetmap.org/api/0.6/user/490556.json"
151+
152+
async with AsyncClient(timeout=10.0) as client:
153+
response = await client.get(
154+
url,
155+
headers={"User-Agent": settings.OSM_USER_AGENT},
156+
)
157+
assert response.status_code == 200
158+
assert response.status_code != 429

tests/api/unit/services/users/test_user_service.py

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -137,24 +137,45 @@ async def test_get_and_save_stats(self, mock_get):
137137
stats = await UserStats.get_for_user(self.test_user.id, self.db)
138138
assert stats.stats == '{"changeset": 251.0}'
139139

140-
@patch.object(AsyncClient, "get")
141-
async def test_get_and_save_stats_error(self, mock_get):
142-
# Arrange
143-
mock_response = AsyncMock()
144-
mock_response.status_code = 500
145-
mock_response.json = MagicMock(
140+
async def test_get_and_save_stats_handles_ohsome_500(self):
141+
# Arrange: prepare an OHsome error response (500) and a harmless OSM response (200)
142+
ohsome_resp = AsyncMock()
143+
ohsome_resp.status_code = 500
144+
ohsome_resp.text = "Internal Server Error"
145+
ohsome_resp.json = MagicMock(
146146
return_value={
147147
"status": 500,
148148
"error": "Internal Server Error",
149149
"path": "/api/stats/user",
150-
},
150+
}
151151
)
152-
mock_get.return_value = mock_response
153152

154-
# Assert
155-
with pytest.raises(UserServiceError):
153+
changeset_resp = AsyncMock()
154+
changeset_resp.status_code = 200
155+
changeset_resp.json = MagicMock(
156+
return_value={"user": {"changesets": {"count": 0}}}
157+
)
158+
159+
# Patch AsyncClient.get and UserStats.update
160+
with (
161+
patch.object(AsyncClient, "get", new_callable=AsyncMock) as mock_get,
162+
patch.object(UserStats, "update", new_callable=AsyncMock) as mock_update,
163+
):
164+
# The function does two gets in sequence: ohsome then changeset
165+
mock_get.side_effect = [ohsome_resp, changeset_resp]
166+
156167
# Act
157-
await UserService.get_and_save_stats(self.test_user.id, self.db)
168+
result = await UserService.get_and_save_stats(self.test_user.id, self.db)
169+
170+
# Assert
171+
# function should return empty dict on OHsome 500
172+
assert result == {}
173+
174+
# Ensure we tried the external calls (at least awaited once)
175+
mock_get.assert_awaited()
176+
177+
# And we must NOT call UserStats.update when an upstream failed
178+
mock_update.assert_not_awaited()
158179

159180
@patch.object(AsyncClient, "get")
160181
async def test_check_and_update_mapper_level_happy_path(self, mock_get):

0 commit comments

Comments
 (0)