3
0
Fork 0

fix: flush session when creating new object via MasterView

whoops guess that got missed in the refactor.

this also adds our first functional test!  to reproduce the problem
since unit tests didn't catch it.  unfortunately i'm still missing
something about how the functional TestApp is supposed to work, in
conjunction with the test DB etc.  seems to be acting strangely with
regard to permission checks especially...
This commit is contained in:
Lance Edgar 2025-12-28 22:48:36 -06:00
parent ac2d520bde
commit 3af8e8aaf2
6 changed files with 242 additions and 8 deletions

View file

@ -53,7 +53,7 @@ dependencies = [
[project.optional-dependencies] [project.optional-dependencies]
continuum = ["Wutta-Continuum>=0.3.0"] continuum = ["Wutta-Continuum>=0.3.0"]
docs = ["Sphinx", "furo", "sphinxcontrib-programoutput"] docs = ["Sphinx", "furo", "sphinxcontrib-programoutput"]
tests = ["pylint", "pytest", "pytest-cov", "tox"] tests = ["pylint", "pytest", "pytest-cov", "tox", "WebTest"]
[project.entry-points."fanstatic.libraries"] [project.entry-points."fanstatic.libraries"]

View file

@ -24,17 +24,19 @@
WuttaWeb - test utilities WuttaWeb - test utilities
""" """
import re
import sys import sys
from unittest.mock import MagicMock from unittest.mock import MagicMock
import fanstatic import fanstatic
import pytest import pytest
from pyramid import testing from pyramid import testing
from webtest import TestApp
from wuttjamaican.testing import DataTestCase from wuttjamaican.testing import DataTestCase
from wuttjamaican.db.model.base import metadata from wuttjamaican.db.model.base import metadata
from wuttaweb import subscribers from wuttaweb import subscribers, app as appmod
from wuttaweb.conf import WuttaWebConfigExtension from wuttaweb.conf import WuttaWebConfigExtension
@ -170,3 +172,48 @@ class VersionWebTestCase(WebTestCase):
ext.startup(config) ext.startup(config)
return 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'<input name="_csrf" type="hidden" value="(\w+)" />', res.text
)
self.assertTrue(match)
return match.group(1)

View file

@ -575,8 +575,11 @@ class MasterView(View): # pylint: disable=too-many-public-methods
form = self.make_create_form() form = self.make_create_form()
if form.validate(): if form.validate():
session = self.Session()
try: try:
result = self.save_create_form(form) 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 except Exception as err: # pylint: disable=broad-exception-caught
log.warning("failed to save 'create' form", exc_info=True) log.warning("failed to save 'create' form", exc_info=True)
self.request.session.flash(f"Create failed: {err}", "error") self.request.session.flash(f"Create failed: {err}", "error")

View file

@ -15,8 +15,9 @@ def release(c, skip_tests=False):
Release a new version of WuttJamaican Release a new version of WuttJamaican
""" """
if not skip_tests: 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 'versioned'")
c.run("pytest -m 'functional'")
# rebuild pkg # rebuild pkg
if os.path.exists("dist"): if os.path.exists("dist"):

View file

@ -8,7 +8,7 @@ import colander
from wuttaweb.grids import Grid from wuttaweb.grids import Grid
from wuttaweb.views import users as mod from wuttaweb.views import users as mod
from wuttaweb.testing import WebTestCase from wuttaweb.testing import WebTestCase, FunctionalTestCase
class TestUserView(WebTestCase): class TestUserView(WebTestCase):
@ -471,3 +471,182 @@ class TestUserView(WebTestCase):
): ):
result = view.delete_api_token() result = view.delete_api_token()
self.assertEqual(result, {"error": "API token not found"}) 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")

12
tox.ini
View file

@ -5,12 +5,15 @@ envlist = py38, py39, py310, py311, nox
[testenv] [testenv]
extras = continuum,tests extras = continuum,tests
commands = commands =
pytest -m 'not versioned' {posargs} pytest -m 'not versioned and not functional' {posargs}
pytest -m 'versioned' {posargs} pytest -m 'versioned' {posargs}
pytest -m 'functional' {posargs}
[testenv:nox] [testenv:nox]
extras = tests extras = tests
commands = pytest -m 'not versioned' {posargs} commands =
pytest -m 'not versioned and not functional' {posargs}
pytest -m 'functional' {posargs}
[testenv:pylint] [testenv:pylint]
basepython = python3.11 basepython = python3.11
@ -19,8 +22,9 @@ commands = pylint wuttaweb
[testenv:coverage] [testenv:coverage]
basepython = python3.11 basepython = python3.11
commands = commands =
pytest -m 'not versioned' --cov=wuttaweb pytest -m 'not versioned and not functional' --cov=wuttaweb
pytest -m 'versioned' --cov-append --cov=wuttaweb --cov-report=html --cov-fail-under=100 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] [testenv:docs]
basepython = python3.11 basepython = python3.11