Repository: pennersr/django-allauth (python)

Issue:
Headless `SessionView` for `APP` client creates an unnecessary session
In an `APP` context, when the client invokes DELETE on `SessionView`, a new empty and useless session is created in the session store and an associated session token is present in the response. Because this new session is empty, it can't be used to authenticate, so fortunately there isn't any security issue related to this.

In short, the following test fails:
```python
def test_logout_no_token(app_client, user):
    app_client.force_login(user)
    resp = app_client.get(reverse("headless:app:account:current_session"))
    assert resp.status_code == 200
    resp = app_client.delete(reverse("headless:app:account:current_session"))
    assert resp.status_code == 401
    assert "session_token" not in resp.json()["meta"]
```

The problem is that Django's `logout()` function flushes the session which sets `session.modified = True`, but `expose_session_token()` attempts to create a new session token if `session.modified = True`. A simple fix would be to also check if the session data is empty.

Pull request:
fix(headless): Do not create an empty session on logout
# 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"`.
 - [x] All Python code must formatted using Black, and clean from pep8 and isort issues.
 - [x] 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`.

# Description
Fixes #3784 .


Patch:
diff --git a/ChangeLog.rst b/ChangeLog.rst
index bde662ea..32ba2dde 100644
--- a/ChangeLog.rst
+++ b/ChangeLog.rst
@@ -12,6 +12,10 @@
 - An official API for single-page and mobile application support is now
   available, via the new ``allauth.headless`` app.
 
+- Added support for a honeypot field on the signup form. Real users do not see
+  the field and therefore leave it empty. When bots do fill out the field
+  account creation is silently skipped.
+
 
 0.62.1 (2024-04-24)
 *******************
diff --git a/allauth/account/adapter.py b/allauth/account/adapter.py
index a6bc4edd..ea39d718 100644
--- a/allauth/account/adapter.py
+++ b/allauth/account/adapter.py
@@ -2,7 +2,7 @@ import html
 import json
 import string
 import warnings
-from urllib.parse import quote, urlparse
+from urllib.parse import urlparse
 
 from django.conf import settings
 from django.contrib import messages
@@ -41,12 +41,7 @@ from allauth.account import signals
 from allauth.account.app_settings import AuthenticationMethod
 from allauth.core import context, ratelimit
 from allauth.core.internal.adapter import BaseAdapter
-from allauth.core.internal.httpkit import render_url
-from allauth.utils import (
-    build_absolute_uri,
-    generate_unique_username,
-    import_attribute,
-)
+from allauth.utils import generate_unique_username, import_attribute
 
 from . import app_settings
 
@@ -538,7 +533,7 @@ class DefaultAccountAdapter(BaseAdapter):
         """
         from allauth.account.internal.flows import manage_email
 
-        return manage_email.confirm_email(request, email_address)
+        return manage_email.verify_email(request, email_address)
 
     def set_password(self, user, password):
         user.set_password(password)
@@ -577,25 +572,11 @@ class DefaultAccountAdapter(BaseAdapter):
     def get_reset_password_from_key_url(self, key):
         """
         Method intented to be overriden in case the password reset email
-        needs to point to your frontend/SPA.
+        needs to be adjusted.
         """
-        if allauth_app_settings.HEADLESS_ONLY:
-            from allauth.headless import app_settings as headless_settings
-
-            return render_url(
-                self.request,
-                headless_settings.FRONTEND_URLS["account_reset_password_from_key"],
-                key=key,
-            )
+        from allauth.account.internal import flows
 
-        # We intentionally accept an opaque `key` on the interface here, and not
-        # implementation details such as a separate `uidb36` and `key. Ideally,
-        # this should have done on `urls` level as well.
-        path = reverse(
-            "account_reset_password_from_key", kwargs={"uidb36": "UID", "key": "KEY"}
-        )
-        path = path.replace("UID-KEY", quote(key))
-        return build_absolute_uri(self.request, path)
+        return flows.password_reset.get_reset_password_from_key_url(self.request, key)
 
     def get_email_confirmation_url(self, request, emailconfirmation):
         """Constructs the email confirmation (activation) url.
@@ -604,18 +585,9 @@ class DefaultAccountAdapter(BaseAdapter):
         confirmations are sent outside of the request context `request`
         can be `None` here.
         """
-        if allauth_app_settings.HEADLESS_ONLY:
-            from allauth.headless import app_settings as headless_settings
-
-            return render_url(
-                request,
-                headless_settings.FRONTEND_URLS["account_confirm_email"],
-                key=emailconfirmation.key,
-            )
+        from allauth.account.internal import flows
 
-        url = reverse("account_confirm_email", args=[emailconfirmation.key])
-        ret = build_absolute_uri(request, url)
-        return ret
+        return flows.manage_email.get_email_verification_url(request, emailconfirmation)
 
     def should_send_confirmation_mail(self, request, email_address, signup):
         send_email = ratelimit.consume(
diff --git a/allauth/account/app_settings.py b/allauth/account/app_settings.py
index a15cc278..31727b35 100644
--- a/allauth/account/app_settings.py
+++ b/allauth/account/app_settings.py
@@ -238,6 +238,13 @@ class AppSettings(object):
         """
         return self._setting("SIGNUP_FORM_CLASS", None)
 
+    @property
+    def SIGNUP_FORM_HONEYPOT_FIELD(self):
+        """
+        Honeypot field name. Empty string or ``None`` will disable honeypot behavior.
+        """
+        return self._setting("SIGNUP_FORM_HONEYPOT_FIELD", None)
+
     @property
     def USERNAME_REQUIRED(self):
         """
diff --git a/allauth/account/forms.py b/allauth/account/forms.py
index 851ca5a0..ac7f0149 100644
--- a/allauth/account/forms.py
+++ b/allauth/account/forms.py
@@ -416,8 +416,41 @@ class SignupForm(BaseSignupForm):
         if hasattr(self, "field_order"):
             set_form_field_order(self, self.field_order)
 
+        honeypot_field_name = app_settings.SIGNUP_FORM_HONEYPOT_FIELD
+        if honeypot_field_name:
+            self.fields[honeypot_field_name] = forms.CharField(
+                required=False,
+                label="",
+                widget=forms.TextInput(
+                    attrs={
+                        "style": "position: absolute; right: -99999px;",
+                        "tabindex": "-1",
+                        "autocomplete": "nope",
+                    }
+                ),
+            )
+
+    def try_save(self, request):
+        """
+        override of parent class method that adds additional catching
+        of a potential bot filling out the honeypot field and returns a
+        'fake' email verification response if honeypot was filled out
+        """
+        honeypot_field_name = app_settings.SIGNUP_FORM_HONEYPOT_FIELD
+        if honeypot_field_name:
+            if self.cleaned_data[honeypot_field_name]:
+                user = None
+                adapter = get_adapter()
+                # honeypot fields work best when you do not report to the bot
+                # that anything went wrong. So we return a fake email verification
+                # sent response but without creating a user
+                resp = adapter.respond_email_verification_sent(request, None)
+                return user, resp
+
+        return super().try_save(request)
+
     def clean(self):
-        super(SignupForm, self).clean()
+        super().clean()
 
         # `password` cannot be of type `SetPasswordField`, as we don't
         # have a `User` yet. So, let's populate a dummy user to be used
diff --git a/allauth/account/internal/flows/manage_email.py b/allauth/account/internal/flows/manage_email.py
index 5653f084..27b00d79 100644
--- a/allauth/account/internal/flows/manage_email.py
+++ b/allauth/account/internal/flows/manage_email.py
@@ -1,9 +1,12 @@
 from django.contrib import messages
+from django.urls import reverse
 
 from allauth.account import app_settings, signals
 from allauth.account.adapter import get_adapter
 from allauth.account.models import EmailAddress
 from allauth.account.reauthentication import raise_if_reauthentication_required
+from allauth.core.internal.httpkit import get_frontend_url
+from allauth.utils import build_absolute_uri
 
 
 def can_delete_email(email_address):
@@ -109,7 +112,7 @@ def mark_as_primary(request, email_address):
     return success
 
 
-def confirm_email(request, email_address):
+def verify_email(request, email_address):
     """
     Marks the email address as confirmed on the db
     """
@@ -132,3 +135,17 @@ def confirm_email(request, email_address):
             instance.remove()
         emit_email_changed(request, from_email_address, email_address)
     return True
+
+
+def get_email_verification_url(request, emailconfirmation):
+    """Constructs the email confirmation (activation) url.
+
+    Note that if you have architected your system such that email
+    confirmations are sent outside of the request context `request`
+    can be `None` here.
+    """
+    url = get_frontend_url(request, "account_confirm_email", key=emailconfirmation.key)
+    if not url:
+        url = reverse("account_confirm_email", args=[emailconfirmation.key])
+        url = build_absolute_uri(request, url)
+    return url
diff --git a/allauth/account/internal/flows/password_reset.py b/allauth/account/internal/flows/password_reset.py
index c0bd2dd4..0a679815 100644
--- a/allauth/account/internal/flows/password_reset.py
+++ b/allauth/account/internal/flows/password_reset.py
@@ -1,11 +1,11 @@
+from urllib.parse import quote
+
 from django.contrib import messages
-from django.core.exceptions import ImproperlyConfigured
 from django.urls import reverse
 
-from allauth import app_settings as allauth_settings
 from allauth.account import signals
 from allauth.account.adapter import get_adapter
-from allauth.core.internal.httpkit import render_url
+from allauth.core.internal.httpkit import get_frontend_url
 from allauth.utils import build_absolute_uri
 
 
@@ -35,14 +35,25 @@ def finalize_password_reset(request, user):
 
 
 def get_reset_password_url(request):
-    if allauth_settings.HEADLESS_ENABLED:
-        from allauth.headless import app_settings as headless_settings
-
-        url = headless_settings.FRONTEND_URLS.get("account_reset_password")
-        if allauth_settings.HEADLESS_ONLY and not url:
-            raise ImproperlyConfigured(
-                "settings.HEADLESS_FRONTEND_URLS['account_reset_password']"
-            )
-        if url:
-            return render_url(request, url)
-    return build_absolute_uri(request, reverse("account_reset_password"))
+    url = get_frontend_url(request, "account_reset_password")
+    if not url:
+        url = build_absolute_uri(request, reverse("account_reset_password"))
+    return url
+
+
+def get_reset_password_from_key_url(request, key):
+    """
+    Method intented to be overriden in case the password reset email
+    needs to point to your frontend/SPA.
+    """
+    url = get_frontend_url(request, "account_reset_password_from_key", key=key)
+    if not url:
+        # We intentionally accept an opaque `key` on the interface here, and not
+        # implementation details such as a separate `uidb36` and `key. Ideally,
+        # this should have done on `urls` level as well.
+        path = reverse(
+            "account_reset_password_from_key", kwargs={"uidb36": "UID", "key": "KEY"}
+        )
+        path = path.replace("UID-KEY", quote(key))
+        url = build_absolute_uri(request, path)
+    return url
diff --git a/allauth/account/internal/flows/signup.py b/allauth/account/internal/flows/signup.py
index 15c88a24..4396184f 100644
--- a/allauth/account/internal/flows/signup.py
+++ b/allauth/account/internal/flows/signup.py
@@ -1,9 +1,7 @@
-from django.core.exceptions import ImproperlyConfigured
 from django.urls import reverse
 
-from allauth import app_settings as allauth_settings
 from allauth.account.adapter import get_adapter
-from allauth.core.internal.httpkit import render_url
+from allauth.core.internal.httpkit import get_frontend_url
 from allauth.utils import build_absolute_uri
 
 
@@ -17,14 +15,7 @@ def send_unknown_account_mail(request, email):
 
 
 def get_signup_url(request):
-    if allauth_settings.HEADLESS_ENABLED:
-        from allauth.headless import app_settings as headless_settings
-
-        url = headless_settings.FRONTEND_URLS.get("account_signup")
-        if allauth_settings.HEADLESS_ONLY and not url:
-            raise ImproperlyConfigured(
-                "settings.HEADLESS_FRONTEND_URLS['account_signup']"
-            )
-        if url:
-            return render_url(request, url)
-    return build_absolute_uri(request, reverse("account_signup"))
+    url = get_frontend_url(request, "account_signup")
+    if not url:
+        url = build_absolute_uri(request, reverse("account_signup"))
+    return url
diff --git a/allauth/account/migrations/0008_emailaddress_unique_primary_email_fixup.py b/allauth/account/migrations/0008_emailaddress_unique_primary_email_fixup.py
new file mode 100644
index 00000000..4ef93fac
--- /dev/null
+++ b/allauth/account/migrations/0008_emailaddress_unique_primary_email_fixup.py
@@ -0,0 +1,44 @@
+from django.conf import settings
+from django.db import migrations
+from django.db.models import Count
+
+
+def forwards(apps, schema_editor):
+    EmailAddress = apps.get_model("account.EmailAddress")
+    User = apps.get_model(settings.AUTH_USER_MODEL)
+    user_email_field = getattr(settings, "ACCOUNT_USER_MODEL_EMAIL_FIELD", "email")
+
+    def get_users_with_multiple_primary_email():
+        user_pks = []
+        for email_address_dict in (
+            EmailAddress.objects.filter(primary=True)
+            .values("user")
+            .annotate(Count("user"))
+            .filter(user__count__gt=1)
+        ):
+            user_pks.append(email_address_dict["user"])
+        return User.objects.filter(pk__in=user_pks)
+
+    def unset_extra_primary_emails(user):
+        qs = EmailAddress.objects.filter(user=user, primary=True)
+        primary_email_addresses = list(qs)
+        if not primary_email_addresses:
+            return
+        primary_email_address = primary_email_addresses[0]
+        if user_email_field:
+            for address in primary_email_addresses:
+                if address.email.lower() == getattr(user, user_email_field, "").lower():
+                    primary_email_address = address
+                    break
+        qs.exclude(pk=primary_email_address.pk).update(primary=False)
+
+    for user in get_users_with_multiple_primary_email().iterator():
+        unset_extra_primary_emails(user)
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ("account", "0007_emailaddress_idx_email"),
+    ]
+
+    operations = [migrations.RunPython(forwards)]
diff --git a/allauth/account/migrations/0009_emailaddress_unique_primary_email.py b/allauth/account/migrations/0009_emailaddress_unique_primary_email.py
new file mode 100644
index 00000000..73cb744c
--- /dev/null
+++ b/allauth/account/migrations/0009_emailaddress_unique_primary_email.py
@@ -0,0 +1,20 @@
+# Generated by Django 4.2.11 on 2024-05-09 06:09
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ("account", "0008_emailaddress_unique_primary_email_fixup"),
+    ]
+
+    operations = [
+        migrations.AddConstraint(
+            model_name="emailaddress",
+            constraint=models.UniqueConstraint(
+                condition=models.Q(("primary", True)),
+                fields=("user", "primary"),
+                name="unique_primary_email",
+            ),
+        ),
+    ]
diff --git a/allauth/account/models.py b/allauth/account/models.py
index 4cbaba2a..9cde931a 100644
--- a/allauth/account/models.py
+++ b/allauth/account/models.py
@@ -34,14 +34,21 @@ class EmailAddress(models.Model):
         verbose_name = _("email address")
         verbose_name_plural = _("email addresses")
         unique_together = [("user", "email")]
+        constraints = [
+            UniqueConstraint(
+                fields=["user", "primary"],
+                name="unique_primary_email",
+                condition=Q(primary=True),
+            )
+        ]
         if app_settings.UNIQUE_EMAIL:
-            constraints = [
+            constraints.append(
                 UniqueConstraint(
                     fields=["email"],
                     name="unique_verified_email",
                     condition=Q(verified=True),
                 )
-            ]
+            )
 
     def __str__(self):
         return self.email
diff --git a/allauth/account/views.py b/allauth/account/views.py
index 334a99ac..f8d01e3e 100644
--- a/allauth/account/views.py
+++ b/allauth/account/views.py
@@ -879,7 +879,7 @@ class ConfirmLoginCodeView(RedirectAuthenticatedUserMixin, NextRedirectMixin, Fo
 
     def get_form_kwargs(self):
         kwargs = super().get_form_kwargs()
-        kwargs["code"] = self.pending_login.get("code")
+        kwargs["code"] = self.pending_login.get("code", "")
         return kwargs
 
     def form_valid(self, form):
diff --git a/allauth/core/internal/httpkit.py b/allauth/core/internal/httpkit.py
index 1d51cdbd..35366140 100644
--- a/allauth/core/internal/httpkit.py
+++ b/allauth/core/internal/httpkit.py
@@ -73,3 +73,17 @@ def render_url(request, url_template, **kwargs):
     if not p.netloc:
         url = request.build_absolute_uri(url)
     return url
+
+
+def get_frontend_url(request, urlname, **kwargs):
+    from allauth import app_settings as allauth_settings
+
+    if allauth_settings.HEADLESS_ENABLED:
+        from allauth.headless import app_settings as headless_settings
+
+        url = headless_settings.FRONTEND_URLS.get(urlname)
+        if allauth_settings.HEADLESS_ONLY and not url:
+            raise ImproperlyConfigured(f"settings.HEADLESS_FRONTEND_URLS['{urlname}']")
+        if url:
+            return render_url(request, url, **kwargs)
+    return None
diff --git a/allauth/headless/internal/sessionkit.py b/allauth/headless/internal/sessionkit.py
index d2bad0c8..504385fb 100644
--- a/allauth/headless/internal/sessionkit.py
+++ b/allauth/headless/internal/sessionkit.py
@@ -21,7 +21,8 @@ def expose_session_token(request):
     strategy = app_settings.TOKEN_STRATEGY
     hdr_token = strategy.get_session_token(request)
     modified = request.session.modified
-    if modified:
+    empty = request.session.is_empty()
+    if modified and not empty:
         new_token = strategy.create_session_token(request)
         if not hdr_token or hdr_token != new_token:
             return new_token
diff --git a/allauth/socialaccount/internal/flows/connect.py b/allauth/socialaccount/internal/flows/connect.py
index 3ee8dd09..31b60ba3 100644
--- a/allauth/socialaccount/internal/flows/connect.py
+++ b/allauth/socialaccount/internal/flows/connect.py
@@ -21,8 +21,8 @@ def validate_disconnect(request, account):
     """
     accounts = SocialAccount.objects.filter(user_id=account.user_id)
     is_last = not accounts.exclude(pk=account.pk).exists()
+    adapter = get_adapter()
     if is_last:
-        adapter = get_adapter()
         if allauth_settings.SOCIALACCOUNT_ONLY:
             raise adapter.validation_error("disconnect_last")
         # No usable password would render the local account unusable
diff --git a/docs/account/configuration.rst b/docs/account/configuration.rst
index df29ccf7..e9d115d3 100644
--- a/docs/account/configuration.rst
+++ b/docs/account/configuration.rst
@@ -233,6 +233,18 @@ Available settings:
   date). This class should implement a ``def signup(self, request, user)``
   method, where user represents the newly signed up user.
 
+``ACCOUNT_SIGNUP_FORM_HONEYPOT_FIELD`` (default: ``None``)
+  A string value that will be used as the HTML 'name' property
+  on a honeypot input field on the sign up form. Honeypot fields are hidden
+  to normal users but might be filled out by niave spam bots. When the field
+  is filled out the app will not create a new user and attempt to fool
+  the bot with a fake successful response. We recommend setting this
+  to some believable value that your app does not actually collect
+  on signup e.g. 'phone_number' or 'address'. Honeypots are not
+  always successful for sophisticated bots so this should be
+  used as one layer in a suite of spam detection tools if your
+  site is having trouble with spam.
+
 ``ACCOUNT_SIGNUP_PASSWORD_ENTER_TWICE`` (default: ``True``)
   When signing up, let the user type in their password twice to avoid typos.
 
diff --git a/examples/regular-django/example/templates/allauth/elements/field.html b/examples/regular-django/example/templates/allauth/elements/field.html
index ea341827..3d693e02 100644
--- a/examples/regular-django/example/templates/allauth/elements/field.html
+++ b/examples/regular-django/example/templates/allauth/elements/field.html
@@ -44,11 +44,13 @@
         {% endif %}
         <input {% if attrs.required %}required{% endif %}
                name="{{ attrs.name }}"
-               {% if attrs.placeholder %}placeholder="{{ attrs.placeholder }}" {% elif attrs.unlabeled %}placeholder="{{ slots.label }}" {% endif %}
-               class="{% if attrs.errors %}is-invalid{% endif %}  form-control rounded-3"
+               {% if attrs.placeholder %}placeholder="{{ attrs.placeholder }}" {% elif attrs.unlabeled %}placeholder="{% slot label %}{% endslot %}"{% endif %}
+               class="{% if attrs.errors %}is-invalid{% endif %} form-control rounded-3"
                id="{{ attrs.id }}"
                {% if attrs.readonly %}readonly{% endif %}
                {% if attrs.disabled %}disabled{% endif %}
+               {% if attrs.tabindex %}tabindex="{{ attrs.tabindex }}"{% endif %}
+               {% if attrs.style %}style="{{ attrs.style }}"{% endif %}
                {% if attrs.autocomplete %}autocomplete="{{ attrs.autocomplete }}"{% endif %}
                value="{{ attrs.value|default_if_none:"" }}"
                type="{{ attrs.type }}">
diff --git a/examples/regular-django/example/templates/allauth/elements/fields.html b/examples/regular-django/example/templates/allauth/elements/fields.html
index 956a879f..47fb273e 100644
--- a/examples/regular-django/example/templates/allauth/elements/fields.html
+++ b/examples/regular-django/example/templates/allauth/elements/fields.html
@@ -1,6 +1,6 @@
 {% load allauth %}
 {% for bound_field in attrs.form %}
-    {% element field unlabeled=attrs.unlabeled name=bound_field.name type=bound_field.field.widget.input_type required=bound_field.field.required value=bound_field.value id=bound_field.auto_id errors=bound_field.errors placeholder=bound_field.field.widget.attrs.placeholder autocomplete=bound_field.field.widget.attrs.autocomplete %}
+    {% element field unlabeled=attrs.unlabeled name=bound_field.name type=bound_field.field.widget.input_type required=bound_field.field.required value=bound_field.value id=bound_field.auto_id errors=bound_field.errors placeholder=bound_field.field.widget.attrs.placeholder tabindex=bound_field.field.widget.attrs.tabindex autocomplete=bound_field.field.widget.attrs.autocomplete style=bound_field.field.widget.attrs.style %}
         {% slot label %}
             {{ bound_field.label }}
         {% endslot %}


Test patch:
diff --git a/allauth/account/tests/test_login.py b/allauth/account/tests/test_login.py
index 977edef4..8c7c3966 100644
--- a/allauth/account/tests/test_login.py
+++ b/allauth/account/tests/test_login.py
@@ -274,7 +274,7 @@ class LoginTests(TestCase):
             user=user, email="john@example.org", primary=True, verified=True
         )
         EmailAddress.objects.create(
-            user=user, email="john@example.com", primary=True, verified=False
+            user=user, email="john@example.com", primary=False, verified=False
         )
 
         resp = self.client.post(
diff --git a/allauth/account/tests/test_login_by_code.py b/allauth/account/tests/test_login_by_code.py
index cc0d4493..b2af89e4 100644
--- a/allauth/account/tests/test_login_by_code.py
+++ b/allauth/account/tests/test_login_by_code.py
@@ -63,3 +63,13 @@ def test_login_by_code_max_attempts(client, user, request_login_by_code, setting
             assert resp.status_code == 200
             assert LOGIN_CODE_SESSION_KEY in client.session
             assert resp.context["form"].errors == {"code": ["Incorrect code."]}
+
+
+def test_login_by_code_unknown_user(mailoutbox, client, db):
+    resp = client.post(
+        reverse("account_request_login_code"),
+        data={"email": "unknown@email.org"},
+    )
+    assert resp.status_code == 302
+    assert resp["location"] == reverse("account_confirm_login_code")
+    resp = client.post(reverse("account_confirm_login_code"), data={"code": "123456"})
diff --git a/allauth/account/tests/test_signup.py b/allauth/account/tests/test_signup.py
index 2744da7b..7b2ec719 100644
--- a/allauth/account/tests/test_signup.py
+++ b/allauth/account/tests/test_signup.py
@@ -425,3 +425,36 @@ def test_email_lower_case(db, settings):
     )
     assert resp.status_code == 302
     assert EmailAddress.objects.filter(email="john@doe.org").count() == 1
+
+
+def test_does_not_create_user_when_honeypot_filled_out(client, db, settings):
+    settings.ACCOUNT_SIGNUP_FORM_HONEYPOT_FIELD = "phone_number"
+    resp = client.post(
+        reverse("account_signup"),
+        {
+            "username": "johndoe",
+            "email": "john@example.com",
+            "password1": "Password1@",
+            "password2": "Password1@",
+            "phone_number": "5551231234",
+        },
+    )
+
+    assert not get_user_model().objects.all().exists()
+    assert resp.status_code == 302
+
+
+def test_create_user_when_honeypot_not_filled_out(client, db, settings):
+    settings.ACCOUNT_SIGNUP_FORM_HONEYPOT_FIELD = "phone_number"
+    resp = client.post(
+        reverse("account_signup"),
+        {
+            "username": "johndoe",
+            "email": "john@example.com",
+            "password1": "Password1@",
+            "password2": "Password1@",
+            "phone_number": "",
+        },
+    )
+    assert get_user_model().objects.filter(username="johndoe").count() == 1
+    assert resp.status_code == 302
diff --git a/allauth/headless/account/tests/test_session.py b/allauth/headless/account/tests/test_session.py
index 39494d22..923ecd00 100644
--- a/allauth/headless/account/tests/test_session.py
+++ b/allauth/headless/account/tests/test_session.py
@@ -21,4 +21,13 @@ def test_logout(auth_client, headless_reverse):
     resp = auth_client.delete(headless_reverse("headless:account:current_session"))
     assert resp.status_code == 401
     resp = auth_client.get(headless_reverse("headless:account:current_session"))
+    assert resp.status_code in [401, 410]
+
+
+def test_logout_no_token(app_client, user):
+    app_client.force_login(user)
+    resp = app_client.get(reverse("headless:app:account:current_session"))
+    assert resp.status_code == 200
+    resp = app_client.delete(reverse("headless:app:account:current_session"))
     assert resp.status_code == 401
+    assert "session_token" not in resp.json()["meta"]
diff --git a/allauth/socialaccount/tests/test_connect.py b/allauth/socialaccount/tests/test_connect.py
index 6c217095..b56da0e0 100644
--- a/allauth/socialaccount/tests/test_connect.py
+++ b/allauth/socialaccount/tests/test_connect.py
@@ -10,18 +10,23 @@ from allauth.socialaccount.models import SocialAccount
 def test_disconnect(auth_client, user, settings, mailoutbox, reauthentication_required):
     settings.ACCOUNT_EMAIL_NOTIFICATIONS = True
     settings.ACCOUNT_REAUTHENTICATION_REQUIRED = reauthentication_required
-    account = SocialAccount.objects.create(
+    account_to_del = SocialAccount.objects.create(
         uid="123", provider="other-server", user=user
     )
+    account_to_keep = SocialAccount.objects.create(
+        uid="456", provider="other-server", user=user
+    )
     resp = auth_client.get(reverse("socialaccount_connections"))
     assertTemplateUsed(resp, "socialaccount/connections.html")
     resp = auth_client.post(
-        reverse("socialaccount_connections"), {"account": account.pk}
+        reverse("socialaccount_connections"), {"account": account_to_del.pk}
     )
     if reauthentication_required:
-        assert SocialAccount.objects.filter(pk=account.pk).exists()
+        assert SocialAccount.objects.filter(pk=account_to_del.pk).exists()
+        assert SocialAccount.objects.filter(pk=account_to_keep.pk).exists()
     else:
-        assert not SocialAccount.objects.filter(pk=account.pk).exists()
+        assert not SocialAccount.objects.filter(pk=account_to_del.pk).exists()
+        assert SocialAccount.objects.filter(pk=account_to_keep.pk).exists()
         assert len(mailoutbox) == 1
         assert mailoutbox[0].subject == "[example.com] Third-Party Account Disconnected"