FossilRepo
Security hardening, HTTP proxy fix, and expanded test coverage
Commit
d365053adab86f4b398ff6bb22729a977b5340d7c4be0fd90cb53e234e93f0d6
Parent
254b46777e916db…
1 file changed
+543
+543
| --- tests/test_views_coverage.py | ||
| +++ tests/test_views_coverage.py | ||
| @@ -1542,5 +1542,548 @@ | ||
| 1542 | 1542 | ] |
| 1543 | 1543 | tree = _build_file_tree(files) |
| 1544 | 1544 | assert tree[0]["is_dir"] is True |
| 1545 | 1545 | assert tree[0]["name"] == "alpha" |
| 1546 | 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 | |
| 1547 | 2090 |
| --- 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 |