FossilRepo

fossilrepo / tests / test_repo_lifecycle.py
Blame History Raw 302 lines
1
"""Tests for the repository lifecycle UI: project creation with repo source, and repo settings."""
2
3
from unittest.mock import MagicMock, patch
4
5
import pytest
6
from django.contrib.auth.models import User
7
from django.test import Client
8
9
from fossil.models import FossilRepository
10
from organization.models import Team
11
from projects.models import Project, ProjectTeam
12
13
14
@pytest.fixture
15
def fossil_repo_obj(sample_project):
16
"""Return the auto-created FossilRepository for sample_project."""
17
return FossilRepository.objects.get(project=sample_project, deleted_at__isnull=True)
18
19
20
@pytest.fixture
21
def writer_user(db, admin_user, sample_project):
22
"""User with write access but not admin."""
23
writer = User.objects.create_user(username="writer", password="testpass123")
24
team = Team.objects.create(name="Writers", organization=sample_project.organization, created_by=admin_user)
25
team.members.add(writer)
26
ProjectTeam.objects.create(project=sample_project, team=team, role="write", created_by=admin_user)
27
return writer
28
29
30
@pytest.fixture
31
def writer_client(writer_user):
32
client = Client()
33
client.login(username="writer", password="testpass123")
34
return client
35
36
37
@pytest.fixture
38
def admin_team_user(db, admin_user, sample_project):
39
"""User with admin team role on the sample project."""
40
admin_team_member = User.objects.create_user(username="projadmin", password="testpass123")
41
team = Team.objects.create(name="Admins", organization=sample_project.organization, created_by=admin_user)
42
team.members.add(admin_team_member)
43
ProjectTeam.objects.create(project=sample_project, team=team, role="admin", created_by=admin_user)
44
return admin_team_member
45
46
47
@pytest.fixture
48
def admin_team_client(admin_team_user):
49
client = Client()
50
client.login(username="projadmin", password="testpass123")
51
return client
52
53
54
# --- Project Create Form Tests ---
55
56
57
@pytest.mark.django_db
58
class TestProjectCreateForm:
59
def test_create_form_shows_repo_source(self, admin_client):
60
response = admin_client.get("/projects/create/")
61
assert response.status_code == 200
62
content = response.content.decode()
63
assert "repo_source" in content
64
assert "Create empty repository" in content
65
assert "Clone from Fossil URL" in content
66
67
def test_create_empty_repo(self, admin_client, org):
68
response = admin_client.post(
69
"/projects/create/",
70
{"name": "Empty Repo", "visibility": "private", "repo_source": "empty"},
71
)
72
assert response.status_code == 302
73
project = Project.objects.get(name="Empty Repo")
74
assert project is not None
75
fossil_repo = FossilRepository.objects.get(project=project)
76
assert fossil_repo.filename == f"{project.slug}.fossil"
77
assert fossil_repo.remote_url == ""
78
79
def test_create_with_missing_clone_url_fails(self, admin_client, org):
80
response = admin_client.post(
81
"/projects/create/",
82
{"name": "Clone Fail", "visibility": "private", "repo_source": "fossil_url", "clone_url": ""},
83
)
84
# Form should re-render with errors, not redirect
85
assert response.status_code == 200
86
content = response.content.decode()
87
assert "Clone URL is required" in content
88
89
@patch("projects.views._clone_fossil_repo")
90
def test_create_clone_calls_helper(self, mock_clone, admin_client, org):
91
response = admin_client.post(
92
"/projects/create/",
93
{
94
"name": "Cloned Repo",
95
"visibility": "private",
96
"repo_source": "fossil_url",
97
"clone_url": "https://fossil-scm.org/home",
98
},
99
)
100
assert response.status_code == 302
101
project = Project.objects.get(name="Cloned Repo")
102
mock_clone.assert_called_once()
103
call_args = mock_clone.call_args
104
assert call_args[0][1] == project
105
assert call_args[0][2] == "https://fossil-scm.org/home"
106
107
def test_create_without_repo_source_defaults_to_empty(self, admin_client, org):
108
response = admin_client.post(
109
"/projects/create/",
110
{"name": "Default Source", "visibility": "private"},
111
)
112
assert response.status_code == 302
113
project = Project.objects.get(name="Default Source")
114
fossil_repo = FossilRepository.objects.get(project=project)
115
assert fossil_repo.remote_url == ""
116
117
def test_edit_form_does_not_show_repo_source(self, admin_client, sample_project):
118
response = admin_client.get(f"/projects/{sample_project.slug}/edit/")
119
assert response.status_code == 200
120
content = response.content.decode()
121
assert "Repository Source" not in content
122
123
124
# --- Project Update Form Tests (no repo source fields) ---
125
126
127
@pytest.mark.django_db
128
class TestProjectUpdateExcludesRepoSource:
129
def test_update_preserves_project(self, admin_client, sample_project):
130
response = admin_client.post(
131
f"/projects/{sample_project.slug}/edit/",
132
{"name": "Updated Name", "visibility": "public"},
133
)
134
assert response.status_code == 302
135
sample_project.refresh_from_db()
136
assert sample_project.name == "Updated Name"
137
138
139
# --- Repo Settings View Tests ---
140
141
142
@pytest.mark.django_db
143
class TestRepoSettingsAccess:
144
def test_settings_denied_for_anon(self, client, sample_project, fossil_repo_obj):
145
response = client.get(f"/projects/{sample_project.slug}/fossil/settings/")
146
# Redirects to login for anon
147
assert response.status_code == 302
148
149
def test_settings_denied_for_writer(self, writer_client, sample_project, fossil_repo_obj):
150
response = writer_client.get(f"/projects/{sample_project.slug}/fossil/settings/")
151
assert response.status_code == 403
152
153
def test_settings_allowed_for_superuser(self, admin_client, sample_project, fossil_repo_obj):
154
response = admin_client.get(f"/projects/{sample_project.slug}/fossil/settings/")
155
assert response.status_code == 200
156
157
def test_settings_allowed_for_project_admin(self, admin_team_client, sample_project, fossil_repo_obj):
158
response = admin_team_client.get(f"/projects/{sample_project.slug}/fossil/settings/")
159
assert response.status_code == 200
160
161
162
@pytest.mark.django_db
163
class TestRepoSettingsContent:
164
def test_settings_page_shows_filename(self, admin_client, sample_project, fossil_repo_obj):
165
response = admin_client.get(f"/projects/{sample_project.slug}/fossil/settings/")
166
content = response.content.decode()
167
assert fossil_repo_obj.filename in content
168
169
def test_settings_page_shows_remote_form(self, admin_client, sample_project, fossil_repo_obj):
170
response = admin_client.get(f"/projects/{sample_project.slug}/fossil/settings/")
171
content = response.content.decode()
172
assert 'name="remote_url"' in content
173
assert "Save Remote" in content
174
175
def test_settings_page_shows_clone_urls(self, admin_client, sample_project, fossil_repo_obj):
176
response = admin_client.get(f"/projects/{sample_project.slug}/fossil/settings/")
177
content = response.content.decode()
178
assert "Clone URLs" in content
179
180
def test_settings_page_shows_danger_zone(self, admin_client, sample_project, fossil_repo_obj):
181
response = admin_client.get(f"/projects/{sample_project.slug}/fossil/settings/")
182
content = response.content.decode()
183
assert "Danger Zone" in content
184
185
def test_settings_active_tab(self, admin_client, sample_project, fossil_repo_obj):
186
response = admin_client.get(f"/projects/{sample_project.slug}/fossil/settings/")
187
content = response.content.decode()
188
# The Settings tab should be active (has the active CSS classes)
189
assert "Settings" in content
190
191
192
@pytest.mark.django_db
193
class TestRepoSettingsActions:
194
def test_update_remote_url(self, admin_client, sample_project, fossil_repo_obj):
195
response = admin_client.post(
196
f"/projects/{sample_project.slug}/fossil/settings/",
197
{"action": "update_remote", "remote_url": "https://fossil-scm.org/home"},
198
)
199
assert response.status_code == 302
200
fossil_repo_obj.refresh_from_db()
201
assert fossil_repo_obj.remote_url == "https://fossil-scm.org/home"
202
203
def test_clear_remote_url(self, admin_client, sample_project, fossil_repo_obj):
204
fossil_repo_obj.remote_url = "https://old-url.example.com"
205
fossil_repo_obj.save()
206
response = admin_client.post(
207
f"/projects/{sample_project.slug}/fossil/settings/",
208
{"action": "update_remote", "remote_url": ""},
209
)
210
assert response.status_code == 302
211
fossil_repo_obj.refresh_from_db()
212
assert fossil_repo_obj.remote_url == ""
213
214
def test_update_remote_denied_for_writer(self, writer_client, sample_project, fossil_repo_obj):
215
response = writer_client.post(
216
f"/projects/{sample_project.slug}/fossil/settings/",
217
{"action": "update_remote", "remote_url": "https://evil.example.com"},
218
)
219
assert response.status_code == 403
220
fossil_repo_obj.refresh_from_db()
221
assert fossil_repo_obj.remote_url != "https://evil.example.com"
222
223
224
# --- Nav Tab Tests ---
225
226
227
@pytest.mark.django_db
228
class TestProjectNavSettings:
229
def test_settings_tab_visible_for_admin(self, admin_client, sample_project, fossil_repo_obj):
230
"""The Settings tab should appear in the nav for admins on fossil views."""
231
response = admin_client.get(f"/projects/{sample_project.slug}/fossil/settings/")
232
content = response.content.decode()
233
assert "Settings" in content
234
assert f"/projects/{sample_project.slug}/fossil/settings/" in content
235
236
237
# --- Signal Guard Tests ---
238
239
240
@pytest.mark.django_db
241
class TestSignalExistingFileGuard:
242
@patch("fossil.cli.FossilCLI")
243
def test_signal_skips_init_when_file_exists(self, mock_cli_cls, org, admin_user, tmp_path):
244
"""When a .fossil file already exists, the signal should skip fossil init."""
245
mock_cli = MagicMock()
246
mock_cli.is_available.return_value = True
247
mock_cli_cls.return_value = mock_cli
248
249
# Create the project -- the signal fires
250
project = Project.objects.create(name="Pre-existing", organization=org, created_by=admin_user)
251
252
# The signal creates a FossilRepository record. Since the .fossil file won't exist
253
# on disk in tests (no real FOSSIL_DATA_DIR), the signal will attempt init via CLI.
254
# The key assertion is that the record was created and the code path doesn't crash.
255
assert FossilRepository.objects.filter(project=project).exists()
256
257
def test_signal_creates_repo_record(self, org, admin_user):
258
"""The signal creates a FossilRepository record when a Project is created."""
259
project = Project.objects.create(name="Signal Test", organization=org, created_by=admin_user)
260
assert FossilRepository.objects.filter(project=project).exists()
261
fossil_repo = FossilRepository.objects.get(project=project)
262
assert fossil_repo.filename == f"{project.slug}.fossil"
263
264
265
# --- Form Validation Tests ---
266
267
268
@pytest.mark.django_db
269
class TestProjectFormValidation:
270
def test_form_valid_with_empty_source(self):
271
from projects.forms import ProjectForm
272
273
form = ProjectForm(data={"name": "Test", "visibility": "private", "repo_source": "empty"})
274
assert form.is_valid()
275
276
def test_form_valid_with_clone_url(self):
277
from projects.forms import ProjectForm
278
279
form = ProjectForm(
280
data={
281
"name": "Test Clone",
282
"visibility": "private",
283
"repo_source": "fossil_url",
284
"clone_url": "https://fossil-scm.org/home",
285
}
286
)
287
assert form.is_valid()
288
289
def test_form_invalid_clone_without_url(self):
290
from projects.forms import ProjectForm
291
292
form = ProjectForm(
293
data={
294
"name": "No URL",
295
"visibility": "private",
296
"repo_source": "fossil_url",
297
"clone_url": "",
298
}
299
)
300
assert not form.is_valid()
301
assert "clone_url" in form.errors
302

Keyboard Shortcuts

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