Skip to content

Commit 12efa9d

Browse files
authored
Merge branch 'main' into chore/warnings
2 parents 494f26f + 551a4be commit 12efa9d

File tree

8 files changed

+169
-18
lines changed

8 files changed

+169
-18
lines changed

.github/workflows/docker-publish-image.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ name: Publish Docker Images
22

33
on:
44
push:
5-
branches:
6-
- main
75
tags:
86
- "*"
97

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
repos:
22
- repo: https://github.com/astral-sh/ruff-pre-commit
3-
rev: v0.6.8
3+
rev: v0.9.10
44
hooks:
55
- id: ruff
66
- id: ruff-format

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ Create an example configuration by running the `edge-proxy-render-config` entryp
6363
rye run edge-proxy-render-config
6464
```
6565

66-
This will output the example configuration to stdout and write it to `./config.json`.
66+
This will write the default settings to `./config.json`.
6767

6868
Here's how to mount the file into Edge Proxy's Docker container:
6969

src/edge_proxy/environments.py

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626

2727
logger = structlog.get_logger(__name__)
2828

29+
SERVER_API_KEY_PREFIX = "ser."
30+
2931

3032
class EnvironmentService:
3133
def __init__(
@@ -77,11 +79,12 @@ def get_flags_response_data(
7779
) -> dict[str, typing.Any]:
7880
environment_document = self.get_environment(environment_key)
7981
environment = EnvironmentModel.model_validate(environment_document)
82+
is_server_key = environment_key.startswith(SERVER_API_KEY_PREFIX)
8083

8184
if feature:
8285
feature_state = get_environment_feature_state(environment, feature)
8386

84-
if not filter_out_server_key_only_feature_states(
87+
if not is_server_key and not filter_out_server_key_only_feature_states(
8588
feature_states=[feature_state],
8689
environment=environment,
8790
):
@@ -90,10 +93,12 @@ def get_flags_response_data(
9093
data = map_feature_state_to_response_data(feature_state)
9194

9295
else:
93-
feature_states = filter_out_server_key_only_feature_states(
94-
feature_states=get_environment_feature_states(environment),
95-
environment=environment,
96-
)
96+
feature_states = get_environment_feature_states(environment)
97+
if not is_server_key:
98+
feature_states = filter_out_server_key_only_feature_states(
99+
feature_states=feature_states,
100+
environment=environment,
101+
)
97102
data = map_feature_states_to_response_data(feature_states)
98103

99104
return data
@@ -103,21 +108,26 @@ def get_identity_response_data(
103108
) -> dict[str, typing.Any]:
104109
environment_document = self.get_environment(environment_key)
105110
environment = EnvironmentModel.model_validate(environment_document)
111+
is_server_key = environment_key.startswith(SERVER_API_KEY_PREFIX)
112+
106113
identity = IdentityModel.model_validate(
107114
self.cache.get_identity(
108115
environment_api_key=environment_key,
109116
identifier=input_data.identifier,
110117
)
111118
)
112119
trait_models = input_data.traits
113-
flags = filter_out_server_key_only_feature_states(
114-
feature_states=get_identity_feature_states(
115-
environment,
116-
identity,
117-
override_traits=trait_models,
118-
),
119-
environment=environment,
120+
flags = get_identity_feature_states(
121+
environment,
122+
identity,
123+
override_traits=trait_models,
120124
)
125+
126+
if not is_server_key:
127+
flags = filter_out_server_key_only_feature_states(
128+
feature_states=flags,
129+
environment=environment,
130+
)
121131
data = {
122132
"traits": map_traits_to_response_data(trait_models),
123133
"flags": map_feature_states_to_response_data(

src/edge_proxy/server.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,17 @@ async def identity(
108108
return ORJSONResponse(data)
109109

110110

111+
@app.get("/api/v1/identities/", response_class=ORJSONResponse)
112+
async def get_identities(
113+
identifier: str,
114+
x_environment_key: str = Header(None),
115+
) -> ORJSONResponse:
116+
data = environment_service.get_identity_response_data(
117+
IdentityWithTraits(identifier=identifier), x_environment_key
118+
)
119+
return ORJSONResponse(data)
120+
121+
111122
app.add_middleware(
112123
CORSMiddleware,
113124
allow_origins=settings.allow_origins,

src/edge_proxy/settings.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ class AppSettings(BaseModel):
113113
)
114114
]
115115
)
116-
api_url: HttpUrl = "https://edge.api.flagsmith.com/api/v1"
116+
api_url: HttpUrl = HttpUrl("https://edge.api.flagsmith.com/api/v1")
117117
api_poll_frequency_seconds: int = Field(
118118
default=10,
119119
validation_alias=AliasChoices(
@@ -136,6 +136,9 @@ class AppSettings(BaseModel):
136136

137137

138138
class AppConfig(AppSettings, BaseSettings):
139+
class Config:
140+
extra = "ignore"
141+
139142
@classmethod
140143
def settings_customise_sources(
141144
cls,

tests/test_environments.py

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@
99
from pytest_mock import MockerFixture
1010

1111
from edge_proxy.environments import EnvironmentService
12-
from edge_proxy.exceptions import FlagsmithUnknownKeyError
12+
from edge_proxy.exceptions import (
13+
FeatureNotFoundError,
14+
FlagsmithUnknownKeyError,
15+
)
1316
from edge_proxy.models import IdentityWithTraits
1417
from edge_proxy.settings import (
1518
EndpointCacheSettings,
@@ -230,3 +233,101 @@ async def test_get_identity_flags_response_skips_cache_for_different_identity(
230233
assert environment_service.get_identity_response_data.cache_info().currsize == 2
231234
assert environment_service.get_identity_response_data.cache_info().misses == 2
232235
assert environment_service.get_identity_response_data.cache_info().hits == 0
236+
237+
238+
@pytest.mark.asyncio
239+
async def test_get_flags_response_data_skips_filter_for_server_key(
240+
mocker: MockerFixture,
241+
) -> None:
242+
# Given
243+
# We create a new settings object that contains a server key as a client_side_key
244+
api_key = "ser." + environment_1_api_key
245+
_settings = AppSettings(
246+
environment_key_pairs=[
247+
{"client_side_key": api_key, "server_side_key": "ser.key"}
248+
]
249+
)
250+
251+
mocked_client = mocker.AsyncMock()
252+
mocked_client.get.return_value = mocker.MagicMock(
253+
text=orjson.dumps(environment_1), raise_for_status=lambda: None
254+
)
255+
256+
environment_service = EnvironmentService(settings=_settings, client=mocked_client)
257+
await environment_service.refresh_environment_caches()
258+
259+
# When
260+
# We retrieve the flag response data
261+
flags = environment_service.get_flags_response_data(api_key)
262+
specific_flag = environment_service.get_flags_response_data(api_key, "feature_3")
263+
264+
# Then
265+
# we get the server-side only flag
266+
assert len(flags) == 3
267+
assert flags[2].get("feature").get("name") == "feature_3"
268+
assert specific_flag.get("feature").get("name") == "feature_3"
269+
270+
271+
@pytest.mark.asyncio
272+
async def test_get_flags_response_data_filters_server_side_features_for_client_key(
273+
mocker: MockerFixture,
274+
) -> None:
275+
# Given
276+
# We create a new settings object that contains a client side key
277+
_settings = AppSettings(
278+
environment_key_pairs=[
279+
{"client_side_key": environment_1_api_key, "server_side_key": "ser.key"}
280+
]
281+
)
282+
283+
mocked_client = mocker.AsyncMock()
284+
mocked_client.get.return_value = mocker.MagicMock(
285+
text=orjson.dumps(environment_1), raise_for_status=lambda: None
286+
)
287+
288+
environment_service = EnvironmentService(settings=_settings, client=mocked_client)
289+
await environment_service.refresh_environment_caches()
290+
291+
# When
292+
# We retrieve the flag response data
293+
flags = environment_service.get_flags_response_data(environment_1_api_key)
294+
with pytest.raises(FeatureNotFoundError):
295+
environment_service.get_flags_response_data(environment_1_api_key, "feature_3")
296+
297+
# Then
298+
# we only get the two client side flags
299+
assert len(flags) == 2
300+
301+
302+
@pytest.mark.asyncio
303+
async def test_get_identity_flags_response_skips_filter_for_server_key(
304+
mocker: MockerFixture,
305+
) -> None:
306+
# Given
307+
# We create a new settings object that contains a server key as a client_side_key
308+
api_key = "ser." + environment_1_api_key
309+
_settings = AppSettings(
310+
environment_key_pairs=[
311+
{"client_side_key": api_key, "server_side_key": "ser.key"}
312+
]
313+
)
314+
315+
mocked_client = mocker.AsyncMock()
316+
mocked_client.get.return_value = mocker.MagicMock(
317+
text=orjson.dumps(environment_1), raise_for_status=lambda: None
318+
)
319+
320+
environment_service = EnvironmentService(settings=_settings, client=mocked_client)
321+
await environment_service.refresh_environment_caches()
322+
323+
# When
324+
# We retrieve the flags for an identity
325+
result = environment_service.get_identity_response_data(
326+
IdentityWithTraits(identifier="foo"), api_key
327+
)
328+
329+
# Then
330+
# we get the server-side only flag
331+
flags = result.get("flags")
332+
assert len(flags) == 3
333+
assert flags[2].get("feature").get("name") == "feature_3"

tests/test_server.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,3 +259,31 @@ def test_post_identity__invalid_trait_data__expected_response(
259259
"constrained-str",
260260
]
261261
assert response.json()["detail"][-1]["type"] == "string_too_long"
262+
263+
264+
def test_get_identities(
265+
mocker: MockerFixture,
266+
client: TestClient,
267+
) -> None:
268+
x_environment_key = "test_environment_key"
269+
identifier = "test_identifier"
270+
271+
mocked_environment_cache = mocker.patch(
272+
"edge_proxy.server.environment_service.cache"
273+
)
274+
mocked_environment_cache.get_environment.return_value = environment_1
275+
mocked_environment_cache.get_identity.return_value = {
276+
"environment_api_key": x_environment_key,
277+
"identifier": identifier,
278+
}
279+
280+
response = client.get(
281+
"/api/v1/identities/",
282+
headers={"x-environment-key": x_environment_key},
283+
params={"identifier": identifier},
284+
)
285+
data = response.json()
286+
287+
assert response.status_code == 200
288+
assert data["traits"] == []
289+
assert data["flags"]

0 commit comments

Comments
 (0)