FossilRepo

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

Keyboard Shortcuts

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