From b46102bb75f1a8acfbc7ff249442aa7ece206e69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89loi=20Rivard?= Date: Tue, 14 May 2024 23:15:41 +0200 Subject: [PATCH] fix: crash for passwordless users at login when no SMTP server was configured --- CHANGES.rst | 1 + canaille/core/endpoints/auth.py | 4 +- tests/core/test_auth.py | 109 -------------------------- tests/core/test_firstlogin.py | 135 ++++++++++++++++++++++++++++++++ 4 files changed, 138 insertions(+), 111 deletions(-) create mode 100644 tests/core/test_firstlogin.py diff --git a/CHANGES.rst b/CHANGES.rst index 8ef10200..de572782 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -15,6 +15,7 @@ Fixed ^^^^^ - Dark theme colors for better readability +- Crash for passwordless users at login when no SMTP server was configured. [0.0.53] - 2024-04-22 --------------------- diff --git a/canaille/core/endpoints/auth.py b/canaille/core/endpoints/auth.py index ba21861c..e716af1a 100644 --- a/canaille/core/endpoints/auth.py +++ b/canaille/core/endpoints/auth.py @@ -49,7 +49,7 @@ def login(): return render_template("login.html", form=form) user = Backend.instance.get_user_from_login(form.login.data) - if user and not user.has_password(): + if user and not user.has_password() and current_app.features.has_smtp: return redirect(url_for("core.auth.firstlogin", user=user)) if not form.validate(): @@ -81,7 +81,7 @@ def password(): ) user = Backend.instance.get_user_from_login(session["attempt_login"]) - if user and not user.has_password(): + if user and not user.has_password() and current_app.features.has_smtp: return redirect(url_for("core.auth.firstlogin", user=user)) if not form.validate() or not user: diff --git a/tests/core/test_auth.py b/tests/core/test_auth.py index 00b0a067..b44aaf35 100644 --- a/tests/core/test_auth.py +++ b/tests/core/test_auth.py @@ -1,8 +1,5 @@ import datetime import logging -from unittest import mock - -from canaille.app import models def test_signin_and_out(testclient, user, caplog): @@ -146,112 +143,6 @@ def test_password_page_already_logged_in(testclient, logged_user): assert res.location == "/profile/user" -def test_user_without_password_first_login(testclient, backend, smtpd): - assert len(smtpd.messages) == 0 - u = models.User( - formatted_name="Temp User", - family_name="Temp", - user_name="temp", - emails=["john@doe.com", "johhny@doe.com"], - ) - backend.save(u) - - res = testclient.get("/login", status=200) - res.form["login"] = "temp" - res = res.form.submit(status=302) - - assert res.location == "/firstlogin/temp" - res = res.follow(status=200) - res.mustcontain("First login") - - res = res.form.submit(name="action", value="sendmail") - assert ( - "success", - "A password initialization link has been sent at your email address. " - "You should receive it within a few minutes.", - ) in res.flashes - assert len(smtpd.messages) == 2 - assert [message["X-RcptTo"] for message in smtpd.messages] == u.emails - assert "Password initialization" in smtpd.messages[0].get("Subject") - backend.delete(u) - - -@mock.patch("smtplib.SMTP") -def test_first_login_account_initialization_mail_sending_failed( - SMTP, testclient, backend, smtpd -): - SMTP.side_effect = mock.Mock(side_effect=OSError("unit test mail error")) - assert len(smtpd.messages) == 0 - - u = models.User( - formatted_name="Temp User", - family_name="Temp", - user_name="temp", - emails=["john@doe.com"], - ) - backend.save(u) - - res = testclient.get("/firstlogin/temp") - res = res.form.submit(name="action", value="sendmail", expect_errors=True) - assert ( - "success", - "A password initialization link has been sent at your email address. " - "You should receive it within a few minutes.", - ) not in res.flashes - assert ("error", "Could not send the password initialization email") in res.flashes - assert len(smtpd.messages) == 0 - backend.delete(u) - - -def test_first_login_form_error(testclient, backend, smtpd): - assert len(smtpd.messages) == 0 - u = models.User( - formatted_name="Temp User", - family_name="Temp", - user_name="temp", - emails=["john@doe.com"], - ) - backend.save(u) - - res = testclient.get("/firstlogin/temp", status=200) - res.form["csrf_token"] = "invalid" - res = res.form.submit( - name="action", value="sendmail", status=400, expect_errors=True - ) - assert len(smtpd.messages) == 0 - backend.delete(u) - - -def test_first_login_page_unavailable_for_users_with_password( - testclient, backend, user -): - testclient.get("/firstlogin/user", status=404) - - -def test_user_password_deleted_during_login(testclient, backend): - u = models.User( - formatted_name="Temp User", - family_name="Temp", - user_name="temp", - emails=["john@doe.com"], - password="correct horse battery staple", - ) - backend.save(u) - - res = testclient.get("/login") - res.form["login"] = "temp" - res = res.form.submit().follow() - res.form["password"] = "correct horse battery staple" - - u.password = None - backend.save(u) - - res = res.form.submit(status=302) - assert res.location == "/firstlogin/temp" - - backend.delete(u) - - def test_wrong_login(testclient, user): testclient.app.config["CANAILLE"]["HIDE_INVALID_LOGINS"] = True diff --git a/tests/core/test_firstlogin.py b/tests/core/test_firstlogin.py new file mode 100644 index 00000000..074f1431 --- /dev/null +++ b/tests/core/test_firstlogin.py @@ -0,0 +1,135 @@ +from unittest import mock + +from canaille.app import models + + +def test_user_without_password_first_login(testclient, backend, smtpd): + assert len(smtpd.messages) == 0 + u = models.User( + formatted_name="Temp User", + family_name="Temp", + user_name="temp", + emails=["john@doe.com", "johhny@doe.com"], + ) + backend.save(u) + + res = testclient.get("/login", status=200) + res.form["login"] = "temp" + res = res.form.submit(status=302) + + assert res.location == "/firstlogin/temp" + res = res.follow(status=200) + res.mustcontain("First login") + + res = res.form.submit(name="action", value="sendmail") + assert ( + "success", + "A password initialization link has been sent at your email address. " + "You should receive it within a few minutes.", + ) in res.flashes + assert len(smtpd.messages) == 2 + assert [message["X-RcptTo"] for message in smtpd.messages] == u.emails + assert "Password initialization" in smtpd.messages[0].get("Subject") + backend.delete(u) + + +@mock.patch("smtplib.SMTP") +def test_first_login_account_initialization_mail_sending_failed( + SMTP, testclient, backend, smtpd +): + SMTP.side_effect = mock.Mock(side_effect=OSError("unit test mail error")) + assert len(smtpd.messages) == 0 + + u = models.User( + formatted_name="Temp User", + family_name="Temp", + user_name="temp", + emails=["john@doe.com"], + ) + backend.save(u) + + res = testclient.get("/firstlogin/temp") + res = res.form.submit(name="action", value="sendmail", expect_errors=True) + assert ( + "success", + "A password initialization link has been sent at your email address. " + "You should receive it within a few minutes.", + ) not in res.flashes + assert ("error", "Could not send the password initialization email") in res.flashes + assert len(smtpd.messages) == 0 + backend.delete(u) + + +def test_first_login_form_error(testclient, backend, smtpd): + assert len(smtpd.messages) == 0 + u = models.User( + formatted_name="Temp User", + family_name="Temp", + user_name="temp", + emails=["john@doe.com"], + ) + backend.save(u) + + res = testclient.get("/firstlogin/temp", status=200) + res.form["csrf_token"] = "invalid" + res = res.form.submit( + name="action", value="sendmail", status=400, expect_errors=True + ) + assert len(smtpd.messages) == 0 + backend.delete(u) + + +def test_first_login_page_unavailable_for_users_with_password( + testclient, backend, user +): + testclient.get("/firstlogin/user", status=404) + + +def test_user_password_deleted_during_login(testclient, backend): + u = models.User( + formatted_name="Temp User", + family_name="Temp", + user_name="temp", + emails=["john@doe.com"], + password="correct horse battery staple", + ) + backend.save(u) + + res = testclient.get("/login") + res.form["login"] = "temp" + res = res.form.submit().follow() + res.form["password"] = "correct horse battery staple" + + u.password = None + backend.save(u) + + res = res.form.submit(status=302) + assert res.location == "/firstlogin/temp" + + backend.delete(u) + + +def test_smtp_disabled(testclient, backend, smtpd): + testclient.app.config["CANAILLE"]["SMTP"] = None + + assert len(smtpd.messages) == 0 + u = models.User( + formatted_name="Temp User", + family_name="Temp", + user_name="temp", + emails=["john@doe.com", "johhny@doe.com"], + ) + backend.save(u) + + res = testclient.get("/login", status=200) + res.form["login"] = "temp" + res = res.form.submit() + res = res.follow() + res.form["password"] = "incorrect horse" + res = res.form.submit() + assert ("error", "Login failed, please check your information") in res.flashes + res.form["password"] = "" + res = res.form.submit() + assert ("error", "Login failed, please check your information") in res.flashes + + backend.delete(u)