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)

Reply via email to