3
0
Fork 0
wuttaweb/tests/views/test_users.py
Lance Edgar 3af8e8aaf2 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...
2025-12-28 22:48:36 -06:00

652 lines
25 KiB
Python

# -*- coding: utf-8; -*-
from unittest.mock import patch
from sqlalchemy import orm
import colander
from wuttaweb.grids import Grid
from wuttaweb.views import users as mod
from wuttaweb.testing import WebTestCase, FunctionalTestCase
class TestUserView(WebTestCase):
def make_view(self):
return mod.UserView(self.request)
def test_includeme(self):
self.pyramid_config.include("wuttaweb.views.users")
def test_get_query(self):
view = self.make_view()
query = view.get_query(session=self.session)
self.assertIsInstance(query, orm.Query)
def test_configure_grid(self):
model = self.app.model
view = self.make_view()
grid = view.make_grid(model_class=model.User)
self.assertFalse(grid.is_linked("person"))
view.configure_grid(grid)
self.assertTrue(grid.is_linked("person"))
def test_grid_row_class(self):
model = self.app.model
user = model.User(username="barney", active=True)
data = dict(user)
view = self.make_view()
self.assertIsNone(view.grid_row_class(user, data, 1))
user.active = False
self.assertEqual(view.grid_row_class(user, data, 1), "has-background-warning")
def test_is_editable(self):
model = self.app.model
view = self.make_view()
# active user is editable
user = model.User(username="barney", active=True)
self.assertTrue(view.is_editable(user))
# inactive also editable
user = model.User(username="barney", active=False)
self.assertTrue(view.is_editable(user))
# but not if prevent_edit flag is set
user = model.User(username="barney", prevent_edit=True)
self.assertFalse(view.is_editable(user))
# unless request user is root
self.request.is_root = True
self.assertTrue(view.is_editable(user))
def test_configure_form(self):
model = self.app.model
person = model.Person(
first_name="Barney", last_name="Rubble", full_name="Barney Rubble"
)
barney = model.User(username="barney", person=person)
self.session.add(barney)
self.session.commit()
view = self.make_view()
# person replaced with first/last name when creating or editing
with patch.object(view, "viewing", new=True):
form = view.make_form(model_instance=barney)
self.assertIn("person", form)
self.assertNotIn("first_name", form)
self.assertNotIn("last_name", form)
view.configure_form(form)
self.assertIn("person", form)
self.assertNotIn("first_name", form)
self.assertNotIn("last_name", form)
with patch.object(view, "creating", new=True):
form = view.make_form(model_instance=barney)
self.assertIn("person", form)
self.assertNotIn("first_name", form)
self.assertNotIn("last_name", form)
view.configure_form(form)
self.assertNotIn("person", form)
self.assertIn("first_name", form)
self.assertIn("last_name", form)
with patch.object(view, "editing", new=True):
form = view.make_form(model_instance=barney)
self.assertIn("person", form)
self.assertNotIn("first_name", form)
self.assertNotIn("last_name", form)
view.configure_form(form)
self.assertNotIn("person", form)
self.assertIn("first_name", form)
self.assertIn("last_name", form)
# first/last name have default values when editing
with patch.object(view, "editing", new=True):
form = view.make_form(model_instance=barney)
self.assertNotIn("first_name", form.defaults)
self.assertNotIn("last_name", form.defaults)
view.configure_form(form)
self.assertIn("first_name", form.defaults)
self.assertEqual(form.defaults["first_name"], "Barney")
self.assertIn("last_name", form.defaults)
self.assertEqual(form.defaults["last_name"], "Rubble")
# password removed (always, for now)
with patch.object(view, "viewing", new=True):
form = view.make_form(model_instance=barney)
self.assertIn("password", form)
view.configure_form(form)
self.assertNotIn("password", form)
with patch.object(view, "editing", new=True):
form = view.make_form(model_instance=barney)
self.assertIn("password", form)
view.configure_form(form)
self.assertNotIn("password", form)
# api tokens grid shown only if current user has perm
with patch.object(view, "viewing", new=True):
form = view.make_form(model_instance=barney)
self.assertIn("api_tokens", form)
view.configure_form(form)
self.assertNotIn("api_tokens", form)
with patch.object(self.request, "is_root", new=True):
form = view.make_form(model_instance=barney)
self.assertIn("api_tokens", form)
view.configure_form(form)
self.assertIn("api_tokens", form)
def test_unique_username(self):
model = self.app.model
view = self.make_view()
user = model.User(username="foo")
self.session.add(user)
self.session.commit()
with patch.object(view, "Session", return_value=self.session):
# invalid if same username in data
node = colander.SchemaNode(colander.String(), name="username")
self.assertRaises(colander.Invalid, view.unique_username, node, "foo")
# but not if username belongs to current user
view.editing = True
self.request.matchdict = {"uuid": user.uuid}
node = colander.SchemaNode(colander.String(), name="username")
self.assertIsNone(view.unique_username(node, "foo"))
def test_objectify(self):
model = self.app.model
auth = self.app.get_auth_handler()
view = self.make_view()
blokes = model.Role(name="Blokes")
self.session.add(blokes)
others = model.Role(name="Others")
self.session.add(others)
barney = model.User(username="barney")
auth.set_user_password(barney, "testpass")
barney.roles.append(blokes)
self.session.add(barney)
self.session.commit()
with patch.object(self.request, "matchdict", new={"uuid": barney.uuid}):
with patch.object(view, "editing", new=True):
# sanity check, user is just in 'blokes' role
self.session.refresh(barney)
self.assertEqual(len(barney.roles), 1)
self.assertEqual(barney.roles[0].name, "Blokes")
# form can update user password
self.assertTrue(auth.check_user_password(barney, "testpass"))
form = view.make_model_form(model_instance=barney)
form.validated = {"username": "barney", "set_password": "testpass2"}
with patch.object(view, "Session", return_value=self.session):
user = view.objectify(form)
self.assertIs(user, barney)
self.assertTrue(auth.check_user_password(barney, "testpass2"))
# form can update user roles
form = view.make_model_form(model_instance=barney)
form.validated = {"username": "barney", "roles": {others.uuid}}
with patch.object(view, "Session", return_value=self.session):
user = view.objectify(form)
self.assertIs(user, barney)
self.assertEqual(len(user.roles), 1)
self.assertEqual(user.roles[0].name, "Others")
# person is auto-created
self.assertIsNone(barney.person)
form = view.make_model_form(model_instance=barney)
form.validated = {
"username": "barney",
"first_name": "Barney",
"last_name": "Rubble",
}
with patch.object(view, "Session", return_value=self.session):
user = view.objectify(form)
self.assertIsNotNone(barney.person)
self.assertEqual(barney.person.first_name, "Barney")
self.assertEqual(barney.person.last_name, "Rubble")
self.assertEqual(barney.person.full_name, "Barney Rubble")
# person is auto-removed
self.assertIsNotNone(barney.person)
form = view.make_model_form(model_instance=barney)
form.validated = {
"username": "barney",
"first_name": "",
"last_name": "",
}
with patch.object(view, "Session", return_value=self.session):
user = view.objectify(form)
self.assertIsNone(barney.person)
# nb. re-attach the person
barney.person = self.session.query(model.Person).one()
# person name is updated
self.assertEqual(barney.person.first_name, "Barney")
self.assertEqual(barney.person.last_name, "Rubble")
self.assertEqual(barney.person.full_name, "Barney Rubble")
form = view.make_model_form(model_instance=barney)
form.validated = {
"username": "barney",
"first_name": "Fred",
"last_name": "Flintstone",
}
with patch.object(view, "Session", return_value=self.session):
user = view.objectify(form)
self.assertIsNotNone(barney.person)
self.assertEqual(barney.person.first_name, "Fred")
self.assertEqual(barney.person.last_name, "Flintstone")
self.assertEqual(barney.person.full_name, "Fred Flintstone")
with patch.object(view, "creating", new=True):
# person is auto-created when making new user
form = view.make_model_form()
form.validated = {
"username": "betty",
"first_name": "Betty",
"last_name": "Boop",
}
with patch.object(view, "Session", return_value=self.session):
user = view.objectify(form)
self.assertIsNotNone(user.person)
self.assertEqual(user.person.first_name, "Betty")
self.assertEqual(user.person.last_name, "Boop")
self.assertEqual(user.person.full_name, "Betty Boop")
# nb. keep ref to last user
last_user = user
# person is *not* auto-created if no name provided
form = view.make_model_form()
form.validated = {"username": "betty", "first_name": "", "last_name": ""}
with patch.object(view, "Session", return_value=self.session):
user = view.objectify(form)
self.assertIsNone(user.person)
self.assertIsNot(user, last_user)
def test_update_roles(self):
model = self.app.model
auth = self.app.get_auth_handler()
admin = auth.get_role_administrator(self.session)
authed = auth.get_role_authenticated(self.session)
anon = auth.get_role_anonymous(self.session)
blokes = model.Role(name="Blokes")
self.session.add(blokes)
others = model.Role(name="Others")
self.session.add(others)
barney = model.User(username="barney")
barney.roles.append(blokes)
self.session.add(barney)
self.session.commit()
view = self.make_view()
view.editing = True
self.request.matchdict = {"uuid": barney.uuid}
# no error if data is missing roles
form = view.make_model_form(model_instance=barney)
form.validated = {"username": "barneyx"}
user = view.objectify(form)
self.assertIs(user, barney)
self.assertEqual(barney.username, "barneyx")
# sanity check, user is just in 'blokes' role
self.session.refresh(barney)
self.assertEqual(len(barney.roles), 1)
self.assertEqual(barney.roles[0].name, "Blokes")
# let's test a bunch at once to ensure:
# - user roles are updated
# - authed / anon roles are not added
# - admin role not added if current user is not root
form = view.make_model_form(model_instance=barney)
form.validated = {
"username": "barney",
"roles": {admin.uuid, authed.uuid, anon.uuid, others.uuid},
}
with patch.object(view, "Session", return_value=self.session):
user = view.objectify(form)
self.assertIs(user, barney)
self.assertEqual(len(user.roles), 1)
self.assertEqual(user.roles[0].name, "Others")
# let's test a bunch at once to ensure:
# - user roles are updated
# - admin role is added if current user is root
self.request.is_root = True
form = view.make_model_form(model_instance=barney)
form.validated = {
"username": "barney",
"roles": {admin.uuid, blokes.uuid, others.uuid},
}
with patch.object(view, "Session", return_value=self.session):
user = view.objectify(form)
self.assertIs(user, barney)
self.assertEqual(len(user.roles), 3)
role_uuids = set([role.uuid for role in user.roles])
self.assertEqual(role_uuids, {admin.uuid, blokes.uuid, others.uuid})
# admin role not removed if current user is not root
self.request.is_root = False
form = view.make_model_form(model_instance=barney)
form.validated = {"username": "barney", "roles": {blokes.uuid, others.uuid}}
with patch.object(view, "Session", return_value=self.session):
user = view.objectify(form)
self.assertIs(user, barney)
self.assertEqual(len(user.roles), 3)
# admin role is removed if current user is root
self.request.is_root = True
form = view.make_model_form(model_instance=barney)
form.validated = {"username": "barney", "roles": {blokes.uuid, others.uuid}}
with patch.object(view, "Session", return_value=self.session):
user = view.objectify(form)
self.assertIs(user, barney)
self.assertEqual(len(user.roles), 2)
def test_normalize_api_token(self):
model = self.app.model
auth = self.app.get_auth_handler()
view = self.make_view()
user = model.User(username="foo")
self.session.add(user)
token = auth.add_api_token(user, "test token")
self.session.commit()
normal = view.normalize_api_token(token)
self.assertIn("uuid", normal)
self.assertEqual(normal["uuid"], token.uuid.hex)
self.assertIn("description", normal)
self.assertEqual(normal["description"], "test token")
self.assertIn("created", normal)
def test_make_api_tokens_grid(self):
model = self.app.model
auth = self.app.get_auth_handler()
view = self.make_view()
user = model.User(username="foo")
self.session.add(user)
token1 = auth.add_api_token(user, "test1")
token2 = auth.add_api_token(user, "test2")
self.session.commit()
# grid should have 2 records but no tools/actions
grid = view.make_api_tokens_grid(user)
self.assertIsInstance(grid, Grid)
self.assertEqual(len(grid.data), 2)
self.assertEqual(len(grid.tools), 0)
self.assertEqual(len(grid.actions), 0)
# create + delete allowed
with patch.object(self.request, "is_root", new=True):
grid = view.make_api_tokens_grid(user)
self.assertEqual(len(grid.tools), 1)
self.assertIn("create", grid.tools)
self.assertEqual(len(grid.actions), 1)
self.assertEqual(grid.actions[0].key, "delete")
def test_add_api_token(self):
model = self.app.model
view = self.make_view()
user = model.User(username="foo")
self.session.add(user)
self.session.commit()
self.session.refresh(user)
self.assertEqual(len(user.api_tokens), 0)
with patch.object(view, "Session", return_value=self.session):
with patch.object(self.request, "matchdict", new={"uuid": user.uuid}):
with patch.object(
self.request,
"json_body",
create=True,
new={"description": "testing"},
):
result = view.add_api_token()
self.assertEqual(len(user.api_tokens), 1)
token = user.api_tokens[0]
self.assertEqual(result["token_string"], token.token_string)
self.assertEqual(result["description"], "testing")
def test_delete_api_token(self):
model = self.app.model
auth = self.app.get_auth_handler()
view = self.make_view()
user = model.User(username="foo")
self.session.add(user)
token1 = auth.add_api_token(user, "test1")
token2 = auth.add_api_token(user, "test2")
self.session.commit()
self.session.refresh(user)
self.assertEqual(len(user.api_tokens), 2)
with patch.object(view, "Session", return_value=self.session):
with patch.object(self.request, "matchdict", new={"uuid": user.uuid}):
# normal behavior
with patch.object(
self.request,
"json_body",
create=True,
new={"uuid": token1.uuid.hex},
):
result = view.delete_api_token()
self.assertEqual(result, {})
self.session.refresh(user)
self.assertEqual(len(user.api_tokens), 1)
token = user.api_tokens[0]
self.assertIs(token, token2)
# token for wrong user
user2 = model.User(username="bar")
self.session.add(user2)
token3 = auth.add_api_token(user2, "test3")
self.session.commit()
with patch.object(
self.request,
"json_body",
create=True,
new={"uuid": token3.uuid.hex},
):
result = view.delete_api_token()
self.assertEqual(result, {"error": "API token not found"})
# token not found
with patch.object(
self.request,
"json_body",
create=True,
new={"uuid": self.app.make_true_uuid().hex},
):
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")