1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
from __future__ import annotations
import json
from collections.abc import Generator
from pathlib import Path
from unittest.mock import patch
import pygit2
import pytest
from fastapi.testclient import TestClient
from pygit2 import Signature
from pygitweb.config import settings
from pygitweb.main import app
SIG = Signature("tester", "tester@example.com")
def _commit_tree(repo: pygit2.Repository, repo_dir: Path, files: dict[str, str], message: str) -> None:
for rel, content in files.items():
path = repo_dir / rel
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content, encoding="utf-8")
index = repo.index
for rel in files:
index.add(rel)
index.write()
tree = index.write_tree()
parents = [repo.head.target] if not repo.head_is_unborn else []
repo.create_commit("HEAD", SIG, SIG, message, tree, parents)
@pytest.fixture
def blog_env(tmp_path: Path) -> Generator[dict[str, str | TestClient], None, None]:
root = tmp_path / "blog-root"
repo_dir = root / "demo"
repo_dir.mkdir(parents=True)
repo = pygit2.init_repository(str(repo_dir), bare=False)
manifest = [
{
"name": "Hello Blog",
"url": "?article=articles/2024.01.01/post.md",
"tags": "intro test",
"created": "Jan 1 2024",
"preview": "",
"prev": "",
"next": "",
}
]
_commit_tree(
repo,
repo_dir,
{
"README.md": "seed\n",
"articles/manifest.json": json.dumps(manifest),
"articles/2024.01.01/post.md": "# Hello\n\nBody text.\n",
"assets/note.txt": "asset\n",
},
"add blog",
)
with (
patch.object(settings, "PROJECTROOT", str(root)),
patch.object(settings, "PROJECTS_LIST", str(root)),
patch.object(settings, "STRICT_EXPORT", False),
patch.object(settings, "EXPORT_OK", ""),
patch.object(settings, "AUTH", False),
):
yield {"project": "demo", "client": TestClient(app), "root": str(root)}
@pytest.fixture
def no_blog_env(tmp_path: Path) -> Generator[dict[str, str | TestClient], None, None]:
root = tmp_path / "no-blog-root"
repo_dir = root / "plain"
repo_dir.mkdir(parents=True)
repo = pygit2.init_repository(str(repo_dir), bare=False)
_commit_tree(repo, repo_dir, {"README.md": "no blog here\n"}, "initial")
with (
patch.object(settings, "PROJECTROOT", str(root)),
patch.object(settings, "PROJECTS_LIST", str(root)),
patch.object(settings, "STRICT_EXPORT", False),
patch.object(settings, "EXPORT_OK", ""),
patch.object(settings, "AUTH", False),
):
yield {"project": "plain", "client": TestClient(app)}
def test_blog_page_renders_when_manifest_exists(blog_env: dict[str, str | TestClient]) -> None:
client = blog_env["client"]
assert isinstance(client, TestClient)
resp = client.get(f"/project/{blog_env['project']}/blog/")
assert resp.status_code == 200
assert 'id="blog-root"' in resp.text
assert 'data-manifest="articles/manifest.json"' in resp.text
assert f'data-content-base="/project/{blog_env["project"]}/blog/raw/"' in resp.text
assert "/static/blog.js" in resp.text
def test_blog_page_404_without_manifest(no_blog_env: dict[str, str | TestClient]) -> None:
client = no_blog_env["client"]
assert isinstance(client, TestClient)
resp = client.get(f"/project/{no_blog_env['project']}/blog/")
assert resp.status_code == 404
def test_blog_raw_serves_manifest_and_markdown(blog_env: dict[str, str | TestClient]) -> None:
client = blog_env["client"]
assert isinstance(client, TestClient)
project = blog_env["project"]
manifest_resp = client.get(f"/project/{project}/blog/raw/articles/manifest.json")
assert manifest_resp.status_code == 200
payload = manifest_resp.json()
assert payload[0]["name"] == "Hello Blog"
md_resp = client.get(f"/project/{project}/blog/raw/articles/2024.01.01/post.md")
assert md_resp.status_code == 200
assert "# Hello" in md_resp.text
asset_resp = client.get(f"/project/{project}/blog/raw/assets/note.txt")
assert asset_resp.status_code == 200
assert asset_resp.text == "asset\n"
def test_blog_raw_rejects_missing_and_traversal(blog_env: dict[str, str | TestClient]) -> None:
client = blog_env["client"]
assert isinstance(client, TestClient)
project = blog_env["project"]
assert client.get(f"/project/{project}/blog/raw/missing.md").status_code == 404
assert client.get(f"/project/{project}/blog/raw/../README.md").status_code == 404
def test_summary_includes_blog_link(blog_env: dict[str, str | TestClient]) -> None:
client = blog_env["client"]
assert isinstance(client, TestClient)
resp = client.get(f"/project/{blog_env['project']}")
assert resp.status_code == 200
assert ">Blog</td>" in resp.text or ">Blog<" in resp.text
assert f'href="/project/{blog_env["project"]}/blog/">view blog</a>' in resp.text
def test_summary_shows_no_blog_when_missing(no_blog_env: dict[str, str | TestClient]) -> None:
client = no_blog_env["client"]
assert isinstance(client, TestClient)
resp = client.get(f"/project/{no_blog_env['project']}")
assert resp.status_code == 200
assert ">no blog</a>" in resp.text
assert 'style="color: #999; cursor: not-allowed;"' in resp.text