""" Utilities for writing third_party_auth tests. Used by Django and non-Django tests; must not have Django deps. """ from contextlib import contextmanager from django.conf import settings import django.test import mock import os.path from third_party_auth.models import OAuth2ProviderConfig, SAMLProviderConfig, SAMLConfiguration, cache as config_cache AUTH_FEATURES_KEY = 'ENABLE_THIRD_PARTY_AUTH' AUTH_FEATURE_ENABLED = AUTH_FEATURES_KEY in settings.FEATURES class FakeDjangoSettings(object): """A fake for Django settings.""" def __init__(self, mappings): """Initializes the fake from mappings dict.""" for key, value in mappings.iteritems(): setattr(self, key, value) class ThirdPartyAuthTestMixin(object): """ Helper methods useful for testing third party auth functionality """ def tearDown(self): config_cache.clear() super(ThirdPartyAuthTestMixin, self).tearDown() def enable_saml(self, **kwargs): """ Enable SAML support (via SAMLConfiguration, not for any particular provider) """ kwargs.setdefault('enabled', True) SAMLConfiguration(**kwargs).save() @staticmethod def configure_oauth_provider(**kwargs): """ Update the settings for an OAuth2-based third party auth provider """ obj = OAuth2ProviderConfig(**kwargs) obj.save() return obj def configure_saml_provider(self, **kwargs): """ Update the settings for a SAML-based third party auth provider """ self.assertTrue(SAMLConfiguration.is_enabled(), "SAML Provider Configuration only works if SAML is enabled.") obj = SAMLProviderConfig(**kwargs) obj.save() return obj @classmethod def configure_google_provider(cls, **kwargs): """ Update the settings for the Google third party auth provider/backend """ kwargs.setdefault("name", "Google") kwargs.setdefault("backend_name", "google-oauth2") kwargs.setdefault("icon_class", "fa-google-plus") kwargs.setdefault("key", "test-fake-key.apps.googleusercontent.com") kwargs.setdefault("secret", "opensesame") return cls.configure_oauth_provider(**kwargs) @classmethod def configure_facebook_provider(cls, **kwargs): """ Update the settings for the Facebook third party auth provider/backend """ kwargs.setdefault("name", "Facebook") kwargs.setdefault("backend_name", "facebook") kwargs.setdefault("icon_class", "fa-facebook") kwargs.setdefault("key", "FB_TEST_APP") kwargs.setdefault("secret", "opensesame") return cls.configure_oauth_provider(**kwargs) @classmethod def configure_linkedin_provider(cls, **kwargs): """ Update the settings for the LinkedIn third party auth provider/backend """ kwargs.setdefault("name", "LinkedIn") kwargs.setdefault("backend_name", "linkedin-oauth2") kwargs.setdefault("icon_class", "fa-linkedin") kwargs.setdefault("key", "test") kwargs.setdefault("secret", "test") return cls.configure_oauth_provider(**kwargs) class TestCase(ThirdPartyAuthTestMixin, django.test.TestCase): """Base class for auth test cases.""" pass class SAMLTestCase(TestCase): """ Base class for SAML-related third_party_auth tests """ def setUp(self): super(SAMLTestCase, self).setUp() self.client.defaults['SERVER_NAME'] = 'example.none' # The SAML lib we use doesn't like testserver' as a domain self.url_prefix = 'http://example.none' @classmethod def _get_public_key(cls, key_name='saml_key'): """ Get a public key for use in the test. """ return cls._read_data_file('{}.pub'.format(key_name)) @classmethod def _get_private_key(cls, key_name='saml_key'): """ Get a private key for use in the test. """ return cls._read_data_file('{}.key'.format(key_name)) @staticmethod def _read_data_file(filename): """ Read the contents of a file in the data folder """ with open(os.path.join(os.path.dirname(__file__), 'data', filename)) as f: return f.read() def enable_saml(self, **kwargs): """ Enable SAML support (via SAMLConfiguration, not for any particular provider) """ if 'private_key' not in kwargs: kwargs['private_key'] = self._get_private_key() if 'public_key' not in kwargs: kwargs['public_key'] = self._get_public_key() kwargs.setdefault('entity_id', "https://saml.example.none") super(SAMLTestCase, self).enable_saml(**kwargs) @contextmanager def simulate_running_pipeline(pipeline_target, backend, email=None, fullname=None, username=None): """Simulate that a pipeline is currently running. You can use this context manager to test packages that rely on third party auth. This uses `mock.patch` to override some calls in `third_party_auth.pipeline`, so you will need to provide the "target" module *as it is imported* in the software under test. For example, if `foo/bar.py` does this: >>> from third_party_auth import pipeline then you will need to do something like this: >>> with simulate_running_pipeline("foo.bar.pipeline", "google-oauth2"): >>> bar.do_something_with_the_pipeline() If, on the other hand, `foo/bar.py` had done this: >>> import third_party_auth then you would use the target "foo.bar.third_party_auth.pipeline" instead. Arguments: pipeline_target (string): The path to `third_party_auth.pipeline` as it is imported in the software under test. backend (string): The name of the backend currently running, for example "google-oauth2". Note that this is NOT the same as the name of the *provider*. See the Python social auth documentation for the names of the backends. Keyword Arguments: email (string): If provided, simulate that the current provider has included the user's email address (useful for filling in the registration form). fullname (string): If provided, simulate that the current provider has included the user's full name (useful for filling in the registration form). username (string): If provided, simulate that the pipeline has provided this suggested username. This is something that the `third_party_auth` app generates itself and should be available by the time the user is authenticating with a third-party provider. Returns: None """ pipeline_data = { "backend": backend, "kwargs": { "details": {} } } if email is not None: pipeline_data["kwargs"]["details"]["email"] = email if fullname is not None: pipeline_data["kwargs"]["details"]["fullname"] = fullname if username is not None: pipeline_data["kwargs"]["username"] = username pipeline_get = mock.patch("{pipeline}.get".format(pipeline=pipeline_target), spec=True) pipeline_running = mock.patch("{pipeline}.running".format(pipeline=pipeline_target), spec=True) mock_get = pipeline_get.start() mock_running = pipeline_running.start() mock_get.return_value = pipeline_data mock_running.return_value = True try: yield finally: pipeline_get.stop() pipeline_running.stop()