From f1bfa568844bc4eb43e85a5202eaa89dc5f99d11 Mon Sep 17 00:00:00 2001 From: Braden MacDonald Date: Wed, 21 Oct 2015 23:35:37 -0700 Subject: [PATCH] Clean up integration tests, test logging in without activation --- .../third_party_auth/tests/specs/base.py | 175 ++++++++++++++++++ .../tests/specs/test_generic.py | 33 ++++ .../tests/specs/test_testshib.py | 162 +++------------- .../third_party_auth/tests/testutil.py | 26 ++- 4 files changed, 251 insertions(+), 145 deletions(-) create mode 100644 common/djangoapps/third_party_auth/tests/specs/test_generic.py diff --git a/common/djangoapps/third_party_auth/tests/specs/base.py b/common/djangoapps/third_party_auth/tests/specs/base.py index 3b69b1340c..570906b6cf 100644 --- a/common/djangoapps/third_party_auth/tests/specs/base.py +++ b/common/djangoapps/third_party_auth/tests/specs/base.py @@ -10,6 +10,7 @@ from django.contrib import auth from django.contrib.auth import models as auth_models from django.contrib.messages.storage import fallback from django.contrib.sessions.backends import cache +from django.core.urlresolvers import reverse from django.test import utils as django_utils from django.conf import settings as django_settings from edxmako.tests import mako_middleware_process_request @@ -18,6 +19,7 @@ from social.apps.django_app import utils as social_utils from social.apps.django_app import views as social_views from student import models as student_models from student import views as student_views +from student.tests.factories import UserFactory from student_account.views import account_settings_context from third_party_auth import middleware, pipeline @@ -25,6 +27,179 @@ from third_party_auth import settings as auth_settings from third_party_auth.tests import testutil +class IntegrationTestMixin(object): + """ + Mixin base class for third_party_auth integration tests. + This class is newer and simpler than the 'IntegrationTest' alternative below, but it is + currently less comprehensive. Some providers are tested with this, others with + IntegrationTest. + """ + # Provider information: + PROVIDER_NAME = "override" + PROVIDER_BACKEND = "override" + PROVIDER_ID = "override" + # Information about the user expected from the provider: + USER_EMAIL = "override" + USER_NAME = "override" + USER_USERNAME = "override" + + def setUp(self): + super(IntegrationTestMixin, self).setUp() + self.login_page_url = reverse('signin_user') + self.register_page_url = reverse('register_user') + # Set the server name: + 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' + patcher = testutil.patch_mako_templates() + patcher.start() + self.addCleanup(patcher.stop) + # Override this method in a subclass and enable at least one provider. + + def test_register(self): + # The user goes to the register page, and sees a button to register with the provider: + provider_register_url = self._check_register_page() + # The user clicks on the Dummy button: + try_login_response = self.client.get(provider_register_url) + # The user should be redirected to the provider's login page: + self.assertEqual(try_login_response.status_code, 302) + provider_response = self.do_provider_login(try_login_response['Location']) + # We should be redirected to the register screen since this account is not linked to an edX account: + self.assertEqual(provider_response.status_code, 302) + self.assertEqual(provider_response['Location'], self.url_prefix + self.register_page_url) + register_response = self.client.get(self.register_page_url) + tpa_context = register_response.context["data"]["third_party_auth"] + self.assertEqual(tpa_context["errorMessage"], None) + # Check that the "You've successfully signed into [PROVIDER_NAME]" message is shown. + self.assertEqual(tpa_context["currentProvider"], self.PROVIDER_NAME) + # Check that the data (e.g. email) from the provider is displayed in the form: + form_data = register_response.context['data']['registration_form_desc'] + form_fields = {field['name']: field for field in form_data['fields']} + self.assertEqual(form_fields['email']['defaultValue'], self.USER_EMAIL) + self.assertEqual(form_fields['name']['defaultValue'], self.USER_NAME) + self.assertEqual(form_fields['username']['defaultValue'], self.USER_USERNAME) + # Now complete the form: + ajax_register_response = self.client.post( + reverse('user_api_registration'), + { + 'email': 'email-edited@tpa-test.none', + 'name': 'My Customized Name', + 'username': 'new_username', + 'honor_code': True, + } + ) + self.assertEqual(ajax_register_response.status_code, 200) + # Then the AJAX will finish the third party auth: + continue_response = self.client.get(tpa_context["finishAuthUrl"]) + # And we should be redirected to the dashboard: + self.assertEqual(continue_response.status_code, 302) + self.assertEqual(continue_response['Location'], self.url_prefix + reverse('dashboard')) + + # Now check that we can login again, whether or not we have yet verified the account: + self.client.logout() + self._test_return_login(user_is_activated=False) + + self.client.logout() + self.verify_user_email('email-edited@tpa-test.none') + self._test_return_login(user_is_activated=True) + + def test_login(self): + user = UserFactory.create() + # The user goes to the login page, and sees a button to login with TestShib: + provider_login_url = self._check_login_page() + # The user clicks on the TestShib button: + try_login_response = self.client.get(provider_login_url) + # The user should be redirected to the provider's login page: + self.assertEqual(try_login_response.status_code, 302) + complete_response = self.do_provider_login(try_login_response['Location']) + # We should be redirected to the login screen since this account is not linked to an edX account: + self.assertEqual(complete_response.status_code, 302) + self.assertEqual(complete_response['Location'], self.url_prefix + self.login_page_url) + login_response = self.client.get(self.login_page_url) + tpa_context = login_response.context["data"]["third_party_auth"] + self.assertEqual(tpa_context["errorMessage"], None) + # Check that the "You've successfully signed into [PROVIDER_NAME]" message is shown. + self.assertEqual(tpa_context["currentProvider"], self.PROVIDER_NAME) + # Now the user enters their username and password. + # The AJAX on the page will log them in: + ajax_login_response = self.client.post( + reverse('user_api_login_session'), + {'email': user.email, 'password': 'test'} + ) + self.assertEqual(ajax_login_response.status_code, 200) + # Then the AJAX will finish the third party auth: + continue_response = self.client.get(tpa_context["finishAuthUrl"]) + # And we should be redirected to the dashboard: + self.assertEqual(continue_response.status_code, 302) + self.assertEqual(continue_response['Location'], self.url_prefix + reverse('dashboard')) + + # Now check that we can login again: + self.client.logout() + self._test_return_login() + + def do_provider_login(self, provider_redirect_url): + """ + mock logging in to the provider + Should end with loading self.complete_url, which should be returned + """ + raise NotImplementedError + + def _test_return_login(self, user_is_activated=True): + """ Test logging in to an account that is already linked. """ + # Make sure we're not logged in: + dashboard_response = self.client.get(reverse('dashboard')) + self.assertEqual(dashboard_response.status_code, 302) + # The user goes to the login page, and sees a button to login with TestShib: + provider_login_url = self._check_login_page() + # The user clicks on the TestShib button: + try_login_response = self.client.get(provider_login_url) + # The user should be redirected to the provider: + self.assertEqual(try_login_response.status_code, 302) + login_response = self.do_provider_login(try_login_response['Location']) + # There will be one weird redirect required to set the login cookie: + self.assertEqual(login_response.status_code, 302) + self.assertEqual(login_response['Location'], self.url_prefix + self.complete_url) + # And then we should be redirected to the dashboard: + login_response = self.client.get(login_response['Location']) + self.assertEqual(login_response.status_code, 302) + if user_is_activated: + url_expected = reverse('dashboard') + else: + url_expected = '/auth/inactive?next=/dashboard' + self.assertEqual(login_response['Location'], self.url_prefix + url_expected) + # Now we are logged in: + dashboard_response = self.client.get(reverse('dashboard')) + self.assertEqual(dashboard_response.status_code, 200) + + def _check_login_page(self): + """ + Load the login form and check that it contains a button for the provider. + Return the URL for logging into that provider. + """ + return self._check_login_or_register_page(self.login_page_url, "loginUrl") + + def _check_register_page(self): + """ + Load the registration form and check that it contains a button for the provider. + Return the URL for registering with that provider. + """ + return self._check_login_or_register_page(self.register_page_url, "registerUrl") + + def _check_login_or_register_page(self, url, url_to_return): + """ Shared logic for _check_login_page() and _check_register_page() """ + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + self.assertIn(self.PROVIDER_NAME, response.content) + context_data = response.context['data']['third_party_auth'] + provider_urls = {provider['id']: provider[url_to_return] for provider in context_data['providers']} + self.assertIn(self.PROVIDER_ID, provider_urls) + return provider_urls[self.PROVIDER_ID] + + @property + def complete_url(self): + """ Get the auth completion URL for this provider """ + return reverse('social:complete', kwargs={'backend': self.PROVIDER_BACKEND}) + + @unittest.skipUnless( testutil.AUTH_FEATURES_KEY in django_settings.FEATURES, testutil.AUTH_FEATURES_KEY + ' not in settings.FEATURES') @django_utils.override_settings() # For settings reversion on a method-by-method basis. diff --git a/common/djangoapps/third_party_auth/tests/specs/test_generic.py b/common/djangoapps/third_party_auth/tests/specs/test_generic.py new file mode 100644 index 0000000000..56b7fd8333 --- /dev/null +++ b/common/djangoapps/third_party_auth/tests/specs/test_generic.py @@ -0,0 +1,33 @@ +""" +Use the 'Dummy' auth provider for generic integration tests of third_party_auth. +""" +import unittest +from third_party_auth.tests import testutil + +from .base import IntegrationTestMixin + + +@unittest.skipUnless(testutil.AUTH_FEATURE_ENABLED, 'third_party_auth not enabled') +class GenericIntegrationTest(IntegrationTestMixin, testutil.TestCase): + """ + Basic integration tests of third_party_auth using Dummy provider + """ + PROVIDER_ID = "oa2-dummy" + PROVIDER_NAME = "Dummy" + PROVIDER_BACKEND = "dummy" + + USER_EMAIL = "adama@fleet.colonies.gov" + USER_NAME = "William Adama" + USER_USERNAME = "Galactica1" + + def setUp(self): + super(GenericIntegrationTest, self).setUp() + self.configure_dummy_provider(enabled=True) + + def do_provider_login(self, provider_redirect_url): + """ + Mock logging in to the Dummy provider + """ + # For the Dummy provider, the provider redirect URL is self.complete_url + self.assertEqual(provider_redirect_url, self.url_prefix + self.complete_url) + return self.client.get(provider_redirect_url) diff --git a/common/djangoapps/third_party_auth/tests/specs/test_testshib.py b/common/djangoapps/third_party_auth/tests/specs/test_testshib.py index f9c1870577..defea79520 100644 --- a/common/djangoapps/third_party_auth/tests/specs/test_testshib.py +++ b/common/djangoapps/third_party_auth/tests/specs/test_testshib.py @@ -1,38 +1,36 @@ """ Third_party_auth integration tests using a mock version of the TestShib provider """ - -import json import unittest import httpretty from mock import patch -from django.core.urlresolvers import reverse - -from student.tests.factories import UserFactory from third_party_auth.tasks import fetch_saml_metadata from third_party_auth.tests import testutil -from openedx.core.lib.js_utils import escape_json_dumps + +from .base import IntegrationTestMixin TESTSHIB_ENTITY_ID = 'https://idp.testshib.org/idp/shibboleth' TESTSHIB_METADATA_URL = 'https://mock.testshib.org/metadata/testshib-providers.xml' TESTSHIB_SSO_URL = 'https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO' -TPA_TESTSHIB_LOGIN_URL = '/auth/login/tpa-saml/?auth_entry=login&next=%2Fdashboard&idp=testshib' -TPA_TESTSHIB_REGISTER_URL = '/auth/login/tpa-saml/?auth_entry=register&next=%2Fdashboard&idp=testshib' -TPA_TESTSHIB_COMPLETE_URL = '/auth/complete/tpa-saml/' - @unittest.skipUnless(testutil.AUTH_FEATURE_ENABLED, 'third_party_auth not enabled') -class TestShibIntegrationTest(testutil.SAMLTestCase): +class TestShibIntegrationTest(IntegrationTestMixin, testutil.SAMLTestCase): """ TestShib provider Integration Test, to test SAML functionality """ + PROVIDER_ID = "saml-testshib" + PROVIDER_NAME = "TestShib" + PROVIDER_BACKEND = "tpa-saml" + + USER_EMAIL = "myself@testshib.org" + USER_NAME = "Me Myself And I" + USER_USERNAME = "myself" + def setUp(self): super(TestShibIntegrationTest, self).setUp() - self.login_page_url = reverse('signin_user') - self.register_page_url = reverse('register_user') self.enable_saml( private_key=self._get_private_key(), public_key=self._get_public_key(), @@ -53,13 +51,14 @@ class TestShibIntegrationTest(testutil.SAMLTestCase): uid_patch = patch('onelogin.saml2.utils.OneLogin_Saml2_Utils.generate_unique_id', return_value='TESTID') uid_patch.start() self.addCleanup(uid_patch.stop) + self._freeze_time(timestamp=1434326820) # This is the time when the saved request/response was recorded. def test_login_before_metadata_fetched(self): self._configure_testshib_provider(fetch_metadata=False) # The user goes to the login page, and sees a button to login with TestShib: - self._check_login_page() + testshib_login_url = self._check_login_page() # The user clicks on the TestShib button: - try_login_response = self.client.get(TPA_TESTSHIB_LOGIN_URL) + try_login_response = self.client.get(testshib_login_url) # The user should be redirected to back to the login page: self.assertEqual(try_login_response.status_code, 302) self.assertEqual(try_login_response['Location'], self.url_prefix + self.login_page_url) @@ -68,115 +67,15 @@ class TestShibIntegrationTest(testutil.SAMLTestCase): self.assertEqual(response.status_code, 200) self.assertIn('Authentication with TestShib is currently unavailable.', response.content) - def test_register(self): - self._configure_testshib_provider() - self._freeze_time(timestamp=1434326820) # This is the time when the saved request/response was recorded. - # The user goes to the register page, and sees a button to register with TestShib: - self._check_register_page() - # The user clicks on the TestShib button: - try_login_response = self.client.get(TPA_TESTSHIB_REGISTER_URL) - # The user should be redirected to TestShib: - self.assertEqual(try_login_response.status_code, 302) - self.assertTrue(try_login_response['Location'].startswith(TESTSHIB_SSO_URL)) - # Now the user will authenticate with the SAML provider - testshib_response = self._fake_testshib_login_and_return() - # We should be redirected to the register screen since this account is not linked to an edX account: - self.assertEqual(testshib_response.status_code, 302) - self.assertEqual(testshib_response['Location'], self.url_prefix + self.register_page_url) - register_response = self.client.get(self.register_page_url) - # We'd now like to see if the "You've successfully signed into TestShib" message is - # shown, but it's managed by a JavaScript runtime template, and we can't run JS in this - # type of test, so we just check for the variable that triggers that message. - self.assertIn('"currentProvider": "TestShib"', register_response.content) - self.assertIn('"errorMessage": null', register_response.content) - # Now do a crude check that the data (e.g. email) from the provider is displayed in the form: - self.assertIn('"defaultValue": "myself@testshib.org"', register_response.content) - self.assertIn('"defaultValue": "Me Myself And I"', register_response.content) - # Now complete the form: - ajax_register_response = self.client.post( - reverse('user_api_registration'), - { - 'email': 'myself@testshib.org', - 'name': 'Myself', - 'username': 'myself', - 'honor_code': True, - } - ) - self.assertEqual(ajax_register_response.status_code, 200) - # Then the AJAX will finish the third party auth: - continue_response = self.client.get(TPA_TESTSHIB_COMPLETE_URL) - # And we should be redirected to the dashboard: - self.assertEqual(continue_response.status_code, 302) - self.assertEqual(continue_response['Location'], self.url_prefix + reverse('dashboard')) - - # Now check that we can login again: - self.client.logout() - self.verify_user_email('myself@testshib.org') - self._test_return_login() - def test_login(self): + """ Configure TestShib before running the login test """ self._configure_testshib_provider() - self._freeze_time(timestamp=1434326820) # This is the time when the saved request/response was recorded. - user = UserFactory.create() - # The user goes to the login page, and sees a button to login with TestShib: - self._check_login_page() - # The user clicks on the TestShib button: - try_login_response = self.client.get(TPA_TESTSHIB_LOGIN_URL) - # The user should be redirected to TestShib: - self.assertEqual(try_login_response.status_code, 302) - self.assertTrue(try_login_response['Location'].startswith(TESTSHIB_SSO_URL)) - # Now the user will authenticate with the SAML provider - testshib_response = self._fake_testshib_login_and_return() - # We should be redirected to the login screen since this account is not linked to an edX account: - self.assertEqual(testshib_response.status_code, 302) - self.assertEqual(testshib_response['Location'], self.url_prefix + self.login_page_url) - login_response = self.client.get(self.login_page_url) - # We'd now like to see if the "You've successfully signed into TestShib" message is - # shown, but it's managed by a JavaScript runtime template, and we can't run JS in this - # type of test, so we just check for the variable that triggers that message. - self.assertIn('"currentProvider": "TestShib"', login_response.content) - self.assertIn('"errorMessage": null', login_response.content) - # Now the user enters their username and password. - # The AJAX on the page will log them in: - ajax_login_response = self.client.post( - reverse('user_api_login_session'), - {'email': user.email, 'password': 'test'} - ) - self.assertEqual(ajax_login_response.status_code, 200) - # Then the AJAX will finish the third party auth: - continue_response = self.client.get(TPA_TESTSHIB_COMPLETE_URL) - # And we should be redirected to the dashboard: - self.assertEqual(continue_response.status_code, 302) - self.assertEqual(continue_response['Location'], self.url_prefix + reverse('dashboard')) + super(TestShibIntegrationTest, self).test_login() - # Now check that we can login again: - self.client.logout() - self._test_return_login() - - def _test_return_login(self): - """ Test logging in to an account that is already linked. """ - # Make sure we're not logged in: - dashboard_response = self.client.get(reverse('dashboard')) - self.assertEqual(dashboard_response.status_code, 302) - # The user goes to the login page, and sees a button to login with TestShib: - self._check_login_page() - # The user clicks on the TestShib button: - try_login_response = self.client.get(TPA_TESTSHIB_LOGIN_URL) - # The user should be redirected to TestShib: - self.assertEqual(try_login_response.status_code, 302) - self.assertTrue(try_login_response['Location'].startswith(TESTSHIB_SSO_URL)) - # Now the user will authenticate with the SAML provider - login_response = self._fake_testshib_login_and_return() - # There will be one weird redirect required to set the login cookie: - self.assertEqual(login_response.status_code, 302) - self.assertEqual(login_response['Location'], self.url_prefix + TPA_TESTSHIB_COMPLETE_URL) - # And then we should be redirected to the dashboard: - login_response = self.client.get(TPA_TESTSHIB_COMPLETE_URL) - self.assertEqual(login_response.status_code, 302) - self.assertEqual(login_response['Location'], self.url_prefix + reverse('dashboard')) - # Now we are logged in: - dashboard_response = self.client.get(reverse('dashboard')) - self.assertEqual(dashboard_response.status_code, 200) + def test_register(self): + """ Configure TestShib before running the register test """ + self._configure_testshib_provider() + super(TestShibIntegrationTest, self).test_register() def _freeze_time(self, timestamp): """ Mock the current time for SAML, so we can replay canned requests/responses """ @@ -184,22 +83,6 @@ class TestShibIntegrationTest(testutil.SAMLTestCase): now_patch.start() self.addCleanup(now_patch.stop) - def _check_login_page(self): - """ Load the login form and check that it contains a TestShib button """ - response = self.client.get(self.login_page_url) - self.assertEqual(response.status_code, 200) - self.assertIn("TestShib", response.content) - self.assertIn(escape_json_dumps(TPA_TESTSHIB_LOGIN_URL), response.content) - return response - - def _check_register_page(self): - """ Load the login form and check that it contains a TestShib button """ - response = self.client.get(self.register_page_url) - self.assertEqual(response.status_code, 200) - self.assertIn("TestShib", response.content) - self.assertIn(escape_json_dumps(TPA_TESTSHIB_REGISTER_URL), response.content) - return response - def _configure_testshib_provider(self, **kwargs): """ Enable and configure the TestShib SAML IdP as a third_party_auth provider """ fetch_metadata = kwargs.pop('fetch_metadata', True) @@ -219,11 +102,12 @@ class TestShibIntegrationTest(testutil.SAMLTestCase): self.assertEqual(num_changed, 1) self.assertEqual(num_total, 1) - def _fake_testshib_login_and_return(self): + def do_provider_login(self, provider_redirect_url): """ Mocked: the user logs in to TestShib and then gets redirected back """ # The SAML provider (TestShib) will authenticate the user, then get the browser to POST a response: + self.assertTrue(provider_redirect_url.startswith(TESTSHIB_SSO_URL)) return self.client.post( - TPA_TESTSHIB_COMPLETE_URL, + self.complete_url, content_type='application/x-www-form-urlencoded', data=self.read_data_file('testshib_response.txt'), ) diff --git a/common/djangoapps/third_party_auth/tests/testutil.py b/common/djangoapps/third_party_auth/tests/testutil.py index 9f9127171b..baf1ec5ca4 100644 --- a/common/djangoapps/third_party_auth/tests/testutil.py +++ b/common/djangoapps/third_party_auth/tests/testutil.py @@ -10,6 +10,7 @@ from django.contrib.auth.models import User from provider.oauth2.models import Client as OAuth2Client from provider import constants import django.test +from mako.template import Template import mock import os.path @@ -27,6 +28,18 @@ AUTH_FEATURES_KEY = 'ENABLE_THIRD_PARTY_AUTH' AUTH_FEATURE_ENABLED = AUTH_FEATURES_KEY in settings.FEATURES +def patch_mako_templates(): + """ Patch mako so the django test client can access template context """ + orig_render = Template.render_unicode + + def wrapped_render(*args, **kwargs): + """ Render the template and send the context info to any listeners that want it """ + django.test.signals.template_rendered.send(sender=None, template=None, context=kwargs) + return orig_render(*args, **kwargs) + + return mock.patch.multiple(Template, render_unicode=wrapped_render, render=wrapped_render) + + class FakeDjangoSettings(object): """A fake for Django settings.""" @@ -109,6 +122,13 @@ class ThirdPartyAuthTestMixin(object): kwargs.setdefault("secret", "test") return cls.configure_oauth_provider(**kwargs) + @classmethod + def configure_dummy_provider(cls, **kwargs): + """ Update the settings for the Twitter third party auth provider/backend """ + kwargs.setdefault("name", "Dummy") + kwargs.setdefault("backend_name", "dummy") + return cls.configure_oauth_provider(**kwargs) + @classmethod def verify_user_email(cls, email): """ Mark the user with the given email as verified """ @@ -142,12 +162,6 @@ 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. """