Skip to content

Commit 330a3d6

Browse files
Support userId as alternative user ID source in DevCycle user creation
Co-authored-by: jonathan <[email protected]>
1 parent 491d7e2 commit 330a3d6

File tree

3 files changed

+154
-3
lines changed

3 files changed

+154
-3
lines changed

devcycle_python_sdk/models/user.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,30 +101,39 @@ def create_user_from_context(
101101
) -> "DevCycleUser":
102102
"""
103103
Builds a DevCycleUser instance from the evaluation context. Will raise a TargetingKeyMissingError if
104-
the context does not contain a valid targeting key or user_id attribute
104+
the context does not contain a valid targeting key, user_id, or userId attribute
105105
106106
:param context: The evaluation context to build the user from
107107
:return: A DevCycleUser instance
108108
"""
109109
user_id = None
110+
user_id_source = None
110111

111112
if context:
112113
if context.targeting_key:
113114
user_id = context.targeting_key
115+
user_id_source = "targeting_key"
114116
elif context.attributes and "user_id" in context.attributes.keys():
115117
user_id = context.attributes["user_id"]
118+
user_id_source = "user_id"
119+
elif context.attributes and "userId" in context.attributes.keys():
120+
user_id = context.attributes["userId"]
121+
user_id_source = "userId"
116122

117123
if not user_id or not isinstance(user_id, str):
118124
raise TargetingKeyMissingError(
119-
"DevCycle: Evaluation context does not contain a valid targeting key or user_id attribute"
125+
"DevCycle: Evaluation context does not contain a valid targeting key, user_id, or userId attribute"
120126
)
121127

122128
user = DevCycleUser(user_id=user_id)
123129
custom_data: Dict[str, Any] = {}
124130
private_custom_data: Dict[str, Any] = {}
125131
if context and context.attributes:
126132
for key, value in context.attributes.items():
127-
if key == "user_id":
133+
# Skip user_id and userId if they were used as the user ID
134+
if key == "user_id" and user_id_source == "user_id":
135+
continue
136+
if key == "userId" and user_id_source == "userId":
128137
continue
129138

130139
if value is not None:

test/models/test_user.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,107 @@ def test_create_user_from_context_only_user_id(self):
5353
self.assertIsNotNone(user)
5454
self.assertEqual(user.user_id, test_user_id)
5555

56+
def test_create_user_from_context_with_userId(self):
57+
test_user_id = "userId-12345"
58+
59+
# Test userId as the only user ID source
60+
context = EvaluationContext(
61+
targeting_key=None, attributes={"userId": test_user_id}
62+
)
63+
user = DevCycleUser.create_user_from_context(context)
64+
self.assertIsNotNone(user)
65+
self.assertEqual(user.user_id, test_user_id)
66+
67+
# Test that userId is excluded from custom data when used as user ID
68+
self.assertIsNone(user.customData)
69+
70+
def test_create_user_from_context_user_id_priority(self):
71+
targeting_key_id = "targeting-12345"
72+
user_id = "user_id-12345"
73+
userId = "userId-12345"
74+
75+
# Test targeting_key takes precedence over user_id and userId
76+
context = EvaluationContext(
77+
targeting_key=targeting_key_id,
78+
attributes={"user_id": user_id, "userId": userId}
79+
)
80+
user = DevCycleUser.create_user_from_context(context)
81+
self.assertEqual(user.user_id, targeting_key_id)
82+
83+
# Test user_id takes precedence over userId
84+
context = EvaluationContext(
85+
targeting_key=None,
86+
attributes={"user_id": user_id, "userId": userId}
87+
)
88+
user = DevCycleUser.create_user_from_context(context)
89+
self.assertEqual(user.user_id, user_id)
90+
91+
# Test userId is used when targeting_key and user_id are not available
92+
context = EvaluationContext(
93+
targeting_key=None,
94+
attributes={"userId": userId}
95+
)
96+
user = DevCycleUser.create_user_from_context(context)
97+
self.assertEqual(user.user_id, userId)
98+
99+
def test_create_user_from_context_userId_in_custom_data_when_not_used(self):
100+
targeting_key_id = "targeting-12345"
101+
userId = "userId-12345"
102+
103+
# When targeting_key is used, userId should be in custom data
104+
context = EvaluationContext(
105+
targeting_key=targeting_key_id,
106+
attributes={"userId": userId, "other_field": "value"}
107+
)
108+
user = DevCycleUser.create_user_from_context(context)
109+
self.assertEqual(user.user_id, targeting_key_id)
110+
self.assertIsNotNone(user.customData)
111+
self.assertEqual(user.customData["userId"], userId)
112+
self.assertEqual(user.customData["other_field"], "value")
113+
114+
# When user_id is used, userId should be in custom data
115+
user_id = "user_id-12345"
116+
context = EvaluationContext(
117+
targeting_key=None,
118+
attributes={"user_id": user_id, "userId": userId, "other_field": "value"}
119+
)
120+
user = DevCycleUser.create_user_from_context(context)
121+
self.assertEqual(user.user_id, user_id)
122+
self.assertIsNotNone(user.customData)
123+
self.assertEqual(user.customData["userId"], userId)
124+
self.assertEqual(user.customData["other_field"], "value")
125+
126+
def test_create_user_from_context_userId_excluded_when_used(self):
127+
userId = "userId-12345"
128+
129+
# When userId is used as user ID, it should be excluded from custom data
130+
context = EvaluationContext(
131+
targeting_key=None,
132+
attributes={"userId": userId, "other_field": "value"}
133+
)
134+
user = DevCycleUser.create_user_from_context(context)
135+
self.assertEqual(user.user_id, userId)
136+
self.assertIsNotNone(user.customData)
137+
self.assertNotIn("userId", user.customData)
138+
self.assertEqual(user.customData["other_field"], "value")
139+
140+
def test_create_user_from_context_invalid_userId_types(self):
141+
# Test non-string userId values are ignored and cause error
142+
with self.assertRaises(TargetingKeyMissingError):
143+
DevCycleUser.create_user_from_context(
144+
EvaluationContext(targeting_key=None, attributes={"userId": 12345})
145+
)
146+
147+
with self.assertRaises(TargetingKeyMissingError):
148+
DevCycleUser.create_user_from_context(
149+
EvaluationContext(targeting_key=None, attributes={"userId": None})
150+
)
151+
152+
with self.assertRaises(TargetingKeyMissingError):
153+
DevCycleUser.create_user_from_context(
154+
EvaluationContext(targeting_key=None, attributes={"userId": True})
155+
)
156+
56157
def test_create_user_from_context_with_attributes(self):
57158
test_user_id = "12345"
58159
context = EvaluationContext(

test/openfeature_test/test_provider.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,47 @@ def test_resolve_object_details(self):
214214
self.assertDictEqual(details.value, variable_value)
215215
self.assertEqual(details.reason, Reason.TARGETING_MATCH)
216216

217+
def test_resolve_details_with_userId_priority(self):
218+
"""Test that userId is used with proper priority: targeting_key > user_id > userId"""
219+
key = "test-flag"
220+
default_value = False
221+
222+
self.client.variable.return_value = Variable.create_default_variable(
223+
key=key, default_value=default_value
224+
)
225+
226+
# Test 1: targeting_key takes precedence over user_id and userId
227+
context = EvaluationContext(
228+
targeting_key="targeting-key-user",
229+
attributes={"user_id": "user_id-user", "userId": "userId-user"}
230+
)
231+
details = self.provider.resolve_boolean_details(key, default_value, context)
232+
233+
# Verify the call was made with the right user
234+
self.client.variable.assert_called()
235+
called_user = self.client.variable.call_args[1]['user']
236+
self.assertEqual(called_user.user_id, "targeting-key-user")
237+
238+
# Test 2: user_id takes precedence over userId when no targeting_key
239+
context = EvaluationContext(
240+
targeting_key=None,
241+
attributes={"user_id": "user_id-user", "userId": "userId-user"}
242+
)
243+
details = self.provider.resolve_boolean_details(key, default_value, context)
244+
245+
called_user = self.client.variable.call_args[1]['user']
246+
self.assertEqual(called_user.user_id, "user_id-user")
247+
248+
# Test 3: userId is used when no targeting_key or user_id
249+
context = EvaluationContext(
250+
targeting_key=None,
251+
attributes={"userId": "userId-user"}
252+
)
253+
details = self.provider.resolve_boolean_details(key, default_value, context)
254+
255+
called_user = self.client.variable.call_args[1]['user']
256+
self.assertEqual(called_user.user_id, "userId-user")
257+
217258

218259
if __name__ == "__main__":
219260
unittest.main()

0 commit comments

Comments
 (0)