From 909c620127adcfdaa4e83977a95ee76349c08a09 Mon Sep 17 00:00:00 2001 From: "sasa.kalaba" Date: Thu, 11 Aug 2016 14:30:58 +0200 Subject: [PATCH 01/19] Updated gitignore. --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index d40fdde..b02c7a4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ *.egg-info +*.pyc .cache .coverage +/build +/dist From 1f7b741cbcb534f26d4508ba0f72a83b6c95f4a0 Mon Sep 17 00:00:00 2001 From: "sasa.kalaba" Date: Tue, 17 Jan 2017 13:16:50 +0100 Subject: [PATCH 02/19] Upgraded dependencies. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 358d0d4..5ea4612 100644 --- a/setup.py +++ b/setup.py @@ -42,7 +42,7 @@ def run(self): 'pyyaml>=3.11', ], extras_require = { - 'test': ['codacy-coverage', 'mock', 'pytest', 'pytest-cov', 'python-coveralls', 'stormpath'], + 'test': ['codacy-coverage', 'mock', 'pytest', 'pytest-cov', 'pytest-env', 'pytest-xdist', 'python-coveralls', 'stormpath'], }, packages = find_packages(exclude=['*.tests', '*.tests.*', 'tests.*', 'tests']), classifiers = [ From ee030b346dfca88d4df7b98c0103e3f9d8103b6e Mon Sep 17 00:00:00 2001 From: "sasa.kalaba" Date: Tue, 17 Jan 2017 13:17:31 +0100 Subject: [PATCH 03/19] Removed requirements.txt. --- requirements.txt | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 396fe66..0000000 --- a/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -flatdict==1.2.0 -pyyaml==3.11 \ No newline at end of file From af2fa1ab2e84842bc276975d59b8ce274fc94b5a Mon Sep 17 00:00:00 2001 From: "sasa.kalaba" Date: Fri, 20 Jan 2017 17:16:41 +0100 Subject: [PATCH 04/19] Updated ConfigLoader for the new stormpath key in default config. --- stormpath_config/loader.py | 5 +++++ tests/test_loader.py | 14 ++++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/stormpath_config/loader.py b/stormpath_config/loader.py index d36666d..c1e2a9e 100644 --- a/stormpath_config/loader.py +++ b/stormpath_config/loader.py @@ -1,3 +1,6 @@ +from .helpers import _extend_dict + + """Configuration Loader.""" @@ -35,6 +38,8 @@ def load(self): for strategy in self.post_processing_strategies: config = strategy.process(config) + config = _extend_dict(config, config.pop('stormpath', {})) + for strategy in self.validation_strategies: config = strategy.process(config) diff --git a/tests/test_loader.py b/tests/test_loader.py index 73a5888..c8ff7ab 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -17,7 +17,7 @@ class ConfigLoaderTest(TestCase): def setUp(self): - client_config = { + self.client_config = { 'application': { 'name': 'CLIENT_CONFIG_APP', 'href': None @@ -53,7 +53,7 @@ def setUp(self): # 7. Configuration provided through the SDK client # constructor. - ExtendConfigStrategy(extend_with=client_config) + ExtendConfigStrategy(extend_with=self.client_config) ] self.post_processing_strategies = [ # Post-processing: If the key client.apiKey.file isn't @@ -85,3 +85,13 @@ def test_config_loader(self): self.assertEqual(config['client']['cacheManager']['defaultTtl'], 302) self.assertEqual(config['client']['cacheManager']['defaultTti'], 303) self.assertEqual(config['application']['name'], 'CLIENT_CONFIG_APP') + + def test_stormpath_key_loader(self): + self.client_config['application']['name'] = 'STORMPATH_KEY_APP' + self.load_strategies[6] = ExtendConfigStrategy( + extend_with={'stormpath': self.client_config}) + cl = ConfigLoader(self.load_strategies, self.post_processing_strategies, self.validation_strategies) + config = cl.load() + + self.assertEqual(config['application']['name'], 'STORMPATH_KEY_APP') + self.assertFalse('stormpath' in config) From 7416d0bc469bd5c2af384d5f65b7522deccd790d Mon Sep 17 00:00:00 2001 From: "sasa.kalaba" Date: Mon, 23 Jan 2017 13:19:05 +0100 Subject: [PATCH 05/19] Added test for loading api key and secret from file. --- .../test_load_apikey_from_config.py | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 tests/strategies/test_load_apikey_from_config.py diff --git a/tests/strategies/test_load_apikey_from_config.py b/tests/strategies/test_load_apikey_from_config.py new file mode 100644 index 0000000..b4872ba --- /dev/null +++ b/tests/strategies/test_load_apikey_from_config.py @@ -0,0 +1,40 @@ +from unittest import TestCase +from stormpath_config.loader import ConfigLoader +from stormpath_config.strategies import ( + LoadAPIKeyConfigStrategy, + LoadFileConfigStrategy, + LoadEnvConfigStrategy, + ExtendConfigStrategy, + LoadAPIKeyFromConfigStrategy, + ValidateClientConfigStrategy) + + +class LoadAPIKeyFromConfigStrategyTest(TestCase): + def test_api_key_from_config_with_lesser_loading_order(self): + """ Ensure that api key and secret are properly loaded from file. """ + + load_strategies = [ + # 1. We load the default configuration. + LoadFileConfigStrategy( + 'tests/assets/default_config.yml', must_exist=True), + LoadAPIKeyConfigStrategy('i-do-not-exist'), + # 3. We load apiKeyFile.yml file with apiKey.properties file. + LoadFileConfigStrategy('tests/assets/apiKeyFile.yml'), + LoadAPIKeyConfigStrategy('i-do-not-exist'), + LoadFileConfigStrategy('i-do-not-exist'), + LoadEnvConfigStrategy(prefix='STORMPATH'), + # 7. Configuration provided through the SDK client constructor. + ExtendConfigStrategy(extend_with={}) + ] + post_processing_strategies = [LoadAPIKeyFromConfigStrategy()] + validation_strategies = [ValidateClientConfigStrategy()] + + cl = ConfigLoader( + load_strategies, post_processing_strategies, validation_strategies) + config = cl.load() + + self.assertEqual( + config['client']['apiKey']['id'], 'API_KEY_PROPERTIES_ID') + self.assertEqual( + config['client']['apiKey']['secret'], 'API_KEY_PROPERTIES_SECRET') + self.assertFalse('file' in config['client']['apiKey']) From 395a7e3a1ffb0891c5b3f3881f0f867ab2572189 Mon Sep 17 00:00:00 2001 From: "sasa.kalaba" Date: Tue, 24 Jan 2017 19:19:15 +0100 Subject: [PATCH 06/19] Added MoveAPIKeyToClientAPIKeyStrategy. --- stormpath_config/strategies/__init__.py | 1 + .../strategies/move_apikey_to_client.py | 21 +++++++ .../strategies/test_move_api_key_to_client.py | 57 +++++++++++++++++++ 3 files changed, 79 insertions(+) create mode 100644 stormpath_config/strategies/move_apikey_to_client.py create mode 100644 tests/strategies/test_move_api_key_to_client.py diff --git a/stormpath_config/strategies/__init__.py b/stormpath_config/strategies/__init__.py index a04fda2..f3dd6a5 100644 --- a/stormpath_config/strategies/__init__.py +++ b/stormpath_config/strategies/__init__.py @@ -9,3 +9,4 @@ from .load_file_config import LoadFileConfigStrategy from .load_file_path import LoadFilePathStrategy from .validate_client_config import ValidateClientConfigStrategy +from .move_apikey_to_client import MoveAPIKeyToClientAPIKeyStrategy diff --git a/stormpath_config/strategies/move_apikey_to_client.py b/stormpath_config/strategies/move_apikey_to_client.py new file mode 100644 index 0000000..279da6b --- /dev/null +++ b/stormpath_config/strategies/move_apikey_to_client.py @@ -0,0 +1,21 @@ +class MoveAPIKeyToClientAPIKeyStrategy(object): + """ Represents a strategy that checks if our config has an outer key named + `apiKey'. If it does, we move it to client.apiKey. """ + def process(self, config=None): + if config is None: + config = {} + + apiKey = config.get('apiKey', {}) + if apiKey: + api_key_id = apiKey.get('id') + api_key_secret = apiKey.get('secret') + if not (api_key_id and api_key_secret): + raise Exception('Unable to load apiKey id and secret.') + + config.setdefault('client', {}) + config['client'].setdefault('apiKey', {}) + config['client']['apiKey']['id'] = api_key_id + config['client']['apiKey']['secret'] = api_key_secret + + config.pop('apiKey', {}) + return config diff --git a/tests/strategies/test_move_api_key_to_client.py b/tests/strategies/test_move_api_key_to_client.py new file mode 100644 index 0000000..7d1a8bb --- /dev/null +++ b/tests/strategies/test_move_api_key_to_client.py @@ -0,0 +1,57 @@ +from unittest import TestCase +from stormpath_config.loader import ConfigLoader +from stormpath_config.strategies import ( + LoadAPIKeyConfigStrategy, + LoadFileConfigStrategy, + LoadEnvConfigStrategy, + ExtendConfigStrategy, + LoadAPIKeyFromConfigStrategy, + ValidateClientConfigStrategy, + MoveAPIKeyToClientAPIKeyStrategy) + + +class MoveAPIKeyToClientAPIKeyStrategyTest(TestCase): + def generateConfig(self, client_config={}): + load_strategies = [ + # 1. We load the default configuration. + LoadFileConfigStrategy( + 'tests/assets/default_config.yml', must_exist=True), + LoadAPIKeyConfigStrategy('i-do-not-exist'), + # 3. We load apiKeyApiKey.json file with apiKey id and secret. + LoadFileConfigStrategy('tests/assets/apiKeyApiKey.json'), + LoadAPIKeyConfigStrategy('i-do-not-exist'), + LoadFileConfigStrategy('i-do-not-exist'), + LoadEnvConfigStrategy(prefix='STORMPATH'), + # 7. Configuration provided through the SDK client constructor. + ExtendConfigStrategy(extend_with=client_config) + ] + post_processing_strategies = [ + LoadAPIKeyFromConfigStrategy(), MoveAPIKeyToClientAPIKeyStrategy()] + validation_strategies = [ValidateClientConfigStrategy()] + + cl = ConfigLoader( + load_strategies, post_processing_strategies, validation_strategies) + return cl.load() + + def test_move_api_key_to_client(self): + """ Ensure that apiKey key is moved to client if set in config outer + keys. """ + config = self.generateConfig() + + # Ensure that id and secret from outer apiKey key have beed loaded to + # client key. + self.assertEqual( + config['client']['apiKey']['id'], 'MY_JSON_CONFIG_API_KEY_ID') + self.assertEqual( + config['client']['apiKey']['secret'], + 'MY_JSON_CONFIG_API_KEY_SECRET') + self.assertFalse('apiKey' in config.keys()) + + def test_move_api_key_to_client_missing_credentials(self): + """ Ensure that missing id or secret will raise an Exception. """ + client_config = {'apiKey': {'foo': 'bar'}} + + with self.assertRaises(Exception) as error: + self.generateConfig(client_config=client_config) + self.assertEqual( + error.exception.message, 'Unable to load apiKey id and secret.') From 9ce01cbe90c80dd58ccbd100c253cc19f2862b67 Mon Sep 17 00:00:00 2001 From: "sasa.kalaba" Date: Mon, 30 Jan 2017 15:55:08 +0100 Subject: [PATCH 07/19] Updated .gitignore. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index b02c7a4..1a1c7cd 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ .coverage /build /dist +/htmlcov From 751fa6c38f57b31a7bf4e8ae9314ead052a0df95 Mon Sep 17 00:00:00 2001 From: "sasa.kalaba" Date: Thu, 2 Feb 2017 17:56:02 +0100 Subject: [PATCH 08/19] Added skeleton for final strategy override testing. --- tests/test_loader.py | 65 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/tests/test_loader.py b/tests/test_loader.py index c8ff7ab..d94288c 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -90,8 +90,71 @@ def test_stormpath_key_loader(self): self.client_config['application']['name'] = 'STORMPATH_KEY_APP' self.load_strategies[6] = ExtendConfigStrategy( extend_with={'stormpath': self.client_config}) - cl = ConfigLoader(self.load_strategies, self.post_processing_strategies, self.validation_strategies) + cl = ConfigLoader( + self.load_strategies, + self.post_processing_strategies, + self.validation_strategies + ) config = cl.load() self.assertEqual(config['application']['name'], 'STORMPATH_KEY_APP') self.assertFalse('stormpath' in config) + + +class OverridingStrategiesTest(TestCase): + """ + This testing class will simulate the loading process for stormpath-flask. + """ + def setUp(self): + self.client_config = {} + + def test_strategies_override_01(self): + # Ensure that original config file is loaded. + + # cl = ConfigLoader( + # self.load_strategies, + # self.post_processing_strategies, + # self.validation_strategies + # ) + # config = cl.load() + pass + + def test_strategies_override_02(self): + # Ensure that apiKey.properties file from HOME directory will override + # any settings from previous config sources. + pass + + def test_strategies_override_03(self): + # Ensure that stormpath.json file from HOME stormpath directory will + # override any settings from previous config sources. + pass + + def test_strategies_override_04(self): + # Ensure that stormpath.yaml file from HOME stormpath directory will + # override any settings from previous config sources. + pass + + def test_strategies_override_05(self): + # Ensure that apiKey.properties file will override any settings from + # previous config sources. + pass + + def test_strategies_override_06(self): + # Ensure that stormpath.json file will override any settings from + # previous config sources. + pass + + def test_strategies_override_07(self): + # Ensure that stormpath.yaml file will override any settings from + # previous config sources. + pass + + def test_strategies_override_08(self): + # Ensure that stormpath environment variables will override any + # settings from previous config sources. + pass + + def test_strategies_override_09(self): + # Ensure that client constructor settings will override any settings + # from previous config sources. + pass From 596d11b741a09eb13a84e1bd74de28e3dc915f35 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 7 Feb 2017 10:37:22 +0100 Subject: [PATCH 09/19] Added setLoadingStrategies 1/2 (WIP). --- tests/test_loader.py | 73 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 66 insertions(+), 7 deletions(-) diff --git a/tests/test_loader.py b/tests/test_loader.py index d94288c..67307c5 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -108,16 +108,75 @@ class OverridingStrategiesTest(TestCase): def setUp(self): self.client_config = {} + self.post_processing_strategies = [ + # Post-processing: If the key client.apiKey.file isn't + # empty, then a apiKey.properties file should be loaded + # from that path. + LoadAPIKeyFromConfigStrategy(), + ] + self.validation_strategies = [ + # Post-processing: Validation + ValidateClientConfigStrategy() + ] + + def setLoadingStrategies(self, assets): + # Our custom strategy loader builder. The asses argument should be a + # tuple with boolean values for enabling/disabling assets. + + self.assertEqual(type(assets), tuple) + self.assertEqual(len(assets), 6) + + load_strategies = [ + # 1. Default configuration. + LoadFileConfigStrategy( + 'tests/assets/default_config.yml' if assets[0] else 'empty', + must_exist=True), + + # 2. apiKey.properties file from ~/.stormpath directory. + LoadAPIKeyConfigStrategy( + 'tests/assets/apiKey.properties' if assets[1] else 'empty'), + + # 3. stormpath.[json or yaml] file from ~/.stormpath + # directory. + LoadFileConfigStrategy( + 'tests/assets/stormpath.yml' if assets[2] else 'empty'), + + # 4. apiKey.properties file from application directory. + LoadAPIKeyConfigStrategy( + 'tests/assets/no_apiKey.properties' if assets[3] else 'empty'), + + # 5. stormpath.[json or yaml] file from application + # directory. + LoadFileConfigStrategy( + 'tests/assets/stormpath.json' if assets[4] else 'empty'), + + # 6. Environment variables. + LoadEnvConfigStrategy(prefix='STORMPATH' if assets[5] else 'empty'), + + # 7. Configuration provided through the SDK client + # constructor. + ExtendConfigStrategy(extend_with=self.client_config) + ] + return load_strategies + def test_strategies_override_01(self): # Ensure that original config file is loaded. - # cl = ConfigLoader( - # self.load_strategies, - # self.post_processing_strategies, - # self.validation_strategies - # ) - # config = cl.load() - pass + # Enable only the first asset. + load_strategies = self.setLoadingStrategies( + (True, False, False, False, False, False)) + cl = ConfigLoader( + load_strategies, + self.post_processing_strategies, + self.validation_strategies + ) + + # Ensure that config file was loaded and an error was raised, since + # our testing asset does not have apiKey credentials. + with self.assertRaises(ValueError) as error: + config = cl.load() + self.assertEqual( + error.exception.message, 'API key ID and secret are required.') def test_strategies_override_02(self): # Ensure that apiKey.properties file from HOME directory will override From fc8202eabf0af77a8e373a69c0b1672203aaa035 Mon Sep 17 00:00:00 2001 From: "sasa.kalaba" Date: Tue, 7 Feb 2017 15:07:54 +0100 Subject: [PATCH 10/19] Added setLoadingStrategies 2/2. --- tests/assets/secondary_apiKey.properties | 2 + tests/test_loader.py | 230 ++++++++++++++++++----- 2 files changed, 182 insertions(+), 50 deletions(-) create mode 100644 tests/assets/secondary_apiKey.properties diff --git a/tests/assets/secondary_apiKey.properties b/tests/assets/secondary_apiKey.properties new file mode 100644 index 0000000..d14424b --- /dev/null +++ b/tests/assets/secondary_apiKey.properties @@ -0,0 +1,2 @@ +apiKey.id = SECONDARY_API_KEY_PROPERTIES_ID +apiKey.secret = SECONDARY_API_KEY_PROPERTIES_SECRET diff --git a/tests/test_loader.py b/tests/test_loader.py index 67307c5..32b1733 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -12,7 +12,8 @@ LoadAPIKeyFromConfigStrategy, \ LoadEnvConfigStrategy, \ LoadFileConfigStrategy, \ - ValidateClientConfigStrategy + ValidateClientConfigStrategy, \ + MoveAPIKeyToClientAPIKeyStrategy class ConfigLoaderTest(TestCase): @@ -106,114 +107,243 @@ class OverridingStrategiesTest(TestCase): This testing class will simulate the loading process for stormpath-flask. """ def setUp(self): - self.client_config = {} - self.post_processing_strategies = [ - # Post-processing: If the key client.apiKey.file isn't - # empty, then a apiKey.properties file should be loaded - # from that path. LoadAPIKeyFromConfigStrategy(), + MoveAPIKeyToClientAPIKeyStrategy() ] self.validation_strategies = [ - # Post-processing: Validation ValidateClientConfigStrategy() - ] - - def setLoadingStrategies(self, assets): - # Our custom strategy loader builder. The asses argument should be a - # tuple with boolean values for enabling/disabling assets. + ] - self.assertEqual(type(assets), tuple) - self.assertEqual(len(assets), 6) + def setLoadingStrategies(self, assets={}): + # Our custom strategy loader builder. load_strategies = [ # 1. Default configuration. LoadFileConfigStrategy( - 'tests/assets/default_config.yml' if assets[0] else 'empty', - must_exist=True), + assets.get('default_config', 'empty'), must_exist=True), # 2. apiKey.properties file from ~/.stormpath directory. - LoadAPIKeyConfigStrategy( - 'tests/assets/apiKey.properties' if assets[1] else 'empty'), + LoadAPIKeyConfigStrategy(assets.get('home_apiKey', 'empty')), - # 3. stormpath.[json or yaml] file from ~/.stormpath - # directory. - LoadFileConfigStrategy( - 'tests/assets/stormpath.yml' if assets[2] else 'empty'), + # 3. stormpath.json file from ~/.stormpath directory. + LoadFileConfigStrategy(assets.get('home_stormpath_json', 'empty')), + + # 3.1. stormpath.yaml file from ~/.stormpath directory. + LoadFileConfigStrategy(assets.get('home_stormpath_yaml', 'empty')), # 4. apiKey.properties file from application directory. - LoadAPIKeyConfigStrategy( - 'tests/assets/no_apiKey.properties' if assets[3] else 'empty'), + LoadAPIKeyConfigStrategy(assets.get('app_apiKey', 'empty')), - # 5. stormpath.[json or yaml] file from application - # directory. - LoadFileConfigStrategy( - 'tests/assets/stormpath.json' if assets[4] else 'empty'), + # 5. stormpath.json file from application directory. + LoadFileConfigStrategy(assets.get('app_stormpath_json', 'empty')), + + # 5.1. stormpath.yaml file from application directory. + LoadFileConfigStrategy(assets.get('app_stormpath_yaml', 'empty')), # 6. Environment variables. - LoadEnvConfigStrategy(prefix='STORMPATH' if assets[5] else 'empty'), + LoadEnvConfigStrategy(prefix=assets.get('env_prefix', 'empty')), - # 7. Configuration provided through the SDK client - # constructor. - ExtendConfigStrategy(extend_with=self.client_config) + # 7. Configuration provided through the SDK client constructor. + ExtendConfigStrategy(extend_with=assets.get('client_config', {})) ] return load_strategies - def test_strategies_override_01(self): - # Ensure that original config file is loaded. + def getConfig(self): + # Returns the final config. - # Enable only the first asset. - load_strategies = self.setLoadingStrategies( - (True, False, False, False, False, False)) cl = ConfigLoader( - load_strategies, + self.load_strategies, self.post_processing_strategies, self.validation_strategies ) + return cl.load() + + def test_strategies_override_01(self): + # Ensure that original config file is loaded. - # Ensure that config file was loaded and an error was raised, since + # Enable only the first asset. + self.load_strategies = self.setLoadingStrategies({ + 'default_config': 'tests/assets/default_config.yml' + }) + + # Ensure that config file was loaded and an error was raised, since # our testing asset does not have apiKey credentials. with self.assertRaises(ValueError) as error: - config = cl.load() + self.getConfig() self.assertEqual( error.exception.message, 'API key ID and secret are required.') def test_strategies_override_02(self): # Ensure that apiKey.properties file from HOME directory will override # any settings from previous config sources. - pass + + # Enable first two assets. + self.load_strategies = self.setLoadingStrategies({ + 'default_config': 'tests/assets/default_config.yml', + 'home_apiKey': 'tests/assets/apiKey.properties' + }) + config = self.getConfig() + + # Ensure that default config file was properly loaded. + self.assertEqual( + config['client']['baseUrl'], 'https://api.stormpath.com/v1') + self.assertIsNone(config['application']['name']) + + # Ensure that apiKey.properties overwrote previous api key id and + # secret. + self.assertEqual( + config['client']['apiKey']['id'], 'API_KEY_PROPERTIES_ID') + self.assertEqual( + config['client']['apiKey']['secret'], 'API_KEY_PROPERTIES_SECRET') def test_strategies_override_03(self): # Ensure that stormpath.json file from HOME stormpath directory will # override any settings from previous config sources. - pass + + # Enable first three assets. + self.load_strategies = self.setLoadingStrategies({ + 'default_config': 'tests/assets/default_config.yml', + 'home_apiKey': 'tests/assets/apiKey.properties', + 'home_stormpath_json': 'tests/assets/apiKeyApiKey.json' + }) + config = self.getConfig() + + # Ensure that json asset overwrote previous api key id and secret. + self.assertEqual( + config['client']['apiKey']['id'], 'MY_JSON_CONFIG_API_KEY_ID') + self.assertEqual( + config['client']['apiKey']['secret'], + 'MY_JSON_CONFIG_API_KEY_SECRET') + self.assertIsNone(config['client']['apiKey']['file']) def test_strategies_override_04(self): # Ensure that stormpath.yaml file from HOME stormpath directory will # override any settings from previous config sources. - pass + + # Enable first four assets. + self.load_strategies = self.setLoadingStrategies({ + 'default_config': 'tests/assets/default_config.yml', + 'home_apiKey': 'tests/assets/apiKey.properties', + 'home_stormpath_json': 'tests/assets/apiKeyApiKey.json', + 'home_stormpath_yaml': 'tests/assets/apiKeyFile.yml', + }) + config = self.getConfig() + + # Ensure that yaml asset overwrote previous api key id and secret. + self.assertEqual( + config['client']['apiKey']['id'], 'API_KEY_PROPERTIES_ID') + self.assertEqual( + config['client']['apiKey']['secret'], 'API_KEY_PROPERTIES_SECRET') + self.assertFalse('file' in config['client']['apiKey']) def test_strategies_override_05(self): - # Ensure that apiKey.properties file will override any settings from - # previous config sources. - pass + # Ensure that apiKey.properties file from app directory will override + # any settings from previous config sources. + + # Enable first five assets. + self.load_strategies = self.setLoadingStrategies({ + 'default_config': 'tests/assets/default_config.yml', + 'home_apiKey': 'tests/assets/apiKey.properties', + 'home_stormpath_json': 'tests/assets/apiKeyApiKey.json', + 'home_stormpath_yaml': 'tests/assets/apiKeyFile.yml', + 'app_apiKey': 'tests/assets/secondary_apiKey.properties', + }) + config = self.getConfig() + + # Ensure that apiKey.properties asset overwrote previous api key id + # and secret. + self.assertEqual( + config['client']['apiKey']['id'], + 'SECONDARY_API_KEY_PROPERTIES_ID') + self.assertEqual( + config['client']['apiKey']['secret'], + 'SECONDARY_API_KEY_PROPERTIES_SECRET') def test_strategies_override_06(self): # Ensure that stormpath.json file will override any settings from # previous config sources. - pass + + # Enable first six assets. + self.load_strategies = self.setLoadingStrategies({ + 'default_config': 'tests/assets/default_config.yml', + 'home_apiKey': 'tests/assets/apiKey.properties', + 'home_stormpath_json': 'tests/assets/apiKeyApiKey.json', + 'home_stormpath_yaml': 'tests/assets/apiKeyFile.yml', + 'app_apiKey': 'tests/assets/secondary_apiKey.properties', + 'app_stormpath_json': 'tests/assets/stormpath.json' + }) + config = self.getConfig() + + # Ensure that stormpath.json asset overwrote previous settings. + self.assertEqual( + config['client']['baseUrl'], 'https://api.stormpath.com/v3') + self.assertEqual(config['application']['name'], 'MY_JSON_APP') def test_strategies_override_07(self): # Ensure that stormpath.yaml file will override any settings from # previous config sources. - pass + + # Enable first seven assets. + self.load_strategies = self.setLoadingStrategies({ + 'default_config': 'tests/assets/default_config.yml', + 'home_apiKey': 'tests/assets/apiKey.properties', + 'home_stormpath_json': 'tests/assets/apiKeyApiKey.json', + 'home_stormpath_yaml': 'tests/assets/apiKeyFile.yml', + 'app_apiKey': 'tests/assets/secondary_apiKey.properties', + 'app_stormpath_json': 'tests/assets/stormpath.json', + 'app_stormpath_yaml': 'tests/assets/stormpath.yml', + }) + config = self.getConfig() + + # Ensure that stormpath.yaml asset overwrote previous settings. + self.assertEqual( + config['client']['baseUrl'], 'https://api.stormpath.com/v2') + self.assertEqual(config['application']['name'], 'MY_APP') def test_strategies_override_08(self): # Ensure that stormpath environment variables will override any # settings from previous config sources. - pass + + # Enable first eight assets. + self.load_strategies = self.setLoadingStrategies({ + 'default_config': 'tests/assets/default_config.yml', + 'home_apiKey': 'tests/assets/apiKey.properties', + 'home_stormpath_json': 'tests/assets/apiKeyApiKey.json', + 'home_stormpath_yaml': 'tests/assets/apiKeyFile.yml', + 'app_apiKey': 'tests/assets/secondary_apiKey.properties', + 'app_stormpath_json': 'tests/assets/stormpath.json', + 'app_stormpath_yaml': 'tests/assets/stormpath.yml', + 'env_prefix': 'STORMPATH' + }) + environ['STORMPATH_APPLICATION_NAME'] = 'MY_ENVIRON_APP' + config = self.getConfig() + + # Ensure that stormpath environment variables overwrote previous + # settings. + self.assertEqual(config['application']['name'], 'MY_ENVIRON_APP') def test_strategies_override_09(self): # Ensure that client constructor settings will override any settings # from previous config sources. - pass + + # Enable all assets. + self.load_strategies = self.setLoadingStrategies({ + 'default_config': 'tests/assets/default_config.yml', + 'home_apiKey': 'tests/assets/apiKey.properties', + 'home_stormpath_json': 'tests/assets/apiKeyApiKey.json', + 'home_stormpath_yaml': 'tests/assets/apiKeyFile.yml', + 'app_apiKey': 'tests/assets/secondary_apiKey.properties', + 'app_stormpath_json': 'tests/assets/stormpath.json', + 'app_stormpath_yaml': 'tests/assets/stormpath.yml', + 'env_prefix': 'STORMPATH', + 'client_config': { + 'application': { + 'name': 'CLIENT_CONFIG_APP' + } + } + }) + config = self.getConfig() + + # Ensure that client config asset overwrote previous settings. + self.assertEqual(config['application']['name'], 'CLIENT_CONFIG_APP') From f5d77ab3670cc62660ae4341e75141a8839f617b Mon Sep 17 00:00:00 2001 From: "sasa.kalaba" Date: Tue, 7 Feb 2017 15:13:11 +0100 Subject: [PATCH 11/19] Formatted test_loader for pep8. --- tests/test_loader.py | 38 ++++++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/tests/test_loader.py b/tests/test_loader.py index 32b1733..a794a9f 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -3,17 +3,17 @@ from os import environ from unittest import TestCase - from mock import patch - from stormpath_config.loader import ConfigLoader -from stormpath_config.strategies import ExtendConfigStrategy, \ - LoadAPIKeyConfigStrategy, \ - LoadAPIKeyFromConfigStrategy, \ - LoadEnvConfigStrategy, \ - LoadFileConfigStrategy, \ - ValidateClientConfigStrategy, \ +from stormpath_config.strategies import ( + ExtendConfigStrategy, + LoadAPIKeyConfigStrategy, + LoadAPIKeyFromConfigStrategy, + LoadEnvConfigStrategy, + LoadFileConfigStrategy, + ValidateClientConfigStrategy, MoveAPIKeyToClientAPIKeyStrategy +) class ConfigLoaderTest(TestCase): @@ -33,7 +33,8 @@ def setUp(self): self.load_strategies = [ # 1. Default configuration. - LoadFileConfigStrategy('tests/assets/default_config.yml', must_exist=True), + LoadFileConfigStrategy( + 'tests/assets/default_config.yml', must_exist=True), # 2. apiKey.properties file from ~/.stormpath directory. LoadAPIKeyConfigStrategy('tests/assets/apiKey.properties'), @@ -78,13 +79,22 @@ def test_empty_config_loader(self): 'STORMPATH_APPLICATION_NAME': 'My app', }) def test_config_loader(self): - cl = ConfigLoader(self.load_strategies, self.post_processing_strategies, self.validation_strategies) + cl = ConfigLoader( + self.load_strategies, + self.post_processing_strategies, + self.validation_strategies + ) config = cl.load() - self.assertEqual(config['client']['apiKey']['id'], 'CLIENT_CONFIG_API_KEY_ID') - self.assertEqual(config['client']['apiKey']['secret'], 'CLIENT_CONFIG_API_KEY_SECRET') - self.assertEqual(config['client']['cacheManager']['defaultTtl'], 302) - self.assertEqual(config['client']['cacheManager']['defaultTti'], 303) + self.assertEqual( + config['client']['apiKey']['id'], 'CLIENT_CONFIG_API_KEY_ID') + self.assertEqual( + config['client']['apiKey']['secret'], + 'CLIENT_CONFIG_API_KEY_SECRET') + self.assertEqual( + config['client']['cacheManager']['defaultTtl'], 302) + self.assertEqual( + config['client']['cacheManager']['defaultTti'], 303) self.assertEqual(config['application']['name'], 'CLIENT_CONFIG_APP') def test_stormpath_key_loader(self): From 86e609063a9240370caf4b8994a7be63a5b3af7a Mon Sep 17 00:00:00 2001 From: "sasa.kalaba" Date: Tue, 7 Feb 2017 15:37:38 +0100 Subject: [PATCH 12/19] Added MoveAPIKeyToClientAPIKeyStrategy to remaining tests. --- tests/strategies/test_edge_cases.py | 12 ++++++++---- tests/strategies/test_load_apikey_from_config.py | 6 ++++-- tests/test_loader.py | 1 + 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/tests/strategies/test_edge_cases.py b/tests/strategies/test_edge_cases.py index ed2cffe..0a77b0d 100644 --- a/tests/strategies/test_edge_cases.py +++ b/tests/strategies/test_edge_cases.py @@ -9,7 +9,8 @@ LoadAPIKeyFromConfigStrategy, \ LoadEnvConfigStrategy, \ LoadFileConfigStrategy, \ - ValidateClientConfigStrategy + ValidateClientConfigStrategy, \ + MoveAPIKeyToClientAPIKeyStrategy class EdgeCasesTest(TestCase): @@ -35,7 +36,8 @@ def test_config_extending(self): # constructor. ExtendConfigStrategy(extend_with=client_config) ] - post_processing_strategies = [LoadAPIKeyFromConfigStrategy()] + post_processing_strategies = [ + LoadAPIKeyFromConfigStrategy(), MoveAPIKeyToClientAPIKeyStrategy()] validation_strategies = [ValidateClientConfigStrategy()] cl = ConfigLoader(load_strategies, post_processing_strategies, validation_strategies) @@ -66,7 +68,8 @@ def test_api_key_file_from_config_with_lesser_loading_order(self): LoadEnvConfigStrategy(prefix='STORMPATH'), ExtendConfigStrategy(extend_with={}) ] - post_processing_strategies = [LoadAPIKeyFromConfigStrategy()] + post_processing_strategies = [ + LoadAPIKeyFromConfigStrategy(), MoveAPIKeyToClientAPIKeyStrategy()] validation_strategies = [ValidateClientConfigStrategy()] cl = ConfigLoader(load_strategies, post_processing_strategies, validation_strategies) @@ -101,7 +104,8 @@ def test_api_key_from_config_with_lesser_loading_order(self): # constructor. ExtendConfigStrategy(extend_with=client_config) ] - post_processing_strategies = [LoadAPIKeyFromConfigStrategy()] + post_processing_strategies = [ + LoadAPIKeyFromConfigStrategy(), MoveAPIKeyToClientAPIKeyStrategy()] validation_strategies = [ValidateClientConfigStrategy()] cl = ConfigLoader(load_strategies, post_processing_strategies, validation_strategies) diff --git a/tests/strategies/test_load_apikey_from_config.py b/tests/strategies/test_load_apikey_from_config.py index b4872ba..d9dcba7 100644 --- a/tests/strategies/test_load_apikey_from_config.py +++ b/tests/strategies/test_load_apikey_from_config.py @@ -6,7 +6,8 @@ LoadEnvConfigStrategy, ExtendConfigStrategy, LoadAPIKeyFromConfigStrategy, - ValidateClientConfigStrategy) + ValidateClientConfigStrategy, + MoveAPIKeyToClientAPIKeyStrategy) class LoadAPIKeyFromConfigStrategyTest(TestCase): @@ -26,7 +27,8 @@ def test_api_key_from_config_with_lesser_loading_order(self): # 7. Configuration provided through the SDK client constructor. ExtendConfigStrategy(extend_with={}) ] - post_processing_strategies = [LoadAPIKeyFromConfigStrategy()] + post_processing_strategies = [ + LoadAPIKeyFromConfigStrategy(), MoveAPIKeyToClientAPIKeyStrategy()] validation_strategies = [ValidateClientConfigStrategy()] cl = ConfigLoader( diff --git a/tests/test_loader.py b/tests/test_loader.py index a794a9f..285caa6 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -62,6 +62,7 @@ def setUp(self): # empty, then a apiKey.properties file should be loaded # from that path. LoadAPIKeyFromConfigStrategy(), + MoveAPIKeyToClientAPIKeyStrategy() ] self.validation_strategies = [ # Post-processing: Validation From 5168dd788cfd2c1a56e87bc00bfc22161d51f4d5 Mon Sep 17 00:00:00 2001 From: "sasa.kalaba" Date: Sun, 26 Feb 2017 20:58:36 +0100 Subject: [PATCH 13/19] Added MoveStormpathSettingsToStormpathConfig. --- stormpath_config/strategies/__init__.py | 1 + ..._stormpath_settings_to_stormpath_config.py | 78 +++++++++ ..._stormpath_settings_to_stormpath_config.py | 160 ++++++++++++++++++ 3 files changed, 239 insertions(+) create mode 100644 stormpath_config/strategies/move_stormpath_settings_to_stormpath_config.py create mode 100644 tests/strategies/test_move_stormpath_settings_to_stormpath_config.py diff --git a/stormpath_config/strategies/__init__.py b/stormpath_config/strategies/__init__.py index f3dd6a5..cdeaa84 100644 --- a/stormpath_config/strategies/__init__.py +++ b/stormpath_config/strategies/__init__.py @@ -10,3 +10,4 @@ from .load_file_path import LoadFilePathStrategy from .validate_client_config import ValidateClientConfigStrategy from .move_apikey_to_client import MoveAPIKeyToClientAPIKeyStrategy +from .move_stormpath_settings_to_stormpath_config import MoveStormpathSettingsToStormpathConfig diff --git a/stormpath_config/strategies/move_stormpath_settings_to_stormpath_config.py b/stormpath_config/strategies/move_stormpath_settings_to_stormpath_config.py new file mode 100644 index 0000000..b341194 --- /dev/null +++ b/stormpath_config/strategies/move_stormpath_settings_to_stormpath_config.py @@ -0,0 +1,78 @@ +from ..helpers import _extend_dict +from .load_apikey_from_config import LoadAPIKeyFromConfigStrategy + + +class MoveStormpathSettingsToStormpathConfig(object): + """ + Checks the outer config and retrieves values whose keys start with + 'STORMPATH' prefix, and stores them in the configuration object properly. + """ + STORMPATH_PREFIX = 'STORMPATH' + KEY_DELIMITER = '*' + MAPPINGS = { + 'APPLICATION': 'application', + 'API_KEY_ID': 'client*apiKey*id', + 'API_KEY_SECRET': 'client*apiKey*secret', + 'API_KEY_FILE': 'client*apiKey*file', + 'ENABLE_FACEBOOK': 'web*social*facebook*enabled', + 'ENABLE_GOOGLE': 'web*social*google*enabled', + 'FACEBOOK_LOGIN_URL': 'web*social*facebook*login_url', + 'GOOGLE_LOGIN_URL': 'web*social*google*login_url', + 'CACHE': 'cache', + 'BASE_TEMPLATE': 'base_template', + 'COOKIE_DOMAIN': 'cookie*domain', + 'COOKIE_DURATION': 'cookie*duration', + } + + def set_key(self, config, key, value): + """ + We use this method to properly map values into stormpath config object. + Some values are nested, in which case we create sub-dictionaries if + needed. + """ + subkeys = key.split(self.KEY_DELIMITER) + if len(subkeys) > 1: + attr = subkeys.pop(-1) + subdict = config + for key in subkeys: + subdict.setdefault(key, {}) + subdict = subdict[key] + subdict[attr] = value + else: + config[key] = value + + def get_updated_config(self, config): + """ + Creates a dictionary with new values whose keys are properly formated + to fit into stormpath config object. + """ + updated_config = {} + for key, value in config.items(): + if key.startswith(self.STORMPATH_PREFIX): + stormpath_key = self.MAPPINGS.get(key.split('STORMPATH_')[1]) + + # Check the format of application information. + if stormpath_key == 'application': + if 'https://api.stormpath.com/v1/applications' in value: + stormpath_key = 'application*href' + else: + stormpath_key = 'application*name' + + if stormpath_key: + self.set_key(updated_config, stormpath_key, value) + + if 'STORMPATH_API_KEY_FILE' in config.keys(): + load_api_from_config = LoadAPIKeyFromConfigStrategy() + load_api_from_config.process(updated_config) + + return updated_config + + def process(self, config=None): + if config is None: + config = {} + + if config.get('stormpath'): + updated_config = self.get_updated_config(config) + _extend_dict(config['stormpath'], updated_config) + + return config diff --git a/tests/strategies/test_move_stormpath_settings_to_stormpath_config.py b/tests/strategies/test_move_stormpath_settings_to_stormpath_config.py new file mode 100644 index 0000000..ec5b491 --- /dev/null +++ b/tests/strategies/test_move_stormpath_settings_to_stormpath_config.py @@ -0,0 +1,160 @@ +from unittest import TestCase +from stormpath_config.strategies import MoveStormpathSettingsToStormpathConfig + + +class MoveStormpathSettingsToStormpathConfigTest(TestCase): + def setUp(self): + stormpath_config = { + 'client': { + 'apiKey': {'id': 'api key id', 'secret': 'api key secret'}, + 'cacheManager': {'defaultTtl': 300, 'defaultTti': 300} + }, + 'web': { + 'social': { + 'facebook': { + 'scope': 'email', + 'uri': '/callbacks/facebook'}, + 'google': { + 'scope': 'email profile', + 'uri': '/callbacks/google'} + } + } + } + self.config = {'stormpath': stormpath_config} + + def test_regular_mapping(self): + """ + Ensures that settings with 'STORMPATH' prefix are properly + copied to stormpath config object. + """ + self.config['STORMPATH_BASE_TEMPLATE'] = 'flask_stormpath/base.html' + + move_stormpath_settings = MoveStormpathSettingsToStormpathConfig() + move_stormpath_settings.process(self.config) + + self.assertEqual( + self.config['stormpath']['base_template'], + 'flask_stormpath/base.html') + + def test_multiple_key_mapping(self): + """ + Ensures that multiple key settings with 'STORMPATH' prefix are + properly copied to stormpath config object. + """ + self.config['STORMPATH_ENABLE_FACEBOOK'] = False + + move_stormpath_settings = MoveStormpathSettingsToStormpathConfig() + move_stormpath_settings.process(self.config) + + self.assertEqual( + self.config['stormpath']['web']['social']['facebook']['enabled'], + False) + + # Ensure that other values form social_facebook are unaltered. + self.assertEqual( + self.config['stormpath']['web']['social']['facebook']['scope'], + 'email') + self.assertEqual( + self.config['stormpath']['web']['social']['facebook']['uri'], + '/callbacks/facebook') + + def test_empty_key_mapping(self): + """ + Ensures that settings with 'STORMPATH' prefix not specified in + MAPPINGS are ignored. + """ + self.config['STORMPATH_FOO'] = 'bar' + + move_stormpath_settings = MoveStormpathSettingsToStormpathConfig() + move_stormpath_settings.process(self.config) + + self.assertNotIn('foo', self.config['stormpath']) + + def test_non_stormpath_key_mapping(self): + """ + Ensures that settings without 'STORMPATH' prefix are skipped. + """ + self.config['FOO'] = 'bar' + + move_stormpath_settings = MoveStormpathSettingsToStormpathConfig() + move_stormpath_settings.process(self.config) + + self.assertNotIn('foo', self.config['stormpath']) + + def test_default_values(self): + """ + Ensures that creating new values will override old values. + """ + self.config['stormpath']['base_template'] = ( + 'flask_stormpath/default_base.html') + self.config['stormpath']['web']['social']['facebook']['enabled'] = True + + self.config['STORMPATH_BASE_TEMPLATE'] = 'flask_stormpath/base.html' + self.config['STORMPATH_ENABLE_FACEBOOK'] = False + + move_stormpath_settings = MoveStormpathSettingsToStormpathConfig() + move_stormpath_settings.process(self.config) + + self.assertEqual( + self.config['stormpath']['base_template'], + 'flask_stormpath/base.html') + self.assertEqual( + self.config['stormpath']['web']['social']['facebook']['enabled'], + False) + + def test_no_stormpath_config(self): + """ + Ensure that missing stormpath config object won't break the + application. + """ + self.config['STORMPATH_BASE_TEMPLATE'] = 'flask_stormpath/base.html' + self.config.pop('stormpath') + + move_stormpath_settings = MoveStormpathSettingsToStormpathConfig() + move_stormpath_settings.process(self.config) + + self.assertEqual( + self.config, + {'STORMPATH_BASE_TEMPLATE': 'flask_stormpath/base.html'}) + + def test_load_api_key_from_config_strategy(self): + """ + Ensure that LoadAPIKeyFromConfigStrategy was called if api_key_file + setting was specified with STORMPATH prefix. + """ + self.config[ + 'STORMPATH_API_KEY_FILE'] = 'tests/assets/apiKey.properties' + + move_stormpath_settings = MoveStormpathSettingsToStormpathConfig() + move_stormpath_settings.process(self.config) + + self.assertEqual( + self.config['stormpath']['client']['apiKey'], + {'id': 'API_KEY_PROPERTIES_ID', + 'secret': 'API_KEY_PROPERTIES_SECRET'}) + + def test_parsing_application_name_href(self): + """ + Ensure that our strategy can properly differentiate between name and + href stored in STORMPATH_APPLICATION. + """ + + # Ensure that application name is stored as name. + self.config['STORMPATH_APPLICATION'] = 'app_name' + + move_stormpath_settings = MoveStormpathSettingsToStormpathConfig() + move_stormpath_settings.process(self.config) + + self.assertEqual( + self.config['stormpath']['application']['name'], 'app_name') + + # Ensure that application uri is stored as href. + self.config['STORMPATH_APPLICATION'] = ( + 'https://api.stormpath.com/v1/applications/foobar') + + move_stormpath_settings = MoveStormpathSettingsToStormpathConfig() + move_stormpath_settings.process(self.config) + + self.assertEqual( + self.config['stormpath']['application']['href'], + 'https://api.stormpath.com/v1/applications/foobar') From 8c36d68dcae7cd207825eccc92373a464f7e96f3 Mon Sep 17 00:00:00 2001 From: "sasa.kalaba" Date: Sun, 26 Feb 2017 21:00:05 +0100 Subject: [PATCH 14/19] Added ConfigurationError. --- stormpath_config/errors.py | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 stormpath_config/errors.py diff --git a/stormpath_config/errors.py b/stormpath_config/errors.py new file mode 100644 index 0000000..106f7ad --- /dev/null +++ b/stormpath_config/errors.py @@ -0,0 +1,8 @@ +"""Custom errors.""" + + +class ConfigurationError(Exception): + """ + This exception is raised if a user has misconfigured Flask-Stormpath. + """ + pass From 504230d3a0455c0ef62e435c53e30b9c34425d8b Mon Sep 17 00:00:00 2001 From: "sasa.kalaba" Date: Tue, 28 Feb 2017 14:10:17 +0100 Subject: [PATCH 15/19] Added configuration validation to EnrichIntegrationFromRemoteConfigStrategy. >> also moved application validation from ValidateClientConfigStrategy --- .../enrich_integration_from_remote_config.py | 96 +++++++ .../strategies/validate_client_config.py | 8 - ...t_enrich_integration_from_remote_config.py | 261 +++++++++++++++++- 3 files changed, 351 insertions(+), 14 deletions(-) diff --git a/stormpath_config/strategies/enrich_integration_from_remote_config.py b/stormpath_config/strategies/enrich_integration_from_remote_config.py index 2650e35..67d2851 100644 --- a/stormpath_config/strategies/enrich_integration_from_remote_config.py +++ b/stormpath_config/strategies/enrich_integration_from_remote_config.py @@ -1,5 +1,7 @@ from datetime import timedelta +from stormpath_config.errors import ConfigurationError +from enrich_client_from_remote_config import _resolve_application_by_name from ..helpers import _extend_dict, to_camel_case @@ -162,6 +164,99 @@ class EnrichIntegrationFromRemoteConfigStrategy(object): def __init__(self, client_factory): self.client_factory = client_factory + def social_enabled_and_empty(self, config): + if not config or not isinstance(config, dict): + return False + return config.get('enabled') and not all([ + config.get('clientId'), + config.get('clientSecret') + ]) + + def validate(self, config): + """ + Ensure the user-specified settings are valid. + This will raise a ConfigurationError if anything mandatory is not + specified. + + :param dict config: The Flask app config. + """ + client = self.client_factory(config) + + # Check if application information is present. + application = config.get('application') + if not application: + raise ConfigurationError('Application cannot be empty.') + + href = application.get('href') + name = application.get('name') + + if href: + if '/applications/' not in href: + raise ConfigurationError( + 'Application HREF "%s" is not a valid Stormpath ' % href + + 'Application HREF.' + ) + elif name: + href = _resolve_application_by_name(client, config, name) + else: + raise ConfigurationError( + 'You must specify application name or href.') + application = client.applications.get(href) + + + # Check if google social information is present. + google_config = config['web']['social'].get('google') + + if not google_config or self.social_enabled_and_empty(google_config): + raise ConfigurationError( + 'You must define your Google app settings.') + + # Check if facebook social information is present. + facebook_config = config['web']['social'].get('facebook') + + if not facebook_config or self.social_enabled_and_empty(facebook_config): + raise ConfigurationError( + 'You must define your Facebook app settings.') + + # Check if default account store is present. + if ( + config['web']['register']['enabled'] and + not application.default_account_store_mapping): + raise ConfigurationError( + "No default account store is mapped to the specified " + "application. A default account store is required for " + "registration.") + + # Ensure that autologin and verify email cannot be active at the same + # time. + if all([config['web']['register']['autoLogin'], + config['web']['verifyEmail']['enabled']]): + raise ConfigurationError( + "Invalid configuration: stormpath.web.register.autoLogin " + "is true, but the default account store of the " + "specified application has the email verification " + "workflow enabled. Auto login is only possible if email " + "verification is disabled. " + "Please disable this workflow on this application's default " + "account store.") + + # Check if cookie information is present. + cookie = config.get('cookie') + + # Check cookie settings. + if not cookie or not isinstance(cookie, dict): + raise ConfigurationError('Cookie settings cannot be empty.') + + # Check cookie domain. + if cookie.get('domain') and not isinstance( + config['cookie']['domain'], str): + raise ConfigurationError('Cookie domain must be a string.') + + # Check cookie duration. + if cookie.get('duration') and not isinstance( + config['cookie']['duration'], timedelta): + raise ConfigurationError('Cookie duration must be a string.') + def process(self, config): if config.get('skipRemoteConfig'): return config @@ -179,4 +274,5 @@ def process(self, config): if policy_config: _extend_dict(config, policy_config) + self.validate(config) return config diff --git a/stormpath_config/strategies/validate_client_config.py b/stormpath_config/strategies/validate_client_config.py index 527694c..9edd6d6 100644 --- a/stormpath_config/strategies/validate_client_config.py +++ b/stormpath_config/strategies/validate_client_config.py @@ -21,14 +21,6 @@ def process(self, config=None): if not apiKey.get('id') or not apiKey.get('secret'): raise ValueError('API key ID and secret are required.') - application = config.get('application') - if not application: - raise ValueError('Application cannot be empty.') - - href = application.get('href') - if href and '/applications/' not in href: - raise ValueError('Application HREF "%s" is not a valid Stormpath Application HREF.' % href) - web_spa = config.get('web', {}).get('spa', {}) if web_spa and web_spa.get('enabled') and web_spa.get('view') is None: raise ValueError('SPA mode is enabled but stormpath.web.spa.view isn\'t ' diff --git a/tests/strategies/test_enrich_integration_from_remote_config.py b/tests/strategies/test_enrich_integration_from_remote_config.py index 597c5f9..3843e15 100644 --- a/tests/strategies/test_enrich_integration_from_remote_config.py +++ b/tests/strategies/test_enrich_integration_from_remote_config.py @@ -1,26 +1,41 @@ from unittest import TestCase +from datetime import timedelta from stormpath_config.strategies import EnrichIntegrationFromRemoteConfigStrategy +from stormpath_config.errors import ConfigurationError from ..base import Application, Client class EnrichIntegrationFromRemoteConfigStrategyTest(TestCase): def setUp(self): - self.application = Application('My named application', 'https://api.stormpath.com/v1/applications/a') - - def test_enrich_client_from_remote_config(self): def _create_client_from_config(config): return Client([self.application]) - config = { + self.application = Application( + 'My named application', + 'https://api.stormpath.com/v1/applications/a') + self.config = { 'application': { 'href': 'https://api.stormpath.com/v1/applications/a' + }, + 'web': { + 'social': {'facebook': {'enabled': False}}, + 'register': { + 'enabled': True, + 'autoLogin': False + } + }, + 'cookie': { + 'domain': 'cookie_domain', + 'duration': timedelta(minutes=30) } } + self.ecfrcs = EnrichIntegrationFromRemoteConfigStrategy( + client_factory=_create_client_from_config) - ecfrcs = EnrichIntegrationFromRemoteConfigStrategy(client_factory=_create_client_from_config) - config = ecfrcs.process(config) + def test_enrich_client_from_remote_config(self): + config = self.ecfrcs.process(self.config) self.assertTrue('oAuthPolicy' in config['application']) self.assertEqual(config['application']['oAuthPolicy'], { @@ -40,6 +55,7 @@ def _create_client_from_config(config): 'maxLength': 100 }) self.assertEqual(config['web']['social'], { + 'facebook': {'enabled': False}, 'google': { 'providerId': 'google', 'clientId': 'id', @@ -52,6 +68,7 @@ def _create_client_from_config(config): }) self.assertEqual(config['web'], { 'social': { + 'facebook': {'enabled': False}, 'google': { 'providerId': 'google', 'clientId': 'id', @@ -65,4 +82,236 @@ def _create_client_from_config(config): 'changePassword': {'enabled': True}, 'forgotPassword': {'enabled': True}, 'verifyEmail': {'enabled': False}, + 'register': {'autoLogin': False, 'enabled': True} }) + + +class ValidateTest(EnrichIntegrationFromRemoteConfigStrategyTest): + """Ensure that our config passes final validation.""" + + def setUp(self): + super(ValidateTest, self).setUp() + self.config['web']['social'] = { + 'google': {'enabled': False}, + 'facebook': {'enabled': False} + } + self.config['web']['register']['autoLogin'] = False + self.config['web']['verifyEmail'] = {'enabled': False} + + def test_social_enabled_and_emtpy(self): + # Ensure that social_enabled_and empty returns proper boolean values. + + social_config = { + 'enabled': True, + 'clientId': 'xxx', + 'clientSecret': 'yyy' + } + + # Empty config. + self.assertFalse(self.ecfrcs.social_enabled_and_empty({})) + + # Invalid config value. + self.assertFalse(self.ecfrcs.social_enabled_and_empty(True)) + + # Social enabled with id and secret. + self.assertFalse(self.ecfrcs.social_enabled_and_empty(social_config)) + + # Social enabled but missing secret. + social_config.pop('clientSecret') + self.assertTrue(self.ecfrcs.social_enabled_and_empty(social_config)) + + def test_application(self): + # Ensure that validation fails if application settings are missing or + # invalid. + + # Invalid application href. + self.config['application']['href'] = 'https://api.stormpath.com/v1/a' + with self.assertRaises(ConfigurationError) as error: + self.ecfrcs.validate(self.config) + self.assertEqual( + error.exception.message, + 'Application HREF "https://api.stormpath.com/v1/a" is not a ' + + 'valid Stormpath Application HREF.') + + # No application name or href. + self.config['application'] = {} + with self.assertRaises(ConfigurationError) as error: + self.ecfrcs.validate(self.config) + self.assertEqual( + error.exception.message, 'Application cannot be empty.') + + # No application settings. + self.config.pop('application') + with self.assertRaises(ConfigurationError) as error: + self.ecfrcs.validate(self.config) + self.assertEqual( + error.exception.message, 'Application cannot be empty.') + + # Ensure that we can resolve application by name. + self.config['application'] = {'name': 'My named application'} + self.ecfrcs.validate(self.config) + + def test_google_settings(self): + # Ensure that validation fails if google config is invalid. + + # Turn off facebook social. + self.config['web']['social'] = {'facebook': {'enabled': False}} + + # Empty google settings. + self.config['web']['social']['google'] = {} + with self.assertRaises(ConfigurationError) as error: + self.ecfrcs.validate(self.config) + self.assertEqual( + error.exception.message, + 'You must define your Google app settings.' + ) + + # Enabled google settings, but otherwise empty. + self.config['web']['social']['google']['enabled'] = True + with self.assertRaises(ConfigurationError) as error: + self.ecfrcs.validate(self.config) + self.assertEqual( + error.exception.message, + 'You must define your Google app settings.' + ) + + # Enabled and clientId provided, but secret missing. + self.config['web']['social']['google']['clientId'] = 'xxx' + with self.assertRaises(ConfigurationError) as error: + self.ecfrcs.validate(self.config) + self.assertEqual( + error.exception.message, + 'You must define your Google app settings.' + ) + + # Now that we've configured things properly, it should work. + self.config['web']['social']['google']['clientSecret'] = 'yyy' + self.ecfrcs.validate(self.config) + + def test_facebook_settings(self): + # Ensure that validation fails if facebook config is invalid. + + # Turn off google social. + self.config['web']['social'] = {'google': {'enabled': False}} + + # No facebook settings. + with self.assertRaises(ConfigurationError) as error: + self.ecfrcs.validate(self.config) + self.assertEqual( + error.exception.message, + 'You must define your Facebook app settings.' + ) + + # Empty facebook settings. + self.config['web']['social']['facebook'] = {} + with self.assertRaises(ConfigurationError) as error: + self.ecfrcs.validate(self.config) + self.assertEqual( + error.exception.message, + 'You must define your Facebook app settings.' + ) + + # Enabled facebook settings, but otherwise empty. + self.config['web']['social']['facebook']['enabled'] = True + with self.assertRaises(ConfigurationError) as error: + self.ecfrcs.validate(self.config) + self.assertEqual( + error.exception.message, + 'You must define your Facebook app settings.' + ) + + # Enabled and clientId provided, but secret missing. + self.config['web']['social']['facebook']['clientId'] = 'xxx' + with self.assertRaises(ConfigurationError) as error: + self.ecfrcs.validate(self.config) + self.assertEqual( + error.exception.message, + 'You must define your Facebook app settings.' + ) + + # Now that we've configured things properly, it should work. + self.config['web']['social']['facebook']['clientSecret'] = 'yyy' + self.ecfrcs.validate(self.config) + + def test_cookie_settings(self): + # Ensure that validation fails if cookie settings are invalid. + + # Missing cookie settings. + self.config.pop('cookie') + with self.assertRaises(ConfigurationError) as error: + self.ecfrcs.validate(self.config) + self.assertEqual( + error.exception.message, 'Cookie settings cannot be empty.') + + # Empty cookie settings. + self.config['cookie'] = {} + with self.assertRaises(ConfigurationError) as error: + self.ecfrcs.validate(self.config) + self.assertEqual( + error.exception.message, 'Cookie settings cannot be empty.') + + # Invalid cookie domain. + self.config['cookie'] = {'domain': 55} + with self.assertRaises(ConfigurationError) as error: + self.ecfrcs.validate(self.config) + self.assertEqual( + error.exception.message, 'Cookie domain must be a string.') + + # Invalid cookie duration. + self.config['cookie'] = { + 'domain': '55', + 'duration': 55 + } + + with self.assertRaises(ConfigurationError) as error: + self.ecfrcs.validate(self.config) + self.assertEqual( + error.exception.message, 'Cookie duration must be a string.') + + # Now that we've configured things properly, it should work. + self.config['cookie'] = { + 'domain': 'cookie_domain', + 'duration': timedelta(minutes=1) + } + self.ecfrcs.validate(self.config) + + def test_verify_email_autologin(self): + # Ensure that validation fails if both autologin and email verification + # are enabled. + + # Turn on verify email and autologin. + self.config['web']['register']['autoLogin'] = True + self.config['web']['verifyEmail']['enabled'] = True + + with self.assertRaises(ConfigurationError) as error: + self.ecfrcs.validate(self.config) + self.assertEqual( + error.exception.message, + 'Invalid configuration: stormpath.web.register.autoLogin is' + + ' true, but the default account store of the specified' + + ' application has the email verification workflow enabled.' + + ' Auto login is only possible if email verification is' + + ' disabled. Please disable this workflow on this' + + ' application\'s default account store.') + + # Turn off one of the settings, and configuration should be valid. + self.config['web']['register']['autoLogin'] = False + self.ecfrcs.validate(self.config) + + def test_register_default_account_store(self): + # Ensure that validation fails if register view is enabled, but default + # account store mapping is missing. + + self.application.default_account_store_mapping = False + with self.assertRaises(ConfigurationError) as error: + self.ecfrcs.validate(self.config) + self.assertEqual( + error.exception.message, + 'No default account store is mapped to the specified ' + + 'application. A default account store is required for ' + + 'registration.' + ) + + # Now that we've configured things properly, it should work. + self.application.default_account_store_mapping = object() + self.ecfrcs.validate(self.config) From 49519a058b32d40c1689049b25ae3e0746c78b4a Mon Sep 17 00:00:00 2001 From: "sasa.kalaba" Date: Tue, 28 Feb 2017 17:01:55 +0100 Subject: [PATCH 16/19] Minor updates from code review. --- stormpath_config/errors.py | 2 +- stormpath_config/loader.py | 2 -- stormpath_config/strategies/__init__.py | 2 +- ..._stormpath_settings_to_stormpath_config.py | 4 ++-- ..._stormpath_settings_to_stormpath_config.py | 23 ++++++++++--------- tests/test_loader.py | 14 ----------- 6 files changed, 16 insertions(+), 31 deletions(-) diff --git a/stormpath_config/errors.py b/stormpath_config/errors.py index 106f7ad..799c11a 100644 --- a/stormpath_config/errors.py +++ b/stormpath_config/errors.py @@ -3,6 +3,6 @@ class ConfigurationError(Exception): """ - This exception is raised if a user has misconfigured Flask-Stormpath. + This exception is raised if a user has stormpath configuration. """ pass diff --git a/stormpath_config/loader.py b/stormpath_config/loader.py index c1e2a9e..1988a87 100644 --- a/stormpath_config/loader.py +++ b/stormpath_config/loader.py @@ -38,8 +38,6 @@ def load(self): for strategy in self.post_processing_strategies: config = strategy.process(config) - config = _extend_dict(config, config.pop('stormpath', {})) - for strategy in self.validation_strategies: config = strategy.process(config) diff --git a/stormpath_config/strategies/__init__.py b/stormpath_config/strategies/__init__.py index cdeaa84..2475df8 100644 --- a/stormpath_config/strategies/__init__.py +++ b/stormpath_config/strategies/__init__.py @@ -10,4 +10,4 @@ from .load_file_path import LoadFilePathStrategy from .validate_client_config import ValidateClientConfigStrategy from .move_apikey_to_client import MoveAPIKeyToClientAPIKeyStrategy -from .move_stormpath_settings_to_stormpath_config import MoveStormpathSettingsToStormpathConfig +from .move_stormpath_settings_to_stormpath_config import MoveStormpathSettingsToStormpathConfigStrategy diff --git a/stormpath_config/strategies/move_stormpath_settings_to_stormpath_config.py b/stormpath_config/strategies/move_stormpath_settings_to_stormpath_config.py index b341194..ec1d81f 100644 --- a/stormpath_config/strategies/move_stormpath_settings_to_stormpath_config.py +++ b/stormpath_config/strategies/move_stormpath_settings_to_stormpath_config.py @@ -2,7 +2,7 @@ from .load_apikey_from_config import LoadAPIKeyFromConfigStrategy -class MoveStormpathSettingsToStormpathConfig(object): +class MoveStormpathSettingsToStormpathConfigStrategy(object): """ Checks the outer config and retrieves values whose keys start with 'STORMPATH' prefix, and stores them in the configuration object properly. @@ -53,7 +53,7 @@ def get_updated_config(self, config): # Check the format of application information. if stormpath_key == 'application': - if 'https://api.stormpath.com/v1/applications' in value: + if 'http' in value: stormpath_key = 'application*href' else: stormpath_key = 'application*name' diff --git a/tests/strategies/test_move_stormpath_settings_to_stormpath_config.py b/tests/strategies/test_move_stormpath_settings_to_stormpath_config.py index ec5b491..4d2bbb0 100644 --- a/tests/strategies/test_move_stormpath_settings_to_stormpath_config.py +++ b/tests/strategies/test_move_stormpath_settings_to_stormpath_config.py @@ -1,8 +1,9 @@ from unittest import TestCase -from stormpath_config.strategies import MoveStormpathSettingsToStormpathConfig +from stormpath_config.strategies import ( + MoveStormpathSettingsToStormpathConfigStrategy) -class MoveStormpathSettingsToStormpathConfigTest(TestCase): +class MoveStormpathSettingsToStormpathConfigStrategyTest(TestCase): def setUp(self): stormpath_config = { 'client': { @@ -29,7 +30,7 @@ def test_regular_mapping(self): """ self.config['STORMPATH_BASE_TEMPLATE'] = 'flask_stormpath/base.html' - move_stormpath_settings = MoveStormpathSettingsToStormpathConfig() + move_stormpath_settings = MoveStormpathSettingsToStormpathConfigStrategy() move_stormpath_settings.process(self.config) self.assertEqual( @@ -43,7 +44,7 @@ def test_multiple_key_mapping(self): """ self.config['STORMPATH_ENABLE_FACEBOOK'] = False - move_stormpath_settings = MoveStormpathSettingsToStormpathConfig() + move_stormpath_settings = MoveStormpathSettingsToStormpathConfigStrategy() move_stormpath_settings.process(self.config) self.assertEqual( @@ -65,7 +66,7 @@ def test_empty_key_mapping(self): """ self.config['STORMPATH_FOO'] = 'bar' - move_stormpath_settings = MoveStormpathSettingsToStormpathConfig() + move_stormpath_settings = MoveStormpathSettingsToStormpathConfigStrategy() move_stormpath_settings.process(self.config) self.assertNotIn('foo', self.config['stormpath']) @@ -76,7 +77,7 @@ def test_non_stormpath_key_mapping(self): """ self.config['FOO'] = 'bar' - move_stormpath_settings = MoveStormpathSettingsToStormpathConfig() + move_stormpath_settings = MoveStormpathSettingsToStormpathConfigStrategy() move_stormpath_settings.process(self.config) self.assertNotIn('foo', self.config['stormpath']) @@ -92,7 +93,7 @@ def test_default_values(self): self.config['STORMPATH_BASE_TEMPLATE'] = 'flask_stormpath/base.html' self.config['STORMPATH_ENABLE_FACEBOOK'] = False - move_stormpath_settings = MoveStormpathSettingsToStormpathConfig() + move_stormpath_settings = MoveStormpathSettingsToStormpathConfigStrategy() move_stormpath_settings.process(self.config) self.assertEqual( @@ -110,7 +111,7 @@ def test_no_stormpath_config(self): self.config['STORMPATH_BASE_TEMPLATE'] = 'flask_stormpath/base.html' self.config.pop('stormpath') - move_stormpath_settings = MoveStormpathSettingsToStormpathConfig() + move_stormpath_settings = MoveStormpathSettingsToStormpathConfigStrategy() move_stormpath_settings.process(self.config) self.assertEqual( @@ -125,7 +126,7 @@ def test_load_api_key_from_config_strategy(self): self.config[ 'STORMPATH_API_KEY_FILE'] = 'tests/assets/apiKey.properties' - move_stormpath_settings = MoveStormpathSettingsToStormpathConfig() + move_stormpath_settings = MoveStormpathSettingsToStormpathConfigStrategy() move_stormpath_settings.process(self.config) self.assertEqual( @@ -142,7 +143,7 @@ def test_parsing_application_name_href(self): # Ensure that application name is stored as name. self.config['STORMPATH_APPLICATION'] = 'app_name' - move_stormpath_settings = MoveStormpathSettingsToStormpathConfig() + move_stormpath_settings = MoveStormpathSettingsToStormpathConfigStrategy() move_stormpath_settings.process(self.config) self.assertEqual( @@ -152,7 +153,7 @@ def test_parsing_application_name_href(self): self.config['STORMPATH_APPLICATION'] = ( 'https://api.stormpath.com/v1/applications/foobar') - move_stormpath_settings = MoveStormpathSettingsToStormpathConfig() + move_stormpath_settings = MoveStormpathSettingsToStormpathConfigStrategy() move_stormpath_settings.process(self.config) self.assertEqual( diff --git a/tests/test_loader.py b/tests/test_loader.py index 285caa6..054f88c 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -98,20 +98,6 @@ def test_config_loader(self): config['client']['cacheManager']['defaultTti'], 303) self.assertEqual(config['application']['name'], 'CLIENT_CONFIG_APP') - def test_stormpath_key_loader(self): - self.client_config['application']['name'] = 'STORMPATH_KEY_APP' - self.load_strategies[6] = ExtendConfigStrategy( - extend_with={'stormpath': self.client_config}) - cl = ConfigLoader( - self.load_strategies, - self.post_processing_strategies, - self.validation_strategies - ) - config = cl.load() - - self.assertEqual(config['application']['name'], 'STORMPATH_KEY_APP') - self.assertFalse('stormpath' in config) - class OverridingStrategiesTest(TestCase): """ From 17bc7fb5eda162ecc05ca3bd13ef59c1e2a1937e Mon Sep 17 00:00:00 2001 From: "sasa.kalaba" Date: Wed, 1 Mar 2017 13:46:55 +0100 Subject: [PATCH 17/19] Renamed MoveStormpathSettingsToStormpathConfigStrategy to MoveSSettingsToConfigStrategy. --- stormpath_config/loader.py | 1 - stormpath_config/strategies/__init__.py | 2 +- ...h_config.py => move_settings_to_config.py} | 2 +- ...fig.py => test_move_settings_to_config.py} | 23 +++++++++---------- 4 files changed, 13 insertions(+), 15 deletions(-) rename stormpath_config/strategies/{move_stormpath_settings_to_stormpath_config.py => move_settings_to_config.py} (97%) rename tests/strategies/{test_move_stormpath_settings_to_stormpath_config.py => test_move_settings_to_config.py} (84%) diff --git a/stormpath_config/loader.py b/stormpath_config/loader.py index 1988a87..0f6ac56 100644 --- a/stormpath_config/loader.py +++ b/stormpath_config/loader.py @@ -1,4 +1,3 @@ -from .helpers import _extend_dict """Configuration Loader.""" diff --git a/stormpath_config/strategies/__init__.py b/stormpath_config/strategies/__init__.py index 2475df8..0ee8095 100644 --- a/stormpath_config/strategies/__init__.py +++ b/stormpath_config/strategies/__init__.py @@ -10,4 +10,4 @@ from .load_file_path import LoadFilePathStrategy from .validate_client_config import ValidateClientConfigStrategy from .move_apikey_to_client import MoveAPIKeyToClientAPIKeyStrategy -from .move_stormpath_settings_to_stormpath_config import MoveStormpathSettingsToStormpathConfigStrategy +from .move_settings_to_config import MoveSettingsToConfigStrategy diff --git a/stormpath_config/strategies/move_stormpath_settings_to_stormpath_config.py b/stormpath_config/strategies/move_settings_to_config.py similarity index 97% rename from stormpath_config/strategies/move_stormpath_settings_to_stormpath_config.py rename to stormpath_config/strategies/move_settings_to_config.py index ec1d81f..083b4a0 100644 --- a/stormpath_config/strategies/move_stormpath_settings_to_stormpath_config.py +++ b/stormpath_config/strategies/move_settings_to_config.py @@ -2,7 +2,7 @@ from .load_apikey_from_config import LoadAPIKeyFromConfigStrategy -class MoveStormpathSettingsToStormpathConfigStrategy(object): +class MoveSettingsToConfigStrategy(object): """ Checks the outer config and retrieves values whose keys start with 'STORMPATH' prefix, and stores them in the configuration object properly. diff --git a/tests/strategies/test_move_stormpath_settings_to_stormpath_config.py b/tests/strategies/test_move_settings_to_config.py similarity index 84% rename from tests/strategies/test_move_stormpath_settings_to_stormpath_config.py rename to tests/strategies/test_move_settings_to_config.py index 4d2bbb0..756750b 100644 --- a/tests/strategies/test_move_stormpath_settings_to_stormpath_config.py +++ b/tests/strategies/test_move_settings_to_config.py @@ -1,9 +1,8 @@ from unittest import TestCase -from stormpath_config.strategies import ( - MoveStormpathSettingsToStormpathConfigStrategy) +from stormpath_config.strategies import MoveSettingsToConfigStrategy -class MoveStormpathSettingsToStormpathConfigStrategyTest(TestCase): +class MoveSettingsToConfigStrategyTest(TestCase): def setUp(self): stormpath_config = { 'client': { @@ -30,7 +29,7 @@ def test_regular_mapping(self): """ self.config['STORMPATH_BASE_TEMPLATE'] = 'flask_stormpath/base.html' - move_stormpath_settings = MoveStormpathSettingsToStormpathConfigStrategy() + move_stormpath_settings = MoveSettingsToConfigStrategy() move_stormpath_settings.process(self.config) self.assertEqual( @@ -44,7 +43,7 @@ def test_multiple_key_mapping(self): """ self.config['STORMPATH_ENABLE_FACEBOOK'] = False - move_stormpath_settings = MoveStormpathSettingsToStormpathConfigStrategy() + move_stormpath_settings = MoveSettingsToConfigStrategy() move_stormpath_settings.process(self.config) self.assertEqual( @@ -66,7 +65,7 @@ def test_empty_key_mapping(self): """ self.config['STORMPATH_FOO'] = 'bar' - move_stormpath_settings = MoveStormpathSettingsToStormpathConfigStrategy() + move_stormpath_settings = MoveSettingsToConfigStrategy() move_stormpath_settings.process(self.config) self.assertNotIn('foo', self.config['stormpath']) @@ -77,7 +76,7 @@ def test_non_stormpath_key_mapping(self): """ self.config['FOO'] = 'bar' - move_stormpath_settings = MoveStormpathSettingsToStormpathConfigStrategy() + move_stormpath_settings = MoveSettingsToConfigStrategy() move_stormpath_settings.process(self.config) self.assertNotIn('foo', self.config['stormpath']) @@ -93,7 +92,7 @@ def test_default_values(self): self.config['STORMPATH_BASE_TEMPLATE'] = 'flask_stormpath/base.html' self.config['STORMPATH_ENABLE_FACEBOOK'] = False - move_stormpath_settings = MoveStormpathSettingsToStormpathConfigStrategy() + move_stormpath_settings = MoveSettingsToConfigStrategy() move_stormpath_settings.process(self.config) self.assertEqual( @@ -111,7 +110,7 @@ def test_no_stormpath_config(self): self.config['STORMPATH_BASE_TEMPLATE'] = 'flask_stormpath/base.html' self.config.pop('stormpath') - move_stormpath_settings = MoveStormpathSettingsToStormpathConfigStrategy() + move_stormpath_settings = MoveSettingsToConfigStrategy() move_stormpath_settings.process(self.config) self.assertEqual( @@ -126,7 +125,7 @@ def test_load_api_key_from_config_strategy(self): self.config[ 'STORMPATH_API_KEY_FILE'] = 'tests/assets/apiKey.properties' - move_stormpath_settings = MoveStormpathSettingsToStormpathConfigStrategy() + move_stormpath_settings = MoveSettingsToConfigStrategy() move_stormpath_settings.process(self.config) self.assertEqual( @@ -143,7 +142,7 @@ def test_parsing_application_name_href(self): # Ensure that application name is stored as name. self.config['STORMPATH_APPLICATION'] = 'app_name' - move_stormpath_settings = MoveStormpathSettingsToStormpathConfigStrategy() + move_stormpath_settings = MoveSettingsToConfigStrategy() move_stormpath_settings.process(self.config) self.assertEqual( @@ -153,7 +152,7 @@ def test_parsing_application_name_href(self): self.config['STORMPATH_APPLICATION'] = ( 'https://api.stormpath.com/v1/applications/foobar') - move_stormpath_settings = MoveStormpathSettingsToStormpathConfigStrategy() + move_stormpath_settings = MoveSettingsToConfigStrategy() move_stormpath_settings.process(self.config) self.assertEqual( From fc5e5c5029ce7f569cd1576bea9101c64c4c3843 Mon Sep 17 00:00:00 2001 From: "sasa.kalaba" Date: Wed, 1 Mar 2017 17:55:27 +0100 Subject: [PATCH 18/19] Updated MoveSettingsToConfigStrategy to work inside ConfigLoader. --- .../strategies/move_settings_to_config.py | 13 ++-- .../test_move_settings_to_config.py | 70 ++++++------------- tests/test_loader.py | 38 +++++++++- 3 files changed, 64 insertions(+), 57 deletions(-) diff --git a/stormpath_config/strategies/move_settings_to_config.py b/stormpath_config/strategies/move_settings_to_config.py index 083b4a0..fd0a05f 100644 --- a/stormpath_config/strategies/move_settings_to_config.py +++ b/stormpath_config/strategies/move_settings_to_config.py @@ -1,5 +1,4 @@ from ..helpers import _extend_dict -from .load_apikey_from_config import LoadAPIKeyFromConfigStrategy class MoveSettingsToConfigStrategy(object): @@ -24,6 +23,9 @@ class MoveSettingsToConfigStrategy(object): 'COOKIE_DURATION': 'cookie*duration', } + def __init__(self, config={}): + self.config = config + def set_key(self, config, key, value): """ We use this method to properly map values into stormpath config object. @@ -61,18 +63,13 @@ def get_updated_config(self, config): if stormpath_key: self.set_key(updated_config, stormpath_key, value) - if 'STORMPATH_API_KEY_FILE' in config.keys(): - load_api_from_config = LoadAPIKeyFromConfigStrategy() - load_api_from_config.process(updated_config) - return updated_config def process(self, config=None): if config is None: config = {} - if config.get('stormpath'): - updated_config = self.get_updated_config(config) - _extend_dict(config['stormpath'], updated_config) + updated_config = self.get_updated_config(self.config) + _extend_dict(config, updated_config) return config diff --git a/tests/strategies/test_move_settings_to_config.py b/tests/strategies/test_move_settings_to_config.py index 756750b..72d1b90 100644 --- a/tests/strategies/test_move_settings_to_config.py +++ b/tests/strategies/test_move_settings_to_config.py @@ -4,7 +4,7 @@ class MoveSettingsToConfigStrategyTest(TestCase): def setUp(self): - stormpath_config = { + self.stormpath_config = { 'client': { 'apiKey': {'id': 'api key id', 'secret': 'api key secret'}, 'cacheManager': {'defaultTtl': 300, 'defaultTti': 300} @@ -20,7 +20,7 @@ def setUp(self): } } } - self.config = {'stormpath': stormpath_config} + self.config = {'stormpath': self.stormpath_config} def test_regular_mapping(self): """ @@ -29,8 +29,9 @@ def test_regular_mapping(self): """ self.config['STORMPATH_BASE_TEMPLATE'] = 'flask_stormpath/base.html' - move_stormpath_settings = MoveSettingsToConfigStrategy() - move_stormpath_settings.process(self.config) + move_stormpath_settings = MoveSettingsToConfigStrategy( + config=self.config) + move_stormpath_settings.process(self.config['stormpath']) self.assertEqual( self.config['stormpath']['base_template'], @@ -43,8 +44,9 @@ def test_multiple_key_mapping(self): """ self.config['STORMPATH_ENABLE_FACEBOOK'] = False - move_stormpath_settings = MoveSettingsToConfigStrategy() - move_stormpath_settings.process(self.config) + move_stormpath_settings = MoveSettingsToConfigStrategy( + config=self.config) + move_stormpath_settings.process(self.config['stormpath']) self.assertEqual( self.config['stormpath']['web']['social']['facebook']['enabled'], @@ -65,8 +67,9 @@ def test_empty_key_mapping(self): """ self.config['STORMPATH_FOO'] = 'bar' - move_stormpath_settings = MoveSettingsToConfigStrategy() - move_stormpath_settings.process(self.config) + move_stormpath_settings = MoveSettingsToConfigStrategy( + config=self.config) + move_stormpath_settings.process(self.config['stormpath']) self.assertNotIn('foo', self.config['stormpath']) @@ -76,8 +79,9 @@ def test_non_stormpath_key_mapping(self): """ self.config['FOO'] = 'bar' - move_stormpath_settings = MoveSettingsToConfigStrategy() - move_stormpath_settings.process(self.config) + move_stormpath_settings = MoveSettingsToConfigStrategy( + config=self.config) + move_stormpath_settings.process(self.config['stormpath']) self.assertNotIn('foo', self.config['stormpath']) @@ -92,8 +96,9 @@ def test_default_values(self): self.config['STORMPATH_BASE_TEMPLATE'] = 'flask_stormpath/base.html' self.config['STORMPATH_ENABLE_FACEBOOK'] = False - move_stormpath_settings = MoveSettingsToConfigStrategy() - move_stormpath_settings.process(self.config) + move_stormpath_settings = MoveSettingsToConfigStrategy( + config=self.config) + move_stormpath_settings.process(self.config['stormpath']) self.assertEqual( self.config['stormpath']['base_template'], @@ -102,37 +107,6 @@ def test_default_values(self): self.config['stormpath']['web']['social']['facebook']['enabled'], False) - def test_no_stormpath_config(self): - """ - Ensure that missing stormpath config object won't break the - application. - """ - self.config['STORMPATH_BASE_TEMPLATE'] = 'flask_stormpath/base.html' - self.config.pop('stormpath') - - move_stormpath_settings = MoveSettingsToConfigStrategy() - move_stormpath_settings.process(self.config) - - self.assertEqual( - self.config, - {'STORMPATH_BASE_TEMPLATE': 'flask_stormpath/base.html'}) - - def test_load_api_key_from_config_strategy(self): - """ - Ensure that LoadAPIKeyFromConfigStrategy was called if api_key_file - setting was specified with STORMPATH prefix. - """ - self.config[ - 'STORMPATH_API_KEY_FILE'] = 'tests/assets/apiKey.properties' - - move_stormpath_settings = MoveSettingsToConfigStrategy() - move_stormpath_settings.process(self.config) - - self.assertEqual( - self.config['stormpath']['client']['apiKey'], - {'id': 'API_KEY_PROPERTIES_ID', - 'secret': 'API_KEY_PROPERTIES_SECRET'}) - def test_parsing_application_name_href(self): """ Ensure that our strategy can properly differentiate between name and @@ -142,8 +116,9 @@ def test_parsing_application_name_href(self): # Ensure that application name is stored as name. self.config['STORMPATH_APPLICATION'] = 'app_name' - move_stormpath_settings = MoveSettingsToConfigStrategy() - move_stormpath_settings.process(self.config) + move_stormpath_settings = MoveSettingsToConfigStrategy( + config=self.config) + move_stormpath_settings.process(self.config['stormpath']) self.assertEqual( self.config['stormpath']['application']['name'], 'app_name') @@ -152,8 +127,9 @@ def test_parsing_application_name_href(self): self.config['STORMPATH_APPLICATION'] = ( 'https://api.stormpath.com/v1/applications/foobar') - move_stormpath_settings = MoveSettingsToConfigStrategy() - move_stormpath_settings.process(self.config) + move_stormpath_settings = MoveSettingsToConfigStrategy( + config=self.config) + move_stormpath_settings.process(self.config['stormpath']) self.assertEqual( self.config['stormpath']['application']['href'], diff --git a/tests/test_loader.py b/tests/test_loader.py index 054f88c..3712e82 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -12,7 +12,8 @@ LoadEnvConfigStrategy, LoadFileConfigStrategy, ValidateClientConfigStrategy, - MoveAPIKeyToClientAPIKeyStrategy + MoveAPIKeyToClientAPIKeyStrategy, + MoveSettingsToConfigStrategy ) @@ -142,7 +143,10 @@ def setLoadingStrategies(self, assets={}): LoadEnvConfigStrategy(prefix=assets.get('env_prefix', 'empty')), # 7. Configuration provided through the SDK client constructor. - ExtendConfigStrategy(extend_with=assets.get('client_config', {})) + ExtendConfigStrategy(extend_with=assets.get('client_config', {})), + + # 8. Configuration provided 'STORMPATH' prefix in outer config. + MoveSettingsToConfigStrategy(config=assets.get('outer_config', {})) ] return load_strategies @@ -344,3 +348,33 @@ def test_strategies_override_09(self): # Ensure that client config asset overwrote previous settings. self.assertEqual(config['application']['name'], 'CLIENT_CONFIG_APP') + + def test_strategies_override_10(self): + # Ensure that settings from outer config with 'STORMPATH' prefix will + # override any settings from previous config sources. + + # Enable all assets. + self.load_strategies = self.setLoadingStrategies({ + 'default_config': 'tests/assets/default_config.yml', + 'home_apiKey': 'tests/assets/apiKey.properties', + 'home_stormpath_json': 'tests/assets/apiKeyApiKey.json', + 'home_stormpath_yaml': 'tests/assets/apiKeyFile.yml', + 'app_apiKey': 'tests/assets/secondary_apiKey.properties', + 'app_stormpath_json': 'tests/assets/stormpath.json', + 'app_stormpath_yaml': 'tests/assets/stormpath.yml', + 'env_prefix': 'STORMPATH', + 'client_config': { + 'application': { + 'name': 'CLIENT_CONFIG_APP' + } + }, + 'outer_config': { + 'STORMPATH_BASE_TEMPLATE': 'stormpath_base_template', + 'STORMPATH_APPLICATION': 'OUTER_STORMPATH_APP' + } + }) + config = self.getConfig() + + # Ensure that outer config asset overwrote previous settings. + self.assertEqual(config['base_template'], 'stormpath_base_template') + self.assertEqual(config['application']['name'], 'OUTER_STORMPATH_APP') From 262bd7a060884160ab9c941bfee235615fa2241a Mon Sep 17 00:00:00 2001 From: "sasa.kalaba" Date: Thu, 2 Mar 2017 15:11:34 +0100 Subject: [PATCH 19/19] Fixed Python3 errors. --- .../enrich_integration_from_remote_config.py | 2 +- ...t_enrich_integration_from_remote_config.py | 32 +++++++++---------- .../strategies/test_move_api_key_to_client.py | 2 +- tests/test_loader.py | 2 +- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/stormpath_config/strategies/enrich_integration_from_remote_config.py b/stormpath_config/strategies/enrich_integration_from_remote_config.py index 67d2851..5e740b9 100644 --- a/stormpath_config/strategies/enrich_integration_from_remote_config.py +++ b/stormpath_config/strategies/enrich_integration_from_remote_config.py @@ -1,7 +1,7 @@ from datetime import timedelta from stormpath_config.errors import ConfigurationError -from enrich_client_from_remote_config import _resolve_application_by_name +from .enrich_client_from_remote_config import _resolve_application_by_name from ..helpers import _extend_dict, to_camel_case diff --git a/tests/strategies/test_enrich_integration_from_remote_config.py b/tests/strategies/test_enrich_integration_from_remote_config.py index 3843e15..aa412a7 100644 --- a/tests/strategies/test_enrich_integration_from_remote_config.py +++ b/tests/strategies/test_enrich_integration_from_remote_config.py @@ -129,7 +129,7 @@ def test_application(self): with self.assertRaises(ConfigurationError) as error: self.ecfrcs.validate(self.config) self.assertEqual( - error.exception.message, + str(error.exception), 'Application HREF "https://api.stormpath.com/v1/a" is not a ' + 'valid Stormpath Application HREF.') @@ -138,14 +138,14 @@ def test_application(self): with self.assertRaises(ConfigurationError) as error: self.ecfrcs.validate(self.config) self.assertEqual( - error.exception.message, 'Application cannot be empty.') + str(error.exception), 'Application cannot be empty.') # No application settings. self.config.pop('application') with self.assertRaises(ConfigurationError) as error: self.ecfrcs.validate(self.config) self.assertEqual( - error.exception.message, 'Application cannot be empty.') + str(error.exception), 'Application cannot be empty.') # Ensure that we can resolve application by name. self.config['application'] = {'name': 'My named application'} @@ -162,7 +162,7 @@ def test_google_settings(self): with self.assertRaises(ConfigurationError) as error: self.ecfrcs.validate(self.config) self.assertEqual( - error.exception.message, + str(error.exception), 'You must define your Google app settings.' ) @@ -171,7 +171,7 @@ def test_google_settings(self): with self.assertRaises(ConfigurationError) as error: self.ecfrcs.validate(self.config) self.assertEqual( - error.exception.message, + str(error.exception), 'You must define your Google app settings.' ) @@ -180,7 +180,7 @@ def test_google_settings(self): with self.assertRaises(ConfigurationError) as error: self.ecfrcs.validate(self.config) self.assertEqual( - error.exception.message, + str(error.exception), 'You must define your Google app settings.' ) @@ -198,7 +198,7 @@ def test_facebook_settings(self): with self.assertRaises(ConfigurationError) as error: self.ecfrcs.validate(self.config) self.assertEqual( - error.exception.message, + str(error.exception), 'You must define your Facebook app settings.' ) @@ -207,7 +207,7 @@ def test_facebook_settings(self): with self.assertRaises(ConfigurationError) as error: self.ecfrcs.validate(self.config) self.assertEqual( - error.exception.message, + str(error.exception), 'You must define your Facebook app settings.' ) @@ -216,7 +216,7 @@ def test_facebook_settings(self): with self.assertRaises(ConfigurationError) as error: self.ecfrcs.validate(self.config) self.assertEqual( - error.exception.message, + str(error.exception), 'You must define your Facebook app settings.' ) @@ -225,7 +225,7 @@ def test_facebook_settings(self): with self.assertRaises(ConfigurationError) as error: self.ecfrcs.validate(self.config) self.assertEqual( - error.exception.message, + str(error.exception), 'You must define your Facebook app settings.' ) @@ -241,21 +241,21 @@ def test_cookie_settings(self): with self.assertRaises(ConfigurationError) as error: self.ecfrcs.validate(self.config) self.assertEqual( - error.exception.message, 'Cookie settings cannot be empty.') + str(error.exception), 'Cookie settings cannot be empty.') # Empty cookie settings. self.config['cookie'] = {} with self.assertRaises(ConfigurationError) as error: self.ecfrcs.validate(self.config) self.assertEqual( - error.exception.message, 'Cookie settings cannot be empty.') + str(error.exception), 'Cookie settings cannot be empty.') # Invalid cookie domain. self.config['cookie'] = {'domain': 55} with self.assertRaises(ConfigurationError) as error: self.ecfrcs.validate(self.config) self.assertEqual( - error.exception.message, 'Cookie domain must be a string.') + str(error.exception), 'Cookie domain must be a string.') # Invalid cookie duration. self.config['cookie'] = { @@ -266,7 +266,7 @@ def test_cookie_settings(self): with self.assertRaises(ConfigurationError) as error: self.ecfrcs.validate(self.config) self.assertEqual( - error.exception.message, 'Cookie duration must be a string.') + str(error.exception), 'Cookie duration must be a string.') # Now that we've configured things properly, it should work. self.config['cookie'] = { @@ -286,7 +286,7 @@ def test_verify_email_autologin(self): with self.assertRaises(ConfigurationError) as error: self.ecfrcs.validate(self.config) self.assertEqual( - error.exception.message, + str(error.exception), 'Invalid configuration: stormpath.web.register.autoLogin is' + ' true, but the default account store of the specified' + ' application has the email verification workflow enabled.' + @@ -306,7 +306,7 @@ def test_register_default_account_store(self): with self.assertRaises(ConfigurationError) as error: self.ecfrcs.validate(self.config) self.assertEqual( - error.exception.message, + str(error.exception), 'No default account store is mapped to the specified ' + 'application. A default account store is required for ' + 'registration.' diff --git a/tests/strategies/test_move_api_key_to_client.py b/tests/strategies/test_move_api_key_to_client.py index 7d1a8bb..f0dd557 100644 --- a/tests/strategies/test_move_api_key_to_client.py +++ b/tests/strategies/test_move_api_key_to_client.py @@ -54,4 +54,4 @@ def test_move_api_key_to_client_missing_credentials(self): with self.assertRaises(Exception) as error: self.generateConfig(client_config=client_config) self.assertEqual( - error.exception.message, 'Unable to load apiKey id and secret.') + str(error.exception), 'Unable to load apiKey id and secret.') diff --git a/tests/test_loader.py b/tests/test_loader.py index 3712e82..733a342 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -173,7 +173,7 @@ def test_strategies_override_01(self): with self.assertRaises(ValueError) as error: self.getConfig() self.assertEqual( - error.exception.message, 'API key ID and secret are required.') + str(error.exception), 'API key ID and secret are required.') def test_strategies_override_02(self): # Ensure that apiKey.properties file from HOME directory will override