FossilRepo

fossilrepo / tests / test_project_groups.py
Blame History Raw 348 lines
1
"""Tests for Project Groups: model, views, sidebar context, and permissions."""
2
3
import pytest
4
from django.contrib.auth.models import Group, Permission
5
from django.test import Client
6
7
from projects.models import Project, ProjectGroup
8
9
10
@pytest.fixture
11
def sample_group(db, admin_user):
12
return ProjectGroup.objects.create(name="Fossil SCM", description="The Fossil repos", created_by=admin_user)
13
14
15
@pytest.fixture
16
def editor_user(db):
17
"""User with full project and projectgroup permissions (add/change/delete/view) but not superuser."""
18
user = __import__("django.contrib.auth.models", fromlist=["User"]).User.objects.create_user(
19
username="editor", email="[email protected]", password="testpass123"
20
)
21
group, _ = Group.objects.get_or_create(name="Editors")
22
perms = Permission.objects.filter(
23
content_type__app_label="projects",
24
)
25
group.permissions.set(perms)
26
user.groups.add(group)
27
return user
28
29
30
@pytest.fixture
31
def editor_client(editor_user):
32
client = Client()
33
client.login(username="editor", password="testpass123")
34
return client
35
36
37
# --- Model Tests ---
38
39
40
@pytest.mark.django_db
41
class TestProjectGroupModel:
42
def test_create_group(self, admin_user):
43
group = ProjectGroup.objects.create(name="Test Group", created_by=admin_user)
44
assert group.slug == "test-group"
45
assert str(group) == "Test Group"
46
assert group.guid is not None
47
48
def test_slug_auto_generated(self, admin_user):
49
group = ProjectGroup.objects.create(name="My Cool Group", created_by=admin_user)
50
assert group.slug == "my-cool-group"
51
52
def test_slug_uniqueness(self, admin_user):
53
ProjectGroup.objects.create(name="Dupes", created_by=admin_user)
54
g2 = ProjectGroup.objects.create(name="Dupes", created_by=admin_user)
55
assert g2.slug == "dupes-1"
56
57
def test_soft_delete(self, admin_user):
58
group = ProjectGroup.objects.create(name="Deletable", created_by=admin_user)
59
group.soft_delete(user=admin_user)
60
assert group.is_deleted
61
assert ProjectGroup.objects.filter(name="Deletable").count() == 0
62
assert ProjectGroup.all_objects.filter(name="Deletable").count() == 1
63
64
def test_project_group_fk(self, admin_user, org, sample_group):
65
project = Project.objects.create(name="Grouped Project", organization=org, group=sample_group, created_by=admin_user)
66
assert project.group == sample_group
67
assert project in sample_group.projects.all()
68
69
def test_project_group_nullable(self, admin_user, org):
70
project = Project.objects.create(name="Ungrouped", organization=org, created_by=admin_user)
71
assert project.group is None
72
73
def test_group_deletion_sets_null(self, admin_user, org, sample_group):
74
project = Project.objects.create(name="Will Unlink", organization=org, group=sample_group, created_by=admin_user)
75
sample_group.delete()
76
project.refresh_from_db()
77
assert project.group is None
78
79
80
# --- View Tests: Group List ---
81
82
83
@pytest.mark.django_db
84
class TestGroupListView:
85
def test_list_allowed_for_superuser(self, admin_client, sample_group):
86
response = admin_client.get("/projects/groups/")
87
assert response.status_code == 200
88
assert "Fossil SCM" in response.content.decode()
89
90
def test_list_allowed_for_viewer(self, viewer_client, sample_group):
91
response = viewer_client.get("/projects/groups/")
92
assert response.status_code == 200
93
94
def test_list_denied_for_no_perm(self, no_perm_client):
95
response = no_perm_client.get("/projects/groups/")
96
assert response.status_code == 403
97
98
def test_list_denied_for_anon(self, client):
99
response = client.get("/projects/groups/")
100
assert response.status_code == 302
101
102
def test_list_shows_project_count(self, admin_client, admin_user, org, sample_group):
103
Project.objects.create(name="P1", organization=org, group=sample_group, created_by=admin_user)
104
Project.objects.create(name="P2", organization=org, group=sample_group, created_by=admin_user)
105
response = admin_client.get("/projects/groups/")
106
content = response.content.decode()
107
assert "Fossil SCM" in content
108
109
def test_list_htmx_returns_partial(self, admin_client, sample_group):
110
response = admin_client.get("/projects/groups/", HTTP_HX_REQUEST="true")
111
assert response.status_code == 200
112
content = response.content.decode()
113
assert "Fossil SCM" in content
114
# Partial should not have full page structure
115
assert "<!DOCTYPE html>" not in content
116
117
118
# --- View Tests: Group Create ---
119
120
121
@pytest.mark.django_db
122
class TestGroupCreateView:
123
def test_create_get_allowed_for_superuser(self, admin_client):
124
response = admin_client.get("/projects/groups/create/")
125
assert response.status_code == 200
126
127
def test_create_get_allowed_for_editor(self, editor_client):
128
response = editor_client.get("/projects/groups/create/")
129
assert response.status_code == 200
130
131
def test_create_denied_for_viewer(self, viewer_client):
132
response = viewer_client.get("/projects/groups/create/")
133
assert response.status_code == 403
134
135
def test_create_denied_for_no_perm(self, no_perm_client):
136
response = no_perm_client.get("/projects/groups/create/")
137
assert response.status_code == 403
138
139
def test_create_denied_for_anon(self, client):
140
response = client.get("/projects/groups/create/")
141
assert response.status_code == 302
142
143
def test_create_saves_group(self, admin_client, admin_user):
144
response = admin_client.post("/projects/groups/create/", {"name": "New Group", "description": "Desc"})
145
assert response.status_code == 302
146
group = ProjectGroup.objects.get(name="New Group")
147
assert group.description == "Desc"
148
assert group.created_by == admin_user
149
150
def test_create_redirects_to_detail(self, admin_client):
151
response = admin_client.post("/projects/groups/create/", {"name": "Redirect Test"})
152
assert response.status_code == 302
153
group = ProjectGroup.objects.get(name="Redirect Test")
154
assert response.url == f"/projects/groups/{group.slug}/"
155
156
def test_create_requires_name(self, admin_client):
157
response = admin_client.post("/projects/groups/create/", {"name": "", "description": "No name"})
158
assert response.status_code == 200 # Re-renders form
159
160
161
# --- View Tests: Group Detail ---
162
163
164
@pytest.mark.django_db
165
class TestGroupDetailView:
166
def test_detail_allowed_for_superuser(self, admin_client, sample_group):
167
response = admin_client.get(f"/projects/groups/{sample_group.slug}/")
168
assert response.status_code == 200
169
assert "Fossil SCM" in response.content.decode()
170
171
def test_detail_allowed_for_viewer(self, viewer_client, sample_group):
172
response = viewer_client.get(f"/projects/groups/{sample_group.slug}/")
173
assert response.status_code == 200
174
175
def test_detail_denied_for_no_perm(self, no_perm_client, sample_group):
176
response = no_perm_client.get(f"/projects/groups/{sample_group.slug}/")
177
assert response.status_code == 403
178
179
def test_detail_denied_for_anon(self, client, sample_group):
180
response = client.get(f"/projects/groups/{sample_group.slug}/")
181
assert response.status_code == 302
182
183
def test_detail_shows_member_projects(self, admin_client, admin_user, org, sample_group):
184
Project.objects.create(name="Fossil Source", organization=org, group=sample_group, created_by=admin_user)
185
response = admin_client.get(f"/projects/groups/{sample_group.slug}/")
186
assert "Fossil Source" in response.content.decode()
187
188
def test_detail_404_for_deleted_group(self, admin_client, admin_user, sample_group):
189
sample_group.soft_delete(user=admin_user)
190
response = admin_client.get(f"/projects/groups/{sample_group.slug}/")
191
assert response.status_code == 404
192
193
194
# --- View Tests: Group Edit ---
195
196
197
@pytest.mark.django_db
198
class TestGroupEditView:
199
def test_edit_get_allowed_for_superuser(self, admin_client, sample_group):
200
response = admin_client.get(f"/projects/groups/{sample_group.slug}/edit/")
201
assert response.status_code == 200
202
203
def test_edit_get_allowed_for_editor(self, editor_client, sample_group):
204
response = editor_client.get(f"/projects/groups/{sample_group.slug}/edit/")
205
assert response.status_code == 200
206
207
def test_edit_denied_for_viewer(self, viewer_client, sample_group):
208
response = viewer_client.get(f"/projects/groups/{sample_group.slug}/edit/")
209
assert response.status_code == 403
210
211
def test_edit_denied_for_no_perm(self, no_perm_client, sample_group):
212
response = no_perm_client.get(f"/projects/groups/{sample_group.slug}/edit/")
213
assert response.status_code == 403
214
215
def test_edit_saves_changes(self, admin_client, admin_user, sample_group):
216
response = admin_client.post(
217
f"/projects/groups/{sample_group.slug}/edit/",
218
{"name": "Fossil SCM Updated", "description": "Updated desc"},
219
)
220
assert response.status_code == 302
221
sample_group.refresh_from_db()
222
assert sample_group.name == "Fossil SCM Updated"
223
assert sample_group.description == "Updated desc"
224
assert sample_group.updated_by == admin_user
225
226
227
# --- View Tests: Group Delete ---
228
229
230
@pytest.mark.django_db
231
class TestGroupDeleteView:
232
def test_delete_get_shows_confirmation(self, admin_client, sample_group):
233
response = admin_client.get(f"/projects/groups/{sample_group.slug}/delete/")
234
assert response.status_code == 200
235
assert "Fossil SCM" in response.content.decode()
236
assert "Delete" in response.content.decode()
237
238
def test_delete_denied_for_viewer(self, viewer_client, sample_group):
239
response = viewer_client.post(f"/projects/groups/{sample_group.slug}/delete/")
240
assert response.status_code == 403
241
242
def test_delete_denied_for_no_perm(self, no_perm_client, sample_group):
243
response = no_perm_client.post(f"/projects/groups/{sample_group.slug}/delete/")
244
assert response.status_code == 403
245
246
def test_delete_soft_deletes_group(self, admin_client, admin_user, sample_group):
247
response = admin_client.post(f"/projects/groups/{sample_group.slug}/delete/")
248
assert response.status_code == 302
249
sample_group.refresh_from_db()
250
assert sample_group.is_deleted
251
252
def test_delete_unlinks_projects(self, admin_client, admin_user, org, sample_group):
253
project = Project.objects.create(name="Linked", organization=org, group=sample_group, created_by=admin_user)
254
admin_client.post(f"/projects/groups/{sample_group.slug}/delete/")
255
project.refresh_from_db()
256
assert project.group is None
257
assert not project.is_deleted # Project survives
258
259
def test_delete_htmx_redirect(self, admin_client, sample_group):
260
response = admin_client.post(f"/projects/groups/{sample_group.slug}/delete/", HTTP_HX_REQUEST="true")
261
assert response.status_code == 200
262
assert response["HX-Redirect"] == "/projects/groups/"
263
264
265
# --- Form Tests ---
266
267
268
@pytest.mark.django_db
269
class TestProjectGroupForm:
270
def test_form_valid(self):
271
from projects.forms import ProjectGroupForm
272
273
form = ProjectGroupForm(data={"name": "Test Group", "description": "A group"})
274
assert form.is_valid()
275
276
def test_form_valid_without_description(self):
277
from projects.forms import ProjectGroupForm
278
279
form = ProjectGroupForm(data={"name": "Test Group", "description": ""})
280
assert form.is_valid()
281
282
def test_form_invalid_without_name(self):
283
from projects.forms import ProjectGroupForm
284
285
form = ProjectGroupForm(data={"name": "", "description": "No name"})
286
assert not form.is_valid()
287
assert "name" in form.errors
288
289
290
# --- ProjectForm group field ---
291
292
293
@pytest.mark.django_db
294
class TestProjectFormGroupField:
295
def test_project_form_includes_group(self, sample_group):
296
from projects.forms import ProjectForm
297
298
form = ProjectForm(data={"name": "Test", "visibility": "private", "group": sample_group.pk})
299
assert form.is_valid()
300
project_data = form.cleaned_data
301
assert project_data["group"] == sample_group
302
303
def test_project_form_group_optional(self):
304
from projects.forms import ProjectForm
305
306
form = ProjectForm(data={"name": "No Group", "visibility": "private"})
307
assert form.is_valid()
308
assert form.cleaned_data["group"] is None
309
310
def test_project_create_with_group(self, admin_client, org, sample_group):
311
response = admin_client.post(
312
"/projects/create/",
313
{"name": "Grouped Via Form", "visibility": "private", "group": sample_group.pk},
314
)
315
assert response.status_code == 302
316
project = Project.objects.get(name="Grouped Via Form")
317
assert project.group == sample_group
318
319
320
# --- Context Processor Tests ---
321
322
323
@pytest.mark.django_db
324
class TestSidebarContext:
325
def test_grouped_projects_in_context(self, admin_client, admin_user, org, sample_group):
326
Project.objects.create(name="Grouped P", organization=org, group=sample_group, created_by=admin_user)
327
Project.objects.create(name="Ungrouped P", organization=org, created_by=admin_user)
328
response = admin_client.get("/dashboard/")
329
assert response.status_code == 200
330
context = response.context
331
assert "sidebar_grouped" in context
332
assert "sidebar_ungrouped" in context
333
assert len(context["sidebar_grouped"]) == 1
334
assert context["sidebar_grouped"][0]["group"].name == "Fossil SCM"
335
ungrouped_names = [p.name for p in context["sidebar_ungrouped"]]
336
assert "Ungrouped P" in ungrouped_names
337
338
def test_empty_group_not_in_sidebar(self, admin_client, sample_group):
339
"""A group with no projects should not appear in sidebar_grouped."""
340
response = admin_client.get("/dashboard/")
341
context = response.context
342
assert len(context["sidebar_grouped"]) == 0
343
344
def test_unauthenticated_gets_empty_context(self, client):
345
response = client.get("/dashboard/")
346
# Redirects to login, but if we could check context it would be empty
347
assert response.status_code == 302
348

Keyboard Shortcuts

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