From 330a3d66a9ff339fa38e4d7418a33bdd33911f3d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 21 Jul 2025 20:44:52 +0000 Subject: [PATCH 1/7] Support userId as alternative user ID source in DevCycle user creation Co-authored-by: jonathan --- devcycle_python_sdk/models/user.py | 15 +++- test/models/test_user.py | 101 +++++++++++++++++++++++++ test/openfeature_test/test_provider.py | 41 ++++++++++ 3 files changed, 154 insertions(+), 3 deletions(-) diff --git a/devcycle_python_sdk/models/user.py b/devcycle_python_sdk/models/user.py index 57d4c56..ab93725 100644 --- a/devcycle_python_sdk/models/user.py +++ b/devcycle_python_sdk/models/user.py @@ -101,22 +101,28 @@ def create_user_from_context( ) -> "DevCycleUser": """ Builds a DevCycleUser instance from the evaluation context. Will raise a TargetingKeyMissingError if - the context does not contain a valid targeting key or user_id attribute + the context does not contain a valid targeting key, user_id, or userId attribute :param context: The evaluation context to build the user from :return: A DevCycleUser instance """ user_id = None + user_id_source = None if context: if context.targeting_key: user_id = context.targeting_key + user_id_source = "targeting_key" elif context.attributes and "user_id" in context.attributes.keys(): user_id = context.attributes["user_id"] + user_id_source = "user_id" + elif context.attributes and "userId" in context.attributes.keys(): + user_id = context.attributes["userId"] + user_id_source = "userId" if not user_id or not isinstance(user_id, str): raise TargetingKeyMissingError( - "DevCycle: Evaluation context does not contain a valid targeting key or user_id attribute" + "DevCycle: Evaluation context does not contain a valid targeting key, user_id, or userId attribute" ) user = DevCycleUser(user_id=user_id) @@ -124,7 +130,10 @@ def create_user_from_context( private_custom_data: Dict[str, Any] = {} if context and context.attributes: for key, value in context.attributes.items(): - if key == "user_id": + # Skip user_id and userId if they were used as the user ID + if key == "user_id" and user_id_source == "user_id": + continue + if key == "userId" and user_id_source == "userId": continue if value is not None: diff --git a/test/models/test_user.py b/test/models/test_user.py index 0bda339..d76b599 100644 --- a/test/models/test_user.py +++ b/test/models/test_user.py @@ -53,6 +53,107 @@ def test_create_user_from_context_only_user_id(self): self.assertIsNotNone(user) self.assertEqual(user.user_id, test_user_id) + def test_create_user_from_context_with_userId(self): + test_user_id = "userId-12345" + + # Test userId as the only user ID source + context = EvaluationContext( + targeting_key=None, attributes={"userId": test_user_id} + ) + user = DevCycleUser.create_user_from_context(context) + self.assertIsNotNone(user) + self.assertEqual(user.user_id, test_user_id) + + # Test that userId is excluded from custom data when used as user ID + self.assertIsNone(user.customData) + + def test_create_user_from_context_user_id_priority(self): + targeting_key_id = "targeting-12345" + user_id = "user_id-12345" + userId = "userId-12345" + + # Test targeting_key takes precedence over user_id and userId + context = EvaluationContext( + targeting_key=targeting_key_id, + attributes={"user_id": user_id, "userId": userId} + ) + user = DevCycleUser.create_user_from_context(context) + self.assertEqual(user.user_id, targeting_key_id) + + # Test user_id takes precedence over userId + context = EvaluationContext( + targeting_key=None, + attributes={"user_id": user_id, "userId": userId} + ) + user = DevCycleUser.create_user_from_context(context) + self.assertEqual(user.user_id, user_id) + + # Test userId is used when targeting_key and user_id are not available + context = EvaluationContext( + targeting_key=None, + attributes={"userId": userId} + ) + user = DevCycleUser.create_user_from_context(context) + self.assertEqual(user.user_id, userId) + + def test_create_user_from_context_userId_in_custom_data_when_not_used(self): + targeting_key_id = "targeting-12345" + userId = "userId-12345" + + # When targeting_key is used, userId should be in custom data + context = EvaluationContext( + targeting_key=targeting_key_id, + attributes={"userId": userId, "other_field": "value"} + ) + user = DevCycleUser.create_user_from_context(context) + self.assertEqual(user.user_id, targeting_key_id) + self.assertIsNotNone(user.customData) + self.assertEqual(user.customData["userId"], userId) + self.assertEqual(user.customData["other_field"], "value") + + # When user_id is used, userId should be in custom data + user_id = "user_id-12345" + context = EvaluationContext( + targeting_key=None, + attributes={"user_id": user_id, "userId": userId, "other_field": "value"} + ) + user = DevCycleUser.create_user_from_context(context) + self.assertEqual(user.user_id, user_id) + self.assertIsNotNone(user.customData) + self.assertEqual(user.customData["userId"], userId) + self.assertEqual(user.customData["other_field"], "value") + + def test_create_user_from_context_userId_excluded_when_used(self): + userId = "userId-12345" + + # When userId is used as user ID, it should be excluded from custom data + context = EvaluationContext( + targeting_key=None, + attributes={"userId": userId, "other_field": "value"} + ) + user = DevCycleUser.create_user_from_context(context) + self.assertEqual(user.user_id, userId) + self.assertIsNotNone(user.customData) + self.assertNotIn("userId", user.customData) + self.assertEqual(user.customData["other_field"], "value") + + def test_create_user_from_context_invalid_userId_types(self): + # Test non-string userId values are ignored and cause error + with self.assertRaises(TargetingKeyMissingError): + DevCycleUser.create_user_from_context( + EvaluationContext(targeting_key=None, attributes={"userId": 12345}) + ) + + with self.assertRaises(TargetingKeyMissingError): + DevCycleUser.create_user_from_context( + EvaluationContext(targeting_key=None, attributes={"userId": None}) + ) + + with self.assertRaises(TargetingKeyMissingError): + DevCycleUser.create_user_from_context( + EvaluationContext(targeting_key=None, attributes={"userId": True}) + ) + def test_create_user_from_context_with_attributes(self): test_user_id = "12345" context = EvaluationContext( diff --git a/test/openfeature_test/test_provider.py b/test/openfeature_test/test_provider.py index 0fb7a54..4e3d9ba 100644 --- a/test/openfeature_test/test_provider.py +++ b/test/openfeature_test/test_provider.py @@ -214,6 +214,47 @@ def test_resolve_object_details(self): self.assertDictEqual(details.value, variable_value) self.assertEqual(details.reason, Reason.TARGETING_MATCH) + def test_resolve_details_with_userId_priority(self): + """Test that userId is used with proper priority: targeting_key > user_id > userId""" + key = "test-flag" + default_value = False + + self.client.variable.return_value = Variable.create_default_variable( + key=key, default_value=default_value + ) + + # Test 1: targeting_key takes precedence over user_id and userId + context = EvaluationContext( + targeting_key="targeting-key-user", + attributes={"user_id": "user_id-user", "userId": "userId-user"} + ) + details = self.provider.resolve_boolean_details(key, default_value, context) + + # Verify the call was made with the right user + self.client.variable.assert_called() + called_user = self.client.variable.call_args[1]['user'] + self.assertEqual(called_user.user_id, "targeting-key-user") + + # Test 2: user_id takes precedence over userId when no targeting_key + context = EvaluationContext( + targeting_key=None, + attributes={"user_id": "user_id-user", "userId": "userId-user"} + ) + details = self.provider.resolve_boolean_details(key, default_value, context) + + called_user = self.client.variable.call_args[1]['user'] + self.assertEqual(called_user.user_id, "user_id-user") + + # Test 3: userId is used when no targeting_key or user_id + context = EvaluationContext( + targeting_key=None, + attributes={"userId": "userId-user"} + ) + details = self.provider.resolve_boolean_details(key, default_value, context) + + called_user = self.client.variable.call_args[1]['user'] + self.assertEqual(called_user.user_id, "userId-user") + if __name__ == "__main__": unittest.main() From 12c04b2531c3388d9963ceb3be458987e5a3e96b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 22 Jul 2025 15:04:37 +0000 Subject: [PATCH 2/7] Exclude user_id and userId from custom data in all cases Co-authored-by: jonathan --- devcycle_python_sdk/models/user.py | 6 +++--- test/models/test_user.py | 16 +++++++++------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/devcycle_python_sdk/models/user.py b/devcycle_python_sdk/models/user.py index ab93725..dc1cdbf 100644 --- a/devcycle_python_sdk/models/user.py +++ b/devcycle_python_sdk/models/user.py @@ -130,10 +130,10 @@ def create_user_from_context( private_custom_data: Dict[str, Any] = {} if context and context.attributes: for key, value in context.attributes.items(): - # Skip user_id and userId if they were used as the user ID - if key == "user_id" and user_id_source == "user_id": + # Skip user_id and userId - these are reserved for user ID mapping + if key == "user_id": continue - if key == "userId" and user_id_source == "userId": + if key == "userId": continue if value is not None: diff --git a/test/models/test_user.py b/test/models/test_user.py index d76b599..a1a6fbf 100644 --- a/test/models/test_user.py +++ b/test/models/test_user.py @@ -96,23 +96,24 @@ def test_create_user_from_context_user_id_priority(self): user = DevCycleUser.create_user_from_context(context) self.assertEqual(user.user_id, userId) - def test_create_user_from_context_userId_in_custom_data_when_not_used(self): + def test_create_user_from_context_user_id_fields_always_excluded(self): targeting_key_id = "targeting-12345" userId = "userId-12345" + user_id = "user_id-12345" - # When targeting_key is used, userId should be in custom data + # When targeting_key is used, both user_id and userId should be excluded from custom data context = EvaluationContext( targeting_key=targeting_key_id, - attributes={"userId": userId, "other_field": "value"} + attributes={"user_id": user_id, "userId": userId, "other_field": "value"} ) user = DevCycleUser.create_user_from_context(context) self.assertEqual(user.user_id, targeting_key_id) self.assertIsNotNone(user.customData) - self.assertEqual(user.customData["userId"], userId) + self.assertNotIn("user_id", user.customData) + self.assertNotIn("userId", user.customData) self.assertEqual(user.customData["other_field"], "value") - # When user_id is used, userId should be in custom data - user_id = "user_id-12345" + # When user_id is used, userId should still be excluded from custom data context = EvaluationContext( targeting_key=None, attributes={"user_id": user_id, "userId": userId, "other_field": "value"} @@ -120,7 +121,8 @@ def test_create_user_from_context_userId_in_custom_data_when_not_used(self): user = DevCycleUser.create_user_from_context(context) self.assertEqual(user.user_id, user_id) self.assertIsNotNone(user.customData) - self.assertEqual(user.customData["userId"], userId) + self.assertNotIn("user_id", user.customData) + self.assertNotIn("userId", user.customData) self.assertEqual(user.customData["other_field"], "value") def test_create_user_from_context_userId_excluded_when_used(self): From fb2408f3a2d4c3ede8693bb4a167fb81e007238e Mon Sep 17 00:00:00 2001 From: Jonathan Norris Date: Tue, 22 Jul 2025 12:16:34 -0400 Subject: [PATCH 3/7] Address PR review feedback: remove unused user_id_source, consolidate exclusion logic, include targeting_key - Remove unused user_id_source variable and assignments (addresses code clutter concern) - Consolidate user ID exclusion logic into single efficient statement - Include targeting_key in exclusion logic to prevent it from being added to custom data - Update comment to mention all three user ID types for clarity - Add comprehensive test coverage for targeting_key exclusion from attributes --- devcycle_python_sdk/models/user.py | 10 ++-------- test/models/test_user.py | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/devcycle_python_sdk/models/user.py b/devcycle_python_sdk/models/user.py index dc1cdbf..35359f2 100644 --- a/devcycle_python_sdk/models/user.py +++ b/devcycle_python_sdk/models/user.py @@ -107,18 +107,14 @@ def create_user_from_context( :return: A DevCycleUser instance """ user_id = None - user_id_source = None if context: if context.targeting_key: user_id = context.targeting_key - user_id_source = "targeting_key" elif context.attributes and "user_id" in context.attributes.keys(): user_id = context.attributes["user_id"] - user_id_source = "user_id" elif context.attributes and "userId" in context.attributes.keys(): user_id = context.attributes["userId"] - user_id_source = "userId" if not user_id or not isinstance(user_id, str): raise TargetingKeyMissingError( @@ -130,10 +126,8 @@ def create_user_from_context( private_custom_data: Dict[str, Any] = {} if context and context.attributes: for key, value in context.attributes.items(): - # Skip user_id and userId - these are reserved for user ID mapping - if key == "user_id": - continue - if key == "userId": + # Skip user_id, userId, and targeting_key - these are reserved for user ID mapping + if key in ("user_id", "userId", "targeting_key"): continue if value is not None: diff --git a/test/models/test_user.py b/test/models/test_user.py index a1a6fbf..88c502a 100644 --- a/test/models/test_user.py +++ b/test/models/test_user.py @@ -125,6 +125,20 @@ def test_create_user_from_context_user_id_fields_always_excluded(self): self.assertNotIn("userId", user.customData) self.assertEqual(user.customData["other_field"], "value") + def test_create_user_from_context_targeting_key_excluded_from_attributes(self): + targeting_key_id = "targeting-12345" + + # When targeting_key appears in attributes, it should be excluded from custom data + context = EvaluationContext( + targeting_key=targeting_key_id, + attributes={"targeting_key": "should-be-excluded", "other_field": "value"} + ) + user = DevCycleUser.create_user_from_context(context) + self.assertEqual(user.user_id, targeting_key_id) + self.assertIsNotNone(user.customData) + self.assertNotIn("targeting_key", user.customData) + self.assertEqual(user.customData["other_field"], "value") + def test_create_user_from_context_userId_excluded_when_used(self): userId = "userId-12345" From 4c0eee6ce547ddcaec9d9abf8d2bd561784c6bdc Mon Sep 17 00:00:00 2001 From: Jonathan Norris Date: Tue, 22 Jul 2025 12:19:02 -0400 Subject: [PATCH 4/7] Improve error messages: split validation and include field name - Split user ID validation into two distinct error cases: 1. Missing user ID (no valid targeting_key, user_id, or userId found) 2. Invalid type (user ID found but not a string) - Include original field name in type validation error for better debugging - Re-introduce user_id_source variable for error reporting purposes only - Enhanced error specificity: 'targeting_key must be a string, got int' vs generic message --- devcycle_python_sdk/models/user.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/devcycle_python_sdk/models/user.py b/devcycle_python_sdk/models/user.py index 35359f2..978dcbf 100644 --- a/devcycle_python_sdk/models/user.py +++ b/devcycle_python_sdk/models/user.py @@ -107,19 +107,28 @@ def create_user_from_context( :return: A DevCycleUser instance """ user_id = None + user_id_source = None if context: if context.targeting_key: user_id = context.targeting_key + user_id_source = "targeting_key" elif context.attributes and "user_id" in context.attributes.keys(): user_id = context.attributes["user_id"] + user_id_source = "user_id" elif context.attributes and "userId" in context.attributes.keys(): user_id = context.attributes["userId"] + user_id_source = "userId" - if not user_id or not isinstance(user_id, str): + if not user_id: raise TargetingKeyMissingError( "DevCycle: Evaluation context does not contain a valid targeting key, user_id, or userId attribute" ) + + if not isinstance(user_id, str): + raise TargetingKeyMissingError( + f"DevCycle: {user_id_source} must be a string, got {type(user_id).__name__}" + ) user = DevCycleUser(user_id=user_id) custom_data: Dict[str, Any] = {} From ac2096715ca213dd02c93af6080d64bf1d7ad68b Mon Sep 17 00:00:00 2001 From: Jonathan Norris Date: Tue, 22 Jul 2025 12:31:42 -0400 Subject: [PATCH 5/7] Fix lint errors: rename functions and variables to follow snake_case convention MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename test functions from camelCase to snake_case: - test_create_user_from_context_with_userId → test_create_user_from_context_with_user_id_attribute - test_create_user_from_context_userId_excluded_when_used → test_create_user_from_context_user_id_attr_excluded_when_used - test_create_user_from_context_invalid_userId_types → test_create_user_from_context_invalid_user_id_attr_types - test_resolve_details_with_userId_priority → test_resolve_details_with_user_id_attr_priority - Rename variables from camelCase to snake_case: - userId → user_id_attr (to avoid confusion with user_id) - Remove unused variable assignments in test_provider.py - Fix whitespace and trailing whitespace issues --- devcycle_python_sdk/models/user.py | 2 +- test/models/test_user.py | 64 +++++++++++++------------- test/openfeature_test/test_provider.py | 18 ++++---- 3 files changed, 42 insertions(+), 42 deletions(-) diff --git a/devcycle_python_sdk/models/user.py b/devcycle_python_sdk/models/user.py index 978dcbf..3110ead 100644 --- a/devcycle_python_sdk/models/user.py +++ b/devcycle_python_sdk/models/user.py @@ -124,7 +124,7 @@ def create_user_from_context( raise TargetingKeyMissingError( "DevCycle: Evaluation context does not contain a valid targeting key, user_id, or userId attribute" ) - + if not isinstance(user_id, str): raise TargetingKeyMissingError( f"DevCycle: {user_id_source} must be a string, got {type(user_id).__name__}" diff --git a/test/models/test_user.py b/test/models/test_user.py index 88c502a..e6537a1 100644 --- a/test/models/test_user.py +++ b/test/models/test_user.py @@ -53,9 +53,9 @@ def test_create_user_from_context_only_user_id(self): self.assertIsNotNone(user) self.assertEqual(user.user_id, test_user_id) - def test_create_user_from_context_with_userId(self): + def test_create_user_from_context_with_user_id_attribute(self): test_user_id = "userId-12345" - + # Test userId as the only user ID source context = EvaluationContext( targeting_key=None, attributes={"userId": test_user_id} @@ -63,48 +63,48 @@ def test_create_user_from_context_with_userId(self): user = DevCycleUser.create_user_from_context(context) self.assertIsNotNone(user) self.assertEqual(user.user_id, test_user_id) - + # Test that userId is excluded from custom data when used as user ID self.assertIsNone(user.customData) def test_create_user_from_context_user_id_priority(self): targeting_key_id = "targeting-12345" user_id = "user_id-12345" - userId = "userId-12345" - + user_id_attr = "userId-12345" + # Test targeting_key takes precedence over user_id and userId context = EvaluationContext( - targeting_key=targeting_key_id, - attributes={"user_id": user_id, "userId": userId} + targeting_key=targeting_key_id, + attributes={"user_id": user_id, "userId": user_id_attr} ) user = DevCycleUser.create_user_from_context(context) self.assertEqual(user.user_id, targeting_key_id) - + # Test user_id takes precedence over userId context = EvaluationContext( - targeting_key=None, - attributes={"user_id": user_id, "userId": userId} + targeting_key=None, + attributes={"user_id": user_id, "userId": user_id_attr} ) user = DevCycleUser.create_user_from_context(context) self.assertEqual(user.user_id, user_id) - + # Test userId is used when targeting_key and user_id are not available context = EvaluationContext( - targeting_key=None, - attributes={"userId": userId} + targeting_key=None, + attributes={"userId": user_id_attr} ) user = DevCycleUser.create_user_from_context(context) - self.assertEqual(user.user_id, userId) + self.assertEqual(user.user_id, user_id_attr) def test_create_user_from_context_user_id_fields_always_excluded(self): targeting_key_id = "targeting-12345" - userId = "userId-12345" + user_id_attr = "userId-12345" user_id = "user_id-12345" - + # When targeting_key is used, both user_id and userId should be excluded from custom data context = EvaluationContext( - targeting_key=targeting_key_id, - attributes={"user_id": user_id, "userId": userId, "other_field": "value"} + targeting_key=targeting_key_id, + attributes={"user_id": user_id, "userId": user_id_attr, "other_field": "value"} ) user = DevCycleUser.create_user_from_context(context) self.assertEqual(user.user_id, targeting_key_id) @@ -112,11 +112,11 @@ def test_create_user_from_context_user_id_fields_always_excluded(self): self.assertNotIn("user_id", user.customData) self.assertNotIn("userId", user.customData) self.assertEqual(user.customData["other_field"], "value") - + # When user_id is used, userId should still be excluded from custom data context = EvaluationContext( - targeting_key=None, - attributes={"user_id": user_id, "userId": userId, "other_field": "value"} + targeting_key=None, + attributes={"user_id": user_id, "userId": user_id_attr, "other_field": "value"} ) user = DevCycleUser.create_user_from_context(context) self.assertEqual(user.user_id, user_id) @@ -127,10 +127,10 @@ def test_create_user_from_context_user_id_fields_always_excluded(self): def test_create_user_from_context_targeting_key_excluded_from_attributes(self): targeting_key_id = "targeting-12345" - + # When targeting_key appears in attributes, it should be excluded from custom data context = EvaluationContext( - targeting_key=targeting_key_id, + targeting_key=targeting_key_id, attributes={"targeting_key": "should-be-excluded", "other_field": "value"} ) user = DevCycleUser.create_user_from_context(context) @@ -139,32 +139,32 @@ def test_create_user_from_context_targeting_key_excluded_from_attributes(self): self.assertNotIn("targeting_key", user.customData) self.assertEqual(user.customData["other_field"], "value") - def test_create_user_from_context_userId_excluded_when_used(self): - userId = "userId-12345" - + def test_create_user_from_context_user_id_attr_excluded_when_used(self): + user_id_attr = "userId-12345" + # When userId is used as user ID, it should be excluded from custom data context = EvaluationContext( - targeting_key=None, - attributes={"userId": userId, "other_field": "value"} + targeting_key=None, + attributes={"userId": user_id_attr, "other_field": "value"} ) user = DevCycleUser.create_user_from_context(context) - self.assertEqual(user.user_id, userId) + self.assertEqual(user.user_id, user_id_attr) self.assertIsNotNone(user.customData) self.assertNotIn("userId", user.customData) self.assertEqual(user.customData["other_field"], "value") - def test_create_user_from_context_invalid_userId_types(self): + def test_create_user_from_context_invalid_user_id_attr_types(self): # Test non-string userId values are ignored and cause error with self.assertRaises(TargetingKeyMissingError): DevCycleUser.create_user_from_context( EvaluationContext(targeting_key=None, attributes={"userId": 12345}) ) - + with self.assertRaises(TargetingKeyMissingError): DevCycleUser.create_user_from_context( EvaluationContext(targeting_key=None, attributes={"userId": None}) ) - + with self.assertRaises(TargetingKeyMissingError): DevCycleUser.create_user_from_context( EvaluationContext(targeting_key=None, attributes={"userId": True}) diff --git a/test/openfeature_test/test_provider.py b/test/openfeature_test/test_provider.py index 4e3d9ba..fdd82ea 100644 --- a/test/openfeature_test/test_provider.py +++ b/test/openfeature_test/test_provider.py @@ -214,7 +214,7 @@ def test_resolve_object_details(self): self.assertDictEqual(details.value, variable_value) self.assertEqual(details.reason, Reason.TARGETING_MATCH) - def test_resolve_details_with_userId_priority(self): + def test_resolve_details_with_user_id_attr_priority(self): """Test that userId is used with proper priority: targeting_key > user_id > userId""" key = "test-flag" default_value = False @@ -228,30 +228,30 @@ def test_resolve_details_with_userId_priority(self): targeting_key="targeting-key-user", attributes={"user_id": "user_id-user", "userId": "userId-user"} ) - details = self.provider.resolve_boolean_details(key, default_value, context) - + self.provider.resolve_boolean_details(key, default_value, context) + # Verify the call was made with the right user self.client.variable.assert_called() called_user = self.client.variable.call_args[1]['user'] self.assertEqual(called_user.user_id, "targeting-key-user") - + # Test 2: user_id takes precedence over userId when no targeting_key context = EvaluationContext( targeting_key=None, attributes={"user_id": "user_id-user", "userId": "userId-user"} ) - details = self.provider.resolve_boolean_details(key, default_value, context) - + self.provider.resolve_boolean_details(key, default_value, context) + called_user = self.client.variable.call_args[1]['user'] self.assertEqual(called_user.user_id, "user_id-user") - + # Test 3: userId is used when no targeting_key or user_id context = EvaluationContext( targeting_key=None, attributes={"userId": "userId-user"} ) - details = self.provider.resolve_boolean_details(key, default_value, context) - + self.provider.resolve_boolean_details(key, default_value, context) + called_user = self.client.variable.call_args[1]['user'] self.assertEqual(called_user.user_id, "userId-user") From 311086034ee8af704826ba6ec5366a5bc2903f4e Mon Sep 17 00:00:00 2001 From: Jonathan Norris Date: Tue, 22 Jul 2025 12:34:20 -0400 Subject: [PATCH 6/7] Consolidate tests: reduce from 7 to 2 comprehensive tests - Merged 6 user model tests into 2 comprehensive tests: 1. test_create_user_from_context_with_user_id_attribute: covers userId functionality, priority order, and error cases 2. test_create_user_from_context_user_id_exclusion: covers all user ID field exclusion scenarios - Removed redundant provider test since core logic is tested in user model - Maintained full test coverage while significantly reducing test count - All tests passing with same functionality coverage --- test/models/test_user.py | 87 ++++++++------------------ test/openfeature_test/test_provider.py | 41 ------------ 2 files changed, 26 insertions(+), 102 deletions(-) diff --git a/test/models/test_user.py b/test/models/test_user.py index e6537a1..31a1de9 100644 --- a/test/models/test_user.py +++ b/test/models/test_user.py @@ -54,25 +54,20 @@ def test_create_user_from_context_only_user_id(self): self.assertEqual(user.user_id, test_user_id) def test_create_user_from_context_with_user_id_attribute(self): - test_user_id = "userId-12345" + """Comprehensive test for userId attribute support including priority, functionality, and error cases""" + targeting_key_id = "targeting-12345" + user_id = "user_id-12345" + user_id_attr = "userId-12345" - # Test userId as the only user ID source + # Test 1: userId as the only user ID source context = EvaluationContext( - targeting_key=None, attributes={"userId": test_user_id} + targeting_key=None, attributes={"userId": user_id_attr} ) user = DevCycleUser.create_user_from_context(context) self.assertIsNotNone(user) - self.assertEqual(user.user_id, test_user_id) - - # Test that userId is excluded from custom data when used as user ID - self.assertIsNone(user.customData) - - def test_create_user_from_context_user_id_priority(self): - targeting_key_id = "targeting-12345" - user_id = "user_id-12345" - user_id_attr = "userId-12345" + self.assertEqual(user.user_id, user_id_attr) - # Test targeting_key takes precedence over user_id and userId + # Test 2: Priority order - targeting_key > user_id > userId context = EvaluationContext( targeting_key=targeting_key_id, attributes={"user_id": user_id, "userId": user_id_attr} @@ -80,7 +75,6 @@ def test_create_user_from_context_user_id_priority(self): user = DevCycleUser.create_user_from_context(context) self.assertEqual(user.user_id, targeting_key_id) - # Test user_id takes precedence over userId context = EvaluationContext( targeting_key=None, attributes={"user_id": user_id, "userId": user_id_attr} @@ -88,32 +82,37 @@ def test_create_user_from_context_user_id_priority(self): user = DevCycleUser.create_user_from_context(context) self.assertEqual(user.user_id, user_id) - # Test userId is used when targeting_key and user_id are not available - context = EvaluationContext( - targeting_key=None, - attributes={"userId": user_id_attr} - ) - user = DevCycleUser.create_user_from_context(context) - self.assertEqual(user.user_id, user_id_attr) + # Test 3: Error cases - invalid userId types + with self.assertRaises(TargetingKeyMissingError): + DevCycleUser.create_user_from_context( + EvaluationContext(targeting_key=None, attributes={"userId": 12345}) + ) + + with self.assertRaises(TargetingKeyMissingError): + DevCycleUser.create_user_from_context( + EvaluationContext(targeting_key=None, attributes={"userId": None}) + ) - def test_create_user_from_context_user_id_fields_always_excluded(self): + def test_create_user_from_context_user_id_exclusion(self): + """Test that all user ID fields (targeting_key, user_id, userId) are excluded from custom data""" targeting_key_id = "targeting-12345" - user_id_attr = "userId-12345" user_id = "user_id-12345" + user_id_attr = "userId-12345" - # When targeting_key is used, both user_id and userId should be excluded from custom data + # Test exclusion when targeting_key is used context = EvaluationContext( targeting_key=targeting_key_id, - attributes={"user_id": user_id, "userId": user_id_attr, "other_field": "value"} + attributes={"user_id": user_id, "userId": user_id_attr, "targeting_key": "should-be-excluded", "other_field": "value"} ) user = DevCycleUser.create_user_from_context(context) self.assertEqual(user.user_id, targeting_key_id) self.assertIsNotNone(user.customData) self.assertNotIn("user_id", user.customData) self.assertNotIn("userId", user.customData) + self.assertNotIn("targeting_key", user.customData) self.assertEqual(user.customData["other_field"], "value") - # When user_id is used, userId should still be excluded from custom data + # Test exclusion when user_id is used context = EvaluationContext( targeting_key=None, attributes={"user_id": user_id, "userId": user_id_attr, "other_field": "value"} @@ -125,24 +124,7 @@ def test_create_user_from_context_user_id_fields_always_excluded(self): self.assertNotIn("userId", user.customData) self.assertEqual(user.customData["other_field"], "value") - def test_create_user_from_context_targeting_key_excluded_from_attributes(self): - targeting_key_id = "targeting-12345" - - # When targeting_key appears in attributes, it should be excluded from custom data - context = EvaluationContext( - targeting_key=targeting_key_id, - attributes={"targeting_key": "should-be-excluded", "other_field": "value"} - ) - user = DevCycleUser.create_user_from_context(context) - self.assertEqual(user.user_id, targeting_key_id) - self.assertIsNotNone(user.customData) - self.assertNotIn("targeting_key", user.customData) - self.assertEqual(user.customData["other_field"], "value") - - def test_create_user_from_context_user_id_attr_excluded_when_used(self): - user_id_attr = "userId-12345" - - # When userId is used as user ID, it should be excluded from custom data + # Test exclusion when userId is used context = EvaluationContext( targeting_key=None, attributes={"userId": user_id_attr, "other_field": "value"} @@ -153,23 +135,6 @@ def test_create_user_from_context_user_id_attr_excluded_when_used(self): self.assertNotIn("userId", user.customData) self.assertEqual(user.customData["other_field"], "value") - def test_create_user_from_context_invalid_user_id_attr_types(self): - # Test non-string userId values are ignored and cause error - with self.assertRaises(TargetingKeyMissingError): - DevCycleUser.create_user_from_context( - EvaluationContext(targeting_key=None, attributes={"userId": 12345}) - ) - - with self.assertRaises(TargetingKeyMissingError): - DevCycleUser.create_user_from_context( - EvaluationContext(targeting_key=None, attributes={"userId": None}) - ) - - with self.assertRaises(TargetingKeyMissingError): - DevCycleUser.create_user_from_context( - EvaluationContext(targeting_key=None, attributes={"userId": True}) - ) - def test_create_user_from_context_with_attributes(self): test_user_id = "12345" context = EvaluationContext( diff --git a/test/openfeature_test/test_provider.py b/test/openfeature_test/test_provider.py index fdd82ea..0fb7a54 100644 --- a/test/openfeature_test/test_provider.py +++ b/test/openfeature_test/test_provider.py @@ -214,47 +214,6 @@ def test_resolve_object_details(self): self.assertDictEqual(details.value, variable_value) self.assertEqual(details.reason, Reason.TARGETING_MATCH) - def test_resolve_details_with_user_id_attr_priority(self): - """Test that userId is used with proper priority: targeting_key > user_id > userId""" - key = "test-flag" - default_value = False - - self.client.variable.return_value = Variable.create_default_variable( - key=key, default_value=default_value - ) - - # Test 1: targeting_key takes precedence over user_id and userId - context = EvaluationContext( - targeting_key="targeting-key-user", - attributes={"user_id": "user_id-user", "userId": "userId-user"} - ) - self.provider.resolve_boolean_details(key, default_value, context) - - # Verify the call was made with the right user - self.client.variable.assert_called() - called_user = self.client.variable.call_args[1]['user'] - self.assertEqual(called_user.user_id, "targeting-key-user") - - # Test 2: user_id takes precedence over userId when no targeting_key - context = EvaluationContext( - targeting_key=None, - attributes={"user_id": "user_id-user", "userId": "userId-user"} - ) - self.provider.resolve_boolean_details(key, default_value, context) - - called_user = self.client.variable.call_args[1]['user'] - self.assertEqual(called_user.user_id, "user_id-user") - - # Test 3: userId is used when no targeting_key or user_id - context = EvaluationContext( - targeting_key=None, - attributes={"userId": "userId-user"} - ) - self.provider.resolve_boolean_details(key, default_value, context) - - called_user = self.client.variable.call_args[1]['user'] - self.assertEqual(called_user.user_id, "userId-user") - if __name__ == "__main__": unittest.main() From 46771d367637f862da910378cfd995503e5f9691 Mon Sep 17 00:00:00 2001 From: Jonathan Norris Date: Tue, 22 Jul 2025 12:35:24 -0400 Subject: [PATCH 7/7] Fix Black code formatting - Applied Black formatter to test files to pass CI formatting checks - All tests still passing after formatting - Resolves 'Check formatting' CI failure --- test/models/test_user.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/test/models/test_user.py b/test/models/test_user.py index 31a1de9..334cab8 100644 --- a/test/models/test_user.py +++ b/test/models/test_user.py @@ -70,14 +70,13 @@ def test_create_user_from_context_with_user_id_attribute(self): # Test 2: Priority order - targeting_key > user_id > userId context = EvaluationContext( targeting_key=targeting_key_id, - attributes={"user_id": user_id, "userId": user_id_attr} + attributes={"user_id": user_id, "userId": user_id_attr}, ) user = DevCycleUser.create_user_from_context(context) self.assertEqual(user.user_id, targeting_key_id) context = EvaluationContext( - targeting_key=None, - attributes={"user_id": user_id, "userId": user_id_attr} + targeting_key=None, attributes={"user_id": user_id, "userId": user_id_attr} ) user = DevCycleUser.create_user_from_context(context) self.assertEqual(user.user_id, user_id) @@ -102,7 +101,12 @@ def test_create_user_from_context_user_id_exclusion(self): # Test exclusion when targeting_key is used context = EvaluationContext( targeting_key=targeting_key_id, - attributes={"user_id": user_id, "userId": user_id_attr, "targeting_key": "should-be-excluded", "other_field": "value"} + attributes={ + "user_id": user_id, + "userId": user_id_attr, + "targeting_key": "should-be-excluded", + "other_field": "value", + }, ) user = DevCycleUser.create_user_from_context(context) self.assertEqual(user.user_id, targeting_key_id) @@ -115,7 +119,11 @@ def test_create_user_from_context_user_id_exclusion(self): # Test exclusion when user_id is used context = EvaluationContext( targeting_key=None, - attributes={"user_id": user_id, "userId": user_id_attr, "other_field": "value"} + attributes={ + "user_id": user_id, + "userId": user_id_attr, + "other_field": "value", + }, ) user = DevCycleUser.create_user_from_context(context) self.assertEqual(user.user_id, user_id) @@ -127,7 +135,7 @@ def test_create_user_from_context_user_id_exclusion(self): # Test exclusion when userId is used context = EvaluationContext( targeting_key=None, - attributes={"userId": user_id_attr, "other_field": "value"} + attributes={"userId": user_id_attr, "other_field": "value"}, ) user = DevCycleUser.create_user_from_context(context) self.assertEqual(user.user_id, user_id_attr)