This commit is contained in:
Iliyan Angelov
2025-09-14 23:24:25 +03:00
commit c67067a2a4
71311 changed files with 6800714 additions and 0 deletions

View File

@@ -0,0 +1,29 @@
from contextlib import contextmanager
from unittest.mock import patch
import pytest
from allauth.mfa import recovery_codes, totp
@pytest.fixture
def user_with_totp(user):
totp.TOTP.activate(user, totp.generate_totp_secret())
return user
@pytest.fixture
def user_with_recovery_codes(user):
recovery_codes.RecoveryCodes.activate(user)
return user
@pytest.fixture
def totp_validation_bypass():
@contextmanager
def f():
with patch("allauth.mfa.totp.validate_totp_code") as m:
m.return_value = True
yield
return f

View File

@@ -0,0 +1,35 @@
from allauth.mfa import app_settings
from allauth.mfa.models import Authenticator
from allauth.mfa.recovery_codes import RecoveryCodes
def test_flow(user):
rc = RecoveryCodes.activate(user)
codes = rc.generate_codes()
assert len(set(codes)) == app_settings.RECOVERY_CODE_COUNT
for i in range(app_settings.RECOVERY_CODE_COUNT):
assert not rc._is_code_used(i)
idx = 3
assert rc.validate_code(codes[idx])
for i in range(app_settings.RECOVERY_CODE_COUNT):
assert rc._is_code_used(i) == (i == idx)
assert not rc.validate_code(codes[idx])
unused_codes = rc.get_unused_codes()
assert codes[idx] not in unused_codes
assert len(unused_codes) == app_settings.RECOVERY_CODE_COUNT - 1
def test_migrated_codes(db, user):
auth = Authenticator(user=user, data={"migrated_codes": ["abc", "def"]})
rc = RecoveryCodes(auth)
assert rc.generate_codes() == ["abc", "def"]
assert rc.get_unused_codes() == ["abc", "def"]
assert not rc.validate_code("bad")
assert rc.validate_code("abc")
auth.refresh_from_db()
rc = RecoveryCodes(auth)
assert rc.generate_codes() == ["def"]
assert rc.get_unused_codes() == ["def"]
rc.validate_code("def")
assert rc.instance.data["migrated_codes"] == []

View File

@@ -0,0 +1,178 @@
from django.conf import settings
from django.urls import reverse
import pytest
from allauth.account.models import EmailAddress
from allauth.mfa import app_settings
from allauth.mfa.adapter import get_adapter
from allauth.mfa.models import Authenticator
@pytest.mark.parametrize(
"url_name",
(
"mfa_activate_totp",
"mfa_index",
"mfa_deactivate_totp",
),
)
def test_login_required_views(client, url_name):
resp = client.get(reverse(url_name))
assert resp.status_code == 302
assert resp["location"].startswith(reverse("account_login"))
def test_activate_totp_with_incorrect_code(auth_client, reauthentication_bypass):
with reauthentication_bypass():
resp = auth_client.get(reverse("mfa_activate_totp"))
resp = auth_client.post(
reverse("mfa_activate_totp"),
{
"code": "123",
},
)
assert resp.context["form"].errors == {
"code": [get_adapter().error_messages["incorrect_code"]]
}
def test_activate_totp_with_unverified_email(
auth_client, user, totp_validation_bypass, reauthentication_bypass
):
EmailAddress.objects.filter(user=user).update(verified=False)
with reauthentication_bypass():
resp = auth_client.get(reverse("mfa_activate_totp"))
with totp_validation_bypass():
resp = auth_client.post(
reverse("mfa_activate_totp"),
{
"code": "123",
},
)
assert resp.context["form"].errors == {
"code": [get_adapter().error_messages["unverified_email"]],
}
def test_activate_totp_success(
auth_client, totp_validation_bypass, user, reauthentication_bypass
):
with reauthentication_bypass():
resp = auth_client.get(reverse("mfa_activate_totp"))
with totp_validation_bypass():
resp = auth_client.post(
reverse("mfa_activate_totp"),
{
"code": "123",
},
)
assert resp["location"] == reverse("mfa_view_recovery_codes")
assert Authenticator.objects.filter(
user=user, type=Authenticator.Type.TOTP
).exists()
assert Authenticator.objects.filter(
user=user, type=Authenticator.Type.RECOVERY_CODES
).exists()
def test_index(auth_client, user_with_totp):
resp = auth_client.get(reverse("mfa_index"))
assert "authenticators" in resp.context
def test_deactivate_totp_success(auth_client, user_with_totp, user_password):
resp = auth_client.post(reverse("mfa_deactivate_totp"))
assert resp.status_code == 302
assert resp["location"].startswith(reverse("account_reauthenticate"))
resp = auth_client.post(resp["location"], {"password": user_password})
assert resp.status_code == 302
resp = auth_client.post(reverse("mfa_deactivate_totp"))
assert resp.status_code == 302
assert resp["location"] == reverse("mfa_index")
def test_user_without_totp_deactivate_totp(auth_client):
resp = auth_client.get(reverse("mfa_deactivate_totp"))
assert resp.status_code == 404
def test_user_with_totp_activate_totp(
auth_client, user_with_totp, reauthentication_bypass
):
with reauthentication_bypass():
resp = auth_client.get(reverse("mfa_activate_totp"))
assert resp.status_code == 302
assert resp["location"] == reverse("mfa_deactivate_totp")
def test_totp_login(client, user_with_totp, user_password, totp_validation_bypass):
resp = client.post(
reverse("account_login"),
{"login": user_with_totp.username, "password": user_password},
)
assert resp.status_code == 302
assert resp["location"] == reverse("mfa_authenticate")
resp = client.get(reverse("mfa_authenticate"))
assert resp.context["request"].user.is_anonymous
resp = client.post(reverse("mfa_authenticate"), {"code": "123"})
assert resp.context["form"].errors == {
"code": [get_adapter().error_messages["incorrect_code"]]
}
with totp_validation_bypass():
resp = client.post(
reverse("mfa_authenticate"),
{"code": "123"},
)
assert resp.status_code == 302
assert resp["location"] == settings.LOGIN_REDIRECT_URL
def test_download_recovery_codes(auth_client, user_with_recovery_codes, user_password):
resp = auth_client.get(reverse("mfa_download_recovery_codes"))
assert resp["location"].startswith(reverse("account_reauthenticate"))
resp = auth_client.post(resp["location"], {"password": user_password})
assert resp.status_code == 302
resp = auth_client.get(resp["location"])
assert resp["content-disposition"] == 'attachment; filename="recovery-codes.txt"'
def test_view_recovery_codes(auth_client, user_with_recovery_codes, user_password):
resp = auth_client.get(reverse("mfa_view_recovery_codes"))
assert resp["location"].startswith(reverse("account_reauthenticate"))
resp = auth_client.post(resp["location"], {"password": user_password})
assert resp.status_code == 302
resp = auth_client.get(resp["location"])
assert len(resp.context["unused_codes"]) == app_settings.RECOVERY_CODE_COUNT
def test_generate_recovery_codes(auth_client, user_with_recovery_codes, user_password):
rc = Authenticator.objects.get(
user=user_with_recovery_codes, type=Authenticator.Type.RECOVERY_CODES
).wrap()
prev_code = rc.get_unused_codes()[0]
resp = auth_client.get(reverse("mfa_generate_recovery_codes"))
assert resp["location"].startswith(reverse("account_reauthenticate"))
resp = auth_client.post(resp["location"], {"password": user_password})
assert resp.status_code == 302
resp = auth_client.post(resp["location"])
assert resp["location"] == reverse("mfa_view_recovery_codes")
rc = Authenticator.objects.get(
user=user_with_recovery_codes, type=Authenticator.Type.RECOVERY_CODES
).wrap()
assert not rc.validate_code(prev_code)
def test_add_email_not_allowed(auth_client, user_with_totp):
resp = auth_client.post(
reverse("account_email"),
{"action_add": "", "email": "change-to@this.org"},
)
assert resp.status_code == 200
assert resp.context["form"].errors == {
"email": [
"You cannot add an email address to an account protected by two-factor authentication."
]
}