Skip to content

Commit

Permalink
Merge c1afef2 into 30ff44c
Browse files Browse the repository at this point in the history
  • Loading branch information
pthompson127 committed Jun 12, 2020
2 parents 30ff44c + c1afef2 commit 946bb51
Show file tree
Hide file tree
Showing 5 changed files with 131 additions and 10 deletions.
46 changes: 46 additions & 0 deletions optimizely/config_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -368,3 +368,49 @@ def start(self):
""" Start the config manager and the thread to periodically fetch datafile. """
if not self.is_running:
self._polling_thread.start()


class AuthDatafilePollingConfigManager(PollingConfigManager):
""" Config manager that polls for authenticated datafile using access token. """

def __init__(
self,
access_token,
**kwargs
):
""" Initialize config manager. access_token must be set to be able to use.
One of sdk_key or url has to be set to be able to use.
Args:
access_token: String to be attached to the request header to fetch the authenticated datafile.
**kwargs: Refer to keyword arguments descriptions in PollingConfigManager
"""
self._set_access_token(access_token)
self._set_url_template(kwargs)
super(AuthDatafilePollingConfigManager, self).__init__(**kwargs)

def _set_url_template(self, kwargs):
""" Helper method to set url template depending on kwargs input. """
if 'url_template' not in kwargs or kwargs['url_template'] is None:
kwargs['url_template'] = enums.ConfigManager.AUTHENTICATED_DATAFILE_URL_TEMPLATE

def _set_access_token(self, access_token):
""" Checks for valid access token input and sets it. """
if not access_token:
raise optimizely_exceptions.InvalidInputException(
'access_token cannot be empty or None.')
self.access_token = access_token

def fetch_datafile(self):
""" Fetch authenticated datafile and set ProjectConfig. """
request_headers = {}
request_headers[enums.HTTPHeaders.AUTHORIZATION] = \
enums.ConfigManager.AUTHORIZATION_HEADER_DATA_TEMPLATE.format(access_token=self.access_token)

if self.last_modified:
request_headers[enums.HTTPHeaders.IF_MODIFIED_SINCE] = self.last_modified

response = requests.get(
self.datafile_url, headers=request_headers, timeout=enums.ConfigManager.REQUEST_TIMEOUT,
)
self._handle_response(response)
3 changes: 3 additions & 0 deletions optimizely/helpers/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ class AudienceEvaluationLogs(object):


class ConfigManager(object):
AUTHENTICATED_DATAFILE_URL_TEMPLATE = 'https://config.optimizely.com/datafiles/auth/{sdk_key}.json'
AUTHORIZATION_HEADER_DATA_TEMPLATE = 'Bearer {access_token}'
DATAFILE_URL_TEMPLATE = 'https://cdn.optimizely.com/datafiles/{sdk_key}.json'
# Default time in seconds to block the 'get_config' method call until 'config' instance has been initialized.
DEFAULT_BLOCKING_TIMEOUT = 10
Expand Down Expand Up @@ -104,6 +106,7 @@ class Errors(object):


class HTTPHeaders(object):
AUTHORIZATION = 'Authorization'
IF_MODIFIED_SINCE = 'If-Modified-Since'
LAST_MODIFIED = 'Last-Modified'

Expand Down
30 changes: 22 additions & 8 deletions optimizely/optimizely.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from . import event_builder
from . import exceptions
from . import logger as _logging
from .config_manager import AuthDatafilePollingConfigManager
from .config_manager import PollingConfigManager
from .config_manager import StaticConfigManager
from .error_handler import NoOpErrorHandler as noop_error_handler
Expand All @@ -43,6 +44,7 @@ def __init__(
config_manager=None,
notification_center=None,
event_processor=None,
access_token=None,
):
""" Optimizely init method for managing Custom projects.
Expand All @@ -65,6 +67,7 @@ def __init__(
By default optimizely.event.event_processor.ForwardingEventProcessor is used
which simply forwards events to the event dispatcher.
To enable event batching configure and use optimizely.event.event_processor.BatchEventProcessor.
access_token: Optional string used to fetch authenticated datafile for a secure project environment.
"""
self.logger_name = '.'.join([__name__, self.__class__.__name__])
self.is_valid = True
Expand All @@ -89,14 +92,25 @@ def __init__(

if not self.config_manager:
if sdk_key:
self.config_manager = PollingConfigManager(
sdk_key=sdk_key,
datafile=datafile,
logger=self.logger,
error_handler=self.error_handler,
notification_center=self.notification_center,
skip_json_validation=skip_json_validation,
)
if access_token:
self.config_manager = AuthDatafilePollingConfigManager(
access_token=access_token,
sdk_key=sdk_key,
datafile=datafile,
logger=self.logger,
error_handler=self.error_handler,
notification_center=self.notification_center,
skip_json_validation=skip_json_validation,
)
else:
self.config_manager = PollingConfigManager(
sdk_key=sdk_key,
datafile=datafile,
logger=self.logger,
error_handler=self.error_handler,
notification_center=self.notification_center,
skip_json_validation=skip_json_validation,
)
else:
self.config_manager = StaticConfigManager(
datafile=datafile,
Expand Down
52 changes: 50 additions & 2 deletions tests/test_config_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -365,9 +365,10 @@ def test_set_last_modified(self, _):

def test_fetch_datafile(self, _):
""" Test that fetch_datafile sets config and last_modified based on response. """
sdk_key = 'some_key'
with mock.patch('optimizely.config_manager.PollingConfigManager.fetch_datafile'):
project_config_manager = config_manager.PollingConfigManager(sdk_key='some_key')
expected_datafile_url = 'https://cdn.optimizely.com/datafiles/some_key.json'
project_config_manager = config_manager.PollingConfigManager(sdk_key=sdk_key)
expected_datafile_url = enums.ConfigManager.DATAFILE_URL_TEMPLATE.format(sdk_key=sdk_key)
test_headers = {'Last-Modified': 'New Time'}
test_datafile = json.dumps(self.config_dict_with_features)
test_response = requests.Response()
Expand Down Expand Up @@ -397,3 +398,50 @@ def test_is_running(self, _):
with mock.patch('optimizely.config_manager.PollingConfigManager.fetch_datafile'):
project_config_manager = config_manager.PollingConfigManager(sdk_key='some_key')
self.assertTrue(project_config_manager.is_running)


@mock.patch('requests.get')
class AuthDatafilePollingConfigManagerTest(base.BaseTest):
def test_init__access_token_none__fails(self, _):
""" Test that initialization fails if access_token is None. """
self.assertRaisesRegexp(
optimizely_exceptions.InvalidInputException,
'access_token cannot be empty or None.',
config_manager.AuthDatafilePollingConfigManager,
access_token=None
)

def test_set_access_token(self, _):
""" Test that access_token is properly set as instance variable. """
access_token = 'some_token'
sdk_key = 'some_key'
with mock.patch('optimizely.config_manager.AuthDatafilePollingConfigManager.fetch_datafile'):
project_config_manager = config_manager.AuthDatafilePollingConfigManager(
access_token=access_token, sdk_key=sdk_key)

self.assertEqual(access_token, project_config_manager.access_token)

def test_fetch_datafile(self, _):
""" Test that fetch_datafile sets authorization header in request header and sets config based on response. """
access_token = 'some_token'
sdk_key = 'some_key'
with mock.patch('optimizely.config_manager.AuthDatafilePollingConfigManager.fetch_datafile'):
project_config_manager = config_manager.AuthDatafilePollingConfigManager(
access_token=access_token, sdk_key=sdk_key)
expected_datafile_url = enums.ConfigManager.AUTHENTICATED_DATAFILE_URL_TEMPLATE.format(sdk_key=sdk_key)
test_datafile = json.dumps(self.config_dict_with_features)
test_response = requests.Response()
test_response.status_code = 200
test_response._content = test_datafile

# Call fetch_datafile and assert that request was sent with correct authorization header
with mock.patch('requests.get', return_value=test_response) as mock_request:
project_config_manager.fetch_datafile()

mock_request.assert_called_once_with(
expected_datafile_url,
headers={'Authorization': 'Bearer {access_token}'.format(access_token=access_token)},
timeout=enums.ConfigManager.REQUEST_TIMEOUT,
)

self.assertIsInstance(project_config_manager.get_config(), project_config.ProjectConfig)
10 changes: 10 additions & 0 deletions tests/test_optimizely.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,16 @@ def test_init__sdk_key_and_datafile(self):

self.assertIs(type(opt_obj.config_manager), config_manager.PollingConfigManager)

def test_init__sdk_key_and_access_token(self):
""" Test that if both sdk_key and access_token is provided then AuthDatafilePollingConfigManager is used. """

with mock.patch('optimizely.config_manager.AuthDatafilePollingConfigManager._set_config'), mock.patch(
'threading.Thread.start'
):
opt_obj = optimizely.Optimizely(access_token="test_access_token", sdk_key='test_sdk_key')

self.assertIs(type(opt_obj.config_manager), config_manager.AuthDatafilePollingConfigManager)

def test_invalid_json_raises_schema_validation_off(self):
""" Test that invalid JSON logs error if schema validation is turned off. """

Expand Down

0 comments on commit 946bb51

Please sign in to comment.