diff --git a/Makefile b/Makefile index 37d665b..eba375f 100644 --- a/Makefile +++ b/Makefile @@ -59,7 +59,7 @@ lint: ## reformat with black and check style with flake8 test: lint ## run tests quickly with the default Python pytest tests -x --disable-warnings -m "not app" -functest: lint ## run tests quickly with the default Python +functest: lint ## run functional tests quickly with the default Python pytest tests/functional -x --disable-warnings -m "not app" @@ -68,6 +68,12 @@ coverage: lint ## full test suite, check code coverage and open coverage report coverage html $(BROWSER) htmlcov/index.html +funcoverage: lint ## only functional test suite, check code coverage and open coverage report + pytest tests/functional --cov=ordr3 + coverage html + $(BROWSER) htmlcov/index.html + + tox: ## run fully isolated tests with tox tox diff --git a/ordr3/resources.py b/ordr3/resources.py index 24e7c41..a20918b 100644 --- a/ordr3/resources.py +++ b/ordr3/resources.py @@ -37,10 +37,10 @@ class User(BaseResource): acl = [ (Allow, "role:admin", "view"), (Allow, "role:admin", "edit"), - DENY_ALL, ] if not self.model.is_active: acl.append((Allow, "role:admin", "delete")) + acl.append(DENY_ALL) return acl @classmethod diff --git a/ordr3/views/account.py b/ordr3/views/account.py index 7d4d766..b27e5ff 100644 --- a/ordr3/views/account.py +++ b/ordr3/views/account.py @@ -77,7 +77,7 @@ def registration(context, request): renderer="ordr3:templates/account/registration.jinja2", ) def register_new_user(context, request): - if "Cancel" in request.POST: + if "Create_Account" not in request.POST: return HTTPFound(request.resource_path(request.root)) form = account.RegistrationSchema.as_form(request) @@ -142,7 +142,7 @@ def forgotten_password(context, request): renderer="ordr3:templates/account/forgotten_password.jinja2", ) def send_reset_link(context, request): - if "Cancel" in request.POST: + if "Send_Reset_Link" not in request.POST: return HTTPFound(request.resource_path(request.root)) provided_identifier = request.POST.get("email_or_username") @@ -196,7 +196,7 @@ def reset_password_form(context, request): renderer="ordr3:templates/account/reset_password_form.jinja2", ) def reset_password(context, request): - if "Cancel" in request.POST: + if "Reset_Password" not in request.POST: return HTTPFound(request.resource_path(request.root)) token = request.GET.get("t") @@ -254,7 +254,7 @@ def myaccount(context, request): renderer="ordr3:templates/account/myaccount.jinja2", ) def edit_myaccount(context, request): - if "Cancel" in request.POST: + if "Save_Changes" not in request.POST: return HTTPFound(request.resource_path(request.root)) form = account.MyAccountSchema.as_form(request) diff --git a/ordr3/views/users.py b/ordr3/views/users.py index 54cfd46..07e4648 100644 --- a/ordr3/views/users.py +++ b/ordr3/views/users.py @@ -81,7 +81,7 @@ def edit_user(context, request): renderer="ordr3:templates/users/edit.jinja2", ) def save_edits(context, request): - if "Cancel" in request.POST: + if "Save_Changes" not in request.POST: return HTTPFound(request.resource_path(context.__parent__)) form = account.EditAccountSchema.as_form(request) diff --git a/tests/functional/conftest.py b/tests/functional/conftest.py index 303fcc6..632b4d1 100644 --- a/tests/functional/conftest.py +++ b/tests/functional/conftest.py @@ -169,6 +169,7 @@ def testapp(_pyramid_app, _example_data): @pytest.fixture def login_as(testapp): def _do_login(username, password): + testapp.get("/logout") response = testapp.get("/login") form = response.form form["username"] = username diff --git a/tests/functional/test_login.py b/tests/functional/test_login.py index 81339d6..c87bfce 100644 --- a/tests/functional/test_login.py +++ b/tests/functional/test_login.py @@ -43,3 +43,8 @@ def test_logout(testapp): response = testapp.get("/logout", status=302).follow(status=200) assert "Please Log In" in response + + +def test_breached_faq(testapp): + response = testapp.get("/breached") + assert "haveibeenpwned" in response diff --git a/tests/functional/test_my_account.py b/tests/functional/test_my_account.py new file mode 100644 index 0000000..66e119f --- /dev/null +++ b/tests/functional/test_my_account.py @@ -0,0 +1,85 @@ +def test_my_account_edit(testapp, login_as): + response = testapp.get("/", status=302).follow(status=200) + assert "Please Log In" in response + + response = login_as("TestUser", "jon").follow(status=200) + assert "My Orders" in response + + response = testapp.get("/myaccount") + form = response.forms[1] + form["first_name"] = "Terry" + form["last_name"] = "Gilliam" + form["email"] = "terry@example.com" + form.submit("Save_Changes") + + login_as("TestAdmin", "jane") + + response = testapp.get("/users/", status=200) + assert "Jon" not in response + assert "Smith" not in response + assert "jon@example.com" not in response + assert "Terry" in response + assert "Gilliam" in response + assert "terry@example.com" in response + + +def test_my_account_edit_cancel(testapp, login_as): + response = testapp.get("/", status=302).follow(status=200) + assert "Please Log In" in response + + response = login_as("TestUser", "jon").follow(status=200) + assert "My Orders" in response + + response = testapp.get("/myaccount") + form = response.forms[1] + form["first_name"] = "Terry" + form["last_name"] = "Gilliam" + form["email"] = "terry@example.com" + form.submit("Cancel") + + login_as("TestAdmin", "jane") + + response = testapp.get("/users/", status=200) + assert "Jon" in response + assert "Smith" in response + assert "jon@example.com" in response + assert "Terry" not in response + assert "Gilliam" not in response + assert "terry@example.com" not in response + + +def test_my_account_edit_form_error(testapp, login_as): + response = testapp.get("/", status=302).follow(status=200) + assert "Please Log In" in response + + response = login_as("TestUser", "jon").follow(status=200) + assert "My Orders" in response + + response = testapp.get("/myaccount") + form = response.forms[1] + form["first_name"] = "" + form["last_name"] = "" + form["email"] = "" + response = form.submit("Save_Changes") + assert "There was a problem with your submission" in response + + +def test_my_account_reset_password(testapp, login_as, parse_latest_mail): + response = testapp.get("/", status=302).follow(status=200) + assert "Please Log In" in response + + response = login_as("TestUser", "jon").follow(status=200) + assert "My Orders" in response + + response = testapp.get("/myaccount") + assert "Change Password" in response + + response = testapp.get("/mypassword").follow().follow() + assert "My Orders" in response + assert "A password reset link has been sent" in response + + parsed = parse_latest_mail() + assert "If you forgot your password" in parsed.body + + # same link target as forgotten password, rest is tested there + assert parsed.link.startswith("http://localhost/reset?t=") diff --git a/tests/functional/test_password_reset.py b/tests/functional/test_password_reset.py index 469b1cb..2f85999 100644 --- a/tests/functional/test_password_reset.py +++ b/tests/functional/test_password_reset.py @@ -13,7 +13,7 @@ def test_password_reset(testapp, parse_latest_mail): form = response.form form["email_or_username"] = "jane@example.com" - response = form.submit("submit").follow() + response = form.submit("Send_Reset_Link").follow() assert "An email for the password reset was sent" in response parsed = parse_latest_mail() @@ -24,7 +24,7 @@ def test_password_reset(testapp, parse_latest_mail): form = response.form form["new_password"] = "jixx" - response = form.submit("Reset Password").follow() + response = form.submit("Reset_Password").follow() assert "You changed your Password." in response response = testapp.get("/", status=302).follow(status=200) @@ -40,3 +40,133 @@ def test_password_reset(testapp, parse_latest_mail): form["password"] = "jixx" response = form.submit("Log In").follow() assert "My Orders" in response + + +def test_password_cancel_forgot_password(testapp): + response = testapp.get("/", status=302).follow(status=200) + assert "Please Log In" in response + + response = testapp.get("/forgot", status=200) + assert "Forgot your Password?" in response + + form = response.form + form["email_or_username"] = "jane@example.com" + response = form.submit("Cancel").follow().follow() + assert "Please Log In" in response + + +def test_password_reset_user_or_email_not_found(testapp): + from pyramid_mailer import get_mailer + + response = testapp.get("/", status=302).follow(status=200) + assert "Please Log In" in response + + response = testapp.get("/forgot", status=200) + assert "Forgot your Password?" in response + + form = response.form + form["email_or_username"] = "Unknown User" + response = form.submit("Send_Reset_Link").follow() + assert "An email for the password reset was sent" in response + + registry = testapp.app.registry + mailer = get_mailer(registry) + assert len(mailer.outbox) == 0 + + +def test_password_reset_cancel_after_token(testapp, parse_latest_mail): + response = testapp.get("/", status=302).follow(status=200) + assert "Please Log In" in response + + response = testapp.get("/forgot", status=200) + assert "Forgot your Password?" in response + + form = response.form + form["email_or_username"] = "jane@example.com" + response = form.submit("Send_Reset_Link").follow() + assert "An email for the password reset was sent" in response + + parsed = parse_latest_mail() + assert "If you forgot your password" in parsed.body + + response = testapp.get(parsed.link) + assert "You can now set a new password" in response + + form = response.form + form["new_password"] = "jixx" + response = form.submit("Cancel").follow(status=302).follow(status=200) + assert "Please Log In" in response + + response = testapp.get("/", status=302).follow(status=200) + form = response.form + form["username"] = "TestAdmin" + form["password"] = "jane" + response = form.submit("Log In").follow() + assert "My Orders" in response + + +def test_password_reset_empty_password(testapp, parse_latest_mail): + response = testapp.get("/", status=302).follow(status=200) + assert "Please Log In" in response + + response = testapp.get("/forgot", status=200) + assert "Forgot your Password?" in response + + form = response.form + form["email_or_username"] = "jane@example.com" + response = form.submit("Send_Reset_Link").follow() + assert "An email for the password reset was sent" in response + + parsed = parse_latest_mail() + assert "If you forgot your password" in parsed.body + + response = testapp.get(parsed.link) + assert "You can now set a new password" in response + + form = response.form + form["new_password"] = "" + response = form.submit("Reset_Password") + assert "There was a problem with your submission" in response + + +def test_password_reset_invalid_token(testapp): + response = testapp.get("/", status=302).follow(status=200) + assert "Please Log In" in response + + response = ( + testapp.get("/reset?t=invalid").follow(status=302).follow(status=200) + ) + assert "Please Log In" in response + + +def test_password_reset_form_invalid_token(testapp, parse_latest_mail): + response = testapp.get("/", status=302).follow(status=200) + assert "Please Log In" in response + + response = testapp.get("/forgot", status=200) + assert "Forgot your Password?" in response + + form = response.form + form["email_or_username"] = "jane@example.com" + response = form.submit("Send_Reset_Link").follow() + assert "An email for the password reset was sent" in response + + parsed = parse_latest_mail() + assert "If you forgot your password" in parsed.body + + response = testapp.get(parsed.link) + assert "You can now set a new password" in response + + form = response.form + form.action = "/reset?t=invalid" + form["new_password"] = "jixx" + response = ( + form.submit("Reset_Password").follow(status=302).follow(status=200) + ) + assert "Please Log In" in response + + form = response.form + form["username"] = "TestAdmin" + form["password"] = "jane" + response = form.submit("Log In").follow() + assert "My Orders" in response diff --git a/tests/functional/test_registration.py b/tests/functional/test_registration.py index 1c5a85b..0747ef0 100644 --- a/tests/functional/test_registration.py +++ b/tests/functional/test_registration.py @@ -11,7 +11,7 @@ def test_registration_procedure(testapp, login_as, parse_latest_mail): form["last_name"] = "Idle" form["email"] = "eric@example.com" form["password"] = "eric" - response = form.submit("Create account").follow() + response = form.submit("Create_Account").follow() assert "The account needs to be activated" in response response = login_as("TestNew", "eric") @@ -27,7 +27,7 @@ def test_registration_procedure(testapp, login_as, parse_latest_mail): form = response.forms[1] form["role"].select(text="User") - response = form.submit("Save changes").follow() + response = form.submit("Save_Changes").follow() assert "TestNew" in response response = testapp.get("/users/?role=new", status=200) @@ -39,5 +39,36 @@ def test_registration_procedure(testapp, login_as, parse_latest_mail): parsed = parse_latest_mail() assert "Your account was activated" in parsed.body - response = login_as("TestNew", "eric").follow() + response = login_as("TestNew", "eric").follow(status=200) assert "My Orders" in response + + +def test_registration_procedure_form_error( + testapp, login_as, parse_latest_mail +): + response = testapp.get("/", status=302).follow(status=200) + assert "Please Log In" in response + + response = testapp.get("/registration", status=200) + assert "Register a new account" in response + + form = response.form + response = form.submit("Create_Account") + assert "There was a problem with your submission" in response + + +def test_registration_procedure_canceled(testapp, login_as, parse_latest_mail): + response = testapp.get("/", status=302).follow(status=200) + assert "Please Log In" in response + + response = testapp.get("/registration", status=200) + assert "Register a new account" in response + + form = response.form + form["user_name"] = "TestNew" + form["first_name"] = "Eric" + form["last_name"] = "Idle" + form["email"] = "eric@example.com" + form["password"] = "eric" + response = form.submit("Cancel").follow(status=302).follow(status=200) + assert "Please Log In" in response diff --git a/tests/functional/test_user_edit.py b/tests/functional/test_user_edit.py new file mode 100644 index 0000000..56d4785 --- /dev/null +++ b/tests/functional/test_user_edit.py @@ -0,0 +1,146 @@ +def test_user_edit(testapp, login_as): + response = testapp.get("/", status=302).follow(status=200) + assert "Please Log In" in response + + response = login_as("TestAdmin", "jane").follow(status=200) + assert "My Orders" in response + + response = testapp.get("/users/TestUser/edit") + form = response.forms[1] + form["first_name"] = "Terry" + form["last_name"] = "Gilliam" + form["email"] = "terry@example.com" + form.submit("Save_Changes") + + response = testapp.get("/users/", status=200) + assert "Jon" not in response + assert "Smith" not in response + assert "jon@example.com" not in response + assert "Terry" in response + assert "Gilliam" in response + assert "terry@example.com" in response + + +def test_user_edit_cancel(testapp, login_as): + response = testapp.get("/", status=302).follow(status=200) + assert "Please Log In" in response + + response = login_as("TestAdmin", "jane").follow(status=200) + assert "My Orders" in response + + response = testapp.get("/users/TestUser/edit") + form = response.forms[1] + form["first_name"] = "Terry" + form["last_name"] = "Gilliam" + form["email"] = "terry@example.com" + form.submit("Cancel") + + response = testapp.get("/users/", status=200) + assert "Jon" in response + assert "Smith" in response + assert "jon@example.com" in response + assert "Terry" not in response + assert "Gilliam" not in response + assert "terry@example.com" not in response + + +def test_user_edit_form_error(testapp, login_as): + response = testapp.get("/", status=302).follow(status=200) + assert "Please Log In" in response + + response = login_as("TestAdmin", "jane").follow(status=200) + assert "My Orders" in response + + response = testapp.get("/users/TestUser/edit") + form = response.forms[1] + form["first_name"] = "" + response = form.submit("Save_Changes") + assert "There was a problem with your submission" in response + + +def test_user_edit_reset_password(testapp, login_as, parse_latest_mail): + response = testapp.get("/", status=302).follow(status=200) + assert "Please Log In" in response + + response = login_as("TestAdmin", "jane").follow(status=200) + assert "My Orders" in response + + response = testapp.get("/users/TestUser/edit") + assert "Reset Password" in response + assert "Delte User" not in response + + response = testapp.get("/users/TestUser/password").follow() + assert "jon@example.com" in response + assert "A password reset link has been sent" in response + + parsed = parse_latest_mail() + assert "If you forgot your password" in parsed.body + + # same link target as forgotten password, rest is tested there + assert parsed.link.startswith("http://localhost/reset?t=") + + +def test_user_delete(testapp, login_as, parse_latest_mail): + response = testapp.get("/", status=302).follow(status=200) + assert "Please Log In" in response + + response = login_as("TestAdmin", "jane").follow(status=200) + assert "My Orders" in response + + response = testapp.get("/users/TestInactive/edit") + assert "Delete User" in response + + response = testapp.get("/users/TestInactive/delete") + assert "I confirm that I want to delete this user." in response + + form = response.forms[1] + form["confirmation"].checked = True + form.submit("delete") + + response = testapp.get("/users") + assert "has been deleted" in response + assert "TestInactive" not in response + + +def test_user_delete_cancel(testapp, login_as, parse_latest_mail): + response = testapp.get("/", status=302).follow(status=200) + assert "Please Log In" in response + + response = login_as("TestAdmin", "jane").follow(status=200) + assert "My Orders" in response + + response = testapp.get("/users/TestInactive/edit") + assert "Delete User" in response + + response = testapp.get("/users/TestInactive/delete") + assert "I confirm that I want to delete this user." in response + + form = response.forms[1] + form["confirmation"].checked = True + form.submit("cancel") + + response = testapp.get("/users") + assert "has been deleted" not in response + assert "TestInactive" in response + + +def test_user_delete_no_confirm(testapp, login_as, parse_latest_mail): + response = testapp.get("/", status=302).follow(status=200) + assert "Please Log In" in response + + response = login_as("TestAdmin", "jane").follow(status=200) + assert "My Orders" in response + + response = testapp.get("/users/TestInactive/edit") + assert "Delete User" in response + + response = testapp.get("/users/TestInactive/delete") + assert "I confirm that I want to delete this user." in response + + form = response.forms[1] + form["confirmation"].checked = False + form.submit("delete") + + response = testapp.get("/users") + assert "has been deleted" not in response + assert "TestInactive" in response