Repository: pennersr/django-allauth
Pull request #3836 (2024-05-21 22:16:01)

docs: Add note when social_account_added is called

This should prevent further misconceptions as in #3834

# Submitting Pull Requests

## General

 - [x] Make sure you use [semantic commit messages](https://seesparkbox.com/foundry/semantic_commit_messages).
       Examples: `"fix(google): Fixed foobar bug"`, `"feat(accounts): Added foobar feature"`.
 - [ ] All Python code must formatted using Black, and clean from pep8 and isort issues.
 - [ ] JavaScript code should adhere to [StandardJS](https://standardjs.com).
 - [ ] If your changes are significant, please update `ChangeLog.rst`.
 - [ ] If your change is substantial, feel free to add yourself to `AUTHORS`.



Patch:
diff --git a/ChangeLog.rst b/ChangeLog.rst
index 3b130f42..f7e318c8 100644
--- a/ChangeLog.rst
+++ b/ChangeLog.rst
@@ -10,6 +10,9 @@
 - In case a headless attempt was made to connect a third-party account that was already
   connected to a different account, no error was communicated to the frontend. Fixed.
 
+- When the headless provider signup endpoint was called while that flow was not pending,
+  a crash would occur. This has been fixed to return a 409 (conflict).
+
 
 0.63.1 (2024-05-17)
 *******************
diff --git a/allauth/headless/base/response.py b/allauth/headless/base/response.py
index 96216e45..3ff235b8 100644
--- a/allauth/headless/base/response.py
+++ b/allauth/headless/base/response.py
@@ -99,6 +99,11 @@ class ForbiddenResponse(APIResponse):
         super().__init__(request, status=403)
 
 
+class ConflictResponse(APIResponse):
+    def __init__(self, request):
+        super().__init__(request, status=409)
+
+
 def user_data(user):
     """Basic user data, also exposed in partly authenticated scenario's
     (e.g. password reset, email verification).
diff --git a/allauth/headless/socialaccount/views.py b/allauth/headless/socialaccount/views.py
index 485ecc47..8c254448 100644
--- a/allauth/headless/socialaccount/views.py
+++ b/allauth/headless/socialaccount/views.py
@@ -3,6 +3,7 @@ from django.core.exceptions import ValidationError
 from allauth.core.exceptions import SignupClosedException
 from allauth.headless.base.response import (
     AuthenticationResponse,
+    ConflictResponse,
     ForbiddenResponse,
 )
 from allauth.headless.base.views import APIView, AuthenticatedAPIView
@@ -26,16 +27,21 @@ from allauth.socialaccount.models import SocialAccount
 class ProviderSignupView(APIView):
     input_class = SignupInput
 
-    def post(self, request, *args, **kwargs):
+    def handle(self, request, *args, **kwargs):
+        self.sociallogin = flows.signup.get_pending_signup(self.request)
+        if not self.sociallogin:
+            return ConflictResponse(request)
         if not get_socialaccount_adapter().is_open_for_signup(
             request, self.sociallogin
         ):
             return ForbiddenResponse(request)
+        return super().handle(request, *args, **kwargs)
+
+    def post(self, request, *args, **kwargs):
         flows.signup.signup_by_form(self.request, self.sociallogin, self.input)
         return AuthenticationResponse(request)
 
     def get_input_kwargs(self):
-        self.sociallogin = flows.signup.get_pending_signup(self.request)
         return {"sociallogin": self.sociallogin}
 
 
diff --git a/docs/headless/openapi-specification/openapi.yaml b/docs/headless/openapi-specification/openapi.yaml
index 4a16f670..d8c669f2 100644
--- a/docs/headless/openapi-specification/openapi.yaml
+++ b/docs/headless/openapi-specification/openapi.yaml
@@ -425,6 +425,13 @@ paths:
             application/json:
               schema:
                 $ref: "#/components/schemas/ForbiddenResponse"
+        "409":
+          description: |
+            Conflict. The provider signup flow is not pending.
+          content:
+            application/json:
+              schema:
+                $ref: "#/components/schemas/ConflictResponse"
   ######################################################################
   # Authentication: 2FA
   ######################################################################
@@ -1426,6 +1433,15 @@ components:
             - 403
       required:
         - status
+    ConflictResponse:
+      type: object
+      properties:
+        status:
+          type: integer
+          enum:
+            - 409
+      required:
+        - status
     ReauthenticationResponse:
       type: object
       description: |
diff --git a/docs/socialaccount/signals.rst b/docs/socialaccount/signals.rst
index 01457d28..210758d3 100644
--- a/docs/socialaccount/signals.rst
+++ b/docs/socialaccount/signals.rst
@@ -13,7 +13,9 @@ hook to them for your own needs.
     provided.
 
 - ``allauth.socialaccount.signals.social_account_added(request, sociallogin)``
-    Sent after a user connects a social account to their local account.
+    Sent after a user connects a social account to their local account. This
+    is an explicit action and does not get called for the creation of a
+    socialaccount.
 
 - ``allauth.socialaccount.signals.social_account_updated(request, sociallogin)``
     Sent after a social account has been updated. This happens when a user


Test patch:
diff --git a/allauth/headless/socialaccount/tests/test_views.py b/allauth/headless/socialaccount/tests/test_views.py
index 974daaeb..4e32c534 100644
--- a/allauth/headless/socialaccount/tests/test_views.py
+++ b/allauth/headless/socialaccount/tests/test_views.py
@@ -377,3 +377,14 @@ def test_token_connect_already_connected(
             }
         ],
     }
+
+
+def test_provider_signup_not_pending(client, headless_reverse, db, settings):
+    resp = client.post(
+        headless_reverse("headless:socialaccount:provider_signup"),
+        data={
+            "email": "a@b.com",
+        },
+        content_type="application/json",
+    )
+    assert resp.status_code == 409