FossilRepo

fossilrepo / tests / test_views_coverage.py
Source Blame History 2089 lines
254b467… ragelink 1 """Tests for fossil/views.py -- covering uncovered view functions and helpers.
254b467… ragelink 2
254b467… ragelink 3 Focuses on views that can be tested by mocking FossilReader (so no real
254b467… ragelink 4 .fossil file is needed) and pure Django CRUD views that don't touch Fossil.
254b467… ragelink 5 """
254b467… ragelink 6
254b467… ragelink 7 from datetime import UTC, datetime
254b467… ragelink 8 from types import SimpleNamespace
254b467… ragelink 9 from unittest.mock import MagicMock, patch
254b467… ragelink 10
254b467… ragelink 11 import pytest
254b467… ragelink 12 from django.contrib.auth.models import User
254b467… ragelink 13 from django.test import Client
254b467… ragelink 14
254b467… ragelink 15 from fossil.models import FossilRepository
254b467… ragelink 16 from fossil.reader import (
254b467… ragelink 17 CheckinDetail,
254b467… ragelink 18 FileEntry,
254b467… ragelink 19 RepoMetadata,
254b467… ragelink 20 TicketEntry,
254b467… ragelink 21 TimelineEntry,
254b467… ragelink 22 WikiPage,
254b467… ragelink 23 )
254b467… ragelink 24 from organization.models import Team
254b467… ragelink 25 from projects.models import ProjectTeam
254b467… ragelink 26
254b467… ragelink 27 # ---------------------------------------------------------------------------
254b467… ragelink 28 # Shared fixtures
254b467… ragelink 29 # ---------------------------------------------------------------------------
254b467… ragelink 30
254b467… ragelink 31
254b467… ragelink 32 @pytest.fixture
254b467… ragelink 33 def fossil_repo_obj(sample_project):
254b467… ragelink 34 return FossilRepository.objects.get(project=sample_project, deleted_at__isnull=True)
254b467… ragelink 35
254b467… ragelink 36
254b467… ragelink 37 @pytest.fixture
254b467… ragelink 38 def writer_user(db, admin_user, sample_project):
254b467… ragelink 39 writer = User.objects.create_user(username="writer_vc", password="testpass123")
254b467… ragelink 40 team = Team.objects.create(name="VC Writers", organization=sample_project.organization, created_by=admin_user)
254b467… ragelink 41 team.members.add(writer)
254b467… ragelink 42 ProjectTeam.objects.create(project=sample_project, team=team, role="write", created_by=admin_user)
254b467… ragelink 43 return writer
254b467… ragelink 44
254b467… ragelink 45
254b467… ragelink 46 @pytest.fixture
254b467… ragelink 47 def writer_client(writer_user):
254b467… ragelink 48 c = Client()
254b467… ragelink 49 c.login(username="writer_vc", password="testpass123")
254b467… ragelink 50 return c
254b467… ragelink 51
254b467… ragelink 52
254b467… ragelink 53 def _url(slug, path):
254b467… ragelink 54 return f"/projects/{slug}/fossil/{path}"
254b467… ragelink 55
254b467… ragelink 56
254b467… ragelink 57 def _mock_reader_ctx(mock_cls, **attrs):
254b467… ragelink 58 """Configure a patched FossilReader class to work as a context manager
254b467… ragelink 59 and attach return values from **attrs to the instance."""
254b467… ragelink 60 instance = mock_cls.return_value
254b467… ragelink 61 instance.__enter__ = MagicMock(return_value=instance)
254b467… ragelink 62 instance.__exit__ = MagicMock(return_value=False)
254b467… ragelink 63 for key, val in attrs.items():
254b467… ragelink 64 setattr(instance, key, MagicMock(return_value=val))
254b467… ragelink 65 return instance
254b467… ragelink 66
254b467… ragelink 67
254b467… ragelink 68 def _make_timeline_entry(**overrides):
254b467… ragelink 69 defaults = {
254b467… ragelink 70 "rid": 1,
254b467… ragelink 71 "uuid": "abc123def456",
254b467… ragelink 72 "event_type": "ci",
254b467… ragelink 73 "timestamp": datetime(2026, 3, 1, 12, 0, 0, tzinfo=UTC),
254b467… ragelink 74 "user": "testuser",
254b467… ragelink 75 "comment": "initial commit",
254b467… ragelink 76 "branch": "trunk",
254b467… ragelink 77 "parent_rid": 0,
254b467… ragelink 78 "is_merge": False,
254b467… ragelink 79 "merge_parent_rids": [],
254b467… ragelink 80 "rail": 0,
254b467… ragelink 81 }
254b467… ragelink 82 defaults.update(overrides)
254b467… ragelink 83 return TimelineEntry(**defaults)
254b467… ragelink 84
254b467… ragelink 85
254b467… ragelink 86 def _make_file_entry(**overrides):
254b467… ragelink 87 defaults = {
254b467… ragelink 88 "name": "README.md",
254b467… ragelink 89 "uuid": "file-uuid-1",
254b467… ragelink 90 "size": 512,
254b467… ragelink 91 "is_dir": False,
254b467… ragelink 92 "last_commit_message": "initial commit",
254b467… ragelink 93 "last_commit_user": "testuser",
254b467… ragelink 94 "last_commit_time": datetime(2026, 3, 1, 12, 0, 0, tzinfo=UTC),
254b467… ragelink 95 }
254b467… ragelink 96 defaults.update(overrides)
254b467… ragelink 97 return FileEntry(**defaults)
254b467… ragelink 98
254b467… ragelink 99
254b467… ragelink 100 # ---------------------------------------------------------------------------
254b467… ragelink 101 # Content rendering helpers (_render_fossil_content, _is_markdown, _rewrite_fossil_links)
254b467… ragelink 102 # ---------------------------------------------------------------------------
254b467… ragelink 103
254b467… ragelink 104
254b467… ragelink 105 class TestRenderFossilContent:
254b467… ragelink 106 """Test the content rendering pipeline that converts Fossil wiki/markdown to HTML."""
254b467… ragelink 107
254b467… ragelink 108 def test_empty_content(self):
254b467… ragelink 109 from fossil.views import _render_fossil_content
254b467… ragelink 110
254b467… ragelink 111 assert _render_fossil_content("") == ""
254b467… ragelink 112
254b467… ragelink 113 def test_markdown_heading(self):
254b467… ragelink 114 from fossil.views import _render_fossil_content
254b467… ragelink 115
254b467… ragelink 116 html = _render_fossil_content("# Hello World")
254b467… ragelink 117 assert "<h1" in html
254b467… ragelink 118 assert "Hello World" in html
254b467… ragelink 119
254b467… ragelink 120 def test_markdown_fenced_code(self):
254b467… ragelink 121 from fossil.views import _render_fossil_content
254b467… ragelink 122
254b467… ragelink 123 content = "```python\nprint('hello')\n```"
254b467… ragelink 124 html = _render_fossil_content(content)
254b467… ragelink 125 assert "print" in html
254b467… ragelink 126
254b467… ragelink 127 def test_fossil_wiki_link_converted(self):
254b467… ragelink 128 from fossil.views import _render_fossil_content
254b467… ragelink 129
254b467… ragelink 130 content = "[/info/abc123 | View Checkin]"
254b467… ragelink 131 html = _render_fossil_content(content, project_slug="my-project")
254b467… ragelink 132 assert "/projects/my-project/fossil/checkin/abc123/" in html
254b467… ragelink 133
254b467… ragelink 134 def test_fossil_wiki_verbatim_block(self):
254b467… ragelink 135 from fossil.views import _render_fossil_content
254b467… ragelink 136
254b467… ragelink 137 content = "<h1>Title</h1>\n<verbatim>code here</verbatim>"
254b467… ragelink 138 html = _render_fossil_content(content)
254b467… ragelink 139 assert "<pre><code>code here</code></pre>" in html
254b467… ragelink 140
254b467… ragelink 141 def test_fossil_wiki_list_bullets(self):
254b467… ragelink 142 from fossil.views import _render_fossil_content
254b467… ragelink 143
254b467… ragelink 144 content = "<p>List:</p>\n* Item one\n* Item two"
254b467… ragelink 145 html = _render_fossil_content(content)
254b467… ragelink 146 assert "<ul>" in html
254b467… ragelink 147 assert "<li>" in html
254b467… ragelink 148 assert "Item one" in html
254b467… ragelink 149
254b467… ragelink 150 def test_fossil_wiki_ordered_list(self):
254b467… ragelink 151 from fossil.views import _render_fossil_content
254b467… ragelink 152
254b467… ragelink 153 # Must start with an HTML element so _is_markdown returns False
254b467… ragelink 154 content = "<p>Steps:</p>\n1. Step one\n2. Step two"
254b467… ragelink 155 html = _render_fossil_content(content)
254b467… ragelink 156 assert "<ol>" in html
254b467… ragelink 157 assert "Step one" in html
254b467… ragelink 158
254b467… ragelink 159 def test_fossil_wiki_nowiki_block(self):
254b467… ragelink 160 from fossil.views import _render_fossil_content
254b467… ragelink 161
254b467… ragelink 162 content = "<p>Before</p>\n<nowiki><b>Bold</b></nowiki>"
254b467… ragelink 163 html = _render_fossil_content(content)
254b467… ragelink 164 assert "<b>Bold</b>" in html
254b467… ragelink 165
254b467… ragelink 166 def test_fossil_interwiki_link(self):
254b467… ragelink 167 from fossil.views import _render_fossil_content
254b467… ragelink 168
254b467… ragelink 169 content = "<p>See [wikipedia:Fossil_(software)]</p>"
254b467… ragelink 170 html = _render_fossil_content(content)
254b467… ragelink 171 assert "en.wikipedia.org/wiki/Fossil_(software)" in html
254b467… ragelink 172
254b467… ragelink 173 def test_fossil_anchor_link(self):
254b467… ragelink 174 from fossil.views import _render_fossil_content
254b467… ragelink 175
254b467… ragelink 176 content = "<p>Jump to [#section1]</p>"
254b467… ragelink 177 html = _render_fossil_content(content)
254b467… ragelink 178 assert 'href="#section1"' in html
254b467… ragelink 179
254b467… ragelink 180 def test_fossil_bare_wiki_link(self):
254b467… ragelink 181 from fossil.views import _render_fossil_content
254b467… ragelink 182
254b467… ragelink 183 content = "<p>See [PageName]</p>"
254b467… ragelink 184 html = _render_fossil_content(content)
254b467… ragelink 185 assert 'href="PageName"' in html
254b467… ragelink 186
254b467… ragelink 187 def test_markdown_fossil_link_resolved(self):
254b467… ragelink 188 from fossil.views import _render_fossil_content
254b467… ragelink 189
254b467… ragelink 190 content = "# Page\n\n[./file.wiki | Link Text]"
254b467… ragelink 191 html = _render_fossil_content(content, project_slug="proj", base_path="www/")
254b467… ragelink 192 assert "Link Text" in html
254b467… ragelink 193
254b467… ragelink 194
254b467… ragelink 195 class TestIsMarkdown:
254b467… ragelink 196 def test_heading_detected(self):
254b467… ragelink 197 from fossil.views import _is_markdown
254b467… ragelink 198
254b467… ragelink 199 assert _is_markdown("# Title\nSome text") is True
254b467… ragelink 200
254b467… ragelink 201 def test_fenced_code_detected(self):
254b467… ragelink 202 from fossil.views import _is_markdown
254b467… ragelink 203
254b467… ragelink 204 assert _is_markdown("Some text\n```\ncode\n```") is True
254b467… ragelink 205
254b467… ragelink 206 def test_html_start_not_markdown(self):
254b467… ragelink 207 from fossil.views import _is_markdown
254b467… ragelink 208
254b467… ragelink 209 assert _is_markdown("<h1>Title</h1>\n<p>Paragraph</p>") is False
254b467… ragelink 210
254b467… ragelink 211 def test_multiple_markdown_headings(self):
254b467… ragelink 212 from fossil.views import _is_markdown
254b467… ragelink 213
254b467… ragelink 214 content = "Some text\n## Heading\n## Another"
254b467… ragelink 215 assert _is_markdown(content) is True
254b467… ragelink 216
254b467… ragelink 217 def test_plain_text_is_markdown(self):
254b467… ragelink 218 from fossil.views import _is_markdown
254b467… ragelink 219
254b467… ragelink 220 # Plain text without HTML tags defaults to markdown
254b467… ragelink 221 assert _is_markdown("Just plain text") is True
254b467… ragelink 222
254b467… ragelink 223
254b467… ragelink 224 class TestRewriteFossilLinks:
254b467… ragelink 225 def test_info_hash_rewrite(self):
254b467… ragelink 226 from fossil.views import _rewrite_fossil_links
254b467… ragelink 227
254b467… ragelink 228 html = '<a href="/info/abc123">link</a>'
254b467… ragelink 229 result = _rewrite_fossil_links(html, "myproj")
254b467… ragelink 230 assert "/projects/myproj/fossil/checkin/abc123/" in result
254b467… ragelink 231
254b467… ragelink 232 def test_doc_trunk_rewrite(self):
254b467… ragelink 233 from fossil.views import _rewrite_fossil_links
254b467… ragelink 234
254b467… ragelink 235 html = '<a href="/doc/trunk/www/readme.wiki">docs</a>'
254b467… ragelink 236 result = _rewrite_fossil_links(html, "myproj")
254b467… ragelink 237 assert "/projects/myproj/fossil/code/file/www/readme.wiki" in result
254b467… ragelink 238
254b467… ragelink 239 def test_wiki_path_rewrite(self):
254b467… ragelink 240 from fossil.views import _rewrite_fossil_links
254b467… ragelink 241
254b467… ragelink 242 html = '<a href="/wiki/HomePage">home</a>'
254b467… ragelink 243 result = _rewrite_fossil_links(html, "myproj")
254b467… ragelink 244 assert "/projects/myproj/fossil/wiki/page/HomePage" in result
254b467… ragelink 245
254b467… ragelink 246 def test_wiki_query_rewrite(self):
254b467… ragelink 247 from fossil.views import _rewrite_fossil_links
254b467… ragelink 248
254b467… ragelink 249 html = '<a href="/wiki?name=HomePage">home</a>'
254b467… ragelink 250 result = _rewrite_fossil_links(html, "myproj")
254b467… ragelink 251 assert "/projects/myproj/fossil/wiki/page/HomePage" in result
254b467… ragelink 252
254b467… ragelink 253 def test_tktview_rewrite(self):
254b467… ragelink 254 from fossil.views import _rewrite_fossil_links
254b467… ragelink 255
254b467… ragelink 256 html = '<a href="/tktview/abc123">ticket</a>'
254b467… ragelink 257 result = _rewrite_fossil_links(html, "myproj")
254b467… ragelink 258 assert "/projects/myproj/fossil/tickets/abc123/" in result
254b467… ragelink 259
254b467… ragelink 260 def test_vdiff_rewrite(self):
254b467… ragelink 261 from fossil.views import _rewrite_fossil_links
254b467… ragelink 262
254b467… ragelink 263 html = '<a href="/vdiff?from=aaa&to=bbb">diff</a>'
254b467… ragelink 264 result = _rewrite_fossil_links(html, "myproj")
254b467… ragelink 265 assert "/projects/myproj/fossil/compare/?from=aaa&to=bbb" in result
254b467… ragelink 266
254b467… ragelink 267 def test_timeline_rewrite(self):
254b467… ragelink 268 from fossil.views import _rewrite_fossil_links
254b467… ragelink 269
254b467… ragelink 270 html = '<a href="/timeline?n=20">tl</a>'
254b467… ragelink 271 result = _rewrite_fossil_links(html, "myproj")
254b467… ragelink 272 assert "/projects/myproj/fossil/timeline/" in result
254b467… ragelink 273
254b467… ragelink 274 def test_forumpost_rewrite(self):
254b467… ragelink 275 from fossil.views import _rewrite_fossil_links
254b467… ragelink 276
254b467… ragelink 277 html = '<a href="/forumpost/abc123">post</a>'
254b467… ragelink 278 result = _rewrite_fossil_links(html, "myproj")
254b467… ragelink 279 assert "/projects/myproj/fossil/forum/abc123/" in result
254b467… ragelink 280
254b467… ragelink 281 def test_forum_base_rewrite(self):
254b467… ragelink 282 from fossil.views import _rewrite_fossil_links
254b467… ragelink 283
254b467… ragelink 284 html = '<a href="/forum">forum</a>'
254b467… ragelink 285 result = _rewrite_fossil_links(html, "myproj")
254b467… ragelink 286 assert "/projects/myproj/fossil/forum/" in result
254b467… ragelink 287
254b467… ragelink 288 def test_www_path_rewrite(self):
254b467… ragelink 289 from fossil.views import _rewrite_fossil_links
254b467… ragelink 290
254b467… ragelink 291 html = '<a href="/www/index.html">page</a>'
254b467… ragelink 292 result = _rewrite_fossil_links(html, "myproj")
254b467… ragelink 293 assert "/projects/myproj/fossil/docs/www/index.html" in result
254b467… ragelink 294
254b467… ragelink 295 def test_dir_rewrite(self):
254b467… ragelink 296 from fossil.views import _rewrite_fossil_links
254b467… ragelink 297
254b467… ragelink 298 html = '<a href="/dir">browse</a>'
254b467… ragelink 299 result = _rewrite_fossil_links(html, "myproj")
254b467… ragelink 300 assert "/projects/myproj/fossil/code/" in result
254b467… ragelink 301
254b467… ragelink 302 def test_help_rewrite(self):
254b467… ragelink 303 from fossil.views import _rewrite_fossil_links
254b467… ragelink 304
254b467… ragelink 305 html = '<a href="/help/clone">help</a>'
254b467… ragelink 306 result = _rewrite_fossil_links(html, "myproj")
254b467… ragelink 307 assert "/projects/myproj/fossil/docs/www/help.wiki" in result
254b467… ragelink 308
254b467… ragelink 309 def test_external_link_preserved(self):
254b467… ragelink 310 from fossil.views import _rewrite_fossil_links
254b467… ragelink 311
254b467… ragelink 312 html = '<a href="https://example.com/page">ext</a>'
254b467… ragelink 313 result = _rewrite_fossil_links(html, "myproj")
254b467… ragelink 314 assert "https://example.com/page" in result
254b467… ragelink 315
254b467… ragelink 316 def test_empty_slug_passthrough(self):
254b467… ragelink 317 from fossil.views import _rewrite_fossil_links
254b467… ragelink 318
254b467… ragelink 319 html = '<a href="/info/abc">link</a>'
254b467… ragelink 320 assert _rewrite_fossil_links(html, "") == html
254b467… ragelink 321
254b467… ragelink 322 def test_scheme_link_info(self):
254b467… ragelink 323 from fossil.views import _rewrite_fossil_links
254b467… ragelink 324
254b467… ragelink 325 html = '<a href="info:abc123">checkin</a>'
254b467… ragelink 326 result = _rewrite_fossil_links(html, "myproj")
254b467… ragelink 327 assert "/projects/myproj/fossil/checkin/abc123/" in result
254b467… ragelink 328
254b467… ragelink 329 def test_scheme_link_wiki(self):
254b467… ragelink 330 from fossil.views import _rewrite_fossil_links
254b467… ragelink 331
254b467… ragelink 332 html = '<a href="wiki:PageName">page</a>'
254b467… ragelink 333 result = _rewrite_fossil_links(html, "myproj")
254b467… ragelink 334 assert "/projects/myproj/fossil/wiki/page/PageName" in result
254b467… ragelink 335
254b467… ragelink 336 def test_builtin_rewrite(self):
254b467… ragelink 337 from fossil.views import _rewrite_fossil_links
254b467… ragelink 338
254b467… ragelink 339 html = '<a href="/builtin/default.css">skin</a>'
254b467… ragelink 340 result = _rewrite_fossil_links(html, "myproj")
254b467… ragelink 341 assert "/projects/myproj/fossil/code/file/skins/default.css" in result
254b467… ragelink 342
254b467… ragelink 343 def test_setup_link_not_rewritten(self):
254b467… ragelink 344 from fossil.views import _rewrite_fossil_links
254b467… ragelink 345
254b467… ragelink 346 html = '<a href="/setup_skin">settings</a>'
254b467… ragelink 347 result = _rewrite_fossil_links(html, "myproj")
254b467… ragelink 348 assert "/setup_skin" in result
254b467… ragelink 349
254b467… ragelink 350 def test_wiki_file_extension_rewrite(self):
254b467… ragelink 351 from fossil.views import _rewrite_fossil_links
254b467… ragelink 352
254b467… ragelink 353 html = '<a href="/concepts.wiki">page</a>'
254b467… ragelink 354 result = _rewrite_fossil_links(html, "myproj")
254b467… ragelink 355 assert "/projects/myproj/fossil/docs/www/concepts.wiki" in result
254b467… ragelink 356
254b467… ragelink 357 def test_external_fossil_scm_rewrite(self):
254b467… ragelink 358 from fossil.views import _rewrite_fossil_links
254b467… ragelink 359
254b467… ragelink 360 html = '<a href="https://fossil-scm.org/home/info/abc123">ext</a>'
254b467… ragelink 361 result = _rewrite_fossil_links(html, "myproj")
254b467… ragelink 362 assert "/projects/myproj/fossil/checkin/abc123/" in result
254b467… ragelink 363
254b467… ragelink 364 def test_scheme_link_forum(self):
254b467… ragelink 365 from fossil.views import _rewrite_fossil_links
254b467… ragelink 366
254b467… ragelink 367 html = '<a href="forum:/forumpost/abc123">post</a>'
254b467… ragelink 368 result = _rewrite_fossil_links(html, "myproj")
254b467… ragelink 369 assert "/projects/myproj/fossil/forum/abc123/" in result
254b467… ragelink 370
254b467… ragelink 371
254b467… ragelink 372 # ---------------------------------------------------------------------------
254b467… ragelink 373 # Split diff helper
254b467… ragelink 374 # ---------------------------------------------------------------------------
254b467… ragelink 375
254b467… ragelink 376
254b467… ragelink 377 class TestComputeSplitLines:
254b467… ragelink 378 def test_context_lines_both_sides(self):
254b467… ragelink 379 from fossil.views import _compute_split_lines
254b467… ragelink 380
254b467… ragelink 381 lines = [{"text": " same", "type": "context", "old_num": 1, "new_num": 1}]
254b467… ragelink 382 left, right = _compute_split_lines(lines)
254b467… ragelink 383 assert len(left) == 1
254b467… ragelink 384 assert left[0]["type"] == "context"
254b467… ragelink 385 assert right[0]["type"] == "context"
254b467… ragelink 386
254b467… ragelink 387 def test_del_add_paired(self):
254b467… ragelink 388 from fossil.views import _compute_split_lines
254b467… ragelink 389
254b467… ragelink 390 lines = [
254b467… ragelink 391 {"text": "-old", "type": "del", "old_num": 1, "new_num": ""},
254b467… ragelink 392 {"text": "+new", "type": "add", "old_num": "", "new_num": 1},
254b467… ragelink 393 ]
254b467… ragelink 394 left, right = _compute_split_lines(lines)
254b467… ragelink 395 assert left[0]["type"] == "del"
254b467… ragelink 396 assert right[0]["type"] == "add"
254b467… ragelink 397
254b467… ragelink 398 def test_orphan_add(self):
254b467… ragelink 399 from fossil.views import _compute_split_lines
254b467… ragelink 400
254b467… ragelink 401 lines = [{"text": "+added", "type": "add", "old_num": "", "new_num": 1}]
254b467… ragelink 402 left, right = _compute_split_lines(lines)
254b467… ragelink 403 assert left[0]["type"] == "empty"
254b467… ragelink 404 assert right[0]["type"] == "add"
254b467… ragelink 405
254b467… ragelink 406 def test_header_hunk_both_sides(self):
254b467… ragelink 407 from fossil.views import _compute_split_lines
254b467… ragelink 408
254b467… ragelink 409 lines = [
254b467… ragelink 410 {"text": "--- a/f", "type": "header", "old_num": "", "new_num": ""},
254b467… ragelink 411 {"text": "@@ -1 +1 @@", "type": "hunk", "old_num": "", "new_num": ""},
254b467… ragelink 412 ]
254b467… ragelink 413 left, right = _compute_split_lines(lines)
254b467… ragelink 414 assert len(left) == 2
254b467… ragelink 415 assert left[0]["type"] == "header"
254b467… ragelink 416 assert left[1]["type"] == "hunk"
254b467… ragelink 417
254b467… ragelink 418 def test_uneven_del_add_padded(self):
254b467… ragelink 419 """When there are more deletions than additions, right side gets empty placeholders."""
254b467… ragelink 420 from fossil.views import _compute_split_lines
254b467… ragelink 421
254b467… ragelink 422 lines = [
254b467… ragelink 423 {"text": "-line1", "type": "del", "old_num": 1, "new_num": ""},
254b467… ragelink 424 {"text": "-line2", "type": "del", "old_num": 2, "new_num": ""},
254b467… ragelink 425 {"text": "+new1", "type": "add", "old_num": "", "new_num": 1},
254b467… ragelink 426 ]
254b467… ragelink 427 left, right = _compute_split_lines(lines)
254b467… ragelink 428 assert len(left) == 2
254b467… ragelink 429 assert left[0]["type"] == "del"
254b467… ragelink 430 assert left[1]["type"] == "del"
254b467… ragelink 431 assert right[0]["type"] == "add"
254b467… ragelink 432 assert right[1]["type"] == "empty"
254b467… ragelink 433
254b467… ragelink 434
254b467… ragelink 435 # ---------------------------------------------------------------------------
254b467… ragelink 436 # Timeline view (mocked FossilReader)
254b467… ragelink 437 # ---------------------------------------------------------------------------
254b467… ragelink 438
254b467… ragelink 439
254b467… ragelink 440 @pytest.mark.django_db
254b467… ragelink 441 class TestTimelineViewMocked:
254b467… ragelink 442 def test_timeline_renders(self, admin_client, sample_project):
254b467… ragelink 443 slug = sample_project.slug
254b467… ragelink 444 entries = [_make_timeline_entry(rid=1)]
254b467… ragelink 445 with patch("fossil.views.FossilReader") as mock_cls:
254b467… ragelink 446 _mock_reader_ctx(mock_cls, get_timeline=entries)
254b467… ragelink 447 with patch("fossil.views._get_repo_and_reader") as mock_grr:
254b467… ragelink 448 repo = FossilRepository.objects.get(project=sample_project)
254b467… ragelink 449 mock_grr.return_value = (sample_project, repo, mock_cls.return_value)
254b467… ragelink 450 response = admin_client.get(_url(slug, "timeline/"))
254b467… ragelink 451 assert response.status_code == 200
254b467… ragelink 452 assert "initial commit" in response.content.decode()
254b467… ragelink 453
254b467… ragelink 454 def test_timeline_with_type_filter(self, admin_client, sample_project):
254b467… ragelink 455 slug = sample_project.slug
254b467… ragelink 456 entries = [_make_timeline_entry(rid=1, event_type="w", comment="wiki edit")]
254b467… ragelink 457 with patch("fossil.views.FossilReader") as mock_cls:
254b467… ragelink 458 _mock_reader_ctx(mock_cls, get_timeline=entries)
254b467… ragelink 459 with patch("fossil.views._get_repo_and_reader") as mock_grr:
254b467… ragelink 460 repo = FossilRepository.objects.get(project=sample_project)
254b467… ragelink 461 mock_grr.return_value = (sample_project, repo, mock_cls.return_value)
254b467… ragelink 462 response = admin_client.get(_url(slug, "timeline/?type=w"))
254b467… ragelink 463 assert response.status_code == 200
254b467… ragelink 464
254b467… ragelink 465 def test_timeline_htmx_partial(self, admin_client, sample_project):
254b467… ragelink 466 slug = sample_project.slug
254b467… ragelink 467 entries = [_make_timeline_entry(rid=1)]
254b467… ragelink 468 with patch("fossil.views.FossilReader") as mock_cls:
254b467… ragelink 469 _mock_reader_ctx(mock_cls, get_timeline=entries)
254b467… ragelink 470 with patch("fossil.views._get_repo_and_reader") as mock_grr:
254b467… ragelink 471 repo = FossilRepository.objects.get(project=sample_project)
254b467… ragelink 472 mock_grr.return_value = (sample_project, repo, mock_cls.return_value)
254b467… ragelink 473 response = admin_client.get(_url(slug, "timeline/"), HTTP_HX_REQUEST="true")
254b467… ragelink 474 assert response.status_code == 200
254b467… ragelink 475
254b467… ragelink 476 def test_timeline_denied_no_perm(self, no_perm_client, sample_project):
254b467… ragelink 477 response = no_perm_client.get(_url(sample_project.slug, "timeline/"))
254b467… ragelink 478 assert response.status_code == 403
254b467… ragelink 479
254b467… ragelink 480
254b467… ragelink 481 # ---------------------------------------------------------------------------
254b467… ragelink 482 # Ticket list/detail (mocked)
254b467… ragelink 483 # ---------------------------------------------------------------------------
254b467… ragelink 484
254b467… ragelink 485
254b467… ragelink 486 @pytest.mark.django_db
254b467… ragelink 487 class TestTicketViewsMocked:
254b467… ragelink 488 def test_ticket_list_renders(self, admin_client, sample_project):
254b467… ragelink 489 slug = sample_project.slug
254b467… ragelink 490 tickets = [
254b467… ragelink 491 TicketEntry(
254b467… ragelink 492 uuid="tkt-uuid-1",
254b467… ragelink 493 title="Bug report",
254b467… ragelink 494 status="Open",
254b467… ragelink 495 type="Code_Defect",
254b467… ragelink 496 created=datetime(2026, 3, 1, tzinfo=UTC),
254b467… ragelink 497 owner="testuser",
254b467… ragelink 498 )
254b467… ragelink 499 ]
254b467… ragelink 500 with patch("fossil.views._get_repo_and_reader") as mock_grr:
254b467… ragelink 501 reader = MagicMock()
254b467… ragelink 502 reader.__enter__ = MagicMock(return_value=reader)
254b467… ragelink 503 reader.__exit__ = MagicMock(return_value=False)
254b467… ragelink 504 reader.get_tickets.return_value = tickets
254b467… ragelink 505 repo = FossilRepository.objects.get(project=sample_project)
254b467… ragelink 506 mock_grr.return_value = (sample_project, repo, reader)
254b467… ragelink 507 response = admin_client.get(_url(slug, "tickets/"))
254b467… ragelink 508 assert response.status_code == 200
254b467… ragelink 509 assert "Bug report" in response.content.decode()
254b467… ragelink 510
254b467… ragelink 511 def test_ticket_list_search_filter(self, admin_client, sample_project):
254b467… ragelink 512 slug = sample_project.slug
254b467… ragelink 513 tickets = [
254b467… ragelink 514 TicketEntry(
254b467… ragelink 515 uuid="t1", title="Login bug", status="Open", type="Code_Defect", created=datetime(2026, 3, 1, tzinfo=UTC), owner="u"
254b467… ragelink 516 ),
254b467… ragelink 517 TicketEntry(
254b467… ragelink 518 uuid="t2", title="Dashboard fix", status="Open", type="Code_Defect", created=datetime(2026, 3, 1, tzinfo=UTC), owner="u"
254b467… ragelink 519 ),
254b467… ragelink 520 ]
254b467… ragelink 521 with patch("fossil.views._get_repo_and_reader") as mock_grr:
254b467… ragelink 522 reader = MagicMock()
254b467… ragelink 523 reader.__enter__ = MagicMock(return_value=reader)
254b467… ragelink 524 reader.__exit__ = MagicMock(return_value=False)
254b467… ragelink 525 reader.get_tickets.return_value = tickets
254b467… ragelink 526 repo = FossilRepository.objects.get(project=sample_project)
254b467… ragelink 527 mock_grr.return_value = (sample_project, repo, reader)
254b467… ragelink 528 response = admin_client.get(_url(slug, "tickets/?search=login"))
254b467… ragelink 529 assert response.status_code == 200
254b467… ragelink 530 content = response.content.decode()
254b467… ragelink 531 assert "Login bug" in content
254b467… ragelink 532 # Dashboard should be filtered out
254b467… ragelink 533 assert "Dashboard fix" not in content
254b467… ragelink 534
254b467… ragelink 535 def test_ticket_list_htmx_partial(self, admin_client, sample_project):
254b467… ragelink 536 slug = sample_project.slug
254b467… ragelink 537 tickets = [
254b467… ragelink 538 TicketEntry(
254b467… ragelink 539 uuid="t1", title="A ticket", status="Open", type="Code_Defect", created=datetime(2026, 3, 1, tzinfo=UTC), owner="u"
254b467… ragelink 540 ),
254b467… ragelink 541 ]
254b467… ragelink 542 with patch("fossil.views._get_repo_and_reader") as mock_grr:
254b467… ragelink 543 reader = MagicMock()
254b467… ragelink 544 reader.__enter__ = MagicMock(return_value=reader)
254b467… ragelink 545 reader.__exit__ = MagicMock(return_value=False)
254b467… ragelink 546 reader.get_tickets.return_value = tickets
254b467… ragelink 547 repo = FossilRepository.objects.get(project=sample_project)
254b467… ragelink 548 mock_grr.return_value = (sample_project, repo, reader)
254b467… ragelink 549 response = admin_client.get(_url(slug, "tickets/"), HTTP_HX_REQUEST="true")
254b467… ragelink 550 assert response.status_code == 200
254b467… ragelink 551
254b467… ragelink 552 def test_ticket_detail_renders(self, admin_client, sample_project):
254b467… ragelink 553 slug = sample_project.slug
254b467… ragelink 554 ticket = TicketEntry(
254b467… ragelink 555 uuid="tkt-detail-1",
254b467… ragelink 556 title="Detail test",
254b467… ragelink 557 status="Open",
254b467… ragelink 558 type="Code_Defect",
254b467… ragelink 559 created=datetime(2026, 3, 1, tzinfo=UTC),
254b467… ragelink 560 owner="testuser",
254b467… ragelink 561 body="Some description **bold**",
254b467… ragelink 562 )
254b467… ragelink 563 with patch("fossil.views._get_repo_and_reader") as mock_grr:
254b467… ragelink 564 reader = MagicMock()
254b467… ragelink 565 reader.__enter__ = MagicMock(return_value=reader)
254b467… ragelink 566 reader.__exit__ = MagicMock(return_value=False)
254b467… ragelink 567 reader.get_ticket_detail.return_value = ticket
254b467… ragelink 568 reader.get_ticket_comments.return_value = [
254b467… ragelink 569 {"user": "dev", "timestamp": datetime(2026, 3, 2, tzinfo=UTC), "comment": "Working on it"}
254b467… ragelink 570 ]
254b467… ragelink 571 repo = FossilRepository.objects.get(project=sample_project)
254b467… ragelink 572 mock_grr.return_value = (sample_project, repo, reader)
254b467… ragelink 573 response = admin_client.get(_url(slug, "tickets/tkt-detail-1/"))
254b467… ragelink 574 assert response.status_code == 200
254b467… ragelink 575 content = response.content.decode()
254b467… ragelink 576 assert "Detail test" in content
254b467… ragelink 577
254b467… ragelink 578 def test_ticket_detail_not_found(self, admin_client, sample_project):
254b467… ragelink 579 slug = sample_project.slug
254b467… ragelink 580 with patch("fossil.views._get_repo_and_reader") as mock_grr:
254b467… ragelink 581 reader = MagicMock()
254b467… ragelink 582 reader.__enter__ = MagicMock(return_value=reader)
254b467… ragelink 583 reader.__exit__ = MagicMock(return_value=False)
254b467… ragelink 584 reader.get_ticket_detail.return_value = None
254b467… ragelink 585 reader.get_ticket_comments.return_value = []
254b467… ragelink 586 repo = FossilRepository.objects.get(project=sample_project)
254b467… ragelink 587 mock_grr.return_value = (sample_project, repo, reader)
254b467… ragelink 588 response = admin_client.get(_url(slug, "tickets/nonexistent/"))
254b467… ragelink 589 assert response.status_code == 404
254b467… ragelink 590
254b467… ragelink 591
254b467… ragelink 592 # ---------------------------------------------------------------------------
254b467… ragelink 593 # Wiki list/page (mocked)
254b467… ragelink 594 # ---------------------------------------------------------------------------
254b467… ragelink 595
254b467… ragelink 596
254b467… ragelink 597 @pytest.mark.django_db
254b467… ragelink 598 class TestWikiViewsMocked:
254b467… ragelink 599 def test_wiki_list_renders(self, admin_client, sample_project):
254b467… ragelink 600 slug = sample_project.slug
254b467… ragelink 601 pages = [
254b467… ragelink 602 WikiPage(name="Home", content="# Home", last_modified=datetime(2026, 3, 1, tzinfo=UTC), user="admin"),
254b467… ragelink 603 WikiPage(name="Setup", content="Setup guide", last_modified=datetime(2026, 3, 1, tzinfo=UTC), user="dev"),
254b467… ragelink 604 ]
254b467… ragelink 605 with patch("fossil.views._get_repo_and_reader") as mock_grr:
254b467… ragelink 606 reader = MagicMock()
254b467… ragelink 607 reader.__enter__ = MagicMock(return_value=reader)
254b467… ragelink 608 reader.__exit__ = MagicMock(return_value=False)
254b467… ragelink 609 reader.get_wiki_pages.return_value = pages
254b467… ragelink 610 reader.get_wiki_page.return_value = pages[0]
254b467… ragelink 611 repo = FossilRepository.objects.get(project=sample_project)
254b467… ragelink 612 mock_grr.return_value = (sample_project, repo, reader)
254b467… ragelink 613 response = admin_client.get(_url(slug, "wiki/"))
254b467… ragelink 614 assert response.status_code == 200
254b467… ragelink 615 content = response.content.decode()
254b467… ragelink 616 assert "Home" in content
254b467… ragelink 617 assert "Setup" in content
254b467… ragelink 618
254b467… ragelink 619 def test_wiki_list_search(self, admin_client, sample_project):
254b467… ragelink 620 slug = sample_project.slug
254b467… ragelink 621 pages = [
254b467… ragelink 622 WikiPage(name="Home", content="# Home", last_modified=datetime(2026, 3, 1, tzinfo=UTC), user="admin"),
254b467… ragelink 623 WikiPage(name="Setup", content="Setup guide", last_modified=datetime(2026, 3, 1, tzinfo=UTC), user="dev"),
254b467… ragelink 624 ]
254b467… ragelink 625 with patch("fossil.views._get_repo_and_reader") as mock_grr:
254b467… ragelink 626 reader = MagicMock()
254b467… ragelink 627 reader.__enter__ = MagicMock(return_value=reader)
254b467… ragelink 628 reader.__exit__ = MagicMock(return_value=False)
254b467… ragelink 629 reader.get_wiki_pages.return_value = pages
254b467… ragelink 630 reader.get_wiki_page.return_value = None
254b467… ragelink 631 repo = FossilRepository.objects.get(project=sample_project)
254b467… ragelink 632 mock_grr.return_value = (sample_project, repo, reader)
254b467… ragelink 633 response = admin_client.get(_url(slug, "wiki/?search=setup"))
254b467… ragelink 634 assert response.status_code == 200
254b467… ragelink 635
254b467… ragelink 636 def test_wiki_page_renders(self, admin_client, sample_project):
254b467… ragelink 637 slug = sample_project.slug
254b467… ragelink 638 page = WikiPage(name="Home", content="# Welcome\nHello world", last_modified=datetime(2026, 3, 1, tzinfo=UTC), user="admin")
254b467… ragelink 639 all_pages = [page]
254b467… ragelink 640 with patch("fossil.views._get_repo_and_reader") as mock_grr:
254b467… ragelink 641 reader = MagicMock()
254b467… ragelink 642 reader.__enter__ = MagicMock(return_value=reader)
254b467… ragelink 643 reader.__exit__ = MagicMock(return_value=False)
254b467… ragelink 644 reader.get_wiki_page.return_value = page
254b467… ragelink 645 reader.get_wiki_pages.return_value = all_pages
254b467… ragelink 646 repo = FossilRepository.objects.get(project=sample_project)
254b467… ragelink 647 mock_grr.return_value = (sample_project, repo, reader)
254b467… ragelink 648 response = admin_client.get(_url(slug, "wiki/page/Home"))
254b467… ragelink 649 assert response.status_code == 200
254b467… ragelink 650 assert "Welcome" in response.content.decode()
254b467… ragelink 651
254b467… ragelink 652 def test_wiki_page_not_found(self, admin_client, sample_project):
254b467… ragelink 653 slug = sample_project.slug
254b467… ragelink 654 with patch("fossil.views._get_repo_and_reader") as mock_grr:
254b467… ragelink 655 reader = MagicMock()
254b467… ragelink 656 reader.__enter__ = MagicMock(return_value=reader)
254b467… ragelink 657 reader.__exit__ = MagicMock(return_value=False)
254b467… ragelink 658 reader.get_wiki_page.return_value = None
254b467… ragelink 659 reader.get_wiki_pages.return_value = []
254b467… ragelink 660 repo = FossilRepository.objects.get(project=sample_project)
254b467… ragelink 661 mock_grr.return_value = (sample_project, repo, reader)
254b467… ragelink 662 response = admin_client.get(_url(slug, "wiki/page/NonexistentPage"))
254b467… ragelink 663 assert response.status_code == 404
254b467… ragelink 664
254b467… ragelink 665
254b467… ragelink 666 # ---------------------------------------------------------------------------
254b467… ragelink 667 # Search view (mocked)
254b467… ragelink 668 # ---------------------------------------------------------------------------
254b467… ragelink 669
254b467… ragelink 670
254b467… ragelink 671 @pytest.mark.django_db
254b467… ragelink 672 class TestSearchViewMocked:
254b467… ragelink 673 def test_search_with_query(self, admin_client, sample_project):
254b467… ragelink 674 slug = sample_project.slug
254b467… ragelink 675 results = [{"type": "ci", "uuid": "abc", "comment": "found it", "user": "dev"}]
254b467… ragelink 676 with patch("fossil.views._get_repo_and_reader") as mock_grr:
254b467… ragelink 677 reader = MagicMock()
254b467… ragelink 678 reader.__enter__ = MagicMock(return_value=reader)
254b467… ragelink 679 reader.__exit__ = MagicMock(return_value=False)
254b467… ragelink 680 reader.search.return_value = results
254b467… ragelink 681 repo = FossilRepository.objects.get(project=sample_project)
254b467… ragelink 682 mock_grr.return_value = (sample_project, repo, reader)
254b467… ragelink 683 response = admin_client.get(_url(slug, "search/?q=found"))
254b467… ragelink 684 assert response.status_code == 200
254b467… ragelink 685
254b467… ragelink 686 def test_search_empty_query(self, admin_client, sample_project):
254b467… ragelink 687 slug = sample_project.slug
254b467… ragelink 688 with patch("fossil.views._get_repo_and_reader") as mock_grr:
254b467… ragelink 689 reader = MagicMock()
254b467… ragelink 690 reader.__enter__ = MagicMock(return_value=reader)
254b467… ragelink 691 reader.__exit__ = MagicMock(return_value=False)
254b467… ragelink 692 repo = FossilRepository.objects.get(project=sample_project)
254b467… ragelink 693 mock_grr.return_value = (sample_project, repo, reader)
254b467… ragelink 694 response = admin_client.get(_url(slug, "search/"))
254b467… ragelink 695 assert response.status_code == 200
254b467… ragelink 696
254b467… ragelink 697
254b467… ragelink 698 # ---------------------------------------------------------------------------
254b467… ragelink 699 # Compare checkins view (mocked)
254b467… ragelink 700 # ---------------------------------------------------------------------------
254b467… ragelink 701
254b467… ragelink 702
254b467… ragelink 703 @pytest.mark.django_db
254b467… ragelink 704 class TestCompareCheckinsViewMocked:
254b467… ragelink 705 def test_compare_no_params(self, admin_client, sample_project):
254b467… ragelink 706 """Compare page renders without from/to params (shows empty form)."""
254b467… ragelink 707 slug = sample_project.slug
254b467… ragelink 708 with patch("fossil.views._get_repo_and_reader") as mock_grr:
254b467… ragelink 709 reader = MagicMock()
254b467… ragelink 710 reader.__enter__ = MagicMock(return_value=reader)
254b467… ragelink 711 reader.__exit__ = MagicMock(return_value=False)
254b467… ragelink 712 repo = FossilRepository.objects.get(project=sample_project)
254b467… ragelink 713 mock_grr.return_value = (sample_project, repo, reader)
254b467… ragelink 714 response = admin_client.get(_url(slug, "compare/"))
254b467… ragelink 715 assert response.status_code == 200
254b467… ragelink 716
254b467… ragelink 717 def test_compare_with_params(self, admin_client, sample_project):
254b467… ragelink 718 """Compare page with from/to parameters renders diffs."""
254b467… ragelink 719 slug = sample_project.slug
254b467… ragelink 720 from_detail = CheckinDetail(
254b467… ragelink 721 uuid="aaa111",
254b467… ragelink 722 timestamp=datetime(2026, 3, 1, tzinfo=UTC),
254b467… ragelink 723 user="dev",
254b467… ragelink 724 comment="from commit",
254b467… ragelink 725 files_changed=[{"name": "f.txt", "uuid": "u1", "prev_uuid": "", "change_type": "A"}],
254b467… ragelink 726 )
254b467… ragelink 727 to_detail = CheckinDetail(
254b467… ragelink 728 uuid="bbb222",
254b467… ragelink 729 timestamp=datetime(2026, 3, 2, tzinfo=UTC),
254b467… ragelink 730 user="dev",
254b467… ragelink 731 comment="to commit",
254b467… ragelink 732 files_changed=[{"name": "f.txt", "uuid": "u2", "prev_uuid": "u1", "change_type": "M"}],
254b467… ragelink 733 )
254b467… ragelink 734 with patch("fossil.views._get_repo_and_reader") as mock_grr:
254b467… ragelink 735 reader = MagicMock()
254b467… ragelink 736 reader.__enter__ = MagicMock(return_value=reader)
254b467… ragelink 737 reader.__exit__ = MagicMock(return_value=False)
254b467… ragelink 738 reader.get_checkin_detail.side_effect = lambda uuid: from_detail if "aaa" in uuid else to_detail
254b467… ragelink 739 reader.get_file_content.return_value = b"file content"
254b467… ragelink 740 repo = FossilRepository.objects.get(project=sample_project)
254b467… ragelink 741 mock_grr.return_value = (sample_project, repo, reader)
254b467… ragelink 742 response = admin_client.get(_url(slug, "compare/?from=aaa111&to=bbb222"))
254b467… ragelink 743 assert response.status_code == 200
254b467… ragelink 744
254b467… ragelink 745
254b467… ragelink 746 # ---------------------------------------------------------------------------
254b467… ragelink 747 # Timeline RSS feed (mocked)
254b467… ragelink 748 # ---------------------------------------------------------------------------
254b467… ragelink 749
254b467… ragelink 750
254b467… ragelink 751 @pytest.mark.django_db
254b467… ragelink 752 class TestTimelineRssViewMocked:
254b467… ragelink 753 def test_rss_feed(self, admin_client, sample_project):
254b467… ragelink 754 slug = sample_project.slug
254b467… ragelink 755 entries = [_make_timeline_entry(rid=1, comment="rss commit")]
254b467… ragelink 756 with patch("fossil.views._get_repo_and_reader") as mock_grr:
254b467… ragelink 757 reader = MagicMock()
254b467… ragelink 758 reader.__enter__ = MagicMock(return_value=reader)
254b467… ragelink 759 reader.__exit__ = MagicMock(return_value=False)
254b467… ragelink 760 reader.get_timeline.return_value = entries
254b467… ragelink 761 repo = FossilRepository.objects.get(project=sample_project)
254b467… ragelink 762 mock_grr.return_value = (sample_project, repo, reader)
254b467… ragelink 763 response = admin_client.get(_url(slug, "timeline/rss/"))
254b467… ragelink 764 assert response.status_code == 200
254b467… ragelink 765 assert response["Content-Type"] == "application/rss+xml"
254b467… ragelink 766 content = response.content.decode()
254b467… ragelink 767 assert "rss commit" in content
254b467… ragelink 768 assert "<rss" in content
254b467… ragelink 769
254b467… ragelink 770
254b467… ragelink 771 # ---------------------------------------------------------------------------
254b467… ragelink 772 # Tickets CSV export (mocked)
254b467… ragelink 773 # ---------------------------------------------------------------------------
254b467… ragelink 774
254b467… ragelink 775
254b467… ragelink 776 @pytest.mark.django_db
254b467… ragelink 777 class TestTicketsCsvViewMocked:
254b467… ragelink 778 def test_csv_export(self, admin_client, sample_project):
254b467… ragelink 779 slug = sample_project.slug
254b467… ragelink 780 tickets = [
254b467… ragelink 781 TicketEntry(
254b467… ragelink 782 uuid="csv-uuid",
254b467… ragelink 783 title="Export test",
254b467… ragelink 784 status="Open",
254b467… ragelink 785 type="Code_Defect",
254b467… ragelink 786 created=datetime(2026, 3, 1, tzinfo=UTC),
254b467… ragelink 787 owner="testuser",
254b467… ragelink 788 priority="High",
254b467… ragelink 789 severity="Critical",
254b467… ragelink 790 )
254b467… ragelink 791 ]
254b467… ragelink 792 with patch("fossil.views._get_repo_and_reader") as mock_grr:
254b467… ragelink 793 reader = MagicMock()
254b467… ragelink 794 reader.__enter__ = MagicMock(return_value=reader)
254b467… ragelink 795 reader.__exit__ = MagicMock(return_value=False)
254b467… ragelink 796 reader.get_tickets.return_value = tickets
254b467… ragelink 797 repo = FossilRepository.objects.get(project=sample_project)
254b467… ragelink 798 mock_grr.return_value = (sample_project, repo, reader)
254b467… ragelink 799 response = admin_client.get(_url(slug, "tickets/export/"))
254b467… ragelink 800 assert response.status_code == 200
254b467… ragelink 801 assert response["Content-Type"] == "text/csv"
254b467… ragelink 802 content = response.content.decode()
254b467… ragelink 803 assert "Export test" in content
254b467… ragelink 804 assert "csv-uuid" in content
254b467… ragelink 805
254b467… ragelink 806
254b467… ragelink 807 # ---------------------------------------------------------------------------
254b467… ragelink 808 # Branch list view (mocked)
254b467… ragelink 809 # ---------------------------------------------------------------------------
254b467… ragelink 810
254b467… ragelink 811
254b467… ragelink 812 @pytest.mark.django_db
254b467… ragelink 813 class TestBranchListViewMocked:
254b467… ragelink 814 def test_branch_list_renders(self, admin_client, sample_project):
254b467… ragelink 815 slug = sample_project.slug
254b467… ragelink 816 branches = [
254b467… ragelink 817 SimpleNamespace(
254b467… ragelink 818 name="trunk", last_user="dev", last_checkin=datetime(2026, 3, 1, tzinfo=UTC), checkin_count=50, last_uuid="abc123"
254b467… ragelink 819 ),
254b467… ragelink 820 ]
254b467… ragelink 821 with patch("fossil.views._get_repo_and_reader") as mock_grr:
254b467… ragelink 822 reader = MagicMock()
254b467… ragelink 823 reader.__enter__ = MagicMock(return_value=reader)
254b467… ragelink 824 reader.__exit__ = MagicMock(return_value=False)
254b467… ragelink 825 reader.get_branches.return_value = branches
254b467… ragelink 826 repo = FossilRepository.objects.get(project=sample_project)
254b467… ragelink 827 mock_grr.return_value = (sample_project, repo, reader)
254b467… ragelink 828 response = admin_client.get(_url(slug, "branches/"))
254b467… ragelink 829 assert response.status_code == 200
254b467… ragelink 830 assert "trunk" in response.content.decode()
254b467… ragelink 831
254b467… ragelink 832 def test_branch_list_search(self, admin_client, sample_project):
254b467… ragelink 833 slug = sample_project.slug
254b467… ragelink 834 branches = [
254b467… ragelink 835 SimpleNamespace(
254b467… ragelink 836 name="trunk", last_user="dev", last_checkin=datetime(2026, 3, 1, tzinfo=UTC), checkin_count=50, last_uuid="abc123"
254b467… ragelink 837 ),
254b467… ragelink 838 SimpleNamespace(
254b467… ragelink 839 name="feature-x", last_user="dev", last_checkin=datetime(2026, 3, 1, tzinfo=UTC), checkin_count=5, last_uuid="def456"
254b467… ragelink 840 ),
254b467… ragelink 841 ]
254b467… ragelink 842 with patch("fossil.views._get_repo_and_reader") as mock_grr:
254b467… ragelink 843 reader = MagicMock()
254b467… ragelink 844 reader.__enter__ = MagicMock(return_value=reader)
254b467… ragelink 845 reader.__exit__ = MagicMock(return_value=False)
254b467… ragelink 846 reader.get_branches.return_value = branches
254b467… ragelink 847 repo = FossilRepository.objects.get(project=sample_project)
254b467… ragelink 848 mock_grr.return_value = (sample_project, repo, reader)
254b467… ragelink 849 response = admin_client.get(_url(slug, "branches/?search=feature"))
254b467… ragelink 850 assert response.status_code == 200
254b467… ragelink 851 content = response.content.decode()
254b467… ragelink 852 assert "feature-x" in content
254b467… ragelink 853
254b467… ragelink 854
254b467… ragelink 855 # ---------------------------------------------------------------------------
254b467… ragelink 856 # Tag list view (mocked)
254b467… ragelink 857 # ---------------------------------------------------------------------------
254b467… ragelink 858
254b467… ragelink 859
254b467… ragelink 860 @pytest.mark.django_db
254b467… ragelink 861 class TestTagListViewMocked:
254b467… ragelink 862 def test_tag_list_renders(self, admin_client, sample_project):
254b467… ragelink 863 slug = sample_project.slug
254b467… ragelink 864 tags = [
254b467… ragelink 865 SimpleNamespace(name="v1.0", uuid="abc123", user="dev", timestamp=datetime(2026, 3, 1, tzinfo=UTC)),
254b467… ragelink 866 ]
254b467… ragelink 867 with patch("fossil.views._get_repo_and_reader") as mock_grr:
254b467… ragelink 868 reader = MagicMock()
254b467… ragelink 869 reader.__enter__ = MagicMock(return_value=reader)
254b467… ragelink 870 reader.__exit__ = MagicMock(return_value=False)
254b467… ragelink 871 reader.get_tags.return_value = tags
254b467… ragelink 872 repo = FossilRepository.objects.get(project=sample_project)
254b467… ragelink 873 mock_grr.return_value = (sample_project, repo, reader)
254b467… ragelink 874 response = admin_client.get(_url(slug, "tags/"))
254b467… ragelink 875 assert response.status_code == 200
254b467… ragelink 876 assert "v1.0" in response.content.decode()
254b467… ragelink 877
254b467… ragelink 878 def test_tag_list_search(self, admin_client, sample_project):
254b467… ragelink 879 slug = sample_project.slug
254b467… ragelink 880 tags = [
254b467… ragelink 881 SimpleNamespace(name="v1.0", uuid="abc123", user="dev", timestamp=datetime(2026, 3, 1, tzinfo=UTC)),
254b467… ragelink 882 SimpleNamespace(name="v2.0-beta", uuid="def456", user="dev", timestamp=datetime(2026, 3, 1, tzinfo=UTC)),
254b467… ragelink 883 ]
254b467… ragelink 884 with patch("fossil.views._get_repo_and_reader") as mock_grr:
254b467… ragelink 885 reader = MagicMock()
254b467… ragelink 886 reader.__enter__ = MagicMock(return_value=reader)
254b467… ragelink 887 reader.__exit__ = MagicMock(return_value=False)
254b467… ragelink 888 reader.get_tags.return_value = tags
254b467… ragelink 889 repo = FossilRepository.objects.get(project=sample_project)
254b467… ragelink 890 mock_grr.return_value = (sample_project, repo, reader)
254b467… ragelink 891 response = admin_client.get(_url(slug, "tags/?search=beta"))
254b467… ragelink 892 assert response.status_code == 200
254b467… ragelink 893 content = response.content.decode()
254b467… ragelink 894 assert "v2.0-beta" in content
254b467… ragelink 895
254b467… ragelink 896
254b467… ragelink 897 # ---------------------------------------------------------------------------
254b467… ragelink 898 # Stats view (mocked)
254b467… ragelink 899 # ---------------------------------------------------------------------------
254b467… ragelink 900
254b467… ragelink 901
254b467… ragelink 902 @pytest.mark.django_db
254b467… ragelink 903 class TestRepoStatsViewMocked:
254b467… ragelink 904 def test_stats_renders(self, admin_client, sample_project):
254b467… ragelink 905 slug = sample_project.slug
254b467… ragelink 906 stats = {"total_artifacts": 100, "checkin_count": 50, "wiki_events": 5, "ticket_events": 10, "forum_events": 2, "total_events": 67}
254b467… ragelink 907 contributors = [{"user": "dev", "count": 50}]
254b467… ragelink 908 activity = [{"count": c} for c in range(52)]
254b467… ragelink 909 with patch("fossil.views._get_repo_and_reader") as mock_grr:
254b467… ragelink 910 reader = MagicMock()
254b467… ragelink 911 reader.__enter__ = MagicMock(return_value=reader)
254b467… ragelink 912 reader.__exit__ = MagicMock(return_value=False)
254b467… ragelink 913 reader.get_repo_statistics.return_value = stats
254b467… ragelink 914 reader.get_top_contributors.return_value = contributors
254b467… ragelink 915 reader.get_commit_activity.return_value = activity
254b467… ragelink 916 repo = FossilRepository.objects.get(project=sample_project)
254b467… ragelink 917 mock_grr.return_value = (sample_project, repo, reader)
254b467… ragelink 918 response = admin_client.get(_url(slug, "stats/"))
254b467… ragelink 919 assert response.status_code == 200
254b467… ragelink 920 content = response.content.decode()
254b467… ragelink 921 assert "Checkins" in content or "50" in content
254b467… ragelink 922
254b467… ragelink 923
254b467… ragelink 924 # ---------------------------------------------------------------------------
254b467… ragelink 925 # File history view (mocked)
254b467… ragelink 926 # ---------------------------------------------------------------------------
254b467… ragelink 927
254b467… ragelink 928
254b467… ragelink 929 @pytest.mark.django_db
254b467… ragelink 930 class TestFileHistoryViewMocked:
254b467… ragelink 931 def test_file_history_renders(self, admin_client, sample_project):
254b467… ragelink 932 slug = sample_project.slug
254b467… ragelink 933 history = [
254b467… ragelink 934 {"uuid": "abc", "timestamp": datetime(2026, 3, 1, tzinfo=UTC), "user": "dev", "comment": "edit file"},
254b467… ragelink 935 ]
254b467… ragelink 936 with patch("fossil.views._get_repo_and_reader") as mock_grr:
254b467… ragelink 937 reader = MagicMock()
254b467… ragelink 938 reader.__enter__ = MagicMock(return_value=reader)
254b467… ragelink 939 reader.__exit__ = MagicMock(return_value=False)
254b467… ragelink 940 reader.get_file_history.return_value = history
254b467… ragelink 941 repo = FossilRepository.objects.get(project=sample_project)
254b467… ragelink 942 mock_grr.return_value = (sample_project, repo, reader)
254b467… ragelink 943 response = admin_client.get(_url(slug, "code/history/README.md"))
254b467… ragelink 944 assert response.status_code == 200
254b467… ragelink 945
254b467… ragelink 946
254b467… ragelink 947 # ---------------------------------------------------------------------------
254b467… ragelink 948 # Code browser (mocked) -- tests the _build_file_tree helper indirectly
254b467… ragelink 949 # ---------------------------------------------------------------------------
254b467… ragelink 950
254b467… ragelink 951
254b467… ragelink 952 @pytest.mark.django_db
254b467… ragelink 953 class TestCodeBrowserViewMocked:
254b467… ragelink 954 def test_code_browser_renders(self, admin_client, sample_project):
254b467… ragelink 955 slug = sample_project.slug
254b467… ragelink 956 files = [
254b467… ragelink 957 _make_file_entry(name="README.md", uuid="f1"),
254b467… ragelink 958 _make_file_entry(name="src/main.py", uuid="f2"),
254b467… ragelink 959 ]
254b467… ragelink 960 metadata = RepoMetadata(project_name="Test", checkin_count=10)
254b467… ragelink 961 latest = [_make_timeline_entry(rid=1)]
254b467… ragelink 962 with patch("fossil.views._get_repo_and_reader") as mock_grr:
254b467… ragelink 963 reader = MagicMock()
254b467… ragelink 964 reader.__enter__ = MagicMock(return_value=reader)
254b467… ragelink 965 reader.__exit__ = MagicMock(return_value=False)
254b467… ragelink 966 reader.get_latest_checkin_uuid.return_value = "abc123"
254b467… ragelink 967 reader.get_files_at_checkin.return_value = files
254b467… ragelink 968 reader.get_metadata.return_value = metadata
254b467… ragelink 969 reader.get_timeline.return_value = latest
254b467… ragelink 970 reader.get_file_content.return_value = b"# README\nHello"
254b467… ragelink 971 repo = FossilRepository.objects.get(project=sample_project)
254b467… ragelink 972 mock_grr.return_value = (sample_project, repo, reader)
254b467… ragelink 973 response = admin_client.get(_url(slug, "code/"))
254b467… ragelink 974 assert response.status_code == 200
254b467… ragelink 975 content = response.content.decode()
254b467… ragelink 976 assert "README" in content
254b467… ragelink 977
254b467… ragelink 978 def test_code_browser_htmx_partial(self, admin_client, sample_project):
254b467… ragelink 979 slug = sample_project.slug
254b467… ragelink 980 files = [_make_file_entry(name="README.md", uuid="f1")]
254b467… ragelink 981 metadata = RepoMetadata()
254b467… ragelink 982 with patch("fossil.views._get_repo_and_reader") as mock_grr:
254b467… ragelink 983 reader = MagicMock()
254b467… ragelink 984 reader.__enter__ = MagicMock(return_value=reader)
254b467… ragelink 985 reader.__exit__ = MagicMock(return_value=False)
254b467… ragelink 986 reader.get_latest_checkin_uuid.return_value = "abc"
254b467… ragelink 987 reader.get_files_at_checkin.return_value = files
254b467… ragelink 988 reader.get_metadata.return_value = metadata
254b467… ragelink 989 reader.get_timeline.return_value = []
254b467… ragelink 990 repo = FossilRepository.objects.get(project=sample_project)
254b467… ragelink 991 mock_grr.return_value = (sample_project, repo, reader)
254b467… ragelink 992 response = admin_client.get(_url(slug, "code/"), HTTP_HX_REQUEST="true")
254b467… ragelink 993 assert response.status_code == 200
254b467… ragelink 994
254b467… ragelink 995
254b467… ragelink 996 # ---------------------------------------------------------------------------
254b467… ragelink 997 # Code file view (mocked)
254b467… ragelink 998 # ---------------------------------------------------------------------------
254b467… ragelink 999
254b467… ragelink 1000
254b467… ragelink 1001 @pytest.mark.django_db
254b467… ragelink 1002 class TestCodeFileViewMocked:
254b467… ragelink 1003 def test_code_file_renders(self, admin_client, sample_project):
254b467… ragelink 1004 slug = sample_project.slug
254b467… ragelink 1005 files = [_make_file_entry(name="main.py", uuid="f1")]
254b467… ragelink 1006 with patch("fossil.views._get_repo_and_reader") as mock_grr:
254b467… ragelink 1007 reader = MagicMock()
254b467… ragelink 1008 reader.__enter__ = MagicMock(return_value=reader)
254b467… ragelink 1009 reader.__exit__ = MagicMock(return_value=False)
254b467… ragelink 1010 reader.get_latest_checkin_uuid.return_value = "abc"
254b467… ragelink 1011 reader.get_files_at_checkin.return_value = files
254b467… ragelink 1012 reader.get_file_content.return_value = b"print('hello')"
254b467… ragelink 1013 repo = FossilRepository.objects.get(project=sample_project)
254b467… ragelink 1014 mock_grr.return_value = (sample_project, repo, reader)
254b467… ragelink 1015 response = admin_client.get(_url(slug, "code/file/main.py"))
254b467… ragelink 1016 assert response.status_code == 200
254b467… ragelink 1017 content = response.content.decode()
254b467… ragelink 1018 assert "print" in content
254b467… ragelink 1019
254b467… ragelink 1020 def test_code_file_not_found(self, admin_client, sample_project):
254b467… ragelink 1021 slug = sample_project.slug
254b467… ragelink 1022 with patch("fossil.views._get_repo_and_reader") as mock_grr:
254b467… ragelink 1023 reader = MagicMock()
254b467… ragelink 1024 reader.__enter__ = MagicMock(return_value=reader)
254b467… ragelink 1025 reader.__exit__ = MagicMock(return_value=False)
254b467… ragelink 1026 reader.get_latest_checkin_uuid.return_value = "abc"
254b467… ragelink 1027 reader.get_files_at_checkin.return_value = []
254b467… ragelink 1028 repo = FossilRepository.objects.get(project=sample_project)
254b467… ragelink 1029 mock_grr.return_value = (sample_project, repo, reader)
254b467… ragelink 1030 response = admin_client.get(_url(slug, "code/file/nonexistent.txt"))
254b467… ragelink 1031 assert response.status_code == 404
254b467… ragelink 1032
254b467… ragelink 1033 def test_code_file_binary(self, admin_client, sample_project):
254b467… ragelink 1034 slug = sample_project.slug
254b467… ragelink 1035 files = [_make_file_entry(name="image.png", uuid="f1")]
254b467… ragelink 1036 with patch("fossil.views._get_repo_and_reader") as mock_grr:
254b467… ragelink 1037 reader = MagicMock()
254b467… ragelink 1038 reader.__enter__ = MagicMock(return_value=reader)
254b467… ragelink 1039 reader.__exit__ = MagicMock(return_value=False)
254b467… ragelink 1040 reader.get_latest_checkin_uuid.return_value = "abc"
254b467… ragelink 1041 reader.get_files_at_checkin.return_value = files
254b467… ragelink 1042 # Deliberately invalid UTF-8 to trigger binary detection
254b467… ragelink 1043 reader.get_file_content.return_value = b"\x89PNG\r\n\x1a\n\x00\x00"
254b467… ragelink 1044 repo = FossilRepository.objects.get(project=sample_project)
254b467… ragelink 1045 mock_grr.return_value = (sample_project, repo, reader)
254b467… ragelink 1046 response = admin_client.get(_url(slug, "code/file/image.png"))
254b467… ragelink 1047 assert response.status_code == 200
254b467… ragelink 1048 assert "Binary file" in response.content.decode()
254b467… ragelink 1049
254b467… ragelink 1050 def test_code_file_rendered_mode(self, admin_client, sample_project):
254b467… ragelink 1051 """Wiki files can be rendered instead of showing source."""
254b467… ragelink 1052 slug = sample_project.slug
254b467… ragelink 1053 files = [_make_file_entry(name="page.md", uuid="f1")]
254b467… ragelink 1054 with patch("fossil.views._get_repo_and_reader") as mock_grr:
254b467… ragelink 1055 reader = MagicMock()
254b467… ragelink 1056 reader.__enter__ = MagicMock(return_value=reader)
254b467… ragelink 1057 reader.__exit__ = MagicMock(return_value=False)
254b467… ragelink 1058 reader.get_latest_checkin_uuid.return_value = "abc"
254b467… ragelink 1059 reader.get_files_at_checkin.return_value = files
254b467… ragelink 1060 reader.get_file_content.return_value = b"# Hello\nWorld"
254b467… ragelink 1061 repo = FossilRepository.objects.get(project=sample_project)
254b467… ragelink 1062 mock_grr.return_value = (sample_project, repo, reader)
254b467… ragelink 1063 response = admin_client.get(_url(slug, "code/file/page.md?mode=rendered"))
254b467… ragelink 1064 assert response.status_code == 200
254b467… ragelink 1065
254b467… ragelink 1066
254b467… ragelink 1067 # ---------------------------------------------------------------------------
254b467… ragelink 1068 # Code raw download (mocked)
254b467… ragelink 1069 # ---------------------------------------------------------------------------
254b467… ragelink 1070
254b467… ragelink 1071
254b467… ragelink 1072 @pytest.mark.django_db
254b467… ragelink 1073 class TestCodeRawViewMocked:
254b467… ragelink 1074 def test_raw_download(self, admin_client, sample_project):
254b467… ragelink 1075 slug = sample_project.slug
254b467… ragelink 1076 files = [_make_file_entry(name="data.csv", uuid="f1")]
254b467… ragelink 1077 with patch("fossil.views._get_repo_and_reader") as mock_grr:
254b467… ragelink 1078 reader = MagicMock()
254b467… ragelink 1079 reader.__enter__ = MagicMock(return_value=reader)
254b467… ragelink 1080 reader.__exit__ = MagicMock(return_value=False)
254b467… ragelink 1081 reader.get_latest_checkin_uuid.return_value = "abc"
254b467… ragelink 1082 reader.get_files_at_checkin.return_value = files
254b467… ragelink 1083 reader.get_file_content.return_value = b"col1,col2\na,b"
254b467… ragelink 1084 repo = FossilRepository.objects.get(project=sample_project)
254b467… ragelink 1085 mock_grr.return_value = (sample_project, repo, reader)
254b467… ragelink 1086 response = admin_client.get(_url(slug, "code/raw/data.csv"))
254b467… ragelink 1087 assert response.status_code == 200
254b467… ragelink 1088 assert response["Content-Disposition"] == 'attachment; filename="data.csv"'
254b467… ragelink 1089
254b467… ragelink 1090 def test_raw_file_not_found(self, admin_client, sample_project):
254b467… ragelink 1091 slug = sample_project.slug
254b467… ragelink 1092 with patch("fossil.views._get_repo_and_reader") as mock_grr:
254b467… ragelink 1093 reader = MagicMock()
254b467… ragelink 1094 reader.__enter__ = MagicMock(return_value=reader)
254b467… ragelink 1095 reader.__exit__ = MagicMock(return_value=False)
254b467… ragelink 1096 reader.get_latest_checkin_uuid.return_value = "abc"
254b467… ragelink 1097 reader.get_files_at_checkin.return_value = []
254b467… ragelink 1098 repo = FossilRepository.objects.get(project=sample_project)
254b467… ragelink 1099 mock_grr.return_value = (sample_project, repo, reader)
254b467… ragelink 1100 response = admin_client.get(_url(slug, "code/raw/missing.txt"))
254b467… ragelink 1101 assert response.status_code == 404
254b467… ragelink 1102
254b467… ragelink 1103
254b467… ragelink 1104 # ---------------------------------------------------------------------------
254b467… ragelink 1105 # Code blame (mocked)
254b467… ragelink 1106 # ---------------------------------------------------------------------------
254b467… ragelink 1107
254b467… ragelink 1108
254b467… ragelink 1109 @pytest.mark.django_db
254b467… ragelink 1110 class TestCodeBlameViewMocked:
254b467… ragelink 1111 def test_blame_renders_with_dates(self, admin_client, sample_project):
254b467… ragelink 1112 slug = sample_project.slug
254b467… ragelink 1113 blame_lines = [
254b467… ragelink 1114 {"user": "dev", "date": "2026-01-01", "uuid": "abc", "line_num": 1, "text": "line one"},
254b467… ragelink 1115 {"user": "dev", "date": "2026-03-01", "uuid": "def", "line_num": 2, "text": "line two"},
254b467… ragelink 1116 ]
254b467… ragelink 1117 with patch("fossil.views._get_repo_and_reader") as mock_grr:
254b467… ragelink 1118 reader = MagicMock()
254b467… ragelink 1119 reader.__enter__ = MagicMock(return_value=reader)
254b467… ragelink 1120 reader.__exit__ = MagicMock(return_value=False)
254b467… ragelink 1121 repo = FossilRepository.objects.get(project=sample_project)
254b467… ragelink 1122 mock_grr.return_value = (sample_project, repo, reader)
254b467… ragelink 1123 with patch("fossil.cli.FossilCLI") as mock_cli_cls:
254b467… ragelink 1124 cli = mock_cli_cls.return_value
254b467… ragelink 1125 cli.is_available.return_value = True
254b467… ragelink 1126 cli.blame.return_value = blame_lines
254b467… ragelink 1127 response = admin_client.get(_url(slug, "code/blame/main.py"))
254b467… ragelink 1128 assert response.status_code == 200
254b467… ragelink 1129
254b467… ragelink 1130 def test_blame_no_fossil_binary(self, admin_client, sample_project):
254b467… ragelink 1131 slug = sample_project.slug
254b467… ragelink 1132 with patch("fossil.views._get_repo_and_reader") as mock_grr:
254b467… ragelink 1133 reader = MagicMock()
254b467… ragelink 1134 reader.__enter__ = MagicMock(return_value=reader)
254b467… ragelink 1135 reader.__exit__ = MagicMock(return_value=False)
254b467… ragelink 1136 repo = FossilRepository.objects.get(project=sample_project)
254b467… ragelink 1137 mock_grr.return_value = (sample_project, repo, reader)
254b467… ragelink 1138 with patch("fossil.cli.FossilCLI") as mock_cli_cls:
254b467… ragelink 1139 cli = mock_cli_cls.return_value
254b467… ragelink 1140 cli.is_available.return_value = False
254b467… ragelink 1141 response = admin_client.get(_url(slug, "code/blame/main.py"))
254b467… ragelink 1142 assert response.status_code == 200
254b467… ragelink 1143
254b467… ragelink 1144
254b467… ragelink 1145 # ---------------------------------------------------------------------------
254b467… ragelink 1146 # Toggle watch / notifications
254b467… ragelink 1147 # ---------------------------------------------------------------------------
254b467… ragelink 1148
254b467… ragelink 1149
254b467… ragelink 1150 @pytest.mark.django_db
254b467… ragelink 1151 class TestToggleWatch:
254b467… ragelink 1152 def test_watch_project(self, admin_client, sample_project, admin_user):
254b467… ragelink 1153 from fossil.notifications import ProjectWatch
254b467… ragelink 1154
254b467… ragelink 1155 response = admin_client.post(_url(sample_project.slug, "watch/"))
254b467… ragelink 1156 assert response.status_code == 302
254b467… ragelink 1157 assert ProjectWatch.objects.filter(user=admin_user, project=sample_project).exists()
254b467… ragelink 1158
254b467… ragelink 1159 def test_unwatch_project(self, admin_client, sample_project, admin_user):
254b467… ragelink 1160 from fossil.notifications import ProjectWatch
254b467… ragelink 1161
254b467… ragelink 1162 ProjectWatch.objects.create(user=admin_user, project=sample_project, event_filter="all", created_by=admin_user)
254b467… ragelink 1163 response = admin_client.post(_url(sample_project.slug, "watch/"))
254b467… ragelink 1164 assert response.status_code == 302
254b467… ragelink 1165 # Should be soft-deleted
254b467… ragelink 1166 assert not ProjectWatch.objects.filter(user=admin_user, project=sample_project, deleted_at__isnull=True).exists()
254b467… ragelink 1167
254b467… ragelink 1168 def test_watch_with_event_filter(self, admin_client, sample_project, admin_user):
254b467… ragelink 1169 from fossil.notifications import ProjectWatch
254b467… ragelink 1170
254b467… ragelink 1171 response = admin_client.post(_url(sample_project.slug, "watch/"), {"event_filter": "checkins"})
254b467… ragelink 1172 assert response.status_code == 302
254b467… ragelink 1173 watch = ProjectWatch.objects.get(user=admin_user, project=sample_project)
254b467… ragelink 1174 assert watch.event_filter == "checkins"
254b467… ragelink 1175
254b467… ragelink 1176 def test_watch_denied_anon(self, client, sample_project):
254b467… ragelink 1177 response = client.post(_url(sample_project.slug, "watch/"))
254b467… ragelink 1178 assert response.status_code == 302 # redirect to login
254b467… ragelink 1179
254b467… ragelink 1180
254b467… ragelink 1181 # ---------------------------------------------------------------------------
254b467… ragelink 1182 # Checkin detail (mocked) -- the diff computation path
254b467… ragelink 1183 # ---------------------------------------------------------------------------
254b467… ragelink 1184
254b467… ragelink 1185
254b467… ragelink 1186 @pytest.mark.django_db
254b467… ragelink 1187 class TestCheckinDetailViewMocked:
254b467… ragelink 1188 def test_checkin_detail_with_diffs(self, admin_client, sample_project):
254b467… ragelink 1189 slug = sample_project.slug
254b467… ragelink 1190 checkin = CheckinDetail(
254b467… ragelink 1191 uuid="abc123full",
254b467… ragelink 1192 timestamp=datetime(2026, 3, 1, tzinfo=UTC),
254b467… ragelink 1193 user="dev",
254b467… ragelink 1194 comment="fix bug",
254b467… ragelink 1195 branch="trunk",
254b467… ragelink 1196 files_changed=[
254b467… ragelink 1197 {"name": "fix.py", "uuid": "new-uuid", "prev_uuid": "old-uuid", "change_type": "M"},
254b467… ragelink 1198 ],
254b467… ragelink 1199 )
254b467… ragelink 1200 with patch("fossil.views._get_repo_and_reader") as mock_grr:
254b467… ragelink 1201 reader = MagicMock()
254b467… ragelink 1202 reader.__enter__ = MagicMock(return_value=reader)
254b467… ragelink 1203 reader.__exit__ = MagicMock(return_value=False)
254b467… ragelink 1204 reader.get_checkin_detail.return_value = checkin
254b467… ragelink 1205
254b467… ragelink 1206 def fake_content(uuid):
254b467… ragelink 1207 if uuid == "old-uuid":
254b467… ragelink 1208 return b"old line\n"
254b467… ragelink 1209 return b"new line\n"
254b467… ragelink 1210
254b467… ragelink 1211 reader.get_file_content.side_effect = fake_content
254b467… ragelink 1212 repo = FossilRepository.objects.get(project=sample_project)
254b467… ragelink 1213 mock_grr.return_value = (sample_project, repo, reader)
254b467… ragelink 1214
254b467… ragelink 1215 with patch("fossil.ci.StatusCheck") as mock_sc:
254b467… ragelink 1216 mock_sc.objects.filter.return_value = []
254b467… ragelink 1217 response = admin_client.get(_url(slug, "checkin/abc123full/"))
254b467… ragelink 1218 assert response.status_code == 200
254b467… ragelink 1219 content = response.content.decode()
254b467… ragelink 1220 assert "fix bug" in content
254b467… ragelink 1221
254b467… ragelink 1222 def test_checkin_not_found(self, admin_client, sample_project):
254b467… ragelink 1223 slug = sample_project.slug
254b467… ragelink 1224 with patch("fossil.views._get_repo_and_reader") as mock_grr:
254b467… ragelink 1225 reader = MagicMock()
254b467… ragelink 1226 reader.__enter__ = MagicMock(return_value=reader)
254b467… ragelink 1227 reader.__exit__ = MagicMock(return_value=False)
254b467… ragelink 1228 reader.get_checkin_detail.return_value = None
254b467… ragelink 1229 repo = FossilRepository.objects.get(project=sample_project)
254b467… ragelink 1230 mock_grr.return_value = (sample_project, repo, reader)
254b467… ragelink 1231 response = admin_client.get(_url(slug, "checkin/nonexistent/"))
254b467… ragelink 1232 assert response.status_code == 404
254b467… ragelink 1233
254b467… ragelink 1234
254b467… ragelink 1235 # ---------------------------------------------------------------------------
254b467… ragelink 1236 # Technote views (mocked)
254b467… ragelink 1237 # ---------------------------------------------------------------------------
254b467… ragelink 1238
254b467… ragelink 1239
254b467… ragelink 1240 @pytest.mark.django_db
254b467… ragelink 1241 class TestTechnoteViewsMocked:
254b467… ragelink 1242 def test_technote_list(self, admin_client, sample_project):
254b467… ragelink 1243 slug = sample_project.slug
254b467… ragelink 1244 notes = [SimpleNamespace(uuid="n1", comment="Release notes", user="dev", timestamp=datetime(2026, 3, 1, tzinfo=UTC))]
254b467… ragelink 1245 with patch("fossil.views._get_repo_and_reader") as mock_grr:
254b467… ragelink 1246 reader = MagicMock()
254b467… ragelink 1247 reader.__enter__ = MagicMock(return_value=reader)
254b467… ragelink 1248 reader.__exit__ = MagicMock(return_value=False)
254b467… ragelink 1249 reader.get_technotes.return_value = notes
254b467… ragelink 1250 repo = FossilRepository.objects.get(project=sample_project)
254b467… ragelink 1251 mock_grr.return_value = (sample_project, repo, reader)
254b467… ragelink 1252 response = admin_client.get(_url(slug, "technotes/"))
254b467… ragelink 1253 assert response.status_code == 200
254b467… ragelink 1254
254b467… ragelink 1255 def test_technote_detail(self, admin_client, sample_project):
254b467… ragelink 1256 slug = sample_project.slug
254b467… ragelink 1257 note = {
254b467… ragelink 1258 "uuid": "n1",
254b467… ragelink 1259 "comment": "Release v1",
254b467… ragelink 1260 "body": "## Changes\n- Fix",
254b467… ragelink 1261 "user": "dev",
254b467… ragelink 1262 "timestamp": datetime(2026, 3, 1, tzinfo=UTC),
254b467… ragelink 1263 }
254b467… ragelink 1264 with patch("fossil.views._get_repo_and_reader") as mock_grr:
254b467… ragelink 1265 reader = MagicMock()
254b467… ragelink 1266 reader.__enter__ = MagicMock(return_value=reader)
254b467… ragelink 1267 reader.__exit__ = MagicMock(return_value=False)
254b467… ragelink 1268 reader.get_technote_detail.return_value = note
254b467… ragelink 1269 repo = FossilRepository.objects.get(project=sample_project)
254b467… ragelink 1270 mock_grr.return_value = (sample_project, repo, reader)
254b467… ragelink 1271 response = admin_client.get(_url(slug, "technotes/n1/"))
254b467… ragelink 1272 assert response.status_code == 200
254b467… ragelink 1273
254b467… ragelink 1274 def test_technote_detail_not_found(self, admin_client, sample_project):
254b467… ragelink 1275 slug = sample_project.slug
254b467… ragelink 1276 with patch("fossil.views._get_repo_and_reader") as mock_grr:
254b467… ragelink 1277 reader = MagicMock()
254b467… ragelink 1278 reader.__enter__ = MagicMock(return_value=reader)
254b467… ragelink 1279 reader.__exit__ = MagicMock(return_value=False)
254b467… ragelink 1280 reader.get_technote_detail.return_value = None
254b467… ragelink 1281 repo = FossilRepository.objects.get(project=sample_project)
254b467… ragelink 1282 mock_grr.return_value = (sample_project, repo, reader)
254b467… ragelink 1283 response = admin_client.get(_url(slug, "technotes/nonexistent/"))
254b467… ragelink 1284 assert response.status_code == 404
254b467… ragelink 1285
254b467… ragelink 1286
254b467… ragelink 1287 # ---------------------------------------------------------------------------
254b467… ragelink 1288 # Unversioned files list (mocked)
254b467… ragelink 1289 # ---------------------------------------------------------------------------
254b467… ragelink 1290
254b467… ragelink 1291
254b467… ragelink 1292 @pytest.mark.django_db
254b467… ragelink 1293 class TestUnversionedListViewMocked:
254b467… ragelink 1294 def test_unversioned_list(self, admin_client, sample_project):
254b467… ragelink 1295 slug = sample_project.slug
254b467… ragelink 1296 files = [SimpleNamespace(name="logo.png", size=1024, mtime=datetime(2026, 3, 1, tzinfo=UTC), hash="abc")]
254b467… ragelink 1297 with patch("fossil.views._get_repo_and_reader") as mock_grr:
254b467… ragelink 1298 reader = MagicMock()
254b467… ragelink 1299 reader.__enter__ = MagicMock(return_value=reader)
254b467… ragelink 1300 reader.__exit__ = MagicMock(return_value=False)
254b467… ragelink 1301 reader.get_unversioned_files.return_value = files
254b467… ragelink 1302 repo = FossilRepository.objects.get(project=sample_project)
254b467… ragelink 1303 mock_grr.return_value = (sample_project, repo, reader)
254b467… ragelink 1304 response = admin_client.get(_url(slug, "files/"))
254b467… ragelink 1305 assert response.status_code == 200
254b467… ragelink 1306
254b467… ragelink 1307 def test_unversioned_search(self, admin_client, sample_project):
254b467… ragelink 1308 slug = sample_project.slug
254b467… ragelink 1309 files = [
254b467… ragelink 1310 SimpleNamespace(name="logo.png", size=1024, mtime=datetime(2026, 3, 1, tzinfo=UTC), hash="abc"),
254b467… ragelink 1311 SimpleNamespace(name="data.csv", size=512, mtime=datetime(2026, 3, 1, tzinfo=UTC), hash="def"),
254b467… ragelink 1312 ]
254b467… ragelink 1313 with patch("fossil.views._get_repo_and_reader") as mock_grr:
254b467… ragelink 1314 reader = MagicMock()
254b467… ragelink 1315 reader.__enter__ = MagicMock(return_value=reader)
254b467… ragelink 1316 reader.__exit__ = MagicMock(return_value=False)
254b467… ragelink 1317 reader.get_unversioned_files.return_value = files
254b467… ragelink 1318 repo = FossilRepository.objects.get(project=sample_project)
254b467… ragelink 1319 mock_grr.return_value = (sample_project, repo, reader)
254b467… ragelink 1320 response = admin_client.get(_url(slug, "files/?search=logo"))
254b467… ragelink 1321 assert response.status_code == 200
254b467… ragelink 1322
254b467… ragelink 1323
254b467… ragelink 1324 # ---------------------------------------------------------------------------
254b467… ragelink 1325 # Fossil docs views (mocked)
254b467… ragelink 1326 # ---------------------------------------------------------------------------
254b467… ragelink 1327
254b467… ragelink 1328
254b467… ragelink 1329 @pytest.mark.django_db
254b467… ragelink 1330 class TestFossilDocsViewsMocked:
254b467… ragelink 1331 def test_docs_index(self, admin_client, sample_project):
254b467… ragelink 1332 slug = sample_project.slug
254b467… ragelink 1333 response = admin_client.get(_url(slug, "docs/"))
254b467… ragelink 1334 assert response.status_code == 200
254b467… ragelink 1335
254b467… ragelink 1336 def test_doc_page_renders(self, admin_client, sample_project):
254b467… ragelink 1337 slug = sample_project.slug
254b467… ragelink 1338 files = [_make_file_entry(name="www/concepts.wiki", uuid="f1")]
254b467… ragelink 1339 with patch("fossil.views._get_repo_and_reader") as mock_grr:
254b467… ragelink 1340 reader = MagicMock()
254b467… ragelink 1341 reader.__enter__ = MagicMock(return_value=reader)
254b467… ragelink 1342 reader.__exit__ = MagicMock(return_value=False)
254b467… ragelink 1343 reader.get_latest_checkin_uuid.return_value = "abc"
254b467… ragelink 1344 reader.get_files_at_checkin.return_value = files
254b467… ragelink 1345 reader.get_file_content.return_value = b"<h1>Concepts</h1>\n<p>Text here</p>"
254b467… ragelink 1346 repo = FossilRepository.objects.get(project=sample_project)
254b467… ragelink 1347 mock_grr.return_value = (sample_project, repo, reader)
254b467… ragelink 1348 response = admin_client.get(_url(slug, "docs/www/concepts.wiki"))
254b467… ragelink 1349 assert response.status_code == 200
254b467… ragelink 1350 assert "Concepts" in response.content.decode()
254b467… ragelink 1351
254b467… ragelink 1352 def test_doc_page_not_found(self, admin_client, sample_project):
254b467… ragelink 1353 slug = sample_project.slug
254b467… ragelink 1354 with patch("fossil.views._get_repo_and_reader") as mock_grr:
254b467… ragelink 1355 reader = MagicMock()
254b467… ragelink 1356 reader.__enter__ = MagicMock(return_value=reader)
254b467… ragelink 1357 reader.__exit__ = MagicMock(return_value=False)
254b467… ragelink 1358 reader.get_latest_checkin_uuid.return_value = "abc"
254b467… ragelink 1359 reader.get_files_at_checkin.return_value = []
254b467… ragelink 1360 repo = FossilRepository.objects.get(project=sample_project)
254b467… ragelink 1361 mock_grr.return_value = (sample_project, repo, reader)
254b467… ragelink 1362 response = admin_client.get(_url(slug, "docs/www/missing.wiki"))
254b467… ragelink 1363 assert response.status_code == 404
254b467… ragelink 1364
254b467… ragelink 1365
254b467… ragelink 1366 # ---------------------------------------------------------------------------
254b467… ragelink 1367 # User activity view (mocked)
254b467… ragelink 1368 # ---------------------------------------------------------------------------
254b467… ragelink 1369
254b467… ragelink 1370
254b467… ragelink 1371 @pytest.mark.django_db
254b467… ragelink 1372 class TestUserActivityViewMocked:
254b467… ragelink 1373 def test_user_activity_renders(self, admin_client, sample_project):
254b467… ragelink 1374 slug = sample_project.slug
254b467… ragelink 1375 activity = {
254b467… ragelink 1376 "checkin_count": 25,
254b467… ragelink 1377 "checkins": [{"uuid": "a", "comment": "fix", "timestamp": datetime(2026, 3, 1, tzinfo=UTC)}],
254b467… ragelink 1378 "daily_activity": {"2026-03-01": 5},
254b467… ragelink 1379 }
254b467… ragelink 1380 with patch("fossil.views._get_repo_and_reader") as mock_grr:
254b467… ragelink 1381 reader = MagicMock()
254b467… ragelink 1382 reader.__enter__ = MagicMock(return_value=reader)
254b467… ragelink 1383 reader.__exit__ = MagicMock(return_value=False)
254b467… ragelink 1384 reader.get_user_activity.return_value = activity
254b467… ragelink 1385 repo = FossilRepository.objects.get(project=sample_project)
254b467… ragelink 1386 mock_grr.return_value = (sample_project, repo, reader)
254b467… ragelink 1387 response = admin_client.get(_url(slug, "user/dev/"))
254b467… ragelink 1388 assert response.status_code == 200
254b467… ragelink 1389
254b467… ragelink 1390
254b467… ragelink 1391 # ---------------------------------------------------------------------------
254b467… ragelink 1392 # Status badge view
254b467… ragelink 1393 # ---------------------------------------------------------------------------
254b467… ragelink 1394
254b467… ragelink 1395
254b467… ragelink 1396 @pytest.mark.django_db
254b467… ragelink 1397 class TestStatusBadgeView:
254b467… ragelink 1398 def test_badge_unknown(self, admin_client, sample_project):
254b467… ragelink 1399 slug = sample_project.slug
254b467… ragelink 1400 response = admin_client.get(_url(slug, "api/status/abc123/badge.svg"))
254b467… ragelink 1401 assert response.status_code == 200
254b467… ragelink 1402 assert response["Content-Type"] == "image/svg+xml"
254b467… ragelink 1403 content = response.content.decode()
254b467… ragelink 1404 assert "unknown" in content
254b467… ragelink 1405
254b467… ragelink 1406 def test_badge_passing(self, admin_client, sample_project, fossil_repo_obj):
254b467… ragelink 1407 from fossil.ci import StatusCheck
254b467… ragelink 1408
254b467… ragelink 1409 StatusCheck.objects.create(repository=fossil_repo_obj, checkin_uuid="pass123", context="ci/test", state="success")
254b467… ragelink 1410 response = admin_client.get(_url(sample_project.slug, "api/status/pass123/badge.svg"))
254b467… ragelink 1411 assert response.status_code == 200
254b467… ragelink 1412 assert "passing" in response.content.decode()
254b467… ragelink 1413
254b467… ragelink 1414 def test_badge_failing(self, admin_client, sample_project, fossil_repo_obj):
254b467… ragelink 1415 from fossil.ci import StatusCheck
254b467… ragelink 1416
254b467… ragelink 1417 StatusCheck.objects.create(repository=fossil_repo_obj, checkin_uuid="fail123", context="ci/test", state="failure")
254b467… ragelink 1418 response = admin_client.get(_url(sample_project.slug, "api/status/fail123/badge.svg"))
254b467… ragelink 1419 assert response.status_code == 200
254b467… ragelink 1420 assert "failing" in response.content.decode()
254b467… ragelink 1421
254b467… ragelink 1422 def test_badge_pending(self, admin_client, sample_project, fossil_repo_obj):
254b467… ragelink 1423 from fossil.ci import StatusCheck
254b467… ragelink 1424
254b467… ragelink 1425 StatusCheck.objects.create(repository=fossil_repo_obj, checkin_uuid="pend123", context="ci/test", state="pending")
254b467… ragelink 1426 response = admin_client.get(_url(sample_project.slug, "api/status/pend123/badge.svg"))
254b467… ragelink 1427 assert response.status_code == 200
254b467… ragelink 1428 assert "pending" in response.content.decode()
254b467… ragelink 1429
254b467… ragelink 1430
254b467… ragelink 1431 # ---------------------------------------------------------------------------
254b467… ragelink 1432 # Status check API (GET path)
254b467… ragelink 1433 # ---------------------------------------------------------------------------
254b467… ragelink 1434
254b467… ragelink 1435
254b467… ragelink 1436 @pytest.mark.django_db
254b467… ragelink 1437 class TestStatusCheckApiGet:
254b467… ragelink 1438 def test_get_status_checks(self, admin_client, sample_project, fossil_repo_obj):
254b467… ragelink 1439 from fossil.ci import StatusCheck
254b467… ragelink 1440
254b467… ragelink 1441 StatusCheck.objects.create(
254b467… ragelink 1442 repository=fossil_repo_obj, checkin_uuid="apicheck", context="ci/lint", state="success", description="OK"
254b467… ragelink 1443 )
254b467… ragelink 1444 response = admin_client.get(_url(sample_project.slug, "api/status?checkin=apicheck"))
254b467… ragelink 1445 assert response.status_code == 200
254b467… ragelink 1446 data = response.json()
254b467… ragelink 1447 assert data["checkin"] == "apicheck"
254b467… ragelink 1448 assert len(data["checks"]) == 1
254b467… ragelink 1449 assert data["checks"][0]["context"] == "ci/lint"
254b467… ragelink 1450
254b467… ragelink 1451 def test_get_status_no_checkin_param(self, admin_client, sample_project, fossil_repo_obj):
254b467… ragelink 1452 response = admin_client.get(_url(sample_project.slug, "api/status"))
254b467… ragelink 1453 assert response.status_code == 400
254b467… ragelink 1454
254b467… ragelink 1455 def test_get_status_denied_private(self, client, sample_project, fossil_repo_obj):
254b467… ragelink 1456 """Anonymous user denied on private project."""
254b467… ragelink 1457 response = client.get(_url(sample_project.slug, "api/status?checkin=abc"))
254b467… ragelink 1458 assert response.status_code == 403
254b467… ragelink 1459
254b467… ragelink 1460
254b467… ragelink 1461 # ---------------------------------------------------------------------------
254b467… ragelink 1462 # Fossil xfer endpoint
254b467… ragelink 1463 # ---------------------------------------------------------------------------
254b467… ragelink 1464
254b467… ragelink 1465
254b467… ragelink 1466 @pytest.mark.django_db
254b467… ragelink 1467 class TestFossilXferView:
254b467… ragelink 1468 def test_xfer_get_public_project(self, client, sample_project, fossil_repo_obj):
254b467… ragelink 1469 """GET on xfer endpoint shows clone info for public projects."""
254b467… ragelink 1470 sample_project.visibility = "public"
254b467… ragelink 1471 sample_project.save()
254b467… ragelink 1472 response = client.get(_url(sample_project.slug, "xfer"))
254b467… ragelink 1473 assert response.status_code == 200
254b467… ragelink 1474 assert "clone" in response.content.decode().lower()
254b467… ragelink 1475
254b467… ragelink 1476 def test_xfer_get_private_denied(self, client, sample_project, fossil_repo_obj):
254b467… ragelink 1477 """GET on xfer endpoint denied for private projects without auth."""
254b467… ragelink 1478 response = client.get(_url(sample_project.slug, "xfer"))
254b467… ragelink 1479 assert response.status_code == 403
254b467… ragelink 1480
254b467… ragelink 1481 def test_xfer_method_not_allowed(self, admin_client, sample_project, fossil_repo_obj):
254b467… ragelink 1482 """PUT/PATCH not supported."""
254b467… ragelink 1483 response = admin_client.put(_url(sample_project.slug, "xfer"))
254b467… ragelink 1484 assert response.status_code == 405
254b467… ragelink 1485
254b467… ragelink 1486
254b467… ragelink 1487 # ---------------------------------------------------------------------------
254b467… ragelink 1488 # Build file tree helper
254b467… ragelink 1489 # ---------------------------------------------------------------------------
254b467… ragelink 1490
254b467… ragelink 1491
254b467… ragelink 1492 class TestBuildFileTree:
254b467… ragelink 1493 def test_root_listing(self):
254b467… ragelink 1494 from fossil.views import _build_file_tree
254b467… ragelink 1495
254b467… ragelink 1496 files = [
254b467… ragelink 1497 _make_file_entry(name="README.md", uuid="f1"),
254b467… ragelink 1498 _make_file_entry(name="src/main.py", uuid="f2"),
254b467… ragelink 1499 _make_file_entry(name="src/utils.py", uuid="f3"),
254b467… ragelink 1500 ]
254b467… ragelink 1501 tree = _build_file_tree(files)
254b467… ragelink 1502 # Should have 1 dir (src) and 1 file (README.md)
254b467… ragelink 1503 dirs = [e for e in tree if e["is_dir"]]
254b467… ragelink 1504 regular_files = [e for e in tree if not e["is_dir"]]
254b467… ragelink 1505 assert len(dirs) == 1
254b467… ragelink 1506 assert dirs[0]["name"] == "src"
254b467… ragelink 1507 assert len(regular_files) == 1
254b467… ragelink 1508 assert regular_files[0]["name"] == "README.md"
254b467… ragelink 1509
254b467… ragelink 1510 def test_subdir_listing(self):
254b467… ragelink 1511 from fossil.views import _build_file_tree
254b467… ragelink 1512
254b467… ragelink 1513 files = [
254b467… ragelink 1514 _make_file_entry(name="src/main.py", uuid="f2"),
254b467… ragelink 1515 _make_file_entry(name="src/utils.py", uuid="f3"),
254b467… ragelink 1516 _make_file_entry(name="src/lib/helper.py", uuid="f4"),
254b467… ragelink 1517 ]
254b467… ragelink 1518 tree = _build_file_tree(files, current_dir="src")
254b467… ragelink 1519 dirs = [e for e in tree if e["is_dir"]]
254b467… ragelink 1520 regular_files = [e for e in tree if not e["is_dir"]]
254b467… ragelink 1521 assert len(dirs) == 1
254b467… ragelink 1522 assert dirs[0]["name"] == "lib"
254b467… ragelink 1523 assert len(regular_files) == 2
254b467… ragelink 1524
254b467… ragelink 1525 def test_skips_bad_filenames(self):
254b467… ragelink 1526 from fossil.views import _build_file_tree
254b467… ragelink 1527
254b467… ragelink 1528 files = [
254b467… ragelink 1529 _make_file_entry(name="good.txt", uuid="f1"),
254b467… ragelink 1530 _make_file_entry(name="bad\nname.txt", uuid="f2"),
254b467… ragelink 1531 ]
254b467… ragelink 1532 tree = _build_file_tree(files)
254b467… ragelink 1533 assert len(tree) == 1
254b467… ragelink 1534 assert tree[0]["name"] == "good.txt"
254b467… ragelink 1535
254b467… ragelink 1536 def test_dirs_sorted_first(self):
254b467… ragelink 1537 from fossil.views import _build_file_tree
254b467… ragelink 1538
254b467… ragelink 1539 files = [
254b467… ragelink 1540 _make_file_entry(name="zebra.txt", uuid="f1"),
254b467… ragelink 1541 _make_file_entry(name="alpha/main.py", uuid="f2"),
254b467… ragelink 1542 ]
254b467… ragelink 1543 tree = _build_file_tree(files)
254b467… ragelink 1544 assert tree[0]["is_dir"] is True
254b467… ragelink 1545 assert tree[0]["name"] == "alpha"
254b467… ragelink 1546 assert tree[1]["is_dir"] is False
d365053… ragelink 1547
d365053… ragelink 1548
d365053… ragelink 1549 # ---------------------------------------------------------------------------
d365053… ragelink 1550 # Content rendering: more edge cases for _render_fossil_content
d365053… ragelink 1551 # ---------------------------------------------------------------------------
d365053… ragelink 1552
d365053… ragelink 1553
d365053… ragelink 1554 class TestRenderFossilContentEdgeCases:
d365053… ragelink 1555 def test_fossil_wiki_list_type_switch(self):
d365053… ragelink 1556 """Test switching from bullet list to ordered list in wiki content."""
d365053… ragelink 1557 from fossil.views import _render_fossil_content
d365053… ragelink 1558
d365053… ragelink 1559 content = "<div>Intro</div>\n* bullet\n1. ordered"
d365053… ragelink 1560 html = _render_fossil_content(content)
d365053… ragelink 1561 assert "<ul>" in html
d365053… ragelink 1562 assert "<ol>" in html
d365053… ragelink 1563 assert "bullet" in html
d365053… ragelink 1564 assert "ordered" in html
d365053… ragelink 1565
d365053… ragelink 1566 def test_fossil_wiki_link_relative_path(self):
d365053… ragelink 1567 from fossil.views import _render_fossil_content
d365053… ragelink 1568
d365053… ragelink 1569 content = "<p>[./subpage | Sub Page]</p>"
d365053… ragelink 1570 html = _render_fossil_content(content, project_slug="proj", base_path="www/")
d365053… ragelink 1571 assert "Sub Page" in html
d365053… ragelink 1572 assert "/www/" in html
d365053… ragelink 1573
d365053… ragelink 1574 def test_fossil_wiki_link_bare_path(self):
d365053… ragelink 1575 from fossil.views import _render_fossil_content
d365053… ragelink 1576
d365053… ragelink 1577 content = "<p>[page.wiki | Page]</p>"
d365053… ragelink 1578 html = _render_fossil_content(content, project_slug="proj", base_path="docs/")
d365053… ragelink 1579 assert "Page" in html
d365053… ragelink 1580
d365053… ragelink 1581 def test_fossil_wiki_p_wrap(self):
d365053… ragelink 1582 """Double newlines in wiki content get wrapped in <p> tags."""
d365053… ragelink 1583 from fossil.views import _render_fossil_content
d365053… ragelink 1584
d365053… ragelink 1585 content = "<div>First</div>\n\nSecond paragraph"
d365053… ragelink 1586 html = _render_fossil_content(content)
d365053… ragelink 1587 assert "<p>" in html
d365053… ragelink 1588
d365053… ragelink 1589 def test_markdown_with_tables(self):
d365053… ragelink 1590 from fossil.views import _render_fossil_content
d365053… ragelink 1591
d365053… ragelink 1592 content = "# Table\n\n| Col1 | Col2 |\n|------|------|\n| a | b |"
d365053… ragelink 1593 html = _render_fossil_content(content)
d365053… ragelink 1594 assert "<table>" in html
d365053… ragelink 1595
d365053… ragelink 1596 def test_markdown_fossil_link_with_base_path(self):
d365053… ragelink 1597 """Markdown-mode Fossil links with relative paths resolve using base_path."""
d365053… ragelink 1598 from fossil.views import _render_fossil_content
d365053… ragelink 1599
d365053… ragelink 1600 content = "# Page\n[file.wiki | Link]"
d365053… ragelink 1601 html = _render_fossil_content(content, project_slug="proj", base_path="docs/")
d365053… ragelink 1602 assert "Link" in html
d365053… ragelink 1603
d365053… ragelink 1604 def test_external_fossil_scm_wiki_rewrite(self):
d365053… ragelink 1605 from fossil.views import _rewrite_fossil_links
d365053… ragelink 1606
d365053… ragelink 1607 html = '<a href="https://fossil-scm.org/home/wiki/PageName">link</a>'
d365053… ragelink 1608 result = _rewrite_fossil_links(html, "proj")
d365053… ragelink 1609 assert "/projects/proj/fossil/wiki/page/PageName" in result
d365053… ragelink 1610
d365053… ragelink 1611 def test_external_fossil_scm_doc_rewrite(self):
d365053… ragelink 1612 from fossil.views import _rewrite_fossil_links
d365053… ragelink 1613
d365053… ragelink 1614 html = '<a href="https://www.fossil-scm.org/home/doc/trunk/www/file.wiki">doc</a>'
d365053… ragelink 1615 result = _rewrite_fossil_links(html, "proj")
d365053… ragelink 1616 assert "/projects/proj/fossil/docs/www/file.wiki" in result
d365053… ragelink 1617
d365053… ragelink 1618
d365053… ragelink 1619 # ---------------------------------------------------------------------------
d365053… ragelink 1620 # Compare checkins: with actual diff computation
d365053… ragelink 1621 # ---------------------------------------------------------------------------
d365053… ragelink 1622
d365053… ragelink 1623
d365053… ragelink 1624 @pytest.mark.django_db
d365053… ragelink 1625 class TestCompareWithDiffs:
d365053… ragelink 1626 def test_compare_produces_diff_lines(self, admin_client, sample_project):
d365053… ragelink 1627 """Compare with two checkins that have overlapping changed files produces unified diff."""
d365053… ragelink 1628 slug = sample_project.slug
d365053… ragelink 1629 from_detail = CheckinDetail(
d365053… ragelink 1630 uuid="from111",
d365053… ragelink 1631 timestamp=datetime(2026, 3, 1, tzinfo=UTC),
d365053… ragelink 1632 user="dev",
d365053… ragelink 1633 comment="before",
d365053… ragelink 1634 files_changed=[{"name": "app.py", "uuid": "old1", "prev_uuid": "", "change_type": "A"}],
d365053… ragelink 1635 )
d365053… ragelink 1636 to_detail = CheckinDetail(
d365053… ragelink 1637 uuid="to222",
d365053… ragelink 1638 timestamp=datetime(2026, 3, 2, tzinfo=UTC),
d365053… ragelink 1639 user="dev",
d365053… ragelink 1640 comment="after",
d365053… ragelink 1641 files_changed=[{"name": "app.py", "uuid": "new1", "prev_uuid": "old1", "change_type": "M"}],
d365053… ragelink 1642 )
d365053… ragelink 1643 with patch("fossil.views._get_repo_and_reader") as mock_grr:
d365053… ragelink 1644 reader = MagicMock()
d365053… ragelink 1645 reader.__enter__ = MagicMock(return_value=reader)
d365053… ragelink 1646 reader.__exit__ = MagicMock(return_value=False)
d365053… ragelink 1647 reader.get_checkin_detail.side_effect = lambda uuid: from_detail if "from" in uuid else to_detail
d365053… ragelink 1648
d365053… ragelink 1649 def file_content(uuid):
d365053… ragelink 1650 if uuid == "old1":
d365053… ragelink 1651 return b"line1\nline2\nline3\n"
d365053… ragelink 1652 return b"line1\nmodified\nline3\nnew_line\n"
d365053… ragelink 1653
d365053… ragelink 1654 reader.get_file_content.side_effect = file_content
d365053… ragelink 1655 repo = FossilRepository.objects.get(project=sample_project)
d365053… ragelink 1656 mock_grr.return_value = (sample_project, repo, reader)
d365053… ragelink 1657 response = admin_client.get(_url(slug, "compare/?from=from111&to=to222"))
d365053… ragelink 1658 assert response.status_code == 200
d365053… ragelink 1659
d365053… ragelink 1660
d365053… ragelink 1661 # ---------------------------------------------------------------------------
d365053… ragelink 1662 # Repo settings view
d365053… ragelink 1663 # ---------------------------------------------------------------------------
d365053… ragelink 1664
d365053… ragelink 1665
d365053… ragelink 1666 @pytest.mark.django_db
d365053… ragelink 1667 class TestRepoSettingsView:
d365053… ragelink 1668 def test_settings_get_denied_for_non_admin(self, no_perm_client, sample_project):
d365053… ragelink 1669 response = no_perm_client.get(_url(sample_project.slug, "settings/"))
d365053… ragelink 1670 assert response.status_code == 403
d365053… ragelink 1671
d365053… ragelink 1672 def test_settings_get_denied_for_anon(self, client, sample_project):
d365053… ragelink 1673 response = client.get(_url(sample_project.slug, "settings/"))
d365053… ragelink 1674 assert response.status_code == 302
d365053… ragelink 1675
d365053… ragelink 1676 def test_settings_post_update_remote(self, admin_client, sample_project, fossil_repo_obj):
d365053… ragelink 1677 response = admin_client.post(
d365053… ragelink 1678 _url(sample_project.slug, "settings/"),
d365053… ragelink 1679 {"action": "update_remote", "remote_url": "https://fossil.example.com/repo"},
d365053… ragelink 1680 )
d365053… ragelink 1681 assert response.status_code == 302
d365053… ragelink 1682 fossil_repo_obj.refresh_from_db()
d365053… ragelink 1683 assert fossil_repo_obj.remote_url == "https://fossil.example.com/repo"
d365053… ragelink 1684
d365053… ragelink 1685
d365053… ragelink 1686 # ---------------------------------------------------------------------------
d365053… ragelink 1687 # Fossil doc_page: directory index fallback
d365053… ragelink 1688 # ---------------------------------------------------------------------------
d365053… ragelink 1689
d365053… ragelink 1690
d365053… ragelink 1691 @pytest.mark.django_db
d365053… ragelink 1692 class TestDocPageIndexFallback:
d365053… ragelink 1693 def test_doc_page_directory_index(self, admin_client, sample_project):
d365053… ragelink 1694 """Requesting a directory path falls back to index.html."""
d365053… ragelink 1695 slug = sample_project.slug
d365053… ragelink 1696 files = [_make_file_entry(name="www/index.html", uuid="idx1")]
d365053… ragelink 1697 with patch("fossil.views._get_repo_and_reader") as mock_grr:
d365053… ragelink 1698 reader = MagicMock()
d365053… ragelink 1699 reader.__enter__ = MagicMock(return_value=reader)
d365053… ragelink 1700 reader.__exit__ = MagicMock(return_value=False)
d365053… ragelink 1701 reader.get_latest_checkin_uuid.return_value = "abc"
d365053… ragelink 1702 reader.get_files_at_checkin.return_value = files
d365053… ragelink 1703 reader.get_file_content.return_value = b"<h1>Index</h1>"
d365053… ragelink 1704 repo = FossilRepository.objects.get(project=sample_project)
d365053… ragelink 1705 mock_grr.return_value = (sample_project, repo, reader)
d365053… ragelink 1706 response = admin_client.get(_url(slug, "docs/www/"))
d365053… ragelink 1707 assert response.status_code == 200
d365053… ragelink 1708 assert "Index" in response.content.decode()
d365053… ragelink 1709
d365053… ragelink 1710
d365053… ragelink 1711 # ---------------------------------------------------------------------------
d365053… ragelink 1712 # Code blame: age coloring edge cases
d365053… ragelink 1713 # ---------------------------------------------------------------------------
d365053… ragelink 1714
d365053… ragelink 1715
d365053… ragelink 1716 @pytest.mark.django_db
d365053… ragelink 1717 class TestCodeBlameAgeColoring:
d365053… ragelink 1718 def test_blame_all_same_date(self, admin_client, sample_project):
d365053… ragelink 1719 """All blame lines have the same date -- date_range is 1 to avoid division by zero."""
d365053… ragelink 1720 slug = sample_project.slug
d365053… ragelink 1721 blame_lines = [
d365053… ragelink 1722 {"user": "dev", "date": "2026-03-01", "uuid": "abc", "line_num": 1, "text": "line1"},
d365053… ragelink 1723 {"user": "dev", "date": "2026-03-01", "uuid": "abc", "line_num": 2, "text": "line2"},
d365053… ragelink 1724 ]
d365053… ragelink 1725 with patch("fossil.views._get_repo_and_reader") as mock_grr:
d365053… ragelink 1726 reader = MagicMock()
d365053… ragelink 1727 reader.__enter__ = MagicMock(return_value=reader)
d365053… ragelink 1728 reader.__exit__ = MagicMock(return_value=False)
d365053… ragelink 1729 repo = FossilRepository.objects.get(project=sample_project)
d365053… ragelink 1730 mock_grr.return_value = (sample_project, repo, reader)
d365053… ragelink 1731 with patch("fossil.cli.FossilCLI") as mock_cli_cls:
d365053… ragelink 1732 cli = mock_cli_cls.return_value
d365053… ragelink 1733 cli.is_available.return_value = True
d365053… ragelink 1734 cli.blame.return_value = blame_lines
d365053… ragelink 1735 response = admin_client.get(_url(slug, "code/blame/main.py"))
d365053… ragelink 1736 assert response.status_code == 200
d365053… ragelink 1737
d365053… ragelink 1738 def test_blame_no_dates(self, admin_client, sample_project):
d365053… ragelink 1739 """Blame lines with no dates -- fallback to gray."""
d365053… ragelink 1740 slug = sample_project.slug
d365053… ragelink 1741 blame_lines = [
d365053… ragelink 1742 {"user": "dev", "date": "", "uuid": "abc", "line_num": 1, "text": "line1"},
d365053… ragelink 1743 ]
d365053… ragelink 1744 with patch("fossil.views._get_repo_and_reader") as mock_grr:
d365053… ragelink 1745 reader = MagicMock()
d365053… ragelink 1746 reader.__enter__ = MagicMock(return_value=reader)
d365053… ragelink 1747 reader.__exit__ = MagicMock(return_value=False)
d365053… ragelink 1748 repo = FossilRepository.objects.get(project=sample_project)
d365053… ragelink 1749 mock_grr.return_value = (sample_project, repo, reader)
d365053… ragelink 1750 with patch("fossil.cli.FossilCLI") as mock_cli_cls:
d365053… ragelink 1751 cli = mock_cli_cls.return_value
d365053… ragelink 1752 cli.is_available.return_value = True
d365053… ragelink 1753 cli.blame.return_value = blame_lines
d365053… ragelink 1754 response = admin_client.get(_url(slug, "code/blame/main.py"))
d365053… ragelink 1755 assert response.status_code == 200
d365053… ragelink 1756
d365053… ragelink 1757
d365053… ragelink 1758 # ---------------------------------------------------------------------------
d365053… ragelink 1759 # Wiki CRUD (create/edit) -- requires mocking FossilCLI
d365053… ragelink 1760 # ---------------------------------------------------------------------------
d365053… ragelink 1761
d365053… ragelink 1762
d365053… ragelink 1763 @pytest.mark.django_db
d365053… ragelink 1764 class TestWikiCreateEditMocked:
d365053… ragelink 1765 def test_wiki_create_get_form(self, admin_client, sample_project):
d365053… ragelink 1766 """GET wiki create shows form for writers."""
d365053… ragelink 1767 slug = sample_project.slug
d365053… ragelink 1768 with patch("fossil.views._get_repo_and_reader") as mock_grr:
d365053… ragelink 1769 reader = MagicMock()
d365053… ragelink 1770 reader.__enter__ = MagicMock(return_value=reader)
d365053… ragelink 1771 reader.__exit__ = MagicMock(return_value=False)
d365053… ragelink 1772 repo = FossilRepository.objects.get(project=sample_project)
d365053… ragelink 1773 mock_grr.return_value = (sample_project, repo, reader)
d365053… ragelink 1774 response = admin_client.get(_url(slug, "wiki/create/"))
d365053… ragelink 1775 assert response.status_code == 200
d365053… ragelink 1776 assert "New Wiki Page" in response.content.decode()
d365053… ragelink 1777
d365053… ragelink 1778 def test_wiki_create_post(self, admin_client, sample_project):
d365053… ragelink 1779 slug = sample_project.slug
d365053… ragelink 1780 with patch("fossil.views._get_repo_and_reader") as mock_grr:
d365053… ragelink 1781 reader = MagicMock()
d365053… ragelink 1782 reader.__enter__ = MagicMock(return_value=reader)
d365053… ragelink 1783 reader.__exit__ = MagicMock(return_value=False)
d365053… ragelink 1784 repo = FossilRepository.objects.get(project=sample_project)
d365053… ragelink 1785 mock_grr.return_value = (sample_project, repo, reader)
d365053… ragelink 1786 with patch("fossil.cli.FossilCLI") as mock_cli_cls:
d365053… ragelink 1787 cli = mock_cli_cls.return_value
d365053… ragelink 1788 cli.wiki_create.return_value = True
d365053… ragelink 1789 response = admin_client.post(_url(slug, "wiki/create/"), {"name": "NewPage", "content": "# New Page"})
d365053… ragelink 1790 assert response.status_code == 302
d365053… ragelink 1791
d365053… ragelink 1792 def test_wiki_edit_get_form(self, admin_client, sample_project):
d365053… ragelink 1793 slug = sample_project.slug
d365053… ragelink 1794 page = WikiPage(name="EditMe", content="old content", last_modified=datetime(2026, 3, 1, tzinfo=UTC), user="admin")
d365053… ragelink 1795 with patch("fossil.views._get_repo_and_reader") as mock_grr:
d365053… ragelink 1796 reader = MagicMock()
d365053… ragelink 1797 reader.__enter__ = MagicMock(return_value=reader)
d365053… ragelink 1798 reader.__exit__ = MagicMock(return_value=False)
d365053… ragelink 1799 reader.get_wiki_page.return_value = page
d365053… ragelink 1800 repo = FossilRepository.objects.get(project=sample_project)
d365053… ragelink 1801 mock_grr.return_value = (sample_project, repo, reader)
d365053… ragelink 1802 response = admin_client.get(_url(slug, "wiki/edit/EditMe"))
d365053… ragelink 1803 assert response.status_code == 200
d365053… ragelink 1804
d365053… ragelink 1805 def test_wiki_edit_post(self, admin_client, sample_project):
d365053… ragelink 1806 slug = sample_project.slug
d365053… ragelink 1807 page = WikiPage(name="EditMe", content="old content", last_modified=datetime(2026, 3, 1, tzinfo=UTC), user="admin")
d365053… ragelink 1808 with patch("fossil.views._get_repo_and_reader") as mock_grr:
d365053… ragelink 1809 reader = MagicMock()
d365053… ragelink 1810 reader.__enter__ = MagicMock(return_value=reader)
d365053… ragelink 1811 reader.__exit__ = MagicMock(return_value=False)
d365053… ragelink 1812 reader.get_wiki_page.return_value = page
d365053… ragelink 1813 repo = FossilRepository.objects.get(project=sample_project)
d365053… ragelink 1814 mock_grr.return_value = (sample_project, repo, reader)
d365053… ragelink 1815 with patch("fossil.cli.FossilCLI") as mock_cli_cls:
d365053… ragelink 1816 cli = mock_cli_cls.return_value
d365053… ragelink 1817 cli.wiki_commit.return_value = True
d365053… ragelink 1818 response = admin_client.post(_url(slug, "wiki/edit/EditMe"), {"content": "# Updated"})
d365053… ragelink 1819 assert response.status_code == 302
d365053… ragelink 1820
d365053… ragelink 1821 def test_wiki_edit_not_found(self, admin_client, sample_project):
d365053… ragelink 1822 slug = sample_project.slug
d365053… ragelink 1823 with patch("fossil.views._get_repo_and_reader") as mock_grr:
d365053… ragelink 1824 reader = MagicMock()
d365053… ragelink 1825 reader.__enter__ = MagicMock(return_value=reader)
d365053… ragelink 1826 reader.__exit__ = MagicMock(return_value=False)
d365053… ragelink 1827 reader.get_wiki_page.return_value = None
d365053… ragelink 1828 repo = FossilRepository.objects.get(project=sample_project)
d365053… ragelink 1829 mock_grr.return_value = (sample_project, repo, reader)
d365053… ragelink 1830 response = admin_client.get(_url(slug, "wiki/edit/Missing"))
d365053… ragelink 1831 assert response.status_code == 404
d365053… ragelink 1832
d365053… ragelink 1833 def test_wiki_create_denied_for_no_perm(self, no_perm_client, sample_project):
d365053… ragelink 1834 response = no_perm_client.get(_url(sample_project.slug, "wiki/create/"))
d365053… ragelink 1835 assert response.status_code == 403
d365053… ragelink 1836
d365053… ragelink 1837
d365053… ragelink 1838 # ---------------------------------------------------------------------------
d365053… ragelink 1839 # Ticket CRUD (create/edit/comment) -- requires mocking FossilCLI
d365053… ragelink 1840 # ---------------------------------------------------------------------------
d365053… ragelink 1841
d365053… ragelink 1842
d365053… ragelink 1843 @pytest.mark.django_db
d365053… ragelink 1844 class TestTicketCrudMocked:
d365053… ragelink 1845 def test_ticket_create_get_form(self, admin_client, sample_project):
d365053… ragelink 1846 slug = sample_project.slug
d365053… ragelink 1847 with patch("fossil.views._get_repo_and_reader") as mock_grr:
d365053… ragelink 1848 reader = MagicMock()
d365053… ragelink 1849 reader.__enter__ = MagicMock(return_value=reader)
d365053… ragelink 1850 reader.__exit__ = MagicMock(return_value=False)
d365053… ragelink 1851 repo = FossilRepository.objects.get(project=sample_project)
d365053… ragelink 1852 mock_grr.return_value = (sample_project, repo, reader)
d365053… ragelink 1853 response = admin_client.get(_url(slug, "tickets/create/"))
d365053… ragelink 1854 assert response.status_code == 200
d365053… ragelink 1855 assert "New Ticket" in response.content.decode()
d365053… ragelink 1856
d365053… ragelink 1857 def test_ticket_create_post(self, admin_client, sample_project):
d365053… ragelink 1858 slug = sample_project.slug
d365053… ragelink 1859 with patch("fossil.views._get_repo_and_reader") as mock_grr:
d365053… ragelink 1860 reader = MagicMock()
d365053… ragelink 1861 reader.__enter__ = MagicMock(return_value=reader)
d365053… ragelink 1862 reader.__exit__ = MagicMock(return_value=False)
d365053… ragelink 1863 repo = FossilRepository.objects.get(project=sample_project)
d365053… ragelink 1864 mock_grr.return_value = (sample_project, repo, reader)
d365053… ragelink 1865 with patch("fossil.cli.FossilCLI") as mock_cli_cls:
d365053… ragelink 1866 cli = mock_cli_cls.return_value
d365053… ragelink 1867 cli.ticket_add.return_value = True
d365053… ragelink 1868 response = admin_client.post(
d365053… ragelink 1869 _url(slug, "tickets/create/"),
d365053… ragelink 1870 {"title": "New Bug", "body": "Description", "type": "Code_Defect"},
d365053… ragelink 1871 )
d365053… ragelink 1872 assert response.status_code == 302
d365053… ragelink 1873
d365053… ragelink 1874 @pytest.mark.skip(reason="ticket_edit.html template uses .split which is not valid Django template syntax -- pre-existing bug")
d365053… ragelink 1875 def test_ticket_edit_get_form(self, admin_client, sample_project):
d365053… ragelink 1876 slug = sample_project.slug
d365053… ragelink 1877 ticket = TicketEntry(
d365053… ragelink 1878 uuid="edit-tkt",
d365053… ragelink 1879 title="Edit me",
d365053… ragelink 1880 status="Open",
d365053… ragelink 1881 type="Code_Defect",
d365053… ragelink 1882 created=datetime(2026, 3, 1, tzinfo=UTC),
d365053… ragelink 1883 owner="dev",
d365053… ragelink 1884 )
d365053… ragelink 1885 with patch("fossil.views._get_repo_and_reader") as mock_grr:
d365053… ragelink 1886 reader = MagicMock()
d365053… ragelink 1887 reader.__enter__ = MagicMock(return_value=reader)
d365053… ragelink 1888 reader.__exit__ = MagicMock(return_value=False)
d365053… ragelink 1889 reader.get_ticket_detail.return_value = ticket
d365053… ragelink 1890 repo = FossilRepository.objects.get(project=sample_project)
d365053… ragelink 1891 mock_grr.return_value = (sample_project, repo, reader)
d365053… ragelink 1892 response = admin_client.get(_url(slug, "tickets/edit-tkt/edit/"))
d365053… ragelink 1893 assert response.status_code == 200
d365053… ragelink 1894
d365053… ragelink 1895 @pytest.mark.skip(reason="ticket_edit.html template uses .split which is not valid Django template syntax -- pre-existing bug")
d365053… ragelink 1896 def test_ticket_edit_post(self, admin_client, sample_project):
d365053… ragelink 1897 slug = sample_project.slug
d365053… ragelink 1898 ticket = TicketEntry(
d365053… ragelink 1899 uuid="edit-tkt",
d365053… ragelink 1900 title="Edit me",
d365053… ragelink 1901 status="Open",
d365053… ragelink 1902 type="Code_Defect",
d365053… ragelink 1903 created=datetime(2026, 3, 1, tzinfo=UTC),
d365053… ragelink 1904 owner="dev",
d365053… ragelink 1905 )
d365053… ragelink 1906 with patch("fossil.views._get_repo_and_reader") as mock_grr:
d365053… ragelink 1907 reader = MagicMock()
d365053… ragelink 1908 reader.__enter__ = MagicMock(return_value=reader)
d365053… ragelink 1909 reader.__exit__ = MagicMock(return_value=False)
d365053… ragelink 1910 reader.get_ticket_detail.return_value = ticket
d365053… ragelink 1911 repo = FossilRepository.objects.get(project=sample_project)
d365053… ragelink 1912 mock_grr.return_value = (sample_project, repo, reader)
d365053… ragelink 1913 with patch("fossil.cli.FossilCLI") as mock_cli_cls:
d365053… ragelink 1914 cli = mock_cli_cls.return_value
d365053… ragelink 1915 cli.ticket_change.return_value = True
d365053… ragelink 1916 response = admin_client.post(
d365053… ragelink 1917 _url(slug, "tickets/edit-tkt/edit/"),
d365053… ragelink 1918 {"title": "Updated Title", "status": "Closed", "type": "Code_Defect"},
d365053… ragelink 1919 )
d365053… ragelink 1920 assert response.status_code == 302
d365053… ragelink 1921
d365053… ragelink 1922 def test_ticket_comment_post(self, admin_client, sample_project):
d365053… ragelink 1923 slug = sample_project.slug
d365053… ragelink 1924 with patch("fossil.views._get_repo_and_reader") as mock_grr:
d365053… ragelink 1925 reader = MagicMock()
d365053… ragelink 1926 reader.__enter__ = MagicMock(return_value=reader)
d365053… ragelink 1927 reader.__exit__ = MagicMock(return_value=False)
d365053… ragelink 1928 repo = FossilRepository.objects.get(project=sample_project)
d365053… ragelink 1929 mock_grr.return_value = (sample_project, repo, reader)
d365053… ragelink 1930 with patch("fossil.cli.FossilCLI") as mock_cli_cls:
d365053… ragelink 1931 cli = mock_cli_cls.return_value
d365053… ragelink 1932 cli.ticket_change.return_value = True
d365053… ragelink 1933 response = admin_client.post(_url(slug, "tickets/tkt-uuid/comment/"), {"comment": "Looking into it"})
d365053… ragelink 1934 assert response.status_code == 302
d365053… ragelink 1935
d365053… ragelink 1936 def test_ticket_create_denied_for_no_perm(self, no_perm_client, sample_project):
d365053… ragelink 1937 response = no_perm_client.get(_url(sample_project.slug, "tickets/create/"))
d365053… ragelink 1938 assert response.status_code == 403
d365053… ragelink 1939
d365053… ragelink 1940
d365053… ragelink 1941 # ---------------------------------------------------------------------------
d365053… ragelink 1942 # Technote create/edit (mocked FossilCLI)
d365053… ragelink 1943 # ---------------------------------------------------------------------------
d365053… ragelink 1944
d365053… ragelink 1945
d365053… ragelink 1946 @pytest.mark.django_db
d365053… ragelink 1947 class TestTechnoteCrudMocked:
d365053… ragelink 1948 def test_technote_create_get(self, admin_client, sample_project):
d365053… ragelink 1949 slug = sample_project.slug
d365053… ragelink 1950 with patch("fossil.views._get_repo_and_reader") as mock_grr:
d365053… ragelink 1951 reader = MagicMock()
d365053… ragelink 1952 reader.__enter__ = MagicMock(return_value=reader)
d365053… ragelink 1953 reader.__exit__ = MagicMock(return_value=False)
d365053… ragelink 1954 repo = FossilRepository.objects.get(project=sample_project)
d365053… ragelink 1955 mock_grr.return_value = (sample_project, repo, reader)
d365053… ragelink 1956 response = admin_client.get(_url(slug, "technotes/create/"))
d365053… ragelink 1957 assert response.status_code == 200
d365053… ragelink 1958
d365053… ragelink 1959 def test_technote_create_post(self, admin_client, sample_project):
d365053… ragelink 1960 slug = sample_project.slug
d365053… ragelink 1961 with patch("fossil.views._get_repo_and_reader") as mock_grr:
d365053… ragelink 1962 reader = MagicMock()
d365053… ragelink 1963 reader.__enter__ = MagicMock(return_value=reader)
d365053… ragelink 1964 reader.__exit__ = MagicMock(return_value=False)
d365053… ragelink 1965 repo = FossilRepository.objects.get(project=sample_project)
d365053… ragelink 1966 mock_grr.return_value = (sample_project, repo, reader)
d365053… ragelink 1967 with patch("fossil.cli.FossilCLI") as mock_cli_cls:
d365053… ragelink 1968 cli = mock_cli_cls.return_value
d365053… ragelink 1969 cli.technote_create.return_value = True
d365053… ragelink 1970 response = admin_client.post(_url(slug, "technotes/create/"), {"title": "v1 Release", "body": "Notes"})
d365053… ragelink 1971 assert response.status_code == 302
d365053… ragelink 1972
d365053… ragelink 1973 def test_technote_edit_get(self, admin_client, sample_project):
d365053… ragelink 1974 slug = sample_project.slug
d365053… ragelink 1975 note = {"uuid": "tn1", "comment": "Edit me", "body": "old body", "user": "dev", "timestamp": datetime(2026, 3, 1, tzinfo=UTC)}
d365053… ragelink 1976 with patch("fossil.views._get_repo_and_reader") as mock_grr:
d365053… ragelink 1977 reader = MagicMock()
d365053… ragelink 1978 reader.__enter__ = MagicMock(return_value=reader)
d365053… ragelink 1979 reader.__exit__ = MagicMock(return_value=False)
d365053… ragelink 1980 reader.get_technote_detail.return_value = note
d365053… ragelink 1981 repo = FossilRepository.objects.get(project=sample_project)
d365053… ragelink 1982 mock_grr.return_value = (sample_project, repo, reader)
d365053… ragelink 1983 response = admin_client.get(_url(slug, "technotes/tn1/edit/"))
d365053… ragelink 1984 assert response.status_code == 200
d365053… ragelink 1985
d365053… ragelink 1986 def test_technote_edit_post(self, admin_client, sample_project):
d365053… ragelink 1987 slug = sample_project.slug
d365053… ragelink 1988 note = {"uuid": "tn1", "comment": "Edit me", "body": "old body", "user": "dev", "timestamp": datetime(2026, 3, 1, tzinfo=UTC)}
d365053… ragelink 1989 with patch("fossil.views._get_repo_and_reader") as mock_grr:
d365053… ragelink 1990 reader = MagicMock()
d365053… ragelink 1991 reader.__enter__ = MagicMock(return_value=reader)
d365053… ragelink 1992 reader.__exit__ = MagicMock(return_value=False)
d365053… ragelink 1993 reader.get_technote_detail.return_value = note
d365053… ragelink 1994 repo = FossilRepository.objects.get(project=sample_project)
d365053… ragelink 1995 mock_grr.return_value = (sample_project, repo, reader)
d365053… ragelink 1996 with patch("fossil.cli.FossilCLI") as mock_cli_cls:
d365053… ragelink 1997 cli = mock_cli_cls.return_value
d365053… ragelink 1998 cli.technote_edit.return_value = True
d365053… ragelink 1999 response = admin_client.post(_url(slug, "technotes/tn1/edit/"), {"body": "Updated notes"})
d365053… ragelink 2000 assert response.status_code == 302
d365053… ragelink 2001
d365053… ragelink 2002 def test_technote_edit_not_found(self, admin_client, sample_project):
d365053… ragelink 2003 slug = sample_project.slug
d365053… ragelink 2004 with patch("fossil.views._get_repo_and_reader") as mock_grr:
d365053… ragelink 2005 reader = MagicMock()
d365053… ragelink 2006 reader.__enter__ = MagicMock(return_value=reader)
d365053… ragelink 2007 reader.__exit__ = MagicMock(return_value=False)
d365053… ragelink 2008 reader.get_technote_detail.return_value = None
d365053… ragelink 2009 repo = FossilRepository.objects.get(project=sample_project)
d365053… ragelink 2010 mock_grr.return_value = (sample_project, repo, reader)
d365053… ragelink 2011 response = admin_client.get(_url(slug, "technotes/missing/edit/"))
d365053… ragelink 2012 assert response.status_code == 404
d365053… ragelink 2013
d365053… ragelink 2014 def test_technote_create_denied_for_no_perm(self, no_perm_client, sample_project):
d365053… ragelink 2015 response = no_perm_client.get(_url(sample_project.slug, "technotes/create/"))
d365053… ragelink 2016 assert response.status_code == 403
d365053… ragelink 2017
d365053… ragelink 2018
d365053… ragelink 2019 # ---------------------------------------------------------------------------
d365053… ragelink 2020 # User activity view (mocked) -- with empty heatmap
d365053… ragelink 2021 # ---------------------------------------------------------------------------
d365053… ragelink 2022
d365053… ragelink 2023
d365053… ragelink 2024 @pytest.mark.django_db
d365053… ragelink 2025 class TestUserActivityEmpty:
d365053… ragelink 2026 def test_user_activity_empty_data(self, admin_client, sample_project):
d365053… ragelink 2027 slug = sample_project.slug
d365053… ragelink 2028 activity = {"checkin_count": 0, "checkins": [], "daily_activity": {}}
d365053… ragelink 2029 with patch("fossil.views._get_repo_and_reader") as mock_grr:
d365053… ragelink 2030 reader = MagicMock()
d365053… ragelink 2031 reader.__enter__ = MagicMock(return_value=reader)
d365053… ragelink 2032 reader.__exit__ = MagicMock(return_value=False)
d365053… ragelink 2033 reader.get_user_activity.return_value = activity
d365053… ragelink 2034 repo = FossilRepository.objects.get(project=sample_project)
d365053… ragelink 2035 mock_grr.return_value = (sample_project, repo, reader)
d365053… ragelink 2036 response = admin_client.get(_url(slug, "user/unknown/"))
d365053… ragelink 2037 assert response.status_code == 200
d365053… ragelink 2038
d365053… ragelink 2039
d365053… ragelink 2040 # ---------------------------------------------------------------------------
d365053… ragelink 2041 # Technote list with search
d365053… ragelink 2042 # ---------------------------------------------------------------------------
d365053… ragelink 2043
d365053… ragelink 2044
d365053… ragelink 2045 @pytest.mark.django_db
d365053… ragelink 2046 class TestTechnoteListSearch:
d365053… ragelink 2047 def test_technote_search(self, admin_client, sample_project):
d365053… ragelink 2048 slug = sample_project.slug
d365053… ragelink 2049 notes = [
d365053… ragelink 2050 SimpleNamespace(uuid="n1", comment="Release notes v1", user="dev", timestamp=datetime(2026, 3, 1, tzinfo=UTC)),
d365053… ragelink 2051 SimpleNamespace(uuid="n2", comment="Sprint review", user="dev", timestamp=datetime(2026, 3, 2, tzinfo=UTC)),
d365053… ragelink 2052 ]
d365053… ragelink 2053 with patch("fossil.views._get_repo_and_reader") as mock_grr:
d365053… ragelink 2054 reader = MagicMock()
d365053… ragelink 2055 reader.__enter__ = MagicMock(return_value=reader)
d365053… ragelink 2056 reader.__exit__ = MagicMock(return_value=False)
d365053… ragelink 2057 reader.get_technotes.return_value = notes
d365053… ragelink 2058 repo = FossilRepository.objects.get(project=sample_project)
d365053… ragelink 2059 mock_grr.return_value = (sample_project, repo, reader)
d365053… ragelink 2060 response = admin_client.get(_url(slug, "technotes/?search=release"))
d365053… ragelink 2061 assert response.status_code == 200
d365053… ragelink 2062
d365053… ragelink 2063
d365053… ragelink 2064 # ---------------------------------------------------------------------------
d365053… ragelink 2065 # Code browser subdirectory
d365053… ragelink 2066 # ---------------------------------------------------------------------------
d365053… ragelink 2067
d365053… ragelink 2068
d365053… ragelink 2069 @pytest.mark.django_db
d365053… ragelink 2070 class TestCodeBrowserSubdir:
d365053… ragelink 2071 def test_code_browser_subdir_with_breadcrumbs(self, admin_client, sample_project):
d365053… ragelink 2072 slug = sample_project.slug
d365053… ragelink 2073 files = [
d365053… ragelink 2074 _make_file_entry(name="src/main.py", uuid="f1"),
d365053… ragelink 2075 _make_file_entry(name="src/lib/helper.py", uuid="f2"),
d365053… ragelink 2076 ]
d365053… ragelink 2077 metadata = RepoMetadata(project_name="Test", checkin_count=10)
d365053… ragelink 2078 with patch("fossil.views._get_repo_and_reader") as mock_grr:
d365053… ragelink 2079 reader = MagicMock()
d365053… ragelink 2080 reader.__enter__ = MagicMock(return_value=reader)
d365053… ragelink 2081 reader.__exit__ = MagicMock(return_value=False)
d365053… ragelink 2082 reader.get_latest_checkin_uuid.return_value = "abc"
d365053… ragelink 2083 reader.get_files_at_checkin.return_value = files
d365053… ragelink 2084 reader.get_metadata.return_value = metadata
d365053… ragelink 2085 reader.get_timeline.return_value = []
d365053… ragelink 2086 repo = FossilRepository.objects.get(project=sample_project)
d365053… ragelink 2087 mock_grr.return_value = (sample_project, repo, reader)
d365053… ragelink 2088 response = admin_client.get(_url(slug, "code/tree/src/"))
d365053… ragelink 2089 assert response.status_code == 200

Keyboard Shortcuts

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