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:
parent
ac2d520bde
commit
3af8e8aaf2
6 changed files with 242 additions and 8 deletions
|
|
@ -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"]
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
3
tasks.py
3
tasks.py
|
|
@ -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"):
|
||||||
|
|
|
||||||
|
|
@ -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
12
tox.ini
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue