forked from Github-Mirrors/canaille
524 lines
17 KiB
Python
524 lines
17 KiB
Python
import datetime
|
|
from unittest import mock
|
|
|
|
import freezegun
|
|
from canaille.core.endpoints.account import EmailConfirmationPayload
|
|
from canaille.core.endpoints.account import RegistrationPayload
|
|
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 = EmailConfirmationPayload(
|
|
"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 = EmailConfirmationPayload(
|
|
"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 = EmailConfirmationPayload(
|
|
"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 = EmailConfirmationPayload(
|
|
"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 = EmailConfirmationPayload(
|
|
"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 = EmailConfirmationPayload(
|
|
"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
|
|
|
|
payload = RegistrationPayload(
|
|
datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
|
"someoneelse",
|
|
False,
|
|
"someone@mydomain.tld",
|
|
[],
|
|
)
|
|
hash = payload.build_hash()
|
|
b64 = payload.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
|
|
|
|
payload = RegistrationPayload(
|
|
datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
|
"someoneelse",
|
|
False,
|
|
"someone@mydomain.tld",
|
|
[],
|
|
)
|
|
hash = payload.build_hash()
|
|
b64 = payload.b64()
|
|
|
|
res = testclient.get(f"/register/{b64}/{hash}")
|
|
assert "readonly" not in res.form["emails-0"].attrs
|