diff --git a/Makefile b/Makefile index eb4224f..37d665b 100644 --- a/Makefile +++ b/Makefile @@ -59,6 +59,10 @@ 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 + pytest tests/functional -x --disable-warnings -m "not app" + + coverage: lint ## full test suite, check code coverage and open coverage report pytest tests --cov=ordr3 coverage html diff --git a/development.ini b/development.ini index db5d2bd..d990070 100644 --- a/development.ini +++ b/development.ini @@ -31,15 +31,6 @@ mail.host = localhost mail.port = 2525 mail.default_sender = ordr@example.com -# custom jinja2 filters: -jinja2.filters = - resource_url = pyramid_jinja2.filters:resource_url_filter - as_date = ordr3.views:jinja_date - as_time = ordr3.views:jinja_time - as_datetime = ordr3.views:jinja_datetime - view_comment = ordr3.views:jinja_view_comment - extract_links = ordr3.views:jinja_extract_links - nl2br = ordr3.views:jinja_nl2br # By default, the toolbar only appears for clients from IP addresses # '127.0.0.1' and '::1'. diff --git a/ordr3/__init__.py b/ordr3/__init__.py index 958977c..df0f4c8 100644 --- a/ordr3/__init__.py +++ b/ordr3/__init__.py @@ -23,8 +23,6 @@ def main(global_config, **settings): require_csrf=settings["session.auto_csrf"] ) - config.include("pyramid_jinja2") - config.include(".adapters") config.include(".events") config.include(".resources") diff --git a/ordr3/views/__init__.py b/ordr3/views/__init__.py index 24140ab..5a24813 100644 --- a/ordr3/views/__init__.py +++ b/ordr3/views/__init__.py @@ -65,3 +65,17 @@ def includeme(config): Activate this setup using ``config.include('ordr3.views')``. """ + import pyramid_jinja2.filters + + config.include("pyramid_jinja2") + config.commit() + + j2_env = config.get_jinja2_environment() + j2_env.filters["resource_url"] = pyramid_jinja2.filters.resource_url_filter + + j2_env.filters["as_date"] = jinja_date + j2_env.filters["as_time"] = jinja_time + j2_env.filters["as_datetime"] = jinja_datetime + j2_env.filters["view_comment"] = jinja_view_comment + j2_env.filters["extract_links"] = jinja_extract_links + j2_env.filters["nl2br"] = jinja_nl2br diff --git a/tests/functional/conftest.py b/tests/functional/conftest.py new file mode 100644 index 0000000..303fcc6 --- /dev/null +++ b/tests/functional/conftest.py @@ -0,0 +1,194 @@ +from collections import namedtuple + +import pytest +from sqlalchemy.orm import clear_mappers + +from ordr3.views import RE_SIMPLE_URL + +ParsedMail = namedtuple("ParsedMail", ["body", "link"]) + + +@pytest.fixture +def _pyramid_app(): + from ordr3 import main + + config = { + "pyramid.includes": ["pyramid_debugtoolbar", "pyramid_mailer.testing"], + "sqlalchemy.url": "sqlite:///:memory:", + "retry.attempts": "3", + "auth.secret": "change me for production", + "session.secret": "change me for production", + "session.auto_csrf": "true", + "static_views.cache_max_age": "0", + "mail.host": "localhost", + "mail.port": "2525", + "mail.default_sender": "ordr@example.com", + "jinja2.filters ": [ + "resource_url = pyramid_jinja2.filters:resource_url_filter", + "as_date = ordr3.views:jinja_date", + "as_time = ordr3.views:jinja_time", + "as_datetime = ordr3.views:jinja_datetime", + "view_comment = ordr3.views:jinja_view_comment", + "extract_links = ordr3.views:jinja_extract_links", + "nl2br = ordr3.views:jinja_nl2br", + ], + } + + yield main({}, **config) + clear_mappers() + + +@pytest.fixture +def _sqlite_repo(_pyramid_app): + from pyramid.scripting import prepare + from ordr3 import adapters + + with prepare() as env: + with env["request"].tm: + repo = env["root"].request.repo + adapters.metadata.create_all(repo.session.get_bind()) + yield repo + + +@pytest.fixture +def _example_data(_sqlite_repo): + from ordr3 import models, security + from datetime import datetime, timedelta + + today = datetime.utcnow() + + crypt_context = security.get_passlib_context() + + user = models.User( + 1, + "TestUser", + "Jon", + "Smith", + "jon@example.com", + crypt_context.hash("jon"), + models.UserRole.USER, + ) + _sqlite_repo.add_user(user) + + admin = models.User( + 2, + "TestAdmin", + "Jane", + "Doe", + "jane@example.com", + crypt_context.hash("jane"), + models.UserRole.ADMIN, + ) + _sqlite_repo.add_user(admin) + + inactive = models.User( + 3, + "TestInactive", + "Peter", + "Peter", + "peter@example.com", + crypt_context.hash("peter"), + models.UserRole.INACTIVE, + ) + _sqlite_repo.add_user(inactive) + + order_1 = models.OrderItem( + 1, + "Ethanol", + "123", + "VWR", + models.OrderCategory.SOLVENT, + "1 l", + 12.3, + 1, + "EUR", + "", + "", + ) + _sqlite_repo.add_order(order_1) + log_entry = models.LogEntry( + order_1.id, + models.OrderStatus.COMPLETED, + admin.username, + today - timedelta(days=1), + ) + order_1.add_to_log(log_entry) + + order_2 = models.OrderItem( + 2, + "NaCl", + "234", + "Carl Roth", + models.OrderCategory.CHEMICAL, + "2 kg", + 23.4, + 2, + "EUR", + "Haushalt", + "Kochsalz", + ) + _sqlite_repo.add_order(order_2) + log_entry = models.LogEntry( + order_2.id, + models.OrderStatus.APPROVAL, + admin.username, + today - timedelta(days=2), + ) + order_2.add_to_log(log_entry) + + order_3 = models.OrderItem( + 3, + "Eppis", + "345", + "VWR", + models.OrderCategory.BIOLAB, + "3 St", + 34.5, + 3, + "USD", + "Toto", + "gefunden bei http://www.example.com/foo", + ) + _sqlite_repo.add_order(order_3) + log_entry = models.LogEntry( + order_3.id, + models.OrderStatus.COMPLETED, + user.username, + today - timedelta(days=3), + ) + order_3.add_to_log(log_entry) + + +@pytest.fixture +def testapp(_pyramid_app, _example_data): + from webtest import TestApp + + yield TestApp(_pyramid_app) + + +@pytest.fixture +def login_as(testapp): + def _do_login(username, password): + response = testapp.get("/login") + form = response.form + form["username"] = username + form["password"] = password + return form.submit("submit") + + yield _do_login + + +@pytest.fixture +def parse_latest_mail(testapp): + from pyramid_mailer import get_mailer + + def _parse_mail(): + registry = testapp.app.registry + mailer = get_mailer(registry) + last_mail = mailer.outbox[-1] + body = last_mail.body + links = RE_SIMPLE_URL.findall(body) + link = links[0] if links else None + return ParsedMail(body, link) + + yield _parse_mail diff --git a/tests/functional/test_login.py b/tests/functional/test_login.py new file mode 100644 index 0000000..81339d6 --- /dev/null +++ b/tests/functional/test_login.py @@ -0,0 +1,45 @@ +def test_login_ok(testapp): + response = testapp.get("/", status=302).follow(status=200) + assert "Please Log In" in response + + form = response.form + form["username"] = "TestAdmin" + form["password"] = "jane" + response = form.submit("submit").follow() + assert "My Orders" in response + + +def test_login_wrong_password(testapp): + response = testapp.get("/", status=302).follow(status=200) + assert "Please Log In" in response + + form = response.form + form["username"] = "TestAdmin" + form["password"] = "wrong password" + response = form.submit("Log In") + assert "Credentials are invalid" in response + + +def test_login_fails_inactive_user(testapp): + response = testapp.get("/", status=302).follow(status=200) + assert "Please Log In" in response + + form = response.form + form["username"] = "TestInactive" + form["password"] = "peter" + response = form.submit("Log In") + assert "Credentials are invalid" in response + + +def test_logout(testapp): + response = testapp.get("/", status=302).follow(status=200) + assert "Please Log In" in response + + form = response.form + form["username"] = "TestAdmin" + form["password"] = "jane" + response = form.submit("submit").follow() + assert "My Orders" in response + + response = testapp.get("/logout", status=302).follow(status=200) + assert "Please Log In" in response diff --git a/tests/functional/test_password_reset.py b/tests/functional/test_password_reset.py new file mode 100644 index 0000000..469b1cb --- /dev/null +++ b/tests/functional/test_password_reset.py @@ -0,0 +1,42 @@ +def test_password_reset(testapp, parse_latest_mail): + response = testapp.get("/", status=302).follow(status=200) + assert "Please Log In" in response + + form = response.form + form["username"] = "TestAdmin" + form["password"] = "jixx" + response = form.submit("Log In") + assert "Credentials are invalid" 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("submit").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("Reset Password").follow() + assert "You changed your Password." in response + + response = testapp.get("/", status=302).follow(status=200) + form = response.form + form["username"] = "TestAdmin" + form["password"] = "jane" + response = form.submit("Log In") + assert "Credentials are invalid" in response + + response = testapp.get("/", status=302).follow(status=200) + form = response.form + form["username"] = "TestAdmin" + form["password"] = "jixx" + 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 new file mode 100644 index 0000000..1c5a85b --- /dev/null +++ b/tests/functional/test_registration.py @@ -0,0 +1,43 @@ +def test_registration_procedure(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("Create account").follow() + assert "The account needs to be activated" in response + + response = login_as("TestNew", "eric") + assert "Credentials are invalid" in response + + login_as("TestAdmin", "jane") + + response = testapp.get("/users/?role=new", status=200) + assert "TestNew" in response + + response = testapp.get("/users/TestNew/edit", status=200) + assert "Edit User" in response + + form = response.forms[1] + form["role"].select(text="User") + response = form.submit("Save changes").follow() + assert "TestNew" in response + + response = testapp.get("/users/?role=new", status=200) + assert "TestNew" not in response + + response = testapp.get("/users/?role=user", status=200) + assert "TestNew" in response + + parsed = parse_latest_mail() + assert "Your account was activated" in parsed.body + + response = login_as("TestNew", "eric").follow() + assert "My Orders" in response