Skip to content

Commit 8831cc1

Browse files
committed
FIX: Add x-upstream-source to most /smda responses
1 parent 8d0e881 commit 8831cc1

3 files changed

Lines changed: 70 additions & 14 deletions

File tree

src/fmu_settings_api/deps.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,11 +122,13 @@ async def ensure_smda_session(session: Session) -> None:
122122
raise HTTPException(
123123
status_code=401,
124124
detail="User SMDA API key is not configured",
125+
headers={"x-upstream-source": "SMDA"},
125126
)
126127
if session.access_tokens.smda_api is None:
127128
raise HTTPException(
128129
status_code=401,
129130
detail="SMDA access token is not set",
131+
headers={"x-upstream-source": "SMDA"},
130132
)
131133

132134

src/fmu_settings_api/v1/routes/smda/main.py

Lines changed: 54 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
"""Routes for querying SMDA's API."""
22

33
import asyncio
4+
from collections.abc import Generator
45
from textwrap import dedent
56

67
import httpx
7-
from fastapi import APIRouter, HTTPException
8+
from fastapi import APIRouter, Depends, HTTPException, Response
89
from fmu.settings.models.smda import (
910
CoordinateSystem,
1011
FieldItem,
@@ -29,7 +30,16 @@
2930
)
3031
from fmu_settings_api.v1.responses import GetSessionResponses, inline_add_response
3132

32-
router = APIRouter(prefix="/smda", tags=["smda"])
33+
34+
def _add_response_headers(response: Response) -> Generator[None]:
35+
"""Adds headers specific to the /smda route."""
36+
response.headers["x-upstream-source"] = "SMDA"
37+
yield
38+
39+
40+
router = APIRouter(
41+
prefix="/smda", tags=["smda"], dependencies=[Depends(_add_response_headers)]
42+
)
3343

3444

3545
@router.get(
@@ -57,7 +67,11 @@ async def get_health(session: SessionDep) -> Ok:
5767
"""Returns a simple 200 OK if able to query SMDA."""
5868
# Handled on the route dependency, duplicated for typing
5969
if session.access_tokens.smda_api is None:
60-
raise HTTPException(status_code=401, detail="SMDA access token is not set")
70+
raise HTTPException(
71+
status_code=401,
72+
detail="SMDA access token is not set",
73+
headers={"x-upstream-source": "SMDA"},
74+
)
6175

6276
try:
6377
smda = SmdaAPI(
@@ -75,7 +89,11 @@ async def get_health(session: SessionDep) -> Ok:
7589
headers={"x-upstream-source": "SMDA"},
7690
) from e
7791
except Exception as e:
78-
raise HTTPException(status_code=500, detail=str(e)) from e
92+
raise HTTPException(
93+
status_code=500,
94+
detail=str(e),
95+
headers={"x-upstream-source": "SMDA"},
96+
) from e
7997

8098

8199
@router.post(
@@ -104,7 +122,11 @@ async def get_health(session: SessionDep) -> Ok:
104122
async def post_field(session: SessionDep, field: SmdaField) -> SmdaFieldSearchResult:
105123
"""Searches for a field identifier in SMDA."""
106124
if session.access_tokens.smda_api is None:
107-
raise HTTPException(status_code=401, detail="SMDA access token is not set")
125+
raise HTTPException(
126+
status_code=401,
127+
detail="SMDA access token is not set",
128+
headers={"x-upstream-source": "SMDA"},
129+
)
108130

109131
try:
110132
smda = SmdaAPI(
@@ -127,13 +149,20 @@ async def post_field(session: SessionDep, field: SmdaField) -> SmdaFieldSearchRe
127149
raise HTTPException(
128150
status_code=500,
129151
detail="Malformed response from SMDA: no 'data' field present",
152+
headers={"x-upstream-source": "SMDA"},
130153
) from e
131154
except TimeoutError as e:
132155
raise HTTPException(
133-
status_code=503, detail="SMDA API request timed out. Please try again."
156+
status_code=503,
157+
detail="SMDA API request timed out. Please try again.",
158+
headers={"x-upstream-source": "SMDA"},
134159
) from e
135160
except Exception as e:
136-
raise HTTPException(status_code=500, detail=str(e)) from e
161+
raise HTTPException(
162+
status_code=500,
163+
detail=str(e),
164+
headers={"x-upstream-source": "SMDA"},
165+
) from e
137166

138167

139168
@router.post(
@@ -183,7 +212,11 @@ async def post_masterdata(
183212
)
184213
# Handled on the route dependency, duplicated for typing
185214
if session.access_tokens.smda_api is None:
186-
raise HTTPException(status_code=401, detail="SMDA access token is not set")
215+
raise HTTPException(
216+
status_code=401,
217+
detail="SMDA access token is not set",
218+
headers={"x-upstream-source": "SMDA"},
219+
)
187220

188221
# Sorted for tests as sets don't guarantee order
189222
unique_field_identifiers = sorted({field.identifier for field in smda_fields})
@@ -210,6 +243,7 @@ async def post_masterdata(
210243
raise HTTPException(
211244
status_code=404,
212245
detail=f"No fields found for identifiers: {unique_field_identifiers}",
246+
headers={"x-upstream-source": "SMDA"},
213247
)
214248

215249
field_items = [FieldItem(**field) for field in field_results]
@@ -241,7 +275,9 @@ async def post_masterdata(
241275

242276
if field_coordinate_system is None:
243277
raise HTTPException(
244-
status_code=404, detail="Projected field coordinate system not found"
278+
status_code=404,
279+
detail="Projected field coordinate system not found",
280+
headers={"x-upstream-source": "SMDA"},
245281
)
246282

247283
return SmdaMasterdataResult(
@@ -264,10 +300,17 @@ async def post_masterdata(
264300
raise HTTPException(
265301
status_code=500,
266302
detail="Malformed response from SMDA: {e}",
303+
headers={"x-upstream-source": "SMDA"},
267304
) from e
268305
except Exception as e:
269-
raise HTTPException(status_code=500, detail=str(e)) from e
306+
raise HTTPException(
307+
status_code=500,
308+
detail=str(e),
309+
headers={"x-upstream-source": "SMDA"},
310+
) from e
270311
except TimeoutError as e:
271312
raise HTTPException(
272-
status_code=503, detail="SMDA API request timed out. Please try again."
313+
status_code=503,
314+
detail="SMDA API request timed out. Please try again.",
315+
headers={"x-upstream-source": "SMDA"},
273316
) from e

tests/test_v1/test_smda.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ def test_get_health(client_with_session: TestClient, session_tmp_path: Path) ->
4646
"""Test 401 returns when the user has no SMDA API key set in their configuration."""
4747
response = client_with_session.get(f"{ROUTE}/health")
4848
assert response.status_code == status.HTTP_401_UNAUTHORIZED, response.json()
49+
assert response.headers["x-upstream-source"] == "SMDA"
4950
assert response.json()["detail"] == "User SMDA API key is not configured"
5051

5152

@@ -64,6 +65,7 @@ def test_get_health_has_user_api_key(
6465

6566
response = client_with_session.get(f"{ROUTE}/health")
6667
assert response.status_code == status.HTTP_401_UNAUTHORIZED, response.json()
68+
assert response.headers["x-upstream-source"] == "SMDA"
6769
assert response.json()["detail"] == "SMDA access token is not set"
6870

6971

@@ -72,7 +74,7 @@ async def test_get_health_has_user_api_key_and_access_token(
7274
session_tmp_path: Path,
7375
mock_SmdaAPI_get: AsyncMock,
7476
) -> None:
75-
"""Test 401 returns when an API key exists but an SMDA access token is not set."""
77+
"""Test 200 returns when an API key and SMDA access token are set."""
7678
mock_response = MagicMock(spec=httpx.Response)
7779
mock_response.status_code = httpx.codes.OK
7880
mock_response.json.return_value = {"status": "ok"}
@@ -81,6 +83,7 @@ async def test_get_health_has_user_api_key_and_access_token(
8183

8284
response = client_with_smda_session.get(f"{ROUTE}/health")
8385
assert response.status_code == status.HTTP_200_OK, response.json()
86+
assert response.headers["x-upstream-source"] == "SMDA"
8487
assert response.json()["status"] == "ok"
8588

8689

@@ -103,8 +106,8 @@ async def test_get_health_request_failure_raises_exception(
103106
response = client_with_smda_session.get(f"{ROUTE}/health")
104107

105108
assert response.status_code == status.HTTP_401_UNAUTHORIZED, response.json()
106-
assert response.json()["detail"] == "SMDA error requesting https://smda"
107109
assert response.headers["x-upstream-source"] == "SMDA"
110+
assert response.json()["detail"] == "SMDA error requesting https://smda"
108111

109112

110113
async def test_post_field_succeeds_with_one(
@@ -135,6 +138,7 @@ async def test_post_field_succeeds_with_one(
135138
f"{ROUTE}/field", json={"identifier": "TROLL"}
136139
)
137140
assert response.status_code == status.HTTP_200_OK, response.json()
141+
assert response.headers["x-upstream-source"] == "SMDA"
138142
assert SmdaFieldSearchResult.model_validate(
139143
response.json()
140144
) == SmdaFieldSearchResult(
@@ -169,6 +173,7 @@ async def test_post_field_succeeds_with_none(
169173
)
170174

171175
assert response.status_code == status.HTTP_200_OK, response.json()
176+
assert response.headers["x-upstream-source"] == "SMDA"
172177
assert SmdaFieldSearchResult.model_validate(
173178
response.json()
174179
) == SmdaFieldSearchResult(
@@ -198,6 +203,7 @@ async def test_post_field_with_no_identifier_raises(
198203
response = client_with_smda_session.post(f"{ROUTE}/field", json={"identifier": ""})
199204

200205
assert response.status_code == status.HTTP_200_OK, response.json()
206+
assert response.headers["x-upstream-source"] == "SMDA"
201207
assert SmdaFieldSearchResult.model_validate(
202208
response.json()
203209
) == SmdaFieldSearchResult(
@@ -223,6 +229,7 @@ async def test_post_field_has_bad_response_raises(
223229
assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR, (
224230
response.json()
225231
)
232+
assert response.headers["x-upstream-source"] == "SMDA"
226233
assert (
227234
response.json()["detail"]
228235
== "Malformed response from SMDA: no 'data' field present"
@@ -319,6 +326,7 @@ async def test_post_masterdata_success(
319326
)
320327

321328
assert response.status_code == status.HTTP_200_OK, response.json()
329+
assert response.headers["x-upstream-source"] == "SMDA"
322330
response_data = response.json()
323331
assert len(response_data["field"]) == 1
324332
assert response_data["field"][0]["identifier"] == "DROGON"
@@ -399,6 +407,7 @@ async def test_post_masterdata_missing_coordinate_system(
399407
)
400408

401409
assert response.status_code == status.HTTP_404_NOT_FOUND, response.json()
410+
assert response.headers["x-upstream-source"] == "SMDA"
402411
assert "Projected field coordinate system not found" in response.json()["detail"]
403412

404413

@@ -425,6 +434,7 @@ async def test_post_masterdata_malformed_response(
425434
assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR, (
426435
response.json()
427436
)
437+
assert response.headers["x-upstream-source"] == "SMDA"
428438
assert "Malformed response from SMDA" in response.json()["detail"]
429439

430440

@@ -500,6 +510,7 @@ async def test_post_masterdata_multiple_fields(
500510
)
501511

502512
assert response.status_code == status.HTTP_200_OK, response.json()
513+
assert response.headers["x-upstream-source"] == "SMDA"
503514
response_data = response.json()
504515
assert len(response_data["field"]) == 2 # noqa
505516

@@ -586,5 +597,5 @@ async def test_post_masterdata_request_fails(
586597
)
587598

588599
assert response.status_code == status.HTTP_401_UNAUTHORIZED, response.json()
589-
assert response.json()["detail"] == "SMDA error requesting https://smda"
590600
assert response.headers["x-upstream-source"] == "SMDA"
601+
assert response.json()["detail"] == "SMDA error requesting https://smda"

0 commit comments

Comments
 (0)