FossilRepo

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

Keyboard Shortcuts

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