FossilRepo

fossilrepo / tests / test_ticket_fields.py
Source Blame History 237 lines
c588255… ragelink 1 import pytest
c588255… ragelink 2 from django.contrib.auth.models import User
c588255… ragelink 3 from django.test import Client
c588255… ragelink 4
c588255… ragelink 5 from fossil.models import FossilRepository
c588255… ragelink 6 from fossil.ticket_fields import TicketFieldDefinition
c588255… ragelink 7 from organization.models import Team
c588255… ragelink 8 from projects.models import ProjectTeam
c588255… ragelink 9
c588255… ragelink 10
c588255… ragelink 11 @pytest.fixture
c588255… ragelink 12 def fossil_repo_obj(sample_project):
c588255… ragelink 13 """Return the auto-created FossilRepository for sample_project."""
c588255… ragelink 14 return FossilRepository.objects.get(project=sample_project, deleted_at__isnull=True)
c588255… ragelink 15
c588255… ragelink 16
c588255… ragelink 17 @pytest.fixture
c588255… ragelink 18 def text_field(fossil_repo_obj, admin_user):
c588255… ragelink 19 return TicketFieldDefinition.objects.create(
c588255… ragelink 20 repository=fossil_repo_obj,
c588255… ragelink 21 name="component",
c588255… ragelink 22 label="Component",
c588255… ragelink 23 field_type="text",
c588255… ragelink 24 is_required=False,
c588255… ragelink 25 sort_order=1,
c588255… ragelink 26 created_by=admin_user,
c588255… ragelink 27 )
c588255… ragelink 28
c588255… ragelink 29
c588255… ragelink 30 @pytest.fixture
c588255… ragelink 31 def select_field(fossil_repo_obj, admin_user):
c588255… ragelink 32 return TicketFieldDefinition.objects.create(
c588255… ragelink 33 repository=fossil_repo_obj,
c588255… ragelink 34 name="platform",
c588255… ragelink 35 label="Platform",
c588255… ragelink 36 field_type="select",
c588255… ragelink 37 choices="Linux\nWindows\nmacOS",
c588255… ragelink 38 is_required=True,
c588255… ragelink 39 sort_order=2,
c588255… ragelink 40 created_by=admin_user,
c588255… ragelink 41 )
c588255… ragelink 42
c588255… ragelink 43
c588255… ragelink 44 @pytest.fixture
c588255… ragelink 45 def writer_user(db, admin_user, sample_project):
c588255… ragelink 46 """User with write access but not admin."""
c588255… ragelink 47 writer = User.objects.create_user(username="writer", password="testpass123")
c588255… ragelink 48 team = Team.objects.create(name="Writers", organization=sample_project.organization, created_by=admin_user)
c588255… ragelink 49 team.members.add(writer)
c588255… ragelink 50 ProjectTeam.objects.create(project=sample_project, team=team, role="write", created_by=admin_user)
c588255… ragelink 51 return writer
c588255… ragelink 52
c588255… ragelink 53
c588255… ragelink 54 @pytest.fixture
c588255… ragelink 55 def writer_client(writer_user):
c588255… ragelink 56 client = Client()
c588255… ragelink 57 client.login(username="writer", password="testpass123")
c588255… ragelink 58 return client
c588255… ragelink 59
c588255… ragelink 60
c588255… ragelink 61 # --- Model Tests ---
c588255… ragelink 62
c588255… ragelink 63
c588255… ragelink 64 @pytest.mark.django_db
c588255… ragelink 65 class TestTicketFieldDefinitionModel:
c588255… ragelink 66 def test_create_field(self, text_field):
c588255… ragelink 67 assert text_field.pk is not None
c588255… ragelink 68 assert str(text_field) == "Component (component)"
c588255… ragelink 69
c588255… ragelink 70 def test_choices_list(self, select_field):
c588255… ragelink 71 assert select_field.choices_list == ["Linux", "Windows", "macOS"]
c588255… ragelink 72
c588255… ragelink 73 def test_choices_list_empty(self, text_field):
c588255… ragelink 74 assert text_field.choices_list == []
c588255… ragelink 75
c588255… ragelink 76 def test_soft_delete(self, text_field, admin_user):
c588255… ragelink 77 text_field.soft_delete(user=admin_user)
c588255… ragelink 78 assert text_field.is_deleted
c588255… ragelink 79 assert TicketFieldDefinition.objects.filter(pk=text_field.pk).count() == 0
c588255… ragelink 80 assert TicketFieldDefinition.all_objects.filter(pk=text_field.pk).count() == 1
c588255… ragelink 81
c588255… ragelink 82 def test_ordering(self, text_field, select_field):
c588255… ragelink 83 fields = list(TicketFieldDefinition.objects.filter(repository=text_field.repository))
c588255… ragelink 84 assert fields[0] == text_field # sort_order=1
c588255… ragelink 85 assert fields[1] == select_field # sort_order=2
c588255… ragelink 86
c588255… ragelink 87 def test_unique_name_per_repo(self, fossil_repo_obj, admin_user, text_field):
c588255… ragelink 88 from django.db import IntegrityError
c588255… ragelink 89
c588255… ragelink 90 with pytest.raises(IntegrityError):
c588255… ragelink 91 TicketFieldDefinition.objects.create(
c588255… ragelink 92 repository=fossil_repo_obj,
c588255… ragelink 93 name="component",
c588255… ragelink 94 label="Duplicate Component",
c588255… ragelink 95 created_by=admin_user,
c588255… ragelink 96 )
c588255… ragelink 97
c588255… ragelink 98
c588255… ragelink 99 # --- List View Tests ---
c588255… ragelink 100
c588255… ragelink 101
c588255… ragelink 102 @pytest.mark.django_db
c588255… ragelink 103 class TestTicketFieldsListView:
c588255… ragelink 104 def test_list_fields(self, admin_client, sample_project, text_field, select_field):
c588255… ragelink 105 response = admin_client.get(f"/projects/{sample_project.slug}/fossil/tickets/fields/")
c588255… ragelink 106 assert response.status_code == 200
c588255… ragelink 107 content = response.content.decode()
c588255… ragelink 108 assert "Component" in content
c588255… ragelink 109 assert "Platform" in content
c588255… ragelink 110
c588255… ragelink 111 def test_list_empty(self, admin_client, sample_project, fossil_repo_obj):
c588255… ragelink 112 response = admin_client.get(f"/projects/{sample_project.slug}/fossil/tickets/fields/")
c588255… ragelink 113 assert response.status_code == 200
c588255… ragelink 114 assert "No custom ticket fields defined" in response.content.decode()
c588255… ragelink 115
c588255… ragelink 116 def test_list_denied_for_writer(self, writer_client, sample_project, text_field):
c588255… ragelink 117 """Custom field management requires admin."""
c588255… ragelink 118 response = writer_client.get(f"/projects/{sample_project.slug}/fossil/tickets/fields/")
c588255… ragelink 119 assert response.status_code == 403
c588255… ragelink 120
c588255… ragelink 121 def test_list_denied_for_anon(self, client, sample_project):
c588255… ragelink 122 response = client.get(f"/projects/{sample_project.slug}/fossil/tickets/fields/")
c588255… ragelink 123 assert response.status_code == 302 # redirect to login
c588255… ragelink 124
c588255… ragelink 125
c588255… ragelink 126 # --- Create View Tests ---
c588255… ragelink 127
c588255… ragelink 128
c588255… ragelink 129 @pytest.mark.django_db
c588255… ragelink 130 class TestTicketFieldCreateView:
c588255… ragelink 131 def test_get_form(self, admin_client, sample_project, fossil_repo_obj):
c588255… ragelink 132 response = admin_client.get(f"/projects/{sample_project.slug}/fossil/tickets/fields/create/")
c588255… ragelink 133 assert response.status_code == 200
c588255… ragelink 134 assert "Add Custom Ticket Field" in response.content.decode()
c588255… ragelink 135
c588255… ragelink 136 def test_create_field(self, admin_client, sample_project, fossil_repo_obj):
c588255… ragelink 137 response = admin_client.post(
c588255… ragelink 138 f"/projects/{sample_project.slug}/fossil/tickets/fields/create/",
c588255… ragelink 139 {
c588255… ragelink 140 "name": "affected_version",
c588255… ragelink 141 "label": "Affected Version",
c588255… ragelink 142 "field_type": "text",
c588255… ragelink 143 "sort_order": "5",
c588255… ragelink 144 },
c588255… ragelink 145 )
c588255… ragelink 146 assert response.status_code == 302
c588255… ragelink 147 field = TicketFieldDefinition.objects.get(name="affected_version")
c588255… ragelink 148 assert field.label == "Affected Version"
c588255… ragelink 149 assert field.field_type == "text"
c588255… ragelink 150 assert field.sort_order == 5
c588255… ragelink 151 assert field.is_required is False
c588255… ragelink 152
c588255… ragelink 153 def test_create_select_field_with_choices(self, admin_client, sample_project, fossil_repo_obj):
c588255… ragelink 154 response = admin_client.post(
c588255… ragelink 155 f"/projects/{sample_project.slug}/fossil/tickets/fields/create/",
c588255… ragelink 156 {
c588255… ragelink 157 "name": "env",
c588255… ragelink 158 "label": "Environment",
c588255… ragelink 159 "field_type": "select",
c588255… ragelink 160 "choices": "dev\nstaging\nprod",
c588255… ragelink 161 "is_required": "on",
c588255… ragelink 162 "sort_order": "0",
c588255… ragelink 163 },
c588255… ragelink 164 )
c588255… ragelink 165 assert response.status_code == 302
c588255… ragelink 166 field = TicketFieldDefinition.objects.get(name="env")
c588255… ragelink 167 assert field.choices_list == ["dev", "staging", "prod"]
c588255… ragelink 168 assert field.is_required is True
c588255… ragelink 169
c588255… ragelink 170 def test_create_duplicate_name_rejected(self, admin_client, sample_project, text_field):
c588255… ragelink 171 response = admin_client.post(
c588255… ragelink 172 f"/projects/{sample_project.slug}/fossil/tickets/fields/create/",
c588255… ragelink 173 {"name": "component", "label": "Another Component", "field_type": "text", "sort_order": "0"},
c588255… ragelink 174 )
c588255… ragelink 175 assert response.status_code == 200 # re-renders form
c588255… ragelink 176 assert TicketFieldDefinition.objects.filter(name="component").count() == 1
c588255… ragelink 177
c588255… ragelink 178 def test_create_denied_for_writer(self, writer_client, sample_project):
c588255… ragelink 179 response = writer_client.post(
c588255… ragelink 180 f"/projects/{sample_project.slug}/fossil/tickets/fields/create/",
c588255… ragelink 181 {"name": "evil", "label": "Evil", "field_type": "text", "sort_order": "0"},
c588255… ragelink 182 )
c588255… ragelink 183 assert response.status_code == 403
c588255… ragelink 184
c588255… ragelink 185
c588255… ragelink 186 # --- Edit View Tests ---
c588255… ragelink 187
c588255… ragelink 188
c588255… ragelink 189 @pytest.mark.django_db
c588255… ragelink 190 class TestTicketFieldEditView:
c588255… ragelink 191 def test_get_edit_form(self, admin_client, sample_project, text_field):
c588255… ragelink 192 response = admin_client.get(f"/projects/{sample_project.slug}/fossil/tickets/fields/{text_field.pk}/edit/")
c588255… ragelink 193 assert response.status_code == 200
c588255… ragelink 194 content = response.content.decode()
c588255… ragelink 195 assert "component" in content
c588255… ragelink 196 assert "Component" in content
c588255… ragelink 197
c588255… ragelink 198 def test_edit_field(self, admin_client, sample_project, text_field):
c588255… ragelink 199 response = admin_client.post(
c588255… ragelink 200 f"/projects/{sample_project.slug}/fossil/tickets/fields/{text_field.pk}/edit/",
c588255… ragelink 201 {"name": "component", "label": "SW Component", "field_type": "text", "sort_order": "10"},
c588255… ragelink 202 )
c588255… ragelink 203 assert response.status_code == 302
c588255… ragelink 204 text_field.refresh_from_db()
c588255… ragelink 205 assert text_field.label == "SW Component"
c588255… ragelink 206 assert text_field.sort_order == 10
c588255… ragelink 207
c588255… ragelink 208 def test_edit_denied_for_writer(self, writer_client, sample_project, text_field):
c588255… ragelink 209 response = writer_client.post(
c588255… ragelink 210 f"/projects/{sample_project.slug}/fossil/tickets/fields/{text_field.pk}/edit/",
c588255… ragelink 211 {"name": "component", "label": "Hacked", "field_type": "text", "sort_order": "0"},
c588255… ragelink 212 )
c588255… ragelink 213 assert response.status_code == 403
c588255… ragelink 214
c588255… ragelink 215 def test_edit_nonexistent(self, admin_client, sample_project, fossil_repo_obj):
c588255… ragelink 216 response = admin_client.get(f"/projects/{sample_project.slug}/fossil/tickets/fields/99999/edit/")
c588255… ragelink 217 assert response.status_code == 404
c588255… ragelink 218
c588255… ragelink 219
c588255… ragelink 220 # --- Delete View Tests ---
c588255… ragelink 221
c588255… ragelink 222
c588255… ragelink 223 @pytest.mark.django_db
c588255… ragelink 224 class TestTicketFieldDeleteView:
c588255… ragelink 225 def test_delete_field(self, admin_client, sample_project, text_field):
c588255… ragelink 226 response = admin_client.post(f"/projects/{sample_project.slug}/fossil/tickets/fields/{text_field.pk}/delete/")
c588255… ragelink 227 assert response.status_code == 302
c588255… ragelink 228 text_field.refresh_from_db()
c588255… ragelink 229 assert text_field.is_deleted
c588255… ragelink 230
c588255… ragelink 231 def test_delete_get_redirects(self, admin_client, sample_project, text_field):
c588255… ragelink 232 response = admin_client.get(f"/projects/{sample_project.slug}/fossil/tickets/fields/{text_field.pk}/delete/")
c588255… ragelink 233 assert response.status_code == 302
c588255… ragelink 234
c588255… ragelink 235 def test_delete_denied_for_writer(self, writer_client, sample_project, text_field):
c588255… ragelink 236 response = writer_client.post(f"/projects/{sample_project.slug}/fossil/tickets/fields/{text_field.pk}/delete/")
c588255… ragelink 237 assert response.status_code == 403

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button