1717import threading
1818import time
1919from typing import TYPE_CHECKING , Any , TypedDict , cast
20- from urllib .parse import urlparse
20+ from urllib .parse import parse_qs , urlencode , urlparse
2121
2222import pytest
2323import requests
4444DELAYED_RESPONSE_SECONDS = 0.5
4545TIMEOUT_RESPONSE_SECONDS = 1.5
4646MAX_CONTROL_BODY_BYTES = 512
47- CONFIG_PATH = "/api/v2/feature-flagging/config/server-distribution"
47+ CONFIG_PATH = "/api/v2/feature-flagging/config/rules-based/server"
48+ EXPECTED_DD_ENV = "test"
49+ CONFIG_QUERY = urlencode ({"dd_env" : EXPECTED_DD_ENV })
4850REPO_ROOT = Path (__file__ ).parents [2 ]
4951UFC_FIXTURE_PATH = REPO_ROOT / "tests" / "parametric" / "test_ffe" / "flags-v1.json"
5052MALFORMED_UFC_BYTES = b'{"flags": ['
53+ UFC_RESPONSE_TYPE = "universal-flag-configuration"
5154
5255
5356class MockFFEAgentlessBackendStatus (TypedDict ):
@@ -143,18 +146,20 @@ def __init__(self, server_address: tuple[str, int]) -> None:
143146
144147class MockFFEAgentlessBackendRequestHandler (BaseHTTPRequestHandler ):
145148 # Endpoint contract:
146- # - GET /api/v2/feature-flagging/config/server-distribution
149+ # - GET /api/v2/feature-flagging/config/rules-based/server?dd_env=test
147150 # - GET /status
148151 # - POST /control/responses
149152 # - POST /control/reset
150153 server : MockFFEAgentlessBackendHTTPServer
151154
152155 def do_GET (self ) -> None :
153- path = urlparse (self .path ).path
154- if path == CONFIG_PATH :
156+ parsed = urlparse (self .path )
157+ if parsed .path == CONFIG_PATH and parse_qs (parsed .query , keep_blank_values = True ) == {
158+ "dd_env" : [EXPECTED_DD_ENV ]
159+ }:
155160 self ._handle_config ()
156161 return
157- if path == "/status" :
162+ if parsed . path == "/status" :
158163 self ._write_json (HTTPStatus .OK , self .server .state .status ())
159164 return
160165 self ._write_json (HTTPStatus .NOT_FOUND , {"error" : "not found" })
@@ -232,11 +237,20 @@ def _write_json(self, status_code: HTTPStatus, payload: dict[str, Any] | MockFFE
232237
233238def _has_auth (headers : Mapping [str , str ]) -> bool :
234239 normalized = {key .lower (): value for key , value in headers .items ()}
235- return any ( normalized .get (header ) == EXPECTED_API_KEY for header in ( "dd-api-key" , "x-datadog-api-key" ))
240+ return normalized .get ("dd-api-key" ) == EXPECTED_API_KEY
236241
237242
238243def _valid_ufc_bytes () -> bytes :
239- return UFC_FIXTURE_PATH .read_bytes ()
244+ attributes = json .loads (UFC_FIXTURE_PATH .read_text ())
245+ return json .dumps (
246+ {
247+ "data" : {
248+ "id" : "1" ,
249+ "type" : UFC_RESPONSE_TYPE ,
250+ "attributes" : attributes ,
251+ }
252+ }
253+ ).encode ("utf-8" )
240254
241255
242256def validate_responses (responses : object ) -> list [str ]:
@@ -269,13 +283,19 @@ def _response_for_response(response: str, *, has_auth: bool) -> tuple[int, bytes
269283
270284
271285def _strip_config_path (url : str ) -> str :
272- return url .removesuffix (CONFIG_PATH )
286+ parsed = urlparse (url )
287+ if not parsed .path .endswith (CONFIG_PATH ):
288+ return url
289+
290+ base_path = parsed .path .removesuffix (CONFIG_PATH ).rstrip ("/" )
291+ return parsed ._replace (path = base_path , params = "" , query = "" , fragment = "" ).geturl ().rstrip ("/" )
273292
274293
275294class MockFFEAgentlessBackendServer :
276- def __init__ (self , worker_id : str ) -> None :
277- self .port = get_host_port (worker_id , 4900 )
295+ def __init__ (self , worker_id : str , * , port : int | None = None ) -> None :
296+ self .port = get_host_port (worker_id , 4900 ) if port is None else port
278297 self ._server = MockFFEAgentlessBackendHTTPServer (("0.0.0.0" , self .port )) # noqa: S104 - test fixture must be container-reachable.
298+ self .port = self ._server .server_port
279299 self ._thread = threading .Thread (
280300 target = self ._server .serve_forever , name = "mock-ffe-agentless-backend" , daemon = True
281301 )
@@ -300,7 +320,7 @@ def library_base_url(self) -> str:
300320
301321 @property
302322 def library_config_url (self ) -> str :
303- return f"{ self .library_base_url } { CONFIG_PATH } "
323+ return f"{ self .library_base_url } { CONFIG_PATH } ? { CONFIG_QUERY } "
304324
305325 def reset (self ) -> None :
306326 response = requests .post (f"{ self .base_url } /control/reset" , timeout = 5 )
0 commit comments