diff --git a/devcycle_python_sdk/models/user.py b/devcycle_python_sdk/models/user.py index 57d4c56..3110ead 100644 --- a/devcycle_python_sdk/models/user.py +++ b/devcycle_python_sdk/models/user.py @@ -101,22 +101,33 @@ 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): + if not user_id: 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" + ) + + 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) @@ -124,7 +135,8 @@ 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, 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 0bda339..334cab8 100644 --- a/test/models/test_user.py +++ b/test/models/test_user.py @@ -53,6 +53,96 @@ 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_user_id_attribute(self): + """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 1: userId as the only user ID source + context = EvaluationContext( + targeting_key=None, attributes={"userId": user_id_attr} + ) + user = DevCycleUser.create_user_from_context(context) + self.assertIsNotNone(user) + self.assertEqual(user.user_id, user_id_attr) + + # 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}, + ) + 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} + ) + user = DevCycleUser.create_user_from_context(context) + self.assertEqual(user.user_id, user_id) + + # 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_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 = "user_id-12345" + user_id_attr = "userId-12345" + + # 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", + }, + ) + 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") + + # Test exclusion when user_id is used + context = EvaluationContext( + 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) + self.assertIsNotNone(user.customData) + self.assertNotIn("user_id", user.customData) + self.assertNotIn("userId", user.customData) + self.assertEqual(user.customData["other_field"], "value") + + # Test exclusion when userId is used + context = EvaluationContext( + targeting_key=None, + attributes={"userId": user_id_attr, "other_field": "value"}, + ) + user = DevCycleUser.create_user_from_context(context) + 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_with_attributes(self): test_user_id = "12345" context = EvaluationContext(