Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package python-social-auth-app-django for
openSUSE:Factory checked in at 2026-04-07 16:34:53
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-social-auth-app-django (Old)
and /work/SRC/openSUSE:Factory/.python-social-auth-app-django.new.21863
(New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-social-auth-app-django"
Tue Apr 7 16:34:53 2026 rev:17 rq:1344945 version:5.7.0
Changes:
--------
---
/work/SRC/openSUSE:Factory/python-social-auth-app-django/python-social-auth-app-django.changes
2025-10-03 15:44:32.819321403 +0200
+++
/work/SRC/openSUSE:Factory/.python-social-auth-app-django.new.21863/python-social-auth-app-django.changes
2026-04-07 16:51:33.015100283 +0200
@@ -1,0 +2,23 @@
+Sun Apr 5 14:55:01 UTC 2026 - ecsos <[email protected]>
+
+- Update to 5.7.0
+ * Changed
+ Integrated with social_core using registry instead of monkey patching it
+ * Donations
+ This project welcomes donations to make the development sustainable.
+ The following platforms are available for funding Python Social Auth:
+ GitHub Sponsors
+ Open Collective
+- Changes from 5.6.0
+ * Changed
+ - Fixed possibly unsafe account association (CVE-2025-61783, bsc#1251851)
+ - Storage now filters for active users, you might need to customize
+ SOCIAL_AUTH_ACTIVE_USERS_FILTER if your custom model does not
+ have the is_active field
+ * Added
+ - Django 6.0 and Python 3.14 compatibility
+ - Type annotations
+ - LoginRequiredMiddleware compatibility
+ - RAISE_EXCEPTIONS and LOGIN_ERROR_URL can be configured per backend
+
+-------------------------------------------------------------------
Old:
----
social_auth_app_django-5.5.1.tar.gz
New:
----
social_auth_app_django-5.7.0.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-social-auth-app-django.spec ++++++
--- /var/tmp/diff_new_pack.QUdjbH/_old 2026-04-07 16:51:34.163147854 +0200
+++ /var/tmp/diff_new_pack.QUdjbH/_new 2026-04-07 16:51:34.163147854 +0200
@@ -1,7 +1,7 @@
#
# spec file for package python-social-auth-app-django
#
-# Copyright (c) 2025 SUSE LLC and contributors
+# Copyright (c) 2026 SUSE LLC and contributors
#
# All modifications and additions to the file contributed by third parties
# remain the property of their copyright owners, unless otherwise agreed
@@ -18,7 +18,7 @@
%{?sle15_python_module_pythons}
Name: python-social-auth-app-django
-Version: 5.5.1
+Version: 5.7.0
Release: 0
Summary: Python Social Authentication, Django integration
License: BSD-3-Clause
@@ -26,6 +26,7 @@
URL: https://github.com/python-social-auth/social-app-django
Source:
https://files.pythonhosted.org/packages/source/s/social_auth_app_django/social_auth_app_django-%{version}.tar.gz
BuildRequires: %{python_module Django >= 5.1}
+BuildRequires: %{python_module base >= 3.10}
BuildRequires: %{python_module pip}
BuildRequires: %{python_module setuptools}
BuildRequires: %{python_module social-auth-core >= 4.4.0}
++++++ social_auth_app_django-5.5.1.tar.gz ->
social_auth_app_django-5.7.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/social_auth_app_django-5.5.1/CHANGELOG.md
new/social_auth_app_django-5.7.0/CHANGELOG.md
--- old/social_auth_app_django-5.5.1/CHANGELOG.md 2025-06-27
15:11:29.000000000 +0200
+++ new/social_auth_app_django-5.7.0/CHANGELOG.md 2025-12-18
20:00:08.000000000 +0100
@@ -5,6 +5,26 @@
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).
+##
[5.7.0](https://github.com/python-social-auth/social-app-django/releases/tag/5.7.0)
- 2025-12-18
+
+### Changed
+
+- Integrated with `social_core` using registry instead of monkey patching it
+
+##
[5.6.0](https://github.com/python-social-auth/social-app-django/releases/tag/5.6.0)
- 2025-10-09
+
+### Changed
+
+- Fixed possibly unsafe account association
([CVE-2025-61783](https://github.com/python-social-auth/social-app-django/security/advisories/GHSA-wv4w-6qv2-qqfg))
+- Storage now filters for active users, you might need to customize
`SOCIAL_AUTH_ACTIVE_USERS_FILTER` if your custom model does not have the
`is_active` field
+
+### Added
+
+- Django 6.0 and Python 3.14 compatibility
+- Type annotations
+- LoginRequiredMiddleware compatibility
+- `RAISE_EXCEPTIONS` and `LOGIN_ERROR_URL` can be configured per backend
+
##
[5.5.1](https://github.com/python-social-auth/social-app-django/releases/tag/5.5.1)
- 2025-06-27
### Changed
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/social_auth_app_django-5.5.1/PKG-INFO
new/social_auth_app_django-5.7.0/PKG-INFO
--- old/social_auth_app_django-5.5.1/PKG-INFO 2025-06-27 15:11:31.459310000
+0200
+++ new/social_auth_app_django-5.7.0/PKG-INFO 2025-12-18 20:00:10.786944200
+0100
@@ -1,6 +1,6 @@
Metadata-Version: 2.4
Name: social-auth-app-django
-Version: 5.5.1
+Version: 5.7.0
Summary: Python Social Authentication, Django integration.
Author-email: Matias Aguirre <[email protected]>, Michal Čihař
<[email protected]>
License-Expression: BSD-3-Clause
@@ -16,20 +16,21 @@
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
+Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Python
Classifier: Topic :: Internet
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: Django>=5.1
-Requires-Dist: social-auth-core~=4.4
+Requires-Dist: social-auth-core~=4.8.3
Provides-Extra: dev
Requires-Dist: coverage>=3.6; extra == "dev"
-Requires-Dist: django-stubs-ext==5.2.1; extra == "dev"
-Requires-Dist: django-stubs==5.2.1; extra == "dev"
-Requires-Dist: mypy==1.16.1; extra == "dev"
+Requires-Dist: django-stubs-ext==5.2.8; extra == "dev"
+Requires-Dist: django-stubs==5.2.8; extra == "dev"
+Requires-Dist: mypy==1.19.1; extra == "dev"
Requires-Dist: pre-commit; extra == "dev"
-Requires-Dist: pyright==1.1.402; extra == "dev"
+Requires-Dist: pyright==1.1.407; extra == "dev"
Requires-Dist: tox; extra == "dev"
Dynamic: license-file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/social_auth_app_django-5.5.1/pyproject.toml
new/social_auth_app_django-5.7.0/pyproject.toml
--- old/social_auth_app_django-5.5.1/pyproject.toml 2025-06-27
15:11:29.000000000 +0200
+++ new/social_auth_app_django-5.7.0/pyproject.toml 2025-12-18
20:00:08.000000000 +0100
@@ -5,11 +5,11 @@
[dependency-groups]
dev = [
"coverage>=3.6",
- "django-stubs-ext==5.2.1",
- "django-stubs==5.2.1",
- "mypy==1.16.1",
+ "django-stubs-ext==5.2.8",
+ "django-stubs==5.2.8",
+ "mypy==1.19.1",
"pre-commit",
- "pyright==1.1.402",
+ "pyright==1.1.407",
"tox"
]
@@ -27,12 +27,13 @@
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
+ "Programming Language :: Python :: 3.14",
"Programming Language :: Python",
"Topic :: Internet"
]
dependencies = [
"Django>=5.1",
- "social-auth-core~=4.4"
+ "social-auth-core~=4.8.3"
]
description = "Python Social Authentication, Django integration."
keywords = ["django", "oauth", "openid", "saml", "social auth"]
@@ -41,18 +42,18 @@
name = "social-auth-app-django"
readme = "README.md"
requires-python = ">=3.10"
-version = "5.5.1"
+version = "5.7.0"
[project.optional-dependencies]
# This is present until pip implements supports for PEP 735
# see https://github.com/pypa/pip/issues/12963
dev = [
"coverage>=3.6",
- "django-stubs-ext==5.2.1",
- "django-stubs==5.2.1",
- "mypy==1.16.1",
+ "django-stubs-ext==5.2.8",
+ "django-stubs==5.2.8",
+ "mypy==1.19.1",
"pre-commit",
- "pyright==1.1.402",
+ "pyright==1.1.407",
"tox"
]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/social_auth_app_django-5.5.1/social_auth_app_django.egg-info/PKG-INFO
new/social_auth_app_django-5.7.0/social_auth_app_django.egg-info/PKG-INFO
--- old/social_auth_app_django-5.5.1/social_auth_app_django.egg-info/PKG-INFO
2025-06-27 15:11:31.000000000 +0200
+++ new/social_auth_app_django-5.7.0/social_auth_app_django.egg-info/PKG-INFO
2025-12-18 20:00:10.000000000 +0100
@@ -1,6 +1,6 @@
Metadata-Version: 2.4
Name: social-auth-app-django
-Version: 5.5.1
+Version: 5.7.0
Summary: Python Social Authentication, Django integration.
Author-email: Matias Aguirre <[email protected]>, Michal Čihař
<[email protected]>
License-Expression: BSD-3-Clause
@@ -16,20 +16,21 @@
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
+Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Python
Classifier: Topic :: Internet
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: Django>=5.1
-Requires-Dist: social-auth-core~=4.4
+Requires-Dist: social-auth-core~=4.8.3
Provides-Extra: dev
Requires-Dist: coverage>=3.6; extra == "dev"
-Requires-Dist: django-stubs-ext==5.2.1; extra == "dev"
-Requires-Dist: django-stubs==5.2.1; extra == "dev"
-Requires-Dist: mypy==1.16.1; extra == "dev"
+Requires-Dist: django-stubs-ext==5.2.8; extra == "dev"
+Requires-Dist: django-stubs==5.2.8; extra == "dev"
+Requires-Dist: mypy==1.19.1; extra == "dev"
Requires-Dist: pre-commit; extra == "dev"
-Requires-Dist: pyright==1.1.402; extra == "dev"
+Requires-Dist: pyright==1.1.407; extra == "dev"
Requires-Dist: tox; extra == "dev"
Dynamic: license-file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/social_auth_app_django-5.5.1/social_auth_app_django.egg-info/SOURCES.txt
new/social_auth_app_django-5.7.0/social_auth_app_django.egg-info/SOURCES.txt
---
old/social_auth_app_django-5.5.1/social_auth_app_django.egg-info/SOURCES.txt
2025-06-27 15:11:31.000000000 +0200
+++
new/social_auth_app_django-5.7.0/social_auth_app_django.egg-info/SOURCES.txt
2025-12-18 20:00:10.000000000 +0100
@@ -18,6 +18,7 @@
social_django/managers.py
social_django/middleware.py
social_django/models.py
+social_django/py.typed
social_django/storage.py
social_django/strategy.py
social_django/urls.py
@@ -51,6 +52,7 @@
tests/test_middleware.py
tests/test_migrations.py
tests/test_models.py
+tests/test_storage_integration.py
tests/test_strategy.py
tests/test_views.py
tests/urls.py
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/social_auth_app_django-5.5.1/social_auth_app_django.egg-info/requires.txt
new/social_auth_app_django-5.7.0/social_auth_app_django.egg-info/requires.txt
---
old/social_auth_app_django-5.5.1/social_auth_app_django.egg-info/requires.txt
2025-06-27 15:11:31.000000000 +0200
+++
new/social_auth_app_django-5.7.0/social_auth_app_django.egg-info/requires.txt
2025-12-18 20:00:10.000000000 +0100
@@ -1,11 +1,11 @@
Django>=5.1
-social-auth-core~=4.4
+social-auth-core~=4.8.3
[dev]
coverage>=3.6
-django-stubs-ext==5.2.1
-django-stubs==5.2.1
-mypy==1.16.1
+django-stubs-ext==5.2.8
+django-stubs==5.2.8
+mypy==1.19.1
pre-commit
-pyright==1.1.402
+pyright==1.1.407
tox
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/social_auth_app_django-5.5.1/social_django/__init__.py
new/social_auth_app_django-5.7.0/social_django/__init__.py
--- old/social_auth_app_django-5.5.1/social_django/__init__.py 2025-06-27
15:11:29.000000000 +0200
+++ new/social_auth_app_django-5.7.0/social_django/__init__.py 2025-12-18
20:00:08.000000000 +0100
@@ -1,26 +1,3 @@
import importlib.metadata
__version__ = importlib.metadata.version("social-auth-app-django")
-
-
-from social_core.backends.base import BaseAuth
-
-# django.contrib.auth.load_backend() will import and instantiate the
-# authentication backend ignoring the possibility that it might
-# require more arguments. Here we set a monkey patch to
-# BaseAuth.__init__ to ignore the mandatory strategy argument and load
-# it.
-
-
-def baseauth_init_workaround(original_init):
- def fake_init(self, strategy=None, *args, **kwargs):
- from .utils import load_strategy # noqa: PLC0415
-
- original_init(self, strategy or load_strategy(), *args, **kwargs)
-
- return fake_init
-
-
-if not getattr(BaseAuth, "__init_patched", False):
- BaseAuth.__init__ = baseauth_init_workaround(BaseAuth.__init__) # type:
ignore[method-assign]
- BaseAuth.__init_patched = True # type: ignore[attr-defined] # noqa: SLF001
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/social_auth_app_django-5.5.1/social_django/apps.py
new/social_auth_app_django-5.7.0/social_django/apps.py
--- old/social_auth_app_django-5.5.1/social_django/apps.py 2025-06-27
15:11:29.000000000 +0200
+++ new/social_auth_app_django-5.7.0/social_django/apps.py 2025-12-18
20:00:08.000000000 +0100
@@ -1,4 +1,5 @@
from django.apps import AppConfig
+from social_core.registry import REGISTRY
class PythonSocialAuthConfig(AppConfig):
@@ -10,3 +11,13 @@
label = "social_django"
# Human-readable name for the application eg. "Admin".
verbose_name = "Python Social Auth"
+
+ def ready(self) -> None:
+ from .utils import load_strategy # noqa: PLC0415
+
+ super().ready()
+
+ # django.contrib.auth.load_backend() will import and instantiate the
+ # authentication backend ignoring the possibility that it might
+ # require more arguments. Here we set a default strategy for that case.
+ REGISTRY.default_strategy = load_strategy()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/social_auth_app_django-5.5.1/social_django/middleware.py
new/social_auth_app_django-5.7.0/social_django/middleware.py
--- old/social_auth_app_django-5.5.1/social_django/middleware.py
2025-06-27 15:11:29.000000000 +0200
+++ new/social_auth_app_django-5.7.0/social_django/middleware.py
2025-12-18 20:00:08.000000000 +0100
@@ -58,7 +58,8 @@
def raise_exception(self, request, exception):
strategy = getattr(request, "social_strategy", None)
if strategy is not None:
- return strategy.setting("RAISE_EXCEPTIONS", settings.DEBUG)
+ backend = getattr(request, "backend", None)
+ return strategy.setting("RAISE_EXCEPTIONS", settings.DEBUG,
backend=backend)
return None
def get_message(self, request, exception):
@@ -66,4 +67,5 @@
def get_redirect_uri(self, request, exception):
strategy = getattr(request, "social_strategy", None)
- return strategy.setting("LOGIN_ERROR_URL")
+ backend = getattr(request, "backend", None)
+ return strategy.setting("LOGIN_ERROR_URL", backend=backend)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/social_auth_app_django-5.5.1/social_django/storage.py
new/social_auth_app_django-5.7.0/social_django/storage.py
--- old/social_auth_app_django-5.5.1/social_django/storage.py 2025-06-27
15:11:29.000000000 +0200
+++ new/social_auth_app_django-5.7.0/social_django/storage.py 2025-12-18
20:00:08.000000000 +0100
@@ -1,10 +1,15 @@
"""Django ORM models for Social Auth"""
+from __future__ import annotations
+
import base64
+from typing import TYPE_CHECKING
+from django.conf import settings
from django.core.exceptions import FieldDoesNotExist
from django.db import router, transaction
from django.db.utils import IntegrityError
+from social_core.exceptions import AuthAlreadyAssociated
from social_core.storage import (
AssociationMixin,
BaseStorage,
@@ -13,6 +18,10 @@
PartialMixin,
UserMixin,
)
+from social_core.utils import setting_name
+
+if TYPE_CHECKING:
+ from django.db.models import QuerySet
class DjangoUserMixin(UserMixin):
@@ -53,7 +62,7 @@
"""
if "username" in kwargs:
kwargs[cls.username_field()] = kwargs.pop("username")
- return cls.user_model()._default_manager.filter(*args,
**kwargs).exists() # noqa: SLF001
+ return cls.filter_users(*args, **kwargs).exists()
@classmethod
def get_username(cls, user):
@@ -73,43 +82,42 @@
cls.user_model()._meta.get_field("username") # noqa:
SLF001
except FieldDoesNotExist:
kwargs.pop("username")
+
+ # If the create fails below due to an IntegrityError, ensure that the
transaction
+ # stays undamaged by wrapping the create in an atomic.
+ using = router.db_for_write(cls.user_model())
try:
- if hasattr(transaction, "atomic"):
- # In Django versions that have an "atomic" transaction
decorator / context
- # manager, there's a transaction wrapped around this call.
- # If the create fails below due to an IntegrityError, ensure
that the transaction
- # stays undamaged by wrapping the create in an atomic.
- using = router.db_for_write(cls.user_model())
- with transaction.atomic(using=using):
- user = manager.create_user(*args, **kwargs)
- else:
- user = manager.create_user(*args, **kwargs)
+ with transaction.atomic(using=using):
+ return manager.create_user(*args, **kwargs)
except IntegrityError as exc:
- # If email comes in as None it won't get found in the get
- if kwargs.get("email", True) is None:
- kwargs["email"] = ""
- try:
- user = manager.get(*args, **kwargs)
- except cls.user_model().DoesNotExist:
- raise exc from None
- return user
+ raise AuthAlreadyAssociated(None) from exc
+
+ @classmethod
+ def filter_users(cls, *args, **kwargs) -> QuerySet:
+ model = cls.user_model()
+ manager = model._default_manager # noqa: SLF001
+ return manager.filter(*args, **kwargs)
+
+ @classmethod
+ def filter_active_users(cls, *args, **kwargs) -> QuerySet:
+ active_filter = getattr(settings, setting_name("ACTIVE_USERS_FILTER"),
{"is_active": True})
+ kwargs.update(active_filter)
+ return cls.filter_users(*args, **kwargs)
@classmethod
def get_user(cls, pk=None, **kwargs):
if pk:
kwargs = {"pk": pk}
- try:
- return cls.user_model()._default_manager.get(**kwargs) # noqa:
SLF001
- except cls.user_model().DoesNotExist:
+ users = cls.filter_active_users(**kwargs)
+ if len(users) != 1:
return None
+ return users[0]
@classmethod
def get_users_by_email(cls, email):
user_model = cls.user_model()
email_field = getattr(user_model, "EMAIL_FIELD", "email")
- return user_model._default_manager.filter( # noqa: SLF001
- **{email_field + "__iexact": email}
- )
+ return cls.filter_active_users(**{f"{email_field}__iexact": email})
@classmethod
def get_social_auth(cls, provider, uid):
@@ -135,17 +143,11 @@
def create_social_auth(cls, user, uid, provider):
if not isinstance(uid, str):
uid = str(uid)
- if hasattr(transaction, "atomic"):
- # In Django versions that have an "atomic" transaction decorator /
context
- # manager, there's a transaction wrapped around this call.
- # If the create fails below due to an IntegrityError, ensure that
the transaction
- # stays undamaged by wrapping the create in an atomic.
- using = router.db_for_write(cls)
- with transaction.atomic(using=using):
- social_auth = cls.objects.create(user=user, uid=uid,
provider=provider)
- else:
- social_auth = cls.objects.create(user=user, uid=uid,
provider=provider)
- return social_auth
+ # If the create fails below due to an IntegrityError, ensure that the
transaction
+ # stays undamaged by wrapping the create in an atomic.
+ using = router.db_for_write(cls)
+ with transaction.atomic(using=using):
+ return cls.objects.create(user=user, uid=uid, provider=provider)
class DjangoNonceMixin(NonceMixin):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/social_auth_app_django-5.5.1/social_django/views.py
new/social_auth_app_django-5.7.0/social_django/views.py
--- old/social_auth_app_django-5.5.1/social_django/views.py 2025-06-27
15:11:29.000000000 +0200
+++ new/social_auth_app_django-5.7.0/social_django/views.py 2025-12-18
20:00:08.000000000 +0100
@@ -1,6 +1,6 @@
from django.conf import settings
from django.contrib.auth import REDIRECT_FIELD_NAME, login
-from django.contrib.auth.decorators import login_required
+from django.contrib.auth.decorators import login_not_required, login_required
from django.views.decorators.cache import never_cache
from django.views.decorators.csrf import csrf_exempt, csrf_protect
from django.views.decorators.http import require_POST
@@ -17,6 +17,7 @@
@never_cache
+@login_not_required
@maybe_require_post
@psa(f"{NAMESPACE}:complete")
def auth(request, backend):
@@ -24,6 +25,7 @@
@never_cache
+@login_not_required
@csrf_exempt
@psa(f"{NAMESPACE}:complete")
def complete(request, backend, *args, **kwargs):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/social_auth_app_django-5.5.1/tests/test_middleware.py
new/social_auth_app_django-5.7.0/tests/test_middleware.py
--- old/social_auth_app_django-5.5.1/tests/test_middleware.py 2025-06-27
15:11:29.000000000 +0200
+++ new/social_auth_app_django-5.7.0/tests/test_middleware.py 2025-12-18
20:00:08.000000000 +0100
@@ -51,3 +51,23 @@
response.url,
"/?message=Authentication%20process%20canceled&backend=facebook",
)
+
+ @override_settings(
+ SOCIAL_AUTH_LOGIN_ERROR_URL="/default-error",
+ SOCIAL_AUTH_FACEBOOK_LOGIN_ERROR_URL="/facebook-error",
+ )
+ def test_backend_specific_login_error_url(self, mocked):
+ response = self.client.get(self.complete_url)
+ self.assertTrue(isinstance(response, HttpResponseRedirect))
+ self.assertEqual(response.url, "/facebook-error")
+
+ @override_settings(
+ DEBUG=False,
+ SOCIAL_AUTH_RAISE_EXCEPTIONS=False,
+ SOCIAL_AUTH_FACEBOOK_RAISE_EXCEPTIONS=True,
+ )
+ def test_backend_specific_raise_exceptions(self, mocked):
+ logging.disable(logging.CRITICAL)
+ with self.assertRaises(MockAuthCanceled):
+ self.client.get(self.complete_url)
+ logging.disable(logging.NOTSET)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/social_auth_app_django-5.5.1/tests/test_models.py
new/social_auth_app_django-5.7.0/tests/test_models.py
--- old/social_auth_app_django-5.5.1/tests/test_models.py 2025-06-27
15:11:29.000000000 +0200
+++ new/social_auth_app_django-5.7.0/tests/test_models.py 2025-12-18
20:00:08.000000000 +0100
@@ -4,7 +4,8 @@
from django.contrib.auth import get_user_model
from django.core.management import call_command
from django.db import IntegrityError
-from django.test import TestCase
+from django.test import TestCase, override_settings
+from social_core.exceptions import AuthAlreadyAssociated
from social_django.models import (
AbstractUserSocialAuth,
@@ -101,22 +102,21 @@
self.assertEqual(UserSocialAuth.get_username(self.user),
self.user.username)
def test_create_user(self):
- # Catch integrity error and find existing user
- UserSocialAuth.create_user(username=self.user.username)
+ UserSocialAuth.create_user(username="testuser")
def test_create_user_reraise(self):
- with self.assertRaises(IntegrityError):
+ with self.assertRaises(AuthAlreadyAssociated):
UserSocialAuth.create_user(username=self.user.username, email=None)
@mock.patch("social_django.models.UserSocialAuth.username_field",
return_value="email")
- @mock.patch("django.contrib.auth.models.UserManager.create_user",
side_effect=IntegrityError)
+ @mock.patch("django.contrib.auth.models.UserManager.create_user",
return_value="<User>")
def test_create_user_custom_username(self, *args):
UserSocialAuth.create_user(username=self.user.email)
- @mock.patch("social_django.storage.transaction", spec=[])
- def test_create_user_without_transaction_atomic(self, *args):
- UserSocialAuth.create_user(username="test")
-
self.assertTrue(self.user_model._default_manager.filter(username="test").exists())
# noqa: SLF001
+ @mock.patch("django.contrib.auth.models.UserManager.create_user",
side_effect=IntegrityError)
+ def test_create_user_existing(self, *args):
+ with self.assertRaises(AuthAlreadyAssociated):
+ UserSocialAuth.create_user(username=self.user.email)
def test_get_user(self):
self.assertEqual(UserSocialAuth.get_user(pk=self.user.pk), self.user)
@@ -125,6 +125,13 @@
def test_get_users_by_email(self):
qs = UserSocialAuth.get_users_by_email(email=self.user.email)
self.assertEqual(qs.count(), 1)
+ self.user.is_active = False
+ self.user.save()
+ qs = UserSocialAuth.get_users_by_email(email=self.user.email)
+ self.assertEqual(qs.count(), 0)
+ with override_settings(SOCIAL_AUTH_ACTIVE_USERS_FILTER={}):
+ qs = UserSocialAuth.get_users_by_email(email=self.user.email)
+ self.assertEqual(qs.count(), 1)
def test_get_social_auth(self):
usa = self.usa
@@ -174,11 +181,6 @@
self.assertEqual(usa.uid, "1")
self.assertEqual(str(usa), str(self.user))
- @mock.patch("social_django.storage.transaction", spec=[])
- def test_create_social_auth_without_transaction_atomic(self, *args):
- with self.assertRaises(IntegrityError):
- UserSocialAuth.create_social_auth(user=self.user,
provider=self.usa.provider, uid=self.usa.uid)
-
def test_username_max_length(self):
self.assertEqual(UserSocialAuth.username_max_length(), 150)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/social_auth_app_django-5.5.1/tests/test_storage_integration.py
new/social_auth_app_django-5.7.0/tests/test_storage_integration.py
--- old/social_auth_app_django-5.5.1/tests/test_storage_integration.py
1970-01-01 01:00:00.000000000 +0100
+++ new/social_auth_app_django-5.7.0/tests/test_storage_integration.py
2025-12-18 20:00:08.000000000 +0100
@@ -0,0 +1,261 @@
+"""Integration tests for storage layer to catch breaking changes in
social-core API
+
+These tests are designed to catch breaking changes in the social-core storage
API that could
+go unnoticed without integration testing. The issue that prompted these tests
was:
+
+https://github.com/python-social-auth/social-core/pull/986 introduced a
breaking change
+in the OpenID storage API (specifically in get_association method) that went
unnoticed
+into release. The error manifested as:
+
+ NotImplementedError: Implement in subclass
+ File "social_core/storage.py", line 256, in get_association
+ raise NotImplementedError("Implement in subclass")
+
+The breaking change happened when:
+1. social-core's OpenIdStore.getAssociation() calls self.assoc.oids()
+2. AssociationMixin.oids() calls cls.get(**kwargs)
+3. If get() is not properly implemented in the Django storage layer, it raises
NotImplementedError
+
+These integration tests ensure that:
+- DjangoAssociationMixin.get() works correctly and returns a QuerySet
+- DjangoAssociationMixin.oids() properly calls get() and converts results to
OpenIdAssociation objects
+- OpenIdStore.getAssociation() can successfully retrieve associations through
the full call stack
+- All OpenID association and nonce storage operations work end-to-end
+"""
+
+import time
+from unittest import mock
+
+from django.test import TestCase
+from social_core.store import OpenIdStore
+from social_core.strategy import BaseStrategy
+
+from social_django.models import Association, DjangoStorage, Nonce
+
+
+class TestStorageIntegration(TestCase):
+ """Test integration between DjangoStorage and social-core's OpenIdStore"""
+
+ def setUp(self):
+ # Create a mock strategy with DjangoStorage
+ self.strategy = mock.Mock(spec=BaseStrategy)
+ self.strategy.storage = DjangoStorage
+ self.store = OpenIdStore(self.strategy)
+
+ def test_openid_store_association_workflow(self):
+ """Test the full OpenID association workflow through OpenIdStore"""
+ # Create a mock OpenID association (using string handle as in real
openid library)
+ mock_association = mock.Mock()
+ mock_association.handle = "test_handle"
+ mock_association.secret = b"test_secret"
+ mock_association.issued = int(time.time())
+ mock_association.lifetime = 3600
+ mock_association.assoc_type = "HMAC-SHA1"
+
+ server_url = "https://example.com/openid"
+
+ # Test storeAssociation
+ self.store.storeAssociation(server_url, mock_association)
+
+ # Verify association was stored
+ self.assertEqual(Association.objects.count(), 1)
+ stored = Association.objects.first()
+ self.assertEqual(stored.server_url, server_url)
+ self.assertEqual(stored.handle, "test_handle")
+
+ # Test getAssociation - this is the critical method that was breaking
+ retrieved = self.store.getAssociation(server_url)
+ self.assertIsNotNone(retrieved)
+ self.assertEqual(retrieved.handle, "test_handle")
+
+ # Test getAssociation with handle
+ retrieved_with_handle = self.store.getAssociation(server_url,
"test_handle")
+ self.assertIsNotNone(retrieved_with_handle)
+ self.assertEqual(retrieved_with_handle.handle, "test_handle")
+
+ # Test removeAssociation
+ self.store.removeAssociation(server_url, "test_handle")
+ self.assertEqual(Association.objects.count(), 0)
+
+ # Test getAssociation returns None after removal
+ self.assertIsNone(self.store.getAssociation(server_url))
+
+ def test_openid_store_association_expiration(self):
+ """Test that expired associations are handled correctly"""
+ # Create an expired association
+ mock_association = mock.Mock()
+ mock_association.handle = "expired_handle"
+ mock_association.secret = b"test_secret"
+ mock_association.issued = int(time.time()) - 7200 # 2 hours ago
+ mock_association.lifetime = 3600 # 1 hour lifetime, so expired
+ mock_association.assoc_type = "HMAC-SHA1"
+
+ server_url = "https://example.com/openid"
+
+ self.store.storeAssociation(server_url, mock_association)
+ self.assertEqual(Association.objects.count(), 1)
+
+ # getAssociation should return None for expired associations and clean
them up
+ retrieved = self.store.getAssociation(server_url)
+ self.assertIsNone(retrieved)
+ self.assertEqual(Association.objects.count(), 0)
+
+ def test_openid_store_multiple_associations(self):
+ """Test handling multiple associations for the same server"""
+ server_url = "https://example.com/openid"
+ current_time = int(time.time())
+
+ # Store multiple associations with different handles
+ for i in range(3):
+ mock_association = mock.Mock()
+ mock_association.handle = f"handle_{i}"
+ mock_association.secret = b"test_secret"
+ mock_association.issued = current_time + i
+ mock_association.lifetime = 3600
+ mock_association.assoc_type = "HMAC-SHA1"
+
+ self.store.storeAssociation(server_url, mock_association)
+
+ self.assertEqual(Association.objects.count(), 3)
+
+ # getAssociation should return the most recent one
+ retrieved = self.store.getAssociation(server_url)
+ self.assertIsNotNone(retrieved)
+ self.assertEqual(retrieved.handle, "handle_2")
+
+ # Get specific association by handle
+ retrieved_specific = self.store.getAssociation(server_url, "handle_1")
+ self.assertIsNotNone(retrieved_specific)
+ self.assertEqual(retrieved_specific.handle, "handle_1")
+
+ def test_openid_store_nonce_workflow(self):
+ """Test the OpenID nonce workflow through OpenIdStore"""
+ server_url = "https://example.com/openid"
+ timestamp = int(time.time())
+ salt = "test_salt"
+
+ # First use should succeed
+ self.assertTrue(self.store.useNonce(server_url, timestamp, salt))
+ self.assertEqual(Nonce.objects.count(), 1)
+
+ # Second use with same parameters should fail (nonce already used)
+ self.assertFalse(self.store.useNonce(server_url, timestamp, salt))
+ self.assertEqual(Nonce.objects.count(), 1)
+
+ # Different salt should succeed
+ self.assertTrue(self.store.useNonce(server_url, timestamp,
"different_salt"))
+ self.assertEqual(Nonce.objects.count(), 2)
+
+ def test_openid_store_nonce_timestamp_skew(self):
+ """Test that nonces with excessive timestamp skew are rejected"""
+ server_url = "https://example.com/openid"
+ current_time = int(time.time())
+ old_timestamp = current_time - 7 * 60 * 60 # 7 hours ago (exceeds 6
hour skew)
+ salt = "test_salt"
+
+ # Old timestamp should be rejected
+ self.assertFalse(self.store.useNonce(server_url, old_timestamp, salt))
+ self.assertEqual(Nonce.objects.count(), 0)
+
+
+class TestAssociationMixinIntegration(TestCase):
+ """Test DjangoAssociationMixin methods used by social-core"""
+
+ def test_oids_method(self):
+ """Test the oids method that is called by OpenIdStore.getAssociation"""
+ # Create test associations
+ mock_assoc1 = mock.Mock(handle="handle1", secret=b"secret1",
issued=1000, lifetime=3600, assoc_type="HMAC-SHA1")
+ mock_assoc2 = mock.Mock(handle="handle2", secret=b"secret2",
issued=2000, lifetime=3600, assoc_type="HMAC-SHA1")
+
+ server_url = "https://example.com/openid"
+ Association.store(server_url, mock_assoc1)
+ Association.store(server_url, mock_assoc2)
+
+ # Test oids() method - returns sorted list of (id, association) tuples
+ oids_result = list(Association.oids(server_url))
+ self.assertEqual(len(oids_result), 2)
+
+ # Should be sorted by issued timestamp (most recent first)
+ self.assertEqual(oids_result[0][1].handle, "handle2")
+ self.assertEqual(oids_result[1][1].handle, "handle1")
+
+ def test_oids_method_with_handle(self):
+ """Test oids method with specific handle filter"""
+ mock_assoc1 = mock.Mock(handle="handle1", secret=b"secret1",
issued=1000, lifetime=3600, assoc_type="HMAC-SHA1")
+ mock_assoc2 = mock.Mock(handle="handle2", secret=b"secret2",
issued=2000, lifetime=3600, assoc_type="HMAC-SHA1")
+
+ server_url = "https://example.com/openid"
+ Association.store(server_url, mock_assoc1)
+ Association.store(server_url, mock_assoc2)
+
+ # Test oids() with handle filter
+ oids_result = list(Association.oids(server_url, "handle1"))
+ self.assertEqual(len(oids_result), 1)
+ self.assertEqual(oids_result[0][1].handle, "handle1")
+
+ def test_get_method(self):
+ """Test the get method that is called by oids"""
+ mock_assoc = mock.Mock(
+ handle="test_handle", secret=b"secret", issued=1000,
lifetime=3600, assoc_type="HMAC-SHA1"
+ )
+
+ server_url = "https://example.com/openid"
+ Association.store(server_url, mock_assoc)
+
+ # Test get() method returns QuerySet
+ result = Association.get(server_url=server_url)
+ self.assertEqual(result.count(), 1)
+ self.assertEqual(result.first().handle, "test_handle")
+
+ # Test get() with handle filter
+ result_with_handle = Association.get(server_url=server_url,
handle="test_handle")
+ self.assertEqual(result_with_handle.count(), 1)
+
+ # Test get() with non-existent handle
+ result_none = Association.get(server_url=server_url,
handle="nonexistent")
+ self.assertEqual(result_none.count(), 0)
+
+
+class TestNonceMixinIntegration(TestCase):
+ """Test DjangoNonceMixin methods used by social-core"""
+
+ def test_use_method(self):
+ """Test the use method that is called by OpenIdStore.useNonce"""
+ server_url = "https://example.com/openid"
+ timestamp = 1234567890
+ salt = "test_salt"
+
+ # First use should return True (created)
+ self.assertTrue(Nonce.use(server_url, timestamp, salt))
+ self.assertEqual(Nonce.objects.count(), 1)
+
+ # Second use should return False (already exists)
+ self.assertFalse(Nonce.use(server_url, timestamp, salt))
+ self.assertEqual(Nonce.objects.count(), 1)
+
+ def test_get_method(self):
+ """Test the get method for retrieving nonces"""
+ server_url = "https://example.com/openid"
+ timestamp = 1234567890
+ salt = "test_salt"
+
+ Nonce.use(server_url, timestamp, salt)
+
+ # Test get() method
+ nonce = Nonce.get(server_url, salt)
+ self.assertIsNotNone(nonce)
+ self.assertEqual(nonce.server_url, server_url)
+ self.assertEqual(nonce.salt, salt)
+
+ def test_delete_method(self):
+ """Test the delete method for nonces"""
+ server_url = "https://example.com/openid"
+ timestamp = 1234567890
+ salt = "test_salt"
+
+ Nonce.use(server_url, timestamp, salt)
+ nonce = Nonce.get(server_url, salt)
+
+ # Test delete() method
+ Nonce.delete(nonce)
+ self.assertEqual(Nonce.objects.count(), 0)