Skip to content

Commit a2dba60

Browse files
[FSSDK-9069] fix: odp event validation (#423)
* fix odp send event validation * add unit tests * update action missing error
1 parent 7b1c3f1 commit a2dba60

File tree

5 files changed

+78
-2
lines changed

5 files changed

+78
-2
lines changed

Diff for: optimizely/helpers/enums.py

+1
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ class Errors:
126126
ODP_NOT_INTEGRATED: Final = 'ODP is not integrated.'
127127
ODP_NOT_ENABLED: Final = 'ODP is not enabled.'
128128
ODP_INVALID_DATA: Final = 'ODP data is not valid.'
129+
ODP_INVALID_ACTION: Final = 'ODP action is not valid (cannot be empty).'
129130
MISSING_SDK_KEY: Final = 'SDK key not provided/cannot be found in the datafile.'
130131

131132

Diff for: optimizely/odp/odp_event.py

+16-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import uuid
1818
import json
1919
from optimizely import version
20+
from optimizely.helpers.enums import OdpManagerConfig
2021

2122
OdpDataDict = Dict[str, Union[str, int, float, bool, None]]
2223

@@ -27,7 +28,7 @@ class OdpEvent:
2728
def __init__(self, type: str, action: str, identifiers: dict[str, str], data: OdpDataDict) -> None:
2829
self.type = type
2930
self.action = action
30-
self.identifiers = identifiers
31+
self.identifiers = self._convert_identifers(identifiers)
3132
self.data = self._add_common_event_data(data)
3233

3334
def __repr__(self) -> str:
@@ -51,6 +52,20 @@ def _add_common_event_data(self, custom_data: OdpDataDict) -> OdpDataDict:
5152
data.update(custom_data)
5253
return data
5354

55+
def _convert_identifers(self, identifiers: dict[str, str]) -> dict[str, str]:
56+
"""
57+
Convert incorrect case/separator of identifier key `fs_user_id`
58+
(ie. `fs-user-id`, `FS_USER_ID`).
59+
"""
60+
for key in list(identifiers):
61+
if key == OdpManagerConfig.KEY_FOR_USER_ID:
62+
break
63+
elif key.lower() in ("fs-user-id", OdpManagerConfig.KEY_FOR_USER_ID):
64+
identifiers[OdpManagerConfig.KEY_FOR_USER_ID] = identifiers.pop(key)
65+
break
66+
67+
return identifiers
68+
5469

5570
class OdpEventEncoder(json.JSONEncoder):
5671
def default(self, obj: object) -> Any:

Diff for: optimizely/optimizely.py

+8-1
Original file line numberDiff line numberDiff line change
@@ -1387,7 +1387,7 @@ def send_odp_event(
13871387
Send an event to the ODP server.
13881388
13891389
Args:
1390-
action: The event action name.
1390+
action: The event action name. Cannot be None or empty string.
13911391
identifiers: A dictionary for identifiers. The caller must provide at least one key-value pair.
13921392
type: The event type. Default 'fullstack'.
13931393
data: An optional dictionary for associated data. The default event data will be added to this data
@@ -1397,10 +1397,17 @@ def send_odp_event(
13971397
self.logger.error(enums.Errors.INVALID_OPTIMIZELY.format('send_odp_event'))
13981398
return
13991399

1400+
if action is None or action == "":
1401+
self.logger.error(enums.Errors.ODP_INVALID_ACTION)
1402+
return
1403+
14001404
if not identifiers or not isinstance(identifiers, dict):
14011405
self.logger.error('ODP events must have at least one key-value pair in identifiers.')
14021406
return
14031407

1408+
if type is None or type == "":
1409+
type = enums.OdpManagerConfig.EVENT_TYPE
1410+
14041411
config = self.config_manager.get_config()
14051412
if not config:
14061413
self.logger.error(enums.Errors.INVALID_PROJECT_CONFIG.format('send_odp_event'))

Diff for: tests/test_odp_event_manager.py

+13
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,19 @@ def test_invalid_odp_event(self, *args):
9898
event['data']['invalid-item'] = {}
9999
self.assertStrictFalse(validator.are_odp_data_types_valid(event['data']))
100100

101+
def test_odp_event_identifier_conversion(self, *args):
102+
event = OdpEvent('type', 'action', {'fs-user-id': 'great'}, {})
103+
self.assertDictEqual(event.identifiers, {'fs_user_id': 'great'})
104+
105+
event = OdpEvent('type', 'action', {'FS-user-ID': 'great'}, {})
106+
self.assertDictEqual(event.identifiers, {'fs_user_id': 'great'})
107+
108+
event = OdpEvent('type', 'action', {'FS_USER_ID': 'great', 'fs.user.id': 'wow'}, {})
109+
self.assertDictEqual(event.identifiers, {'fs_user_id': 'great', 'fs.user.id': 'wow'})
110+
111+
event = OdpEvent('type', 'action', {'fs_user_id': 'great', 'fsuserid': 'wow'}, {})
112+
self.assertDictEqual(event.identifiers, {'fs_user_id': 'great', 'fsuserid': 'wow'})
113+
101114
def test_odp_event_manager_success(self, *args):
102115
mock_logger = mock.Mock()
103116
event_manager = OdpEventManager(mock_logger)

Diff for: tests/test_optimizely.py

+40
Original file line numberDiff line numberDiff line change
@@ -5483,3 +5483,43 @@ def test_send_odp_event__log_error_with_missing_integrations_data(self):
54835483

54845484
mock_logger.error.assert_called_with('ODP is not integrated.')
54855485
client.close()
5486+
5487+
def test_send_odp_event__log_error_with_action_none(self):
5488+
mock_logger = mock.Mock()
5489+
client = optimizely.Optimizely(json.dumps(self.config_dict_with_audience_segments), logger=mock_logger)
5490+
5491+
client.send_odp_event(type='wow', action=None, identifiers={'amazing': 'fantastic'}, data={})
5492+
client.close()
5493+
5494+
mock_logger.error.assert_called_once_with('ODP action is not valid (cannot be empty).')
5495+
5496+
def test_send_odp_event__log_error_with_action_empty_string(self):
5497+
mock_logger = mock.Mock()
5498+
client = optimizely.Optimizely(json.dumps(self.config_dict_with_audience_segments), logger=mock_logger)
5499+
5500+
client.send_odp_event(type='wow', action="", identifiers={'amazing': 'fantastic'}, data={})
5501+
client.close()
5502+
5503+
mock_logger.error.assert_called_once_with('ODP action is not valid (cannot be empty).')
5504+
5505+
def test_send_odp_event__default_type_when_none(self):
5506+
mock_logger = mock.Mock()
5507+
5508+
client = optimizely.Optimizely(json.dumps(self.config_dict_with_audience_segments), logger=mock_logger)
5509+
with mock.patch.object(client.odp_manager, 'send_event') as mock_send_event:
5510+
client.send_odp_event(type=None, action="great", identifiers={'amazing': 'fantastic'}, data={})
5511+
client.close()
5512+
5513+
mock_send_event.assert_called_with('fullstack', 'great', {'amazing': 'fantastic'}, {})
5514+
mock_logger.error.assert_not_called()
5515+
5516+
def test_send_odp_event__default_type_when_empty_string(self):
5517+
mock_logger = mock.Mock()
5518+
5519+
client = optimizely.Optimizely(json.dumps(self.config_dict_with_audience_segments), logger=mock_logger)
5520+
with mock.patch.object(client.odp_manager, 'send_event') as mock_send_event:
5521+
client.send_odp_event(type="", action="great", identifiers={'amazing': 'fantastic'}, data={})
5522+
client.close()
5523+
5524+
mock_send_event.assert_called_with('fullstack', 'great', {'amazing': 'fantastic'}, {})
5525+
mock_logger.error.assert_not_called()

0 commit comments

Comments
 (0)