FossilRepo

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

Keyboard Shortcuts

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