diff --git a/pyproject.toml b/pyproject.toml index 263fe66..5e85363 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,7 +53,7 @@ dependencies = [ [project.optional-dependencies] continuum = ["Wutta-Continuum>=0.3.0"] docs = ["Sphinx", "furo", "sphinxcontrib-programoutput"] -tests = ["pylint", "pytest", "pytest-cov", "tox"] +tests = ["pylint", "pytest", "pytest-cov", "tox", "WebTest"] [project.entry-points."fanstatic.libraries"] diff --git a/src/wuttaweb/testing.py b/src/wuttaweb/testing.py index 3a189a2..7ff4301 100644 --- a/src/wuttaweb/testing.py +++ b/src/wuttaweb/testing.py @@ -24,17 +24,19 @@ WuttaWeb - test utilities """ +import re import sys from unittest.mock import MagicMock import fanstatic import pytest from pyramid import testing +from webtest import TestApp from wuttjamaican.testing import DataTestCase from wuttjamaican.db.model.base import metadata -from wuttaweb import subscribers +from wuttaweb import subscribers, app as appmod from wuttaweb.conf import WuttaWebConfigExtension @@ -170,3 +172,48 @@ class VersionWebTestCase(WebTestCase): ext.startup(config) return config + + +# TODO: this interface likely needs to change. it is only used +# by a single test so far, in tests/views/test_users.py +@pytest.mark.functional +class FunctionalTestCase(DataTestCase): + """ + Base class for test suites requiring a fully complete web app, to + respond to functional tests. + """ + + wsgi_main_app = None + + def make_config(self, **kwargs): + sqlite_path = self.write_file("test.sqlite", "") + self.sqlite_engine_url = f"sqlite:///{sqlite_path}" + + config_path = self.write_file( + "test.ini", + f""" +[wutta.db] +default.url = {self.sqlite_engine_url} + +[alembic] +script_location = wuttjamaican.db:alembic +version_locations = wuttjamaican.db:alembic/versions +""", + ) + + return super().make_config([config_path], **kwargs) + + def make_webtest(self): + webapp = appmod.make_wsgi_app( + self.wsgi_main_app or appmod.main, config=self.config + ) + + return TestApp(webapp) + + def get_csrf_token(self, testapp): + res = testapp.get("/login") + match = re.search( + r'', res.text + ) + self.assertTrue(match) + return match.group(1) diff --git a/src/wuttaweb/views/master.py b/src/wuttaweb/views/master.py index 9f1c5d3..f1920ed 100644 --- a/src/wuttaweb/views/master.py +++ b/src/wuttaweb/views/master.py @@ -575,8 +575,11 @@ class MasterView(View): # pylint: disable=too-many-public-methods form = self.make_create_form() if form.validate(): + session = self.Session() try: result = self.save_create_form(form) + # nb. must always flush to ensure primary key is set + session.flush() except Exception as err: # pylint: disable=broad-exception-caught log.warning("failed to save 'create' form", exc_info=True) self.request.session.flash(f"Create failed: {err}", "error") diff --git a/tasks.py b/tasks.py index 1a51cac..7b9ae65 100644 --- a/tasks.py +++ b/tasks.py @@ -15,8 +15,9 @@ def release(c, skip_tests=False): Release a new version of WuttJamaican """ if not skip_tests: - c.run("pytest -m 'not versioned'") + c.run("pytest -m 'not versioned and not functional'") c.run("pytest -m 'versioned'") + c.run("pytest -m 'functional'") # rebuild pkg if os.path.exists("dist"): diff --git a/tests/views/test_users.py b/tests/views/test_users.py index e45ad26..81bb028 100644 --- a/tests/views/test_users.py +++ b/tests/views/test_users.py @@ -8,7 +8,7 @@ import colander from wuttaweb.grids import Grid from wuttaweb.views import users as mod -from wuttaweb.testing import WebTestCase +from wuttaweb.testing import WebTestCase, FunctionalTestCase class TestUserView(WebTestCase): @@ -471,3 +471,182 @@ class TestUserView(WebTestCase): ): result = view.delete_api_token() self.assertEqual(result, {"error": "API token not found"}) + + +# TODO: this test seems to work fine on its own, but not in conjunction +# with the next class below. will have to sort this out before adding +# anymore functional tests probably. but it can wait for the moment. +# class TestListUsers(FunctionalTestCase): + +# def setUp(self): +# super().setUp() +# model = self.app.model +# auth = self.app.get_auth_handler() + +# # add 'fred' user +# self.fred = model.User(username="fred") +# auth.set_user_password(self.fred, "fredpass") +# self.session.add(self.fred) + +# # add 'managers' role +# self.managers = model.Role(name="Managers") +# self.fred.roles.append(self.managers) +# self.session.add(self.managers) + +# self.session.commit() + +# def test_index(self): +# model = self.app.model +# auth = self.app.get_auth_handler() +# testapp = self.make_webtest() +# csrf = self.get_csrf_token(testapp) + +# # cannot list users if not logged in +# res = testapp.get("/users/") +# self.assertEqual(res.status_code, 200) +# self.assertIn("Access Denied", res.text) +# self.assertIn("Login", res.text) +# self.assertNotIn("fred", res.text) + +# # so we login +# res = testapp.post( +# "/login", +# params={ +# "_csrf": csrf, +# "username": "fred", +# "password": "fredpass", +# }, +# ) +# self.assertEqual(res.status_code, 302) +# self.assertEqual(res.location, "http://localhost/") +# res = res.follow() +# self.assertEqual(res.status_code, 200) +# self.assertNotIn("Login", res.text) +# self.assertIn("fred", res.text) + +# perms = self.session.query(model.Permission).all() +# self.assertEqual(len(perms), 0) +# self.assertFalse(auth.has_permission(self.session, self.fred, "users.list")) + +# # but we still cannot list users, b/c no perm +# res = testapp.get("/users/") +# self.assertEqual(res.status_code, 200) +# self.assertIn("Access Denied", res.text) +# self.assertNotIn("Login", res.text) +# self.assertIn("fred", res.text) + +# # so we grant the perm +# auth.grant_permission(self.managers, "users.list") +# self.session.commit() + +# perms = self.session.query(model.Permission).all() + +# # now we can list users +# res = testapp.get("/users/") +# self.assertEqual(res.status_code, 200) +# self.assertNotIn("Access Denied", res.text) +# self.assertNotIn("Login", res.text) +# self.assertIn("fred", res.text) + +# testapp.get("/logout") + + +class TestCreateUser(FunctionalTestCase): + + def setUp(self): + super().setUp() + model = self.app.model + auth = self.app.get_auth_handler() + + # add 'fred' user + self.fred = model.User(username="fred") + auth.set_user_password(self.fred, "fredpass") + self.session.add(self.fred) + + # add 'managers' role + self.managers = model.Role(name="Managers") + self.fred.roles.append(self.managers) + self.session.add(self.managers) + + self.session.commit() + + def test_create(self): + model = self.app.model + auth = self.app.get_auth_handler() + testapp = self.make_webtest() + csrf = self.get_csrf_token(testapp) + + # cannot create user if not logged in + res = testapp.get("/users/new") + self.assertEqual(res.status_code, 200) + self.assertIn("Access Denied", res.text) + self.assertIn("Login", res.text) + self.assertNotIn("fred", res.text) + + # so we login + res = testapp.post( + "/login", + params={ + "_csrf": csrf, + "username": "fred", + "password": "fredpass", + }, + ) + self.assertEqual(res.status_code, 302) + self.assertEqual(res.location, "http://localhost/") + res = res.follow() + self.assertEqual(res.status_code, 200) + self.assertNotIn("Login", res.text) + self.assertIn("fred", res.text) + + # but we still cannot create user, b/c no perm + res = testapp.get("/users/new") + self.assertEqual(res.status_code, 200) + self.assertIn("Access Denied", res.text) + self.assertNotIn("Login", res.text) + self.assertIn("fred", res.text) + + # so we grant the perm; then we can create user + auth.grant_permission(self.managers, "users.list") + auth.grant_permission(self.managers, "users.create") + auth.grant_permission(self.managers, "users.view") + self.session.commit() + + self.assertTrue(auth.has_permission(self.session, self.fred, "users.create")) + + # first get the form + res = testapp.get("/users/new") + self.assertEqual(res.status_code, 200) + self.assertNotIn("Access Denied", res.text) + self.assertNotIn("Login", res.text) + self.assertIn("fred", res.text) + self.assertIn("Username", res.text) + + # then post the form; user should be created + res = testapp.post( + "/users/new", + [ + ("_csrf", csrf), + ("username", "barney"), + ("__start__", "set_password:mapping"), + ("set_password", "barneypass"), + ("set_password-confirm", "barneypass"), + ("__end__", "set_password:mapping"), + ("first_name", "Barney"), + ("last_name", "Rubble"), + ("__start__", "roles:sequence"), + ("checkbox", str(self.managers.uuid)), + ("__end__", "roles:sequence"), + ], + ) + barney = self.session.query(model.User).filter_by(username="barney").first() + self.assertTrue(barney) + self.assertEqual(res.status_code, 302) + self.assertEqual(res.location, f"http://localhost/users/{barney.uuid}") + res = res.follow() + self.assertEqual(res.status_code, 200) + self.assertNotIn("Login", res.text) + self.assertIn("fred", res.text) + self.assertIn("Barney Rubble", res.text) + + testapp.get("/logout") diff --git a/tox.ini b/tox.ini index 5c91961..56adb42 100644 --- a/tox.ini +++ b/tox.ini @@ -5,12 +5,15 @@ envlist = py38, py39, py310, py311, nox [testenv] extras = continuum,tests commands = - pytest -m 'not versioned' {posargs} + pytest -m 'not versioned and not functional' {posargs} pytest -m 'versioned' {posargs} + pytest -m 'functional' {posargs} [testenv:nox] extras = tests -commands = pytest -m 'not versioned' {posargs} +commands = + pytest -m 'not versioned and not functional' {posargs} + pytest -m 'functional' {posargs} [testenv:pylint] basepython = python3.11 @@ -19,8 +22,9 @@ commands = pylint wuttaweb [testenv:coverage] basepython = python3.11 commands = - pytest -m 'not versioned' --cov=wuttaweb - pytest -m 'versioned' --cov-append --cov=wuttaweb --cov-report=html --cov-fail-under=100 + pytest -m 'not versioned and not functional' --cov=wuttaweb + pytest -m 'versioned' --cov-append --cov=wuttaweb --cov-report=html + pytest -m 'functional' --cov-append --cov=wuttaweb --cov-report=html --cov-fail-under=100 [testenv:docs] basepython = python3.11