canaille-globuzma/tests/core/test_email_confirmation.py

524 lines
17 KiB
Python

import datetime
from unittest import mock
import freezegun
from canaille.core.account import EmailConfirmationObject
from canaille.core.account import Invitation
from flask import url_for
def test_confirmation_disabled_email_editable(testclient, backend, logged_user):
"""
If email confirmation is disabled, users should be able to pick
any email.
"""
testclient.app.config["EMAIL_CONFIRMATION"] = False
res = testclient.get("/profile/user")
assert "readonly" not in res.form["emails-0"].attrs
assert not any(field.id == "add_email" for field in res.form.fields["action"])
res = res.form.submit(name="fieldlist_add", value="emails-0")
res.form["emails-0"] = "email1@mydomain.tld"
res.form["emails-1"] = "email2@mydomain.tld"
res = res.form.submit(name="action", value="edit-profile")
assert res.flashes == [("success", "Profile updated successfully.")]
res = res.follow()
logged_user.reload()
assert logged_user.emails == ["email1@mydomain.tld", "email2@mydomain.tld"]
def test_confirmation_unset_smtp_disabled_email_editable(
testclient, backend, logged_admin, user
):
"""
If email confirmation is unset and no SMTP server has
been configured, then email confirmation cannot be enabled,
thus users must be able to pick any email.
"""
del testclient.app.config["SMTP"]
testclient.app.config["EMAIL_CONFIRMATION"] = None
res = testclient.get("/profile/user")
assert "readonly" not in res.form["emails-0"].attrs
assert not any(field.id == "add_email" for field in res.form.fields["action"])
res = res.form.submit(name="fieldlist_add", value="emails-0")
res.form["emails-0"] = "email1@mydomain.tld"
res.form["emails-1"] = "email2@mydomain.tld"
res = res.form.submit(name="action", value="edit-profile")
assert res.flashes == [("success", "Profile updated successfully.")]
res = res.follow()
user.reload()
assert user.emails == ["email1@mydomain.tld", "email2@mydomain.tld"]
def test_confirmation_enabled_smtp_disabled_readonly(testclient, backend, logged_user):
"""
If email confirmation is enabled and no SMTP server is configured,
this might be a misconfiguration, or a temporary SMTP disabling.
In doubt, users cannot edit their emails.
"""
del testclient.app.config["SMTP"]
testclient.app.config["EMAIL_CONFIRMATION"] = True
res = testclient.get("/profile/user")
assert "readonly" in res.forms["emailconfirmationform"]["old_emails-0"].attrs
assert "emails-0" not in res.forms["baseform"].fields
res.forms["emailconfirmationform"]["old_emails-0"] = "email1@mydomain.tld"
assert "action" not in res.forms["emailconfirmationform"].fields
def test_confirmation_unset_smtp_enabled_email_admin_editable(
testclient, backend, logged_admin, user
):
"""
Administrators should be able to edit user email addresses,
even when email confirmation is unset and SMTP is configured.
"""
testclient.app.config["EMAIL_CONFIRMATION"] = None
res = testclient.get("/profile/user")
assert "readonly" not in res.form["emails-0"].attrs
assert not any(field.id == "add_email" for field in res.form.fields["action"])
res = res.form.submit(name="fieldlist_add", value="emails-0")
res.form["emails-0"] = "email1@mydomain.tld"
res.form["emails-1"] = "email2@mydomain.tld"
res = res.form.submit(name="action", value="edit-profile")
assert res.flashes == [("success", "Profile updated successfully.")]
res = res.follow()
user.reload()
assert user.emails == ["email1@mydomain.tld", "email2@mydomain.tld"]
def test_confirmation_enabled_smtp_disabled_admin_editable(
testclient, backend, logged_admin, user
):
"""
Administrators should be able to edit user email addresses,
even when email confirmation is enabled and SMTP is disabled.
"""
testclient.app.config["EMAIL_CONFIRMATION"] = True
del testclient.app.config["SMTP"]
res = testclient.get("/profile/user")
assert "readonly" not in res.form["emails-0"].attrs
assert not any(field.id == "add_email" for field in res.form.fields["action"])
res = res.form.submit(name="fieldlist_add", value="emails-0")
res.form["emails-0"] = "email1@mydomain.tld"
res.form["emails-1"] = "email2@mydomain.tld"
res = res.form.submit(name="action", value="edit-profile")
assert res.flashes == [("success", "Profile updated successfully.")]
res = res.follow()
user.reload()
assert user.emails == ["email1@mydomain.tld", "email2@mydomain.tld"]
def test_confirmation_unset_smtp_enabled_email_user_validation(
smtpd, testclient, backend, user
):
"""
If email confirmation is unset and there is a SMTP server
configured, then users emails should be validated by sending
a confirmation email.
"""
testclient.app.config["EMAIL_CONFIRMATION"] = None
with freezegun.freeze_time("2020-01-01 01:00:00"):
res = testclient.get("/login")
res.form["login"] = "user"
res = res.form.submit().follow()
res.form["password"] = "correct horse battery staple"
res = res.form.submit()
with freezegun.freeze_time("2020-01-01 02:00:00"):
res = testclient.get("/profile/user")
assert "readonly" in res.forms["emailconfirmationform"]["old_emails-0"].attrs
with freezegun.freeze_time("2020-01-01 02:00:00"):
res.forms["emailconfirmationform"]["new_email"] = "new_email@mydomain.tld"
res = res.forms["emailconfirmationform"].submit(
name="action", value="add_email"
)
assert res.flashes == [
(
"success",
"An email has been sent to the email address. "
"Please check your inbox and click on the verification link it contains",
)
]
email_confirmation = EmailConfirmationObject(
"2020-01-01T02:00:00+00:00",
"user",
"new_email@mydomain.tld",
)
email_confirmation_url = url_for(
"core.account.email_confirmation",
data=email_confirmation.b64(),
hash=email_confirmation.build_hash(),
_external=True,
)
assert len(smtpd.messages) == 1
assert email_confirmation_url in str(smtpd.messages[0].get_payload()[0]).replace(
"=\n", ""
)
with freezegun.freeze_time("2020-01-01 03:00:00"):
res = testclient.get(email_confirmation_url)
assert ("success", "Your email address have been confirmed.") in res.flashes
user.reload()
assert "new_email@mydomain.tld" in user.emails
def test_confirmation_invalid_link(testclient, backend, user):
"""
Random confirmation links should fail.
"""
res = testclient.get("/email-confirmation/invalid/invalid")
assert (
"error",
"The email confirmation link that brought you here is invalid.",
) in res.flashes
def test_confirmation_mail_form_failed(testclient, backend, user):
"""
Tests when an error happens during the mail sending.
"""
with freezegun.freeze_time("2020-01-01 01:00:00"):
res = testclient.get("/login")
res.form["login"] = "user"
res = res.form.submit().follow()
res.form["password"] = "correct horse battery staple"
res = res.form.submit()
with freezegun.freeze_time("2020-01-01 02:00:00"):
res = testclient.get("/profile/user")
assert "readonly" in res.forms["emailconfirmationform"]["old_emails-0"].attrs
with freezegun.freeze_time("2020-01-01 02:00:00"):
res.forms["emailconfirmationform"]["new_email"] = "invalid"
res = res.forms["emailconfirmationform"].submit(
name="action", value="add_email"
)
assert res.flashes == [("error", "Email addition failed.")]
user.reload()
assert user.emails == ["john@doe.com"]
@mock.patch("smtplib.SMTP")
def test_confirmation_mail_send_failed(SMTP, smtpd, testclient, backend, user):
"""
Tests when an error happens during the mail sending.
"""
SMTP.side_effect = mock.Mock(side_effect=OSError("unit test mail error"))
with freezegun.freeze_time("2020-01-01 01:00:00"):
res = testclient.get("/login")
res.form["login"] = "user"
res = res.form.submit().follow()
res.form["password"] = "correct horse battery staple"
res = res.form.submit()
with freezegun.freeze_time("2020-01-01 02:00:00"):
res = testclient.get("/profile/user")
assert "readonly" in res.forms["emailconfirmationform"]["old_emails-0"].attrs
with freezegun.freeze_time("2020-01-01 02:00:00"):
res.forms["emailconfirmationform"]["new_email"] = "new_email@mydomain.tld"
res = res.forms["emailconfirmationform"].submit(
name="action", value="add_email", expect_errors=True
)
assert res.flashes == [("error", "Could not send the verification email")]
user.reload()
assert user.emails == ["john@doe.com"]
def test_confirmation_expired_link(testclient, backend, user):
"""
Expired valid confirmation links should fail.
"""
email_confirmation = EmailConfirmationObject(
"2020-01-01T01:00:00+00:00",
"user",
"new_email@mydomain.tld",
)
email_confirmation_url = url_for(
"core.account.email_confirmation",
data=email_confirmation.b64(),
hash=email_confirmation.build_hash(),
_external=True,
)
with freezegun.freeze_time("2021-01-01 01:00:00"):
res = testclient.get(email_confirmation_url)
assert (
"error",
"The email confirmation link that brought you here has expired.",
) in res.flashes
user.reload()
assert "new_email@mydomain.tld" not in user.emails
def test_confirmation_invalid_hash_link(testclient, backend, user):
"""
Confirmation link with invalid hashes should fail.
"""
email_confirmation = EmailConfirmationObject(
"2020-01-01T01:00:00+00:00",
"user",
"new_email@mydomain.tld",
)
email_confirmation_url = url_for(
"core.account.email_confirmation",
data=email_confirmation.b64(),
hash="invalid",
_external=True,
)
with freezegun.freeze_time("2020-01-01 01:00:00"):
res = testclient.get(email_confirmation_url)
assert (
"error",
"The invitation link that brought you here was invalid.",
) in res.flashes
user.reload()
assert "new_email@mydomain.tld" not in user.emails
def test_confirmation_invalid_user_link(testclient, backend, user):
"""
Confirmation link about an unexisting user should fail.
For instance, when the user account has been deleted between
the mail is sent and the link is clicked.
"""
email_confirmation = EmailConfirmationObject(
"2020-01-01T01:00:00+00:00",
"invalid-user",
"new_email@mydomain.tld",
)
email_confirmation_url = url_for(
"core.account.email_confirmation",
data=email_confirmation.b64(),
hash=email_confirmation.build_hash(),
_external=True,
)
with freezegun.freeze_time("2020-01-01 01:00:00"):
res = testclient.get(email_confirmation_url)
assert (
"error",
"The email confirmation link that brought you here is invalid.",
) in res.flashes
user.reload()
assert "new_email@mydomain.tld" not in user.emails
def test_confirmation_email_already_confirmed_link(testclient, backend, user, admin):
"""
Clicking twice on a confirmation link should fail.
"""
email_confirmation = EmailConfirmationObject(
"2020-01-01T01:00:00+00:00",
"user",
"john@doe.com",
)
email_confirmation_url = url_for(
"core.account.email_confirmation",
data=email_confirmation.b64(),
hash=email_confirmation.build_hash(),
_external=True,
)
with freezegun.freeze_time("2020-01-01 01:00:00"):
res = testclient.get(email_confirmation_url)
assert (
"error",
"This address email have already been confirmed.",
) in res.flashes
user.reload()
assert "new_email@mydomain.tld" not in user.emails
def test_confirmation_email_already_used_link(testclient, backend, user, admin):
"""
Confirmation link should fail if the target email is already associated
to another account. For instance, if an administrator already put
this email to someone else's profile.
"""
email_confirmation = EmailConfirmationObject(
"2020-01-01T01:00:00+00:00",
"user",
"jane@doe.com",
)
email_confirmation_url = url_for(
"core.account.email_confirmation",
data=email_confirmation.b64(),
hash=email_confirmation.build_hash(),
_external=True,
)
with freezegun.freeze_time("2020-01-01 01:00:00"):
res = testclient.get(email_confirmation_url)
assert (
"error",
"This address email is already associated with another account.",
) in res.flashes
user.reload()
assert "new_email@mydomain.tld" not in user.emails
def test_delete_email(testclient, logged_user):
"""
Tests that user can deletes its emails unless they have only
one left.
"""
res = testclient.get("/profile/user")
assert "email_remove" not in res.forms["emailconfirmationform"].fields
logged_user.emails = logged_user.emails + ["new@email.com"]
logged_user.save()
res = testclient.get("/profile/user")
assert "email_remove" in res.forms["emailconfirmationform"].fields
res = res.forms["emailconfirmationform"].submit(
name="email_remove", value="new@email.com"
)
assert res.flashes == [("success", "The email have been successfully deleted.")]
logged_user.reload()
assert logged_user.emails == ["john@doe.com"]
def test_delete_wrong_email(testclient, logged_user):
"""
Tests that removing an already removed email do not
produce anything.
"""
logged_user.emails = logged_user.emails + ["new@email.com"]
logged_user.save()
res = testclient.get("/profile/user")
res1 = res.forms["emailconfirmationform"].submit(
name="email_remove", value="new@email.com"
)
assert res1.flashes == [("success", "The email have been successfully deleted.")]
res2 = res.forms["emailconfirmationform"].submit(
name="email_remove", value="new@email.com"
)
assert res2.flashes == [("error", "Email deletion failed.")]
logged_user.reload()
assert logged_user.emails == ["john@doe.com"]
def test_delete_last_email(testclient, logged_user):
"""
Tests that users cannot remove their last email address.
"""
logged_user.emails = logged_user.emails + ["new@email.com"]
logged_user.save()
res = testclient.get("/profile/user")
res1 = res.forms["emailconfirmationform"].submit(
name="email_remove", value="new@email.com"
)
assert res1.flashes == [("success", "The email have been successfully deleted.")]
res2 = res.forms["emailconfirmationform"].submit(
name="email_remove", value="john@doe.com"
)
assert res2.flashes == [("error", "Email deletion failed.")]
logged_user.reload()
assert logged_user.emails == ["john@doe.com"]
def test_edition_forced_mail(testclient, logged_user):
"""
Tests that users that must perform email verification
cannot force the profile form.
"""
res = testclient.get("/profile/user", status=200)
form = res.forms["baseform"]
testclient.post(
"/profile/user",
{
"csrf_token": form["csrf_token"].value,
"emails-0": "new@email.com",
"action": "edit-profile",
},
)
logged_user.reload()
assert logged_user.emails == ["john@doe.com"]
def test_invitation_form_mail_field_readonly(testclient):
"""
Tests that the email field is readonly in the invitation
form creation if email confirmation is enabled.
"""
testclient.app.config["EMAIL_CONFIRMATION"] = True
invitation = Invitation(
datetime.datetime.now(datetime.timezone.utc).isoformat(),
"someoneelse",
False,
"someone@mydomain.tld",
[],
)
hash = invitation.build_hash()
b64 = invitation.b64()
res = testclient.get(f"/register/{b64}/{hash}")
assert "readonly" in res.form["emails-0"].attrs
def test_invitation_form_mail_field_writable(testclient):
"""
Tests that the email field is writable in the invitation
form creation if email confirmation is disabled.
"""
testclient.app.config["EMAIL_CONFIRMATION"] = False
invitation = Invitation(
datetime.datetime.now(datetime.timezone.utc).isoformat(),
"someoneelse",
False,
"someone@mydomain.tld",
[],
)
hash = invitation.build_hash()
b64 = invitation.b64()
res = testclient.get(f"/register/{b64}/{hash}")
assert "readonly" not in res.form["emails-0"].attrs