mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-08 11:16:47 +00:00
The Users, Groups and Removed VMs pages were bare tables: rows of coloured icon buttons without words, five columns of small checks for a user's role and permissions, dates broken over three lines and a lone "+" for a new entry. The owner chose a directory layout from three rendered options; it follows the host pages' cards: - Each list is a card with its count and search in the header and one list-group item per entry, which stacks on a phone without scrolling. - A user shows their role as a badge (Superuser, Template editor) or "User", Blocked as a red badge, their groups, a granted clone permission and live API tokens, and the last login on one line; role and status are searchable. Edit is the visible action, the menu holds View profile, Block or Unblock and Delete; Block and Delete are disabled on one's own account, which the server refuses anyway. - A group shows its member and permission counts; its name opens it. The page's title said "Users". - A removed VM shows its UUID, the host it left, its owners and when the record was made and expires. - Logs keep their table in a card, the server search in its header, the count of matching rows, Done and Failed as badges, and a message column wide enough to read on a phone. The user form was one long column of horizontal fields. The owner rejected three boxed layouts and chose one led by what the user may do: the Active switch, Template editor and Superuser as two cards, groups as chips, the user's own permissions as switches each saying what it allows, then the name and password and the quota folded into details whose summaries tell their values (-1 as "any"); they open on a new user and on an error inside them. A refused group or permission is said beside its field. The group form follows it: the name, what its members may do, its members as chips. Cancel goes back to the list instead of the browser history.
117 lines
6.9 KiB
Python
117 lines
6.9 KiB
Python
"""The Users pages: an administrator cannot lock their own account out, the
|
|
user form says what its fields mean, and a password set by an administrator
|
|
can be one the user must change."""
|
|
|
|
from accounts.models import UserAttributes
|
|
from django.contrib.auth import get_user_model
|
|
from django.contrib.messages import get_messages
|
|
from django.test import TestCase
|
|
from django.urls import reverse
|
|
from lxml import html
|
|
|
|
|
|
class UserPagesTests(TestCase):
|
|
def setUp(self):
|
|
User = get_user_model()
|
|
self.admin = User.objects.create_superuser("up-admin", "up@example.com", "pw")
|
|
self.alice = User.objects.create_user("up-alice", password="Alice-pw-123")
|
|
UserAttributes.objects.get_or_create(user=self.alice)
|
|
self.client.force_login(self.admin)
|
|
|
|
def messages(self, response):
|
|
return [str(m) for m in get_messages(response.wsgi_request)]
|
|
|
|
def form_data(self, user, **changes):
|
|
data = {"username": user.username, "is_active": "on", "is_superuser": "on" if user.is_superuser else "",
|
|
"max_instances": "2", "max_cpus": "2", "max_memory": "2048", "max_disk_size": "20", **changes}
|
|
return {key: value for key, value in data.items() if value != ""}
|
|
|
|
def test_an_administrator_cannot_block_delete_or_demote_themselves(self):
|
|
own = self.admin.pk
|
|
for response in (self.client.post(reverse("admin:user_block", args=[own])),
|
|
self.client.post(reverse("admin:user_delete", args=[own]))):
|
|
self.assertIn("You cannot block, delete or demote your own account", self.messages(response))
|
|
for change in ({"is_superuser": ""}, {"is_active": ""}):
|
|
response = self.client.post(reverse("admin:user_update", args=[own]), self.form_data(self.admin, **change))
|
|
self.assertContains(response, "You cannot block, delete or demote your own account")
|
|
self.admin.refresh_from_db()
|
|
self.assertTrue(self.admin.is_active and self.admin.is_superuser)
|
|
# another user's account is theirs to change
|
|
self.client.post(reverse("admin:user_block", args=[self.alice.pk]))
|
|
self.alice.refresh_from_db()
|
|
self.assertFalse(self.alice.is_active)
|
|
|
|
def test_the_form_tells_the_password_state_and_links_its_page(self):
|
|
page = html.fromstring(self.client.get(reverse("admin:user_update", args=[self.alice.pk])).content)
|
|
text = " ".join(page.text_content().split())
|
|
self.assertIn("Password: Set Change password", text)
|
|
self.assertNotIn("No password set", text)
|
|
self.assertEqual(page.xpath("//a[normalize-space()='Change password']/@href"),
|
|
[reverse("admin:user_update_password", args=[self.alice.pk])])
|
|
self.assertEqual(page.xpath("//a[@href='../password/']"), [])
|
|
|
|
def test_staff_and_quotas_say_what_they_mean(self):
|
|
page = self.client.get(reverse("admin:user_update", args=[self.alice.pk])).content.decode()
|
|
self.assertIn("Template editor", page)
|
|
self.assertIn("May change and delete the template VMs given to them", page)
|
|
self.assertNotIn("log into this admin site", page)
|
|
self.assertIn("Max memory (MiB)", page)
|
|
self.assertIn("Max disk size (GiB)", page)
|
|
self.assertIn("-1 means no limit, 0 none allowed", page)
|
|
|
|
def test_a_refused_choice_is_said_beside_its_field(self):
|
|
url = reverse("admin:user_update", args=[self.alice.pk])
|
|
response = self.client.post(url, {"username": "up-alice", "is_active": "on", "groups": "99999", "user_permissions": "99999",
|
|
"max_instances": 1, "max_cpus": 1, "max_memory": 1, "max_disk_size": 1})
|
|
page = html.fromstring(response.content)
|
|
self.assertEqual(len(page.xpath("//fieldset//div[contains(@class, 'invalid-feedback')]")), 2)
|
|
|
|
def test_the_folded_quota_says_what_it_allows(self):
|
|
UserAttributes.objects.filter(user=self.alice).update(max_instances=-1, max_cpus=1, max_memory=2048, max_disk_size=-1)
|
|
page = html.fromstring(self.client.get(reverse("admin:user_update", args=[self.alice.pk])).content)
|
|
(summary,) = [s for s in page.xpath("//summary") if "Quota" in s.text_content()]
|
|
self.assertEqual(" ".join(summary.text_content().split()), "Quota any number of VMs · 1 vCPU · 2048 MiB · any disk size")
|
|
|
|
def test_a_missing_users_profile_is_not_found(self):
|
|
self.assertEqual(self.client.get(reverse("accounts:account", args=[99999])).status_code, 404)
|
|
|
|
def test_a_user_who_never_logged_in_says_never(self):
|
|
page = html.fromstring(self.client.get(reverse("admin:user_list")).content)
|
|
(row,) = page.xpath("//div[contains(@class, 'list-group-item')][.//a[normalize-space()='up-alice']]")
|
|
self.assertIn("Last login: never", " ".join(row.text_content().split()))
|
|
|
|
def test_the_search_finds_a_user_by_role_and_status(self):
|
|
self.alice.is_active = False
|
|
self.alice.save()
|
|
page = html.fromstring(self.client.get(reverse("admin:user_list")).content)
|
|
(row,) = page.xpath("//div[contains(@class, 'list-group-item')][.//a[normalize-space()='up-alice']]")
|
|
self.assertIn("User Blocked", row.get("data-search"))
|
|
|
|
def test_a_password_set_by_an_administrator_can_be_one_to_change(self):
|
|
url = reverse("admin:user_update_password", args=[self.alice.pk])
|
|
self.client.post(url, {"password1": "New-pw-4567x", "password2": "New-pw-4567x", "must_change_password": "on"})
|
|
self.assertTrue(UserAttributes.objects.get(user=self.alice).must_change_password)
|
|
# the user is sent to the change form until they choose their own
|
|
self.alice.refresh_from_db() # the new password's hash, or the session is not hers
|
|
self.client.force_login(self.alice)
|
|
self.assertRedirects(self.client.get(reverse("instances:index")), reverse("accounts:change_password"),
|
|
fetch_redirect_response=False)
|
|
self.client.force_login(self.admin)
|
|
self.client.post(url, {"password1": "New-pw-4567y", "password2": "New-pw-4567y"})
|
|
self.assertFalse(UserAttributes.objects.get(user=self.alice).must_change_password)
|
|
|
|
def test_a_user_without_a_password_cannot_be_made_to_change_it(self):
|
|
# the change form asks for the old password: one they never had
|
|
url = reverse("admin:user_update_password", args=[self.alice.pk])
|
|
response = self.client.post(url, {"usable_password": "false", "must_change_password": "on"})
|
|
self.assertContains(response, "A user without a password cannot be made to change it")
|
|
self.alice.refresh_from_db()
|
|
self.assertTrue(self.alice.has_usable_password())
|
|
|
|
def test_an_administrator_keeps_their_own_password(self):
|
|
url = reverse("admin:user_update_password", args=[self.admin.pk])
|
|
response = self.client.post(url, {"usable_password": "false"})
|
|
self.assertContains(response, "You cannot block, delete or demote your own account")
|
|
self.admin.refresh_from_db()
|
|
self.assertTrue(self.admin.has_usable_password())
|
|
|