FossilRepo

Security hardening, HTTP proxy fix, and expanded test coverage

ragelink 2026-04-07 23:19 trunk
Commit d365053adab86f4b398ff6bb22729a977b5340d7c4be0fd90cb53e234e93f0d6
--- tests/test_views_coverage.py
+++ tests/test_views_coverage.py
@@ -1542,5 +1542,548 @@
15421542
]
15431543
tree = _build_file_tree(files)
15441544
assert tree[0]["is_dir"] is True
15451545
assert tree[0]["name"] == "alpha"
15461546
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
15472090
--- tests/test_views_coverage.py
+++ tests/test_views_coverage.py
@@ -1542,5 +1542,548 @@
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
--- tests/test_views_coverage.py
+++ tests/test_views_coverage.py
@@ -1542,5 +1542,548 @@
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