Skip to content

Commit a269be8

Browse files
feat: Add environment document endpoint (#150)
* add environment-document endpoint * nit * remove prints * separate environment document test cases * Abstract environment-document into service, make args explicit * lint * Refactor * Refactor --------- Co-authored-by: Matthew Elwell <[email protected]>
1 parent f26b481 commit a269be8

File tree

4 files changed

+91
-21
lines changed

4 files changed

+91
-21
lines changed

src/edge_proxy/environments.py

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import typing
1+
from typing import Any, Optional
22
from datetime import datetime
33
from email.utils import formatdate
44
from functools import lru_cache
@@ -76,8 +76,8 @@ async def refresh_environment_caches(self):
7676

7777
def get_flags_response_data(
7878
self, environment_key: str, feature: str = None
79-
) -> dict[str, typing.Any]:
80-
environment_document = self.get_environment(environment_key)
79+
) -> dict[str, Any]:
80+
environment_document = self.get_environment(environment_key=environment_key)
8181
environment = EnvironmentModel.model_validate(environment_document)
8282
is_server_key = environment_key.startswith(SERVER_API_KEY_PREFIX)
8383

@@ -105,8 +105,8 @@ def get_flags_response_data(
105105

106106
def get_identity_response_data(
107107
self, input_data: IdentityWithTraits, environment_key: str
108-
) -> dict[str, typing.Any]:
109-
environment_document = self.get_environment(environment_key)
108+
) -> dict[str, Any]:
109+
environment_document = self.get_environment(environment_key=environment_key)
110110
environment = EnvironmentModel.model_validate(environment_document)
111111
is_server_key = environment_key.startswith(SERVER_API_KEY_PREFIX)
112112

@@ -137,14 +137,22 @@ def get_identity_response_data(
137137
}
138138
return data
139139

140-
def get_environment(self, client_side_key: str) -> dict[str, typing.Any]:
140+
def get_environment(
141+
self,
142+
*,
143+
environment_key: Optional[str] = None,
144+
) -> dict[str, Any]:
145+
if environment_key and environment_key.startswith(SERVER_API_KEY_PREFIX):
146+
client_side_key = self._get_client_key_from_server_key(environment_key)
147+
else:
148+
client_side_key = environment_key
149+
141150
if environment_document := self.cache.get_environment(client_side_key):
142151
return environment_document
143-
raise FlagsmithUnknownKeyError(client_side_key)
144152

145-
async def _fetch_document(
146-
self, key_pair: EnvironmentKeyPair
147-
) -> dict[str, typing.Any]:
153+
raise FlagsmithUnknownKeyError(environment_key)
154+
155+
async def _fetch_document(self, key_pair: EnvironmentKeyPair) -> dict[str, Any]:
148156
headers = {
149157
"X-Environment-Key": key_pair.server_side_key,
150158
}
@@ -186,3 +194,9 @@ async def _clear_endpoint_caches(self):
186194
func.cache_clear()
187195
except AttributeError:
188196
pass
197+
198+
def _get_client_key_from_server_key(self, server_key: str) -> str:
199+
for key_pair in self.settings.environment_key_pairs:
200+
if key_pair.server_side_key == server_key:
201+
return key_pair.client_side_key
202+
raise FlagsmithUnknownKeyError(server_key)

src/edge_proxy/server.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,17 @@ async def get_identities(
125125
return ORJSONResponse(data)
126126

127127

128+
@app.get("/api/v1/environment-document", response_class=ORJSONResponse)
129+
async def environment_document(
130+
x_environment_key: str = Header(None),
131+
) -> ORJSONResponse:
132+
if environment_doc := environment_service.get_environment(
133+
environment_key=x_environment_key,
134+
):
135+
return ORJSONResponse(environment_doc)
136+
return ORJSONResponse(status_code=401, content=None)
137+
138+
128139
app.add_middleware(
129140
CORSMiddleware,
130141
allow_origins=settings.allow_origins,

tests/test_environments.py

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -111,32 +111,32 @@ async def test_get_environment_works_correctly(mocker: MockerFixture):
111111
# Next, test that get environment return correct document
112112
assert (
113113
environment_service.get_environment(
114-
settings.environment_key_pairs[0].client_side_key
114+
environment_key=settings.environment_key_pairs[0].client_side_key
115115
)
116116
== doc_1
117117
)
118118
assert (
119119
environment_service.get_environment(
120-
settings.environment_key_pairs[1].client_side_key
120+
environment_key=settings.environment_key_pairs[1].client_side_key
121121
)
122122
== doc_2
123123
)
124124
assert mock_client.get.call_count == 2
125125

126126
# Next, let's verify that any additional call to get_environment does not call fetch document
127127
environment_service.get_environment(
128-
settings.environment_key_pairs[0].client_side_key
128+
environment_key=settings.environment_key_pairs[0].client_side_key
129129
)
130130
environment_service.get_environment(
131-
settings.environment_key_pairs[1].client_side_key
131+
environment_key=settings.environment_key_pairs[1].client_side_key
132132
)
133133
assert mock_client.get.call_count == 2
134134

135135

136136
def test_get_environment_raises_for_unknown_keys():
137137
environment_service = EnvironmentService(settings=settings)
138138
with pytest.raises(FlagsmithUnknownKeyError):
139-
environment_service.get_environment("test_env_key_unknown")
139+
environment_service.get_environment(environment_key="test_env_key_unknown")
140140

141141

142142
@pytest.mark.asyncio
@@ -278,9 +278,7 @@ async def test_get_flags_response_data_skips_filter_for_server_key(
278278
# We create a new settings object that contains a server key as a client_side_key
279279
api_key = "ser." + environment_1_api_key
280280
_settings = AppSettings(
281-
environment_key_pairs=[
282-
{"client_side_key": api_key, "server_side_key": "ser.key"}
283-
]
281+
environment_key_pairs=[{"client_side_key": api_key, "server_side_key": api_key}]
284282
)
285283

286284
mocked_client = mocker.AsyncMock()
@@ -342,9 +340,7 @@ async def test_get_identity_flags_response_skips_filter_for_server_key(
342340
# We create a new settings object that contains a server key as a client_side_key
343341
api_key = "ser." + environment_1_api_key
344342
_settings = AppSettings(
345-
environment_key_pairs=[
346-
{"client_side_key": api_key, "server_side_key": "ser.key"}
347-
]
343+
environment_key_pairs=[{"client_side_key": api_key, "server_side_key": api_key}]
348344
)
349345

350346
mocked_client = mocker.AsyncMock()

tests/test_server.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from pytest_mock import MockerFixture
66

77
from edge_proxy.main import serve
8+
from edge_proxy.settings import EnvironmentKeyPair
89
from tests.fixtures.response_data import environment_1
910

1011
if typing.TYPE_CHECKING:
@@ -233,3 +234,51 @@ def test_serve_passes_proxy_headers_setting(mocker: MockerFixture) -> None:
233234
# Then
234235
_, kwargs = mock_uvicorn.call_args
235236
assert kwargs.get("proxy_headers") is True
237+
238+
239+
def test_get_environment_document(
240+
mocker: MockerFixture,
241+
client: TestClient,
242+
) -> None:
243+
# Given
244+
environment_key_pairs = [
245+
EnvironmentKeyPair(server_side_key="ser.good", client_side_key="foo")
246+
]
247+
mocker.patch(
248+
"edge_proxy.server.settings.environment_key_pairs", environment_key_pairs
249+
)
250+
mocker.patch(
251+
"edge_proxy.server.environment_service.cache"
252+
).get_environment.return_value = environment_1
253+
254+
# When
255+
response = client.get(
256+
"/api/v1/environment-document",
257+
headers={"X-Environment-Key": environment_key_pairs[0].server_side_key},
258+
)
259+
260+
# Then
261+
assert response.status_code == 200
262+
assert response.json() == environment_1
263+
264+
265+
def test_get_environment_document_missing_key(
266+
client: TestClient,
267+
) -> None:
268+
# When
269+
response = client.get(
270+
"/api/v1/environment-document",
271+
)
272+
# Then
273+
assert response.status_code == 401
274+
275+
276+
def test_get_environment_document_wrong_key(
277+
client: TestClient,
278+
) -> None:
279+
# When
280+
response = client.get(
281+
"/api/v1/environment-document", headers={"X-Environment-Key": "ser.bad"}
282+
)
283+
# Then
284+
assert response.status_code == 401

0 commit comments

Comments
 (0)