Migrate to latest, split python-social-auth.

PSA was monolothic, now split, with new features, like
a DB-backed partial pipeline. FB OAuth2 version also upped.

Partial pipelines don't get cleared except when necessary.
They persist for special cases like change of browser while
still mid-pipeline (i.e. email validation step).

Refactor, cleanup, and update of a lot of small things as well.

PLEASE NOTE the new `social_auth_partial` table.
This commit is contained in:
Uman Shahzad
2017-05-19 20:46:01 -04:00
parent 4dd4f97900
commit 8b65ca17c5
48 changed files with 270 additions and 234 deletions

View File

@@ -14,9 +14,9 @@ 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 social import actions, exceptions
from social.apps.django_app import utils as social_utils
from social.apps.django_app import views as social_views
from social_core import actions, exceptions
from social_django import utils as social_utils
from social_django import views as social_views
from lms.djangoapps.commerce.tests import TEST_API_URL
from openedx.core.djangoapps.site_configuration.tests.factories import SiteFactory
@@ -26,7 +26,6 @@ from student.tests.factories import UserFactory
from student_account.views import account_settings_context
from third_party_auth import middleware, pipeline
from third_party_auth import settings as auth_settings
from third_party_auth.tests import testutil
@@ -270,7 +269,7 @@ class IntegrationTest(testutil.TestCase, test.TestCase):
form_field_data = self.provider.get_register_form_data(pipeline_kwargs)
for prepopulated_form_data in form_field_data:
if prepopulated_form_data in required_fields:
self.assertIn(form_field_data[prepopulated_form_data], response.content)
self.assertIn(form_field_data[prepopulated_form_data], response.content.decode('utf-8'))
# Implementation details and actual tests past this point -- no more
# configuration needed.
@@ -285,7 +284,7 @@ class IntegrationTest(testutil.TestCase, test.TestCase):
return self.provider.backend_name
# pylint: disable=invalid-name
def assert_account_settings_context_looks_correct(self, context, _user, duplicate=False, linked=None):
def assert_account_settings_context_looks_correct(self, context, duplicate=False, linked=None):
"""Asserts the user's account settings page context is in the expected state.
If duplicate is True, we expect context['duplicate_provider'] to contain
@@ -473,7 +472,7 @@ class IntegrationTest(testutil.TestCase, test.TestCase):
return user
def fake_auth_complete(self, strategy):
"""Fake implementation of social.backends.BaseAuth.auth_complete.
"""Fake implementation of social_core.backends.BaseAuth.auth_complete.
Unlike what the docs say, it does not need to return a user instance.
Sometimes (like when directing users to the /register form) it instead
@@ -515,7 +514,7 @@ class IntegrationTest(testutil.TestCase, test.TestCase):
These two objects contain circular references, so we create them
together. The references themselves are a mixture of normal __init__
stuff and monkey-patching done by python-social-auth. See, for example,
social.apps.django_apps.utils.strategy().
social_django.utils.strategy().
"""
request = self.request_factory.get(
pipeline.get_complete_url(self.backend_name) +
@@ -600,7 +599,7 @@ class IntegrationTest(testutil.TestCase, test.TestCase):
# First we expect that we're in the unlinked state, and that there
# really is no association in the backend.
self.assert_account_settings_context_looks_correct(account_settings_context(request), request.user, linked=False)
self.assert_account_settings_context_looks_correct(account_settings_context(request), linked=False)
self.assert_social_auth_does_not_exist_for_user(request.user, strategy)
# We should be redirected back to the complete page, setting
@@ -614,13 +613,19 @@ class IntegrationTest(testutil.TestCase, test.TestCase):
self.set_logged_in_cookies(request)
# Fire off the auth pipeline to link.
self.assert_redirect_to_dashboard_looks_correct(actions.do_complete(
request.backend, social_views._do_login, request.user, None, # pylint: disable=protected-access
redirect_field_name=auth.REDIRECT_FIELD_NAME))
self.assert_redirect_to_dashboard_looks_correct( # pylint: disable=protected-access
actions.do_complete(
request.backend,
social_views._do_login,
request.user,
None,
redirect_field_name=auth.REDIRECT_FIELD_NAME
)
)
# Now we expect to be in the linked state, with a backend entry.
self.assert_social_auth_exists_for_user(request.user, strategy)
self.assert_account_settings_context_looks_correct(account_settings_context(request), request.user, linked=True)
self.assert_account_settings_context_looks_correct(account_settings_context(request), linked=True)
def test_full_pipeline_succeeds_for_unlinking_account(self):
# First, create, the request and strategy that store pipeline state,
@@ -647,15 +652,21 @@ class IntegrationTest(testutil.TestCase, test.TestCase):
actions.do_complete(request.backend, social_views._do_login, user=user) # pylint: disable=protected-access
# First we expect that we're in the linked state, with a backend entry.
self.assert_account_settings_context_looks_correct(account_settings_context(request), user, linked=True)
self.assert_account_settings_context_looks_correct(account_settings_context(request), linked=True)
self.assert_social_auth_exists_for_user(request.user, strategy)
# Fire off the disconnect pipeline to unlink.
self.assert_redirect_to_dashboard_looks_correct(actions.do_disconnect(
request.backend, request.user, None, redirect_field_name=auth.REDIRECT_FIELD_NAME))
self.assert_redirect_to_dashboard_looks_correct(
actions.do_disconnect(
request.backend,
request.user,
None,
redirect_field_name=auth.REDIRECT_FIELD_NAME
)
)
# Now we expect to be in the unlinked state, with no backend entry.
self.assert_account_settings_context_looks_correct(account_settings_context(request), user, linked=False)
self.assert_account_settings_context_looks_correct(account_settings_context(request), linked=False)
self.assert_social_auth_does_not_exist_for_user(user, strategy)
def test_linking_already_associated_account_raises_auth_already_associated(self):
@@ -712,7 +723,7 @@ class IntegrationTest(testutil.TestCase, test.TestCase):
exceptions.AuthAlreadyAssociated(self.provider.backend_name, 'account is already in use.'))
self.assert_account_settings_context_looks_correct(
account_settings_context(request), user, duplicate=True, linked=True)
account_settings_context(request), duplicate=True, linked=True)
def test_full_pipeline_succeeds_for_signing_in_to_existing_active_account(self):
# First, create, the request and strategy that store pipeline state,
@@ -763,7 +774,7 @@ class IntegrationTest(testutil.TestCase, test.TestCase):
self.assert_redirect_to_dashboard_looks_correct(
actions.do_complete(request.backend, social_views._do_login, user=user))
self.assert_account_settings_context_looks_correct(account_settings_context(request), user)
self.assert_account_settings_context_looks_correct(account_settings_context(request))
def test_signin_fails_if_account_not_active(self):
_, strategy = self.get_request_and_strategy(
@@ -869,7 +880,7 @@ class IntegrationTest(testutil.TestCase, test.TestCase):
actions.do_complete(strategy.request.backend, social_views._do_login, user=created_user))
# Now the user has been redirected to the dashboard. Their third party account should now be linked.
self.assert_social_auth_exists_for_user(created_user, strategy)
self.assert_account_settings_context_looks_correct(account_settings_context(request), created_user, linked=True)
self.assert_account_settings_context_looks_correct(account_settings_context(request), linked=True)
def test_new_account_registration_assigns_distinct_username_on_collision(self):
original_username = self.get_username()

View File

@@ -7,7 +7,7 @@ from third_party_auth.tests import testutil
from .base import IntegrationTestMixin
@unittest.skipUnless(testutil.AUTH_FEATURE_ENABLED, 'third_party_auth not enabled')
@unittest.skipUnless(testutil.AUTH_FEATURE_ENABLED, testutil.AUTH_FEATURES_KEY + ' not enabled')
class GenericIntegrationTest(IntegrationTestMixin, testutil.TestCase):
"""
Basic integration tests of third_party_auth using Dummy provider

View File

@@ -6,7 +6,7 @@ from django.conf import settings
from django.core.urlresolvers import reverse
import json
from mock import patch
from social.exceptions import AuthException
from social_core.exceptions import AuthException
from student.tests.factories import UserFactory
from third_party_auth import pipeline
from third_party_auth.tests.specs import base

View File

@@ -6,10 +6,9 @@ import ddt
import unittest
import httpretty
import json
import time
from mock import patch
from freezegun import freeze_time
from social.apps.django_app.default.models import UserSocialAuth
from social_django.models import UserSocialAuth
from unittest import skip
from third_party_auth.saml import log as saml_log
@@ -119,7 +118,7 @@ class SamlIntegrationTestUtilities(object):
@ddt.ddt
@unittest.skipUnless(testutil.AUTH_FEATURE_ENABLED, 'third_party_auth not enabled')
@unittest.skipUnless(testutil.AUTH_FEATURE_ENABLED, testutil.AUTH_FEATURES_KEY + ' not enabled')
class TestShibIntegrationTest(SamlIntegrationTestUtilities, IntegrationTestMixin, testutil.SAMLTestCase):
"""
TestShib provider Integration Test, to test SAML functionality
@@ -157,7 +156,7 @@ class TestShibIntegrationTest(SamlIntegrationTestUtilities, IntegrationTestMixin
record = UserSocialAuth.objects.get(
user=self.user, provider=self.PROVIDER_BACKEND, uid__startswith=self.PROVIDER_IDP_SLUG
)
attributes = record.extra_data["attributes"]
attributes = record.extra_data
self.assertEqual(
attributes.get("urn:oid:1.3.6.1.4.1.5923.1.1.1.9"), ["Member@testshib.org", "Staff@testshib.org"]
)
@@ -232,7 +231,7 @@ class TestShibIntegrationTest(SamlIntegrationTestUtilities, IntegrationTestMixin
self._test_return_login(previous_session_timed_out=True)
@unittest.skipUnless(testutil.AUTH_FEATURE_ENABLED, 'third_party_auth not enabled')
@unittest.skipUnless(testutil.AUTH_FEATURE_ENABLED, testutil.AUTH_FEATURES_KEY + ' not enabled')
class SuccessFactorsIntegrationTest(SamlIntegrationTestUtilities, IntegrationTestMixin, testutil.SAMLTestCase):
"""
Test basic SAML capability using the TestShib details, and then check that we're able

View File

@@ -20,7 +20,7 @@ class TwitterIntegrationTest(base.Oauth2IntegrationTest):
# To test an OAuth1 provider, we need to patch an additional method:
patcher = patch(
'social.backends.twitter.TwitterOAuth.unauthorized_token',
'social_core.backends.twitter.TwitterOAuth.unauthorized_token',
create=True,
return_value="unauth_token"
)

View File

@@ -16,7 +16,7 @@ from third_party_auth.tests import testutil
# This is necessary because cms does not implement third party auth
@unittest.skipUnless(settings.FEATURES.get('ENABLE_THIRD_PARTY_AUTH'), 'third party auth not enabled')
@unittest.skipUnless(testutil.AUTH_FEATURE_ENABLED, testutil.AUTH_FEATURES_KEY + ' not enabled')
class Oauth2ProviderConfigAdminTest(testutil.TestCase):
"""
Tests for oauth2 provider config admin

View File

@@ -5,6 +5,7 @@ import unittest
from django.conf import settings
from django.test import Client
from social_django.models import Partial
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
@@ -13,39 +14,47 @@ class TestSessionFlushMiddleware(unittest.TestCase):
Ensure that if the pipeline is exited when it's been quarantined,
the entire session is flushed.
"""
def setUp(self):
self.client = Client()
self.fancy_variable = 13025
self.token = 'pipeline_running'
self.tpa_quarantined_modules = ('fake_quarantined_module',)
def tearDown(self):
Partial.objects.all().delete()
def test_session_flush(self):
"""
Test that a quarantined session is flushed when navigating elsewhere
"""
client = Client()
session = client.session
session['fancy_variable'] = 13025
session['partial_pipeline'] = 'pipeline_running'
session['third_party_auth_quarantined_modules'] = ('fake_quarantined_module',)
session = self.client.session
session['fancy_variable'] = self.fancy_variable
session['partial_pipeline_token'] = self.token
session['third_party_auth_quarantined_modules'] = self.tpa_quarantined_modules
session.save()
client.get('/')
self.assertEqual(client.session.get('fancy_variable', None), None)
Partial.objects.create(token=session.get('partial_pipeline_token'))
self.client.get('/')
self.assertEqual(self.client.session.get('fancy_variable', None), None)
def test_session_no_running_pipeline(self):
"""
Test that a quarantined session without a running pipeline is not flushed
"""
client = Client()
session = client.session
session['fancy_variable'] = 13025
session['third_party_auth_quarantined_modules'] = ('fake_quarantined_module',)
session = self.client.session
session['fancy_variable'] = self.fancy_variable
session['third_party_auth_quarantined_modules'] = self.tpa_quarantined_modules
session.save()
client.get('/')
self.assertEqual(client.session.get('fancy_variable', None), 13025)
self.client.get('/')
self.assertEqual(self.client.session.get('fancy_variable', None), self.fancy_variable)
def test_session_no_quarantine(self):
"""
Test that a session with a running pipeline but no quarantine is not flushed
"""
client = Client()
session = client.session
session['fancy_variable'] = 13025
session['partial_pipeline'] = 'pipeline_running'
session = self.client.session
session['fancy_variable'] = self.fancy_variable
session['partial_pipeline_token'] = self.token
session.save()
client.get('/')
self.assertEqual(client.session.get('fancy_variable', None), 13025)
Partial.objects.create(token=session.get('partial_pipeline_token'))
self.client.get('/')
self.assertEqual(self.client.session.get('fancy_variable', None), self.fancy_variable)

View File

@@ -35,7 +35,7 @@ class MakeRandomPasswordTest(testutil.TestCase):
self.assertEqual(expected, pipeline.make_random_password(choice_fn=random_instance.choice))
@unittest.skipUnless(testutil.AUTH_FEATURE_ENABLED, 'third_party_auth not enabled')
@unittest.skipUnless(testutil.AUTH_FEATURE_ENABLED, testutil.AUTH_FEATURES_KEY + ' not enabled')
class ProviderUserStateTestCase(testutil.TestCase):
"""Tests ProviderUserState behavior."""

View File

@@ -6,7 +6,7 @@ import mock
from django import test
from django.conf import settings
from django.contrib.auth import models
from social.apps.django_app.default import models as social_models
from social_django import models as social_models
from third_party_auth import pipeline, provider
from third_party_auth.tests import testutil
@@ -24,8 +24,7 @@ class TestCase(testutil.TestCase, test.TestCase):
self.enabled_provider = self.configure_google_provider(enabled=True)
@unittest.skipUnless(
testutil.AUTH_FEATURES_KEY in settings.FEATURES, testutil.AUTH_FEATURES_KEY + ' not in settings.FEATURES')
@unittest.skipUnless(testutil.AUTH_FEATURE_ENABLED, testutil.AUTH_FEATURES_KEY + ' not enabled')
class GetAuthenticatedUserTestCase(TestCase):
"""Tests for get_authenticated_user."""
@@ -66,8 +65,7 @@ class GetAuthenticatedUserTestCase(TestCase):
self.assertEqual(self.enabled_provider.get_authentication_backend(), user.backend)
@unittest.skipUnless(
testutil.AUTH_FEATURES_KEY in settings.FEATURES, testutil.AUTH_FEATURES_KEY + ' not in settings.FEATURES')
@unittest.skipUnless(testutil.AUTH_FEATURE_ENABLED, testutil.AUTH_FEATURES_KEY + ' not enabled')
class GetProviderUserStatesTestCase(testutil.TestCase, test.TestCase):
"""Tests generation of ProviderUserStates."""
@@ -143,8 +141,7 @@ class GetProviderUserStatesTestCase(testutil.TestCase, test.TestCase):
self.assertEqual(self.user, linkedin_state.user)
@unittest.skipUnless(
testutil.AUTH_FEATURES_KEY in settings.FEATURES, testutil.AUTH_FEATURES_KEY + ' not in settings.FEATURES')
@unittest.skipUnless(testutil.AUTH_FEATURE_ENABLED, testutil.AUTH_FEATURES_KEY + ' not enabled')
class UrlFormationTestCase(TestCase):
"""Tests formation of URLs for pipeline hook points."""
@@ -210,8 +207,7 @@ class UrlFormationTestCase(TestCase):
pipeline.get_complete_url(provider_id)
@unittest.skipUnless(
testutil.AUTH_FEATURES_KEY in settings.FEATURES, testutil.AUTH_FEATURES_KEY + ' not in settings.FEATURES')
@unittest.skipUnless(testutil.AUTH_FEATURE_ENABLED, testutil.AUTH_FEATURES_KEY + ' not enabled')
class TestPipelineUtilityFunctions(TestCase, test.TestCase):
"""
Test some of the isolated utility functions in the pipeline
@@ -230,36 +226,36 @@ class TestPipelineUtilityFunctions(TestCase, test.TestCase):
Test that we can use a dictionary with a UID entry to retrieve a
database-backed UserSocialAuth object.
"""
request = mock.MagicMock(
session={
'partial_pipeline': {
'kwargs': {
'social': {
'uid': 'fake uid'
}
}
request = mock.MagicMock()
pipeline_partial = {
'kwargs': {
'social': {
'uid': 'fake uid'
}
}
)
real_social = pipeline.get_real_social_auth_object(request)
self.assertEqual(real_social, self.social_auth)
}
with mock.patch('third_party_auth.pipeline.get') as get_pipeline:
get_pipeline.return_value = pipeline_partial
real_social = pipeline.get_real_social_auth_object(request)
self.assertEqual(real_social, self.social_auth)
def test_get_real_social_auth(self):
"""
Test that trying to get a database-backed UserSocialAuth from an existing
instance returns correctly.
"""
request = mock.MagicMock(
session={
'partial_pipeline': {
'kwargs': {
'social': self.social_auth
}
}
request = mock.MagicMock()
pipeline_partial = {
'kwargs': {
'social': self.social_auth
}
)
real_social = pipeline.get_real_social_auth_object(request)
self.assertEqual(real_social, self.social_auth)
}
with mock.patch('third_party_auth.pipeline.get') as get_pipeline:
get_pipeline.return_value = pipeline_partial
real_social = pipeline.get_real_social_auth_object(request)
self.assertEqual(real_social, self.social_auth)
def test_get_real_social_auth_no_pipeline(self):
"""

View File

@@ -13,7 +13,7 @@ SITE_DOMAIN_A = 'professionalx.example.com'
SITE_DOMAIN_B = 'somethingelse.example.com'
@unittest.skipUnless(testutil.AUTH_FEATURE_ENABLED, 'third_party_auth not enabled')
@unittest.skipUnless(testutil.AUTH_FEATURE_ENABLED, testutil.AUTH_FEATURES_KEY + ' not enabled')
class RegistryTest(testutil.TestCase):
"""Tests registry discovery and operation."""

View File

@@ -45,7 +45,7 @@ class SettingsUnitTest(testutil.TestCase):
settings.apply_settings(self.settings)
self.assertEqual(settings._FIELDS_STORED_IN_SESSION, self.settings.FIELDS_STORED_IN_SESSION)
@unittest.skipUnless(testutil.AUTH_FEATURE_ENABLED, 'third_party_auth not enabled')
@unittest.skipUnless(testutil.AUTH_FEATURE_ENABLED, testutil.AUTH_FEATURES_KEY + ' not enabled')
def test_apply_settings_enables_no_providers_by_default(self):
# Providers are only enabled via ConfigurationModels in the database
settings.apply_settings(self.settings)

View File

@@ -12,12 +12,12 @@ from onelogin.saml2.errors import OneLogin_Saml2_Error
# Define some XML namespaces:
from third_party_auth.tasks import SAML_XML_NS
from .testutil import AUTH_FEATURE_ENABLED, SAMLTestCase
from .testutil import AUTH_FEATURE_ENABLED, AUTH_FEATURES_KEY, SAMLTestCase
XMLDSIG_XML_NS = 'http://www.w3.org/2000/09/xmldsig#'
@unittest.skipUnless(AUTH_FEATURE_ENABLED, 'third_party_auth not enabled')
@unittest.skipUnless(AUTH_FEATURE_ENABLED, AUTH_FEATURES_KEY + ' not enabled')
@ddt.ddt
class SAMLMetadataTest(SAMLTestCase):
"""
@@ -135,7 +135,7 @@ class SAMLMetadataTest(SAMLTestCase):
self.assertEqual(support_email_node.text, support_email)
@unittest.skipUnless(AUTH_FEATURE_ENABLED, 'third_party_auth not enabled')
@unittest.skipUnless(AUTH_FEATURE_ENABLED, AUTH_FEATURES_KEY + ' not enabled')
class SAMLAuthTest(SAMLTestCase):
"""
Test the SAML auth views

View File

@@ -4,8 +4,8 @@ import json
import httpretty
from provider.constants import PUBLIC
from provider.oauth2.models import Client
from social.apps.django_app.default.models import UserSocialAuth
from social.backends.facebook import FacebookOAuth2
from social_core.backends.facebook import FacebookOAuth2, API_VERSION as FACEBOOK_API_VERSION
from social_django.models import UserSocialAuth, Partial
from student.tests.factories import UserFactory
@@ -39,6 +39,10 @@ class ThirdPartyOAuthTestMixin(ThirdPartyAuthTestMixin):
elif self.BACKEND == 'facebook':
self.configure_facebook_provider(enabled=True, visible=True)
def tearDown(self):
super(ThirdPartyOAuthTestMixin, self).tearDown()
Partial.objects.all().delete()
def _create_client(self):
"""
Create an OAuth2 client application
@@ -81,7 +85,7 @@ class ThirdPartyOAuthTestMixin(ThirdPartyAuthTestMixin):
class ThirdPartyOAuthTestMixinFacebook(object):
"""Tests oauth with the Facebook backend"""
BACKEND = "facebook"
USER_URL = FacebookOAuth2.USER_DATA_URL
USER_URL = FacebookOAuth2.USER_DATA_URL.format(version=FACEBOOK_API_VERSION)
# In facebook responses, the "id" field is used as the user's identifier
UID_FIELD = "id"