FossilRepo

fossilrepo / tests / test_branch_protection.py
Source Blame History 256 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.branch_protection import BranchProtection
c588255… ragelink 6 from fossil.models import FossilRepository
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 protection_rule(fossil_repo_obj, admin_user):
c588255… ragelink 19 return BranchProtection.objects.create(
c588255… ragelink 20 repository=fossil_repo_obj,
c588255… ragelink 21 branch_pattern="trunk",
c588255… ragelink 22 require_status_checks=True,
c588255… ragelink 23 required_contexts="ci/tests\nci/lint",
c588255… ragelink 24 restrict_push=True,
c588255… ragelink 25 created_by=admin_user,
c588255… ragelink 26 )
c588255… ragelink 27
c588255… ragelink 28
c588255… ragelink 29 @pytest.fixture
c588255… ragelink 30 def writer_user(db, admin_user, sample_project):
c588255… ragelink 31 """User with write access but not admin."""
c588255… ragelink 32 writer = User.objects.create_user(username="writer_bp", password="testpass123")
c588255… ragelink 33 team = Team.objects.create(name="BP Writers", organization=sample_project.organization, created_by=admin_user)
c588255… ragelink 34 team.members.add(writer)
c588255… ragelink 35 ProjectTeam.objects.create(project=sample_project, team=team, role="write", created_by=admin_user)
c588255… ragelink 36 return writer
c588255… ragelink 37
c588255… ragelink 38
c588255… ragelink 39 @pytest.fixture
c588255… ragelink 40 def writer_client(writer_user):
c588255… ragelink 41 client = Client()
c588255… ragelink 42 client.login(username="writer_bp", password="testpass123")
c588255… ragelink 43 return client
c588255… ragelink 44
c588255… ragelink 45
c588255… ragelink 46 # --- BranchProtection Model Tests ---
c588255… ragelink 47
c588255… ragelink 48
c588255… ragelink 49 @pytest.mark.django_db
c588255… ragelink 50 class TestBranchProtectionModel:
c588255… ragelink 51 def test_create_rule(self, protection_rule):
c588255… ragelink 52 assert protection_rule.pk is not None
c588255… ragelink 53 assert str(protection_rule) == f"trunk ({protection_rule.repository})"
c588255… ragelink 54
c588255… ragelink 55 def test_soft_delete(self, protection_rule, admin_user):
c588255… ragelink 56 protection_rule.soft_delete(user=admin_user)
c588255… ragelink 57 assert protection_rule.is_deleted
c588255… ragelink 58 assert BranchProtection.objects.filter(pk=protection_rule.pk).count() == 0
c588255… ragelink 59 assert BranchProtection.all_objects.filter(pk=protection_rule.pk).count() == 1
c588255… ragelink 60
c588255… ragelink 61 def test_unique_together(self, fossil_repo_obj, admin_user):
c588255… ragelink 62 BranchProtection.objects.create(
c588255… ragelink 63 repository=fossil_repo_obj,
c588255… ragelink 64 branch_pattern="release-*",
c588255… ragelink 65 created_by=admin_user,
c588255… ragelink 66 )
c588255… ragelink 67 from django.db import IntegrityError
c588255… ragelink 68
c588255… ragelink 69 with pytest.raises(IntegrityError):
c588255… ragelink 70 BranchProtection.objects.create(
c588255… ragelink 71 repository=fossil_repo_obj,
c588255… ragelink 72 branch_pattern="release-*",
c588255… ragelink 73 created_by=admin_user,
c588255… ragelink 74 )
c588255… ragelink 75
c588255… ragelink 76 def test_ordering(self, fossil_repo_obj, admin_user):
c588255… ragelink 77 r1 = BranchProtection.objects.create(repository=fossil_repo_obj, branch_pattern="trunk", created_by=admin_user)
c588255… ragelink 78 r2 = BranchProtection.objects.create(repository=fossil_repo_obj, branch_pattern="develop", created_by=admin_user)
c588255… ragelink 79 rules = list(BranchProtection.objects.filter(repository=fossil_repo_obj))
c588255… ragelink 80 # Ordered by branch_pattern alphabetically
c588255… ragelink 81 assert rules[0] == r2
c588255… ragelink 82 assert rules[1] == r1
c588255… ragelink 83
c588255… ragelink 84 def test_get_required_contexts_list(self, protection_rule):
c588255… ragelink 85 contexts = protection_rule.get_required_contexts_list()
c588255… ragelink 86 assert contexts == ["ci/tests", "ci/lint"]
c588255… ragelink 87
c588255… ragelink 88 def test_get_required_contexts_list_empty(self, fossil_repo_obj, admin_user):
c588255… ragelink 89 rule = BranchProtection.objects.create(
c588255… ragelink 90 repository=fossil_repo_obj,
c588255… ragelink 91 branch_pattern="feature-*",
c588255… ragelink 92 required_contexts="",
c588255… ragelink 93 created_by=admin_user,
c588255… ragelink 94 )
c588255… ragelink 95 assert rule.get_required_contexts_list() == []
c588255… ragelink 96
c588255… ragelink 97 def test_get_required_contexts_list_filters_blanks(self, fossil_repo_obj, admin_user):
c588255… ragelink 98 rule = BranchProtection.objects.create(
c588255… ragelink 99 repository=fossil_repo_obj,
c588255… ragelink 100 branch_pattern="hotfix-*",
c588255… ragelink 101 required_contexts="ci/tests\n\n \nci/lint\n",
c588255… ragelink 102 created_by=admin_user,
c588255… ragelink 103 )
c588255… ragelink 104 assert rule.get_required_contexts_list() == ["ci/tests", "ci/lint"]
c588255… ragelink 105
c588255… ragelink 106
c588255… ragelink 107 # --- Branch Protection List View Tests ---
c588255… ragelink 108
c588255… ragelink 109
c588255… ragelink 110 @pytest.mark.django_db
c588255… ragelink 111 class TestBranchProtectionListView:
c588255… ragelink 112 def test_list_rules(self, admin_client, sample_project, protection_rule):
c588255… ragelink 113 response = admin_client.get(f"/projects/{sample_project.slug}/fossil/branches/protect/")
c588255… ragelink 114 assert response.status_code == 200
c588255… ragelink 115 content = response.content.decode()
c588255… ragelink 116 assert "trunk" in content
c588255… ragelink 117 assert "CI required" in content
c588255… ragelink 118 assert "Push restricted" in content
c588255… ragelink 119
c588255… ragelink 120 def test_list_empty(self, admin_client, sample_project, fossil_repo_obj):
c588255… ragelink 121 response = admin_client.get(f"/projects/{sample_project.slug}/fossil/branches/protect/")
c588255… ragelink 122 assert response.status_code == 200
c588255… ragelink 123 assert "No branch protection rules configured" in response.content.decode()
c588255… ragelink 124
c588255… ragelink 125 def test_list_denied_for_writer(self, writer_client, sample_project, protection_rule):
c588255… ragelink 126 response = writer_client.get(f"/projects/{sample_project.slug}/fossil/branches/protect/")
c588255… ragelink 127 assert response.status_code == 403
c588255… ragelink 128
c588255… ragelink 129 def test_list_denied_for_no_perm(self, no_perm_client, sample_project):
c588255… ragelink 130 response = no_perm_client.get(f"/projects/{sample_project.slug}/fossil/branches/protect/")
c588255… ragelink 131 assert response.status_code == 403
c588255… ragelink 132
c588255… ragelink 133 def test_list_denied_for_anon(self, client, sample_project):
c588255… ragelink 134 response = client.get(f"/projects/{sample_project.slug}/fossil/branches/protect/")
c588255… ragelink 135 assert response.status_code == 302 # redirect to login
c588255… ragelink 136
c588255… ragelink 137
c588255… ragelink 138 # --- Branch Protection Create View Tests ---
c588255… ragelink 139
c588255… ragelink 140
c588255… ragelink 141 @pytest.mark.django_db
c588255… ragelink 142 class TestBranchProtectionCreateView:
c588255… ragelink 143 def test_get_form(self, admin_client, sample_project, fossil_repo_obj):
c588255… ragelink 144 response = admin_client.get(f"/projects/{sample_project.slug}/fossil/branches/protect/create/")
c588255… ragelink 145 assert response.status_code == 200
c588255… ragelink 146 assert "Create Branch Protection Rule" in response.content.decode()
c588255… ragelink 147
c588255… ragelink 148 def test_create_rule(self, admin_client, sample_project, fossil_repo_obj):
c588255… ragelink 149 response = admin_client.post(
c588255… ragelink 150 f"/projects/{sample_project.slug}/fossil/branches/protect/create/",
c588255… ragelink 151 {
c588255… ragelink 152 "branch_pattern": "develop",
c588255… ragelink 153 "require_status_checks": "on",
c588255… ragelink 154 "required_contexts": "ci/tests",
c588255… ragelink 155 "restrict_push": "on",
c588255… ragelink 156 },
c588255… ragelink 157 )
c588255… ragelink 158 assert response.status_code == 302
c588255… ragelink 159 rule = BranchProtection.objects.get(branch_pattern="develop")
c588255… ragelink 160 assert rule.require_status_checks is True
c588255… ragelink 161 assert rule.required_contexts == "ci/tests"
c588255… ragelink 162 assert rule.restrict_push is True
c588255… ragelink 163
c588255… ragelink 164 def test_create_without_pattern_fails(self, admin_client, sample_project, fossil_repo_obj):
c588255… ragelink 165 response = admin_client.post(
c588255… ragelink 166 f"/projects/{sample_project.slug}/fossil/branches/protect/create/",
c588255… ragelink 167 {"branch_pattern": ""},
c588255… ragelink 168 )
c588255… ragelink 169 assert response.status_code == 200 # Re-renders form
c588255… ragelink 170 assert BranchProtection.objects.count() == 0
c588255… ragelink 171
c588255… ragelink 172 def test_create_duplicate_pattern_fails(self, admin_client, sample_project, fossil_repo_obj, protection_rule):
c588255… ragelink 173 response = admin_client.post(
c588255… ragelink 174 f"/projects/{sample_project.slug}/fossil/branches/protect/create/",
c588255… ragelink 175 {"branch_pattern": "trunk"},
c588255… ragelink 176 )
c588255… ragelink 177 assert response.status_code == 200 # Re-renders form with error
c588255… ragelink 178 assert "already exists" in response.content.decode()
c588255… ragelink 179
c588255… ragelink 180 def test_create_denied_for_writer(self, writer_client, sample_project):
c588255… ragelink 181 response = writer_client.post(
c588255… ragelink 182 f"/projects/{sample_project.slug}/fossil/branches/protect/create/",
c588255… ragelink 183 {"branch_pattern": "evil-branch"},
c588255… ragelink 184 )
c588255… ragelink 185 assert response.status_code == 403
c588255… ragelink 186
c588255… ragelink 187
c588255… ragelink 188 # --- Branch Protection Edit View Tests ---
c588255… ragelink 189
c588255… ragelink 190
c588255… ragelink 191 @pytest.mark.django_db
c588255… ragelink 192 class TestBranchProtectionEditView:
c588255… ragelink 193 def test_get_edit_form(self, admin_client, sample_project, protection_rule):
c588255… ragelink 194 response = admin_client.get(f"/projects/{sample_project.slug}/fossil/branches/protect/{protection_rule.pk}/edit/")
c588255… ragelink 195 assert response.status_code == 200
c588255… ragelink 196 content = response.content.decode()
c588255… ragelink 197 assert "trunk" in content
c588255… ragelink 198 assert "Update Rule" in content
c588255… ragelink 199
c588255… ragelink 200 def test_edit_rule(self, admin_client, sample_project, protection_rule):
c588255… ragelink 201 response = admin_client.post(
c588255… ragelink 202 f"/projects/{sample_project.slug}/fossil/branches/protect/{protection_rule.pk}/edit/",
c588255… ragelink 203 {
c588255… ragelink 204 "branch_pattern": "trunk",
c588255… ragelink 205 "require_status_checks": "on",
c588255… ragelink 206 "required_contexts": "ci/tests\nci/lint\nci/build",
c588255… ragelink 207 "restrict_push": "on",
c588255… ragelink 208 },
c588255… ragelink 209 )
c588255… ragelink 210 assert response.status_code == 302
c588255… ragelink 211 protection_rule.refresh_from_db()
c588255… ragelink 212 assert "ci/build" in protection_rule.required_contexts
c588255… ragelink 213
c588255… ragelink 214 def test_edit_change_pattern(self, admin_client, sample_project, protection_rule):
c588255… ragelink 215 response = admin_client.post(
c588255… ragelink 216 f"/projects/{sample_project.slug}/fossil/branches/protect/{protection_rule.pk}/edit/",
c588255… ragelink 217 {"branch_pattern": "main", "restrict_push": "on"},
c588255… ragelink 218 )
c588255… ragelink 219 assert response.status_code == 302
c588255… ragelink 220 protection_rule.refresh_from_db()
c588255… ragelink 221 assert protection_rule.branch_pattern == "main"
c588255… ragelink 222
c588255… ragelink 223 def test_edit_denied_for_writer(self, writer_client, sample_project, protection_rule):
c588255… ragelink 224 response = writer_client.post(
c588255… ragelink 225 f"/projects/{sample_project.slug}/fossil/branches/protect/{protection_rule.pk}/edit/",
c588255… ragelink 226 {"branch_pattern": "evil-branch"},
c588255… ragelink 227 )
c588255… ragelink 228 assert response.status_code == 403
c588255… ragelink 229
c588255… ragelink 230 def test_edit_nonexistent_rule(self, admin_client, sample_project, fossil_repo_obj):
c588255… ragelink 231 response = admin_client.get(f"/projects/{sample_project.slug}/fossil/branches/protect/99999/edit/")
c588255… ragelink 232 assert response.status_code == 404
c588255… ragelink 233
c588255… ragelink 234
c588255… ragelink 235 # --- Branch Protection Delete View Tests ---
c588255… ragelink 236
c588255… ragelink 237
c588255… ragelink 238 @pytest.mark.django_db
c588255… ragelink 239 class TestBranchProtectionDeleteView:
c588255… ragelink 240 def test_delete_rule(self, admin_client, sample_project, protection_rule):
c588255… ragelink 241 response = admin_client.post(f"/projects/{sample_project.slug}/fossil/branches/protect/{protection_rule.pk}/delete/")
c588255… ragelink 242 assert response.status_code == 302
c588255… ragelink 243 protection_rule.refresh_from_db()
c588255… ragelink 244 assert protection_rule.is_deleted
c588255… ragelink 245
c588255… ragelink 246 def test_delete_get_redirects(self, admin_client, sample_project, protection_rule):
c588255… ragelink 247 response = admin_client.get(f"/projects/{sample_project.slug}/fossil/branches/protect/{protection_rule.pk}/delete/")
c588255… ragelink 248 assert response.status_code == 302 # GET redirects to list
c588255… ragelink 249
c588255… ragelink 250 def test_delete_denied_for_writer(self, writer_client, sample_project, protection_rule):
c588255… ragelink 251 response = writer_client.post(f"/projects/{sample_project.slug}/fossil/branches/protect/{protection_rule.pk}/delete/")
c588255… ragelink 252 assert response.status_code == 403
c588255… ragelink 253
c588255… ragelink 254 def test_delete_nonexistent_rule(self, admin_client, sample_project, fossil_repo_obj):
c588255… ragelink 255 response = admin_client.post(f"/projects/{sample_project.slug}/fossil/branches/protect/99999/delete/")
c588255… ragelink 256 assert response.status_code == 404

Keyboard Shortcuts

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