Free and open source ticket system written in python
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

test: add tests for ticketing app

Signed-off-by: A. Ottr <alex@otter.foo>

authored by aottr.dev and committed by

A.Ottr 212d1338 e2487431

+276 -13
+28 -8
core/tests.py
··· 2 2 from django.contrib.auth.models import Group 3 3 from django.urls import reverse 4 4 from .utils.initial_data import populate_groups 5 - from .utils.general import sainitize_username 5 + from .utils.general import sanitize_username 6 6 from .models import PawUser 7 7 from django.conf import settings 8 8 ··· 12 12 populate_groups(None, None) 13 13 14 14 def test_groups_created(self): 15 - self.assertEqual(Group.objects.count(), 2) 16 - self.assertEqual(Group.objects.filter(name="Client").count(), 1) 17 - self.assertEqual(Group.objects.filter(name="Supporter").count(), 1) 15 + self.assertEqual(Group.objects.filter(name__in=["Client", "Supporter"]).count(), 2) 16 + # Run again and confirm no duplicates 17 + populate_groups(None, None) 18 + self.assertEqual(Group.objects.filter(name__in=["Client", "Supporter"]).count(), 2) 18 19 19 - class UsernameSainitizationTestCase(TestCase): 20 - def test_sainitize_username(self): 21 - self.assertEqual(sainitize_username("test"), "test") 22 - self.assertEqual(sainitize_username("test !!"), "test") 20 + class UsernameSanitizationTestCase(TestCase): 21 + def test_allows_safe_chars(self): 22 + self.assertEqual(sanitize_username("Alice-01_@corp"), "Alice-01_@corp") 23 + 24 + def test_strips_spaces_and_symbols(self): 25 + self.assertEqual(sanitize_username(" alice.smith "), "alicesmith") 26 + self.assertEqual(sanitize_username("a/b\\c"), "abc") 27 + self.assertEqual(sanitize_username("a%b^c&d*e"), "abcde") 28 + 29 + def test_empty_and_only_illegal(self): 30 + self.assertEqual(sanitize_username(""), "") 31 + self.assertEqual(sanitize_username("!#$()., +="), "") 32 + 33 + def test_unicode_behavior(self): 34 + # Decide policy: currently regex drops non ASCII letters. 35 + self.assertEqual(sanitize_username("álïçé"), "l") 23 36 24 37 class LoginViewTestCase(TestCase): 25 38 def setUp(self): ··· 47 60 self.client.cookies.load(response.cookies) 48 61 self.assertEqual(self.client.cookies[settings.LANGUAGE_COOKIE_NAME].value, self.user.language) 49 62 self.assertEqual(response.url, reverse("all_tickets")) 63 + 64 + def test_login_sets_session(self): 65 + resp = self.client.post(reverse("login"), {"username": self.username, "password": self.password}) 66 + self.assertEqual(resp.status_code, 302) 67 + # Access a protected view to ensure session auth holds 68 + resp2 = self.client.get(reverse("home")) 69 + self.assertIn("_auth_user_id", self.client.session) 50 70 51 71 class RegisterViewTestCase(TestCase): 52 72
+3 -3
core/utils/general.py
··· 1 1 import re 2 2 3 - def sainitize_username(username: str) -> str: 4 - """Remove illegal characters from a username""" 5 - return re.sub(r'[^a-zA-Z0-9-_@]', "", username) 3 + def sanitize_username(username: str) -> str: 4 + """Remove illegal characters from a username: allow [a-zA-Z0-9-_@].""" 5 + return re.sub(r"[^a-zA-Z0-9-_@]", "", username or "")
+1 -1
core/utils/initial_data.py
··· 5 5 def populate_groups(apps, schema_editor): 6 6 user_roles = ["Client", "Supporter"] 7 7 for name in user_roles: 8 - Group.objects.create(name=name) 8 + Group.objects.get_or_create(name=name)
+244 -1
ticketing/tests.py
··· 1 1 from django.test import TestCase 2 2 3 - # Create your tests here. 3 + from django.contrib.auth import get_user_model 4 + from django.core.files.uploadedfile import SimpleUploadedFile 5 + from django.urls import reverse 6 + from ticketing.models import Ticket, Team, Category, Template, Comment 7 + 8 + User = get_user_model() 9 + 10 + def make_user(username="u", is_superuser=False, **kwargs): 11 + defaults = {"email": f"{username}@example.com", "password": "pass-1234567890"} 12 + defaults.update(kwargs) 13 + user = User.objects.create_user(username=username, **defaults) 14 + user.is_superuser = is_superuser 15 + user.save(update_fields=["is_superuser"]) 16 + return user 17 + 18 + def make_team(name="Team A", **kwargs): 19 + return Team.objects.create(name=name, **kwargs) 20 + 21 + def make_category(name="GeneralCat", team=None): 22 + return Category.objects.create(name=name, team=team) 23 + 24 + def make_ticket(user, title="T", desc="D", status=Ticket.Status.OPEN, category=None, priority=Ticket.Priority.MEDIUM, **kwargs): 25 + return Ticket.objects.create( 26 + title=title, user=user, description=desc, status=status, category=category, priority=priority, **kwargs 27 + ) 28 + 29 + def fake_upload(name="file.pdf", content=b"%PDF-1.4", content_type="application/pdf"): 30 + return SimpleUploadedFile(name=name, content=content, content_type=content_type) 31 + 32 + class TicketListViewsTest(TestCase): 33 + def setUp(self): 34 + self.user = make_user("alice") 35 + self.other = make_user("bob") 36 + 37 + def test_requires_login(self): 38 + resp = self.client.get(reverse("all_tickets")) 39 + self.assertEqual(resp.status_code, 302) # redirect to login 40 + 41 + def test_open_tickets_list_shows_user_related(self): 42 + self.client.force_login(self.user) 43 + make_ticket(self.user, title="Mine Open", status=Ticket.Status.OPEN) 44 + make_ticket(self.user, title="Mine Closed", status=Ticket.Status.CLOSED) 45 + make_ticket(self.other, title="Other Open", status=Ticket.Status.OPEN) 46 + 47 + resp = self.client.get(reverse("all_tickets")) 48 + self.assertEqual(resp.status_code, 200) 49 + body = resp.content.decode() 50 + self.assertIn("Mine Open", body) 51 + self.assertNotIn("Mine Closed", body) 52 + 53 + # Depending on your _get_tickets rules, other user’s tickets might be hidden unless team assignment applies. 54 + self.assertNotIn("Other Open", body) 55 + 56 + def test_closed_tickets_list(self): 57 + self.client.force_login(self.user) 58 + make_ticket(self.user, title="Closed 1", status=Ticket.Status.CLOSED) 59 + make_ticket(self.user, title="Open 1", status=Ticket.Status.OPEN) 60 + 61 + resp = self.client.get(reverse("tickets_history")) 62 + self.assertEqual(resp.status_code, 200) 63 + body = resp.content.decode() 64 + self.assertIn("Closed 1", body) 65 + self.assertNotIn("Open 1", body) 66 + 67 + 68 + class TicketDetailViewTest(TestCase): 69 + def setUp(self): 70 + self.owner = make_user("owner") 71 + self.agent = make_user("agent") 72 + self.team = make_team("Support", readonly_access=False, access_non_category_tickets=True) 73 + self.team.members.add(self.agent) 74 + 75 + def test_cannot_open_without_permission(self): 76 + t = make_ticket(self.owner, title="Secret") 77 + stranger = make_user("stranger") 78 + self.client.force_login(stranger) 79 + resp = self.client.get(reverse("ticket_detail", args=[t.pk])) 80 + # View redirects to all_tickets when cannot open 81 + self.assertEqual(resp.status_code, 302) 82 + self.assertEqual(resp.url, reverse("all_tickets")) 83 + 84 + def test_owner_can_view_and_post_comment(self): 85 + t = make_ticket(self.owner, title="Mine") 86 + self.client.force_login(self.owner) 87 + # GET 88 + resp = self.client.get(reverse("ticket_detail", args=[t.pk])) 89 + self.assertEqual(resp.status_code, 200) 90 + # POST comment 91 + resp = self.client.post( 92 + reverse("ticket_detail", args=[t.pk]), 93 + {"text": "Hello", "hidden_from_client": False} 94 + ) 95 + self.assertEqual(resp.status_code, 302) 96 + self.assertEqual(Comment.objects.filter(ticket=t).count(), 1) 97 + t.refresh_from_db() 98 + self.assertEqual(t.status, Ticket.Status.IN_PROGRESS) 99 + 100 + def test_assign_self_when_can_edit(self): 101 + # Assignable because agent is in team with write access and ticket unassigned 102 + t = make_ticket(self.owner, title="Assignable", assigned_team=self.team) 103 + self.client.force_login(self.agent) 104 + resp = self.client.post(reverse("ticket_detail", args=[t.pk]), {"assign_self": "1"}) 105 + self.assertEqual(resp.status_code, 200 if resp.status_code == 200 else 302) # allow either 106 + t.refresh_from_db() 107 + self.assertEqual(t.assigned_to, self.agent) 108 + 109 + def test_apply_template_prefills_comment(self): 110 + Template.objects.create(name="Tmpl", content="prefilled", category=None) 111 + t = make_ticket(self.owner, title="T") 112 + self.client.force_login(self.agent) 113 + # Grant edit permission: add team and assign ticket 114 + t.assigned_team = self.team 115 + t.save() 116 + 117 + resp = self.client.post(reverse("ticket_detail", args=[t.pk]), {"apply_template": "1", "template_select": Template.objects.first().pk}) 118 + self.assertEqual(resp.status_code, 200) 119 + self.assertContains(resp, "prefilled") 120 + 121 + def test_assign_to_team(self): 122 + t = make_ticket(self.owner) 123 + self.client.force_login(self.agent) 124 + resp = self.client.post(reverse("ticket_detail", args=[t.pk]), {"assign_to_team": "1", "team_select": self.team.pk}) 125 + self.assertIn(resp.status_code, (200, 302)) 126 + t.refresh_from_db() 127 + self.assertEqual(t.assigned_team, self.team) 128 + 129 + def test_assign_to_category(self): 130 + t = make_ticket(self.owner) 131 + cat = make_category("Cat", team=self.team) 132 + self.client.force_login(self.agent) 133 + # Give edit permission: assign agent's team 134 + t.assigned_team = self.team 135 + t.save() 136 + resp = self.client.post(reverse("ticket_detail", args=[t.pk]), {"assign_to_category": "1", "category_select": cat.pk}) 137 + self.assertIn(resp.status_code, (200, 302)) 138 + t.refresh_from_db() 139 + self.assertEqual(t.category, cat) 140 + 141 + def test_reopen_ticket(self): 142 + t = make_ticket(self.owner, status=Ticket.Status.CLOSED) 143 + # Give edit permission 144 + t.assigned_team = self.team 145 + t.save() 146 + self.client.force_login(self.agent) 147 + resp = self.client.post(reverse("ticket_detail", args=[t.pk]), {"reopen_ticket": "1"}) 148 + self.assertIn(resp.status_code, (200, 302)) 149 + t.refresh_from_db() 150 + self.assertEqual(t.status, Ticket.Status.IN_PROGRESS) 151 + 152 + class TicketCreateViewTest(TestCase): 153 + def setUp(self): 154 + self.user = make_user("creator") 155 + 156 + def test_requires_login(self): 157 + resp = self.client.get(reverse("create_ticket")) 158 + self.assertEqual(resp.status_code, 302) 159 + 160 + def test_create_ticket_basic(self): 161 + self.client.force_login(self.user) 162 + resp = self.client.post(reverse("create_ticket"), { 163 + "title": "Issue", 164 + "description": "Something broken", 165 + "category": "", # General 166 + "follow_up_to": "", # No Follow-up 167 + }) 168 + self.assertEqual(resp.status_code, 302) 169 + t = Ticket.objects.get() 170 + self.assertEqual(t.title, "Issue") 171 + self.assertEqual(t.user, self.user) 172 + 173 + def test_create_with_attachments(self): 174 + self.client.force_login(self.user) 175 + f1 = fake_upload("a.pdf", b"%PDF-1.4", "application/pdf") 176 + resp = self.client.post(reverse("create_ticket"), { 177 + "title": "With files", 178 + "description": "desc", 179 + "category": "", 180 + "follow_up_to": "", 181 + "attachments": [f1], 182 + }) 183 + self.assertEqual(resp.status_code, 302) 184 + t = Ticket.objects.get(title="With files") 185 + self.assertEqual(t.fileattachment_set.count(), 1) 186 + 187 + def test_follow_up_queryset_only_closed_by_user(self): 188 + self.client.force_login(self.user) 189 + closed = make_ticket(self.user, title="Closed", status=Ticket.Status.CLOSED) 190 + other_open = make_ticket(self.user, title="Open", status=Ticket.Status.OPEN) 191 + resp = self.client.get(reverse("create_ticket")) 192 + self.assertEqual(resp.status_code, 200) 193 + form = resp.context["form"] 194 + qs = form.fields["follow_up_to"].queryset 195 + self.assertIn(closed, qs) 196 + self.assertNotIn(other_open, qs) 197 + 198 + # class DashboardViewTest(TestCase): 199 + # def setUp(self): 200 + # self.user = make_user("viewer") 201 + # self.client.force_login(self.user) 202 + 203 + # def test_groups_by_status(self): 204 + # make_ticket(self.user, title="O1", status=Ticket.Status.OPEN) 205 + # make_ticket(self.user, title="P1", status=Ticket.Status.IN_PROGRESS) 206 + # make_ticket(self.user, title="C1", status=Ticket.Status.CLOSED) 207 + 208 + # resp = self.client.get(reverse("dashboard")) 209 + # self.assertEqual(resp.status_code, 200) 210 + # ctx = resp.context 211 + # self.assertEqual(ctx["open_tickets"].count(), 1) 212 + # self.assertEqual(ctx["in_progress_tickets"].count(), 1) 213 + # self.assertEqual(ctx["closed_tickets"].count(), 1) 214 + 215 + class TicketAccessTest(TestCase): 216 + def setUp(self): 217 + self.owner = make_user("owner") 218 + self.agent = make_user("agent") 219 + self.viewer = make_user("viewer") 220 + self.team_rw = make_team("RW", access_non_category_tickets=True, readonly_access=False) 221 + self.team_ro = make_team("RO", access_non_category_tickets=True, readonly_access=True) 222 + self.team_rw.members.add(self.agent) 223 + self.team_ro.members.add(self.viewer) 224 + 225 + def test_can_open_rules(self): 226 + t_general = make_ticket(self.owner, category=None, assigned_team=None) 227 + t_assigned_team = make_ticket(self.owner, category=None, assigned_team=self.team_rw) 228 + t_assigned_user = make_ticket(self.owner, category=None, assigned_to=self.agent) 229 + 230 + self.assertTrue(t_general.can_open(self.agent)) # has access_non_category_tickets via team_rw 231 + self.assertTrue(t_assigned_team.can_open(self.agent)) 232 + self.assertTrue(t_assigned_user.can_open(self.agent)) 233 + 234 + stranger = make_user("stranger") 235 + self.assertFalse(t_assigned_team.can_open(stranger)) 236 + 237 + def test_can_edit_requires_write_access(self): 238 + t = make_ticket(self.owner, assigned_team=self.team_rw) 239 + self.assertTrue(t.can_edit(self.agent)) # RW team member 240 + 241 + t2 = make_ticket(self.owner, assigned_team=self.team_ro) 242 + self.assertFalse(t2.can_edit(self.viewer)) # RO team member 243 + 244 + # Unassigned general with access_non_category_tickets but viewer is read-only, so cannot edit 245 + t3 = make_ticket(self.owner, assigned_team=None, category=None) 246 + self.assertFalse(t3.can_edit(self.viewer))