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
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
from __future__ import annotations
from collections.abc import Generator
from pathlib import Path
from unittest.mock import patch
import pygit2
import pytest
from fastapi.testclient import TestClient
from pygitweb.config import settings
from pygitweb.hooks_install import (
HOOK_BUNDLES,
HookStatus,
bundle_status,
get_bundle,
get_installed_version,
get_sample,
install,
install_bundle,
is_installed,
list_bundles,
list_installable_hooks,
list_installed_hooks,
list_samples,
read_hook_version,
remove,
remove_bundle,
status,
)
from pygitweb.main import app
def _create_initial_commit(repo: pygit2.Repository, repo_dir: Path) -> None:
(repo_dir / "README.md").write_text("seed\n", encoding="utf-8")
index = repo.index
index.add("README.md")
index.write()
tree = index.write_tree()
sig = pygit2.Signature("tester", "tester@example.com")
repo.create_commit("HEAD", sig, sig, "initial", tree, [])
@pytest.fixture
def project_env(tmp_path: Path) -> Generator[dict[str, str], None, None]:
root: Path = tmp_path / "root"
root.mkdir()
repo_dir: Path = root / "demo"
repo_dir.mkdir()
_create_initial_commit(pygit2.init_repository(str(repo_dir), bare=False), repo_dir)
with (
patch.object(settings, "PROJECTROOT", str(root)),
patch.object(settings, "PROJECTS_LIST", str(root)),
patch.object(settings, "PROJECT_MAXDEPTH", 3),
patch.object(settings, "STRICT_EXPORT", False),
patch.object(settings, "EXPORT_OK", ""),
patch.object(settings, "LIST_ALL", True),
patch.object(settings, "AUTH", False),
patch.object(settings, "MAXLOAD", None),
):
yield {"root": str(root), "project": "demo", "hooks_dir": str(repo_dir / ".git" / "hooks")}
class TestRegistry:
def test_list_samples_includes_notify_pair(self) -> None:
names: set[str] = {s["name"] for s in list_samples()}
assert {"post-commit.notify", "post-receive.notify"} <= names
def test_get_sample_target_is_filename_prefix(self) -> None:
sample = get_sample("post-receive.notify")
assert sample is not None
assert sample["target"] == "post-receive"
assert Path(sample["source"]).is_file()
def test_get_sample_unknown_returns_none(self) -> None:
assert get_sample("does-not-exist.foo") is None
def test_update_bundle_includes_both_notify_samples(self) -> None:
bundle = get_bundle("update")
assert bundle is not None
assert bundle["label"] == "Update Hook"
assert {m["name"] for m in bundle["members"]} == {"post-commit.notify", "post-receive.notify"}
def test_list_bundles_returns_update(self) -> None:
assert any(b["name"] == "update" for b in list_bundles())
assert "update" in HOOK_BUNDLES
class TestStatusInstallRemove:
def test_status_not_installed_when_no_file(self, project_env: dict[str, str]) -> None:
assert status(project_env["project"], "post-receive.notify") == HookStatus.NOT_INSTALLED
assert is_installed(project_env["project"], "post-receive.notify") is False
def test_install_then_status_installed(self, project_env: dict[str, str]) -> None:
assert install(project_env["project"], "post-receive.notify") == HookStatus.INSTALLED
assert is_installed(project_env["project"], "post-receive.notify") is True
target: Path = Path(project_env["hooks_dir"]) / "post-receive"
assert target.is_file()
def test_install_is_idempotent(self, project_env: dict[str, str]) -> None:
install(project_env["project"], "post-receive.notify")
assert install(project_env["project"], "post-receive.notify") == HookStatus.INSTALLED
def test_install_refuses_to_overwrite_custom_hook(self, project_env: dict[str, str]) -> None:
hooks_dir: Path = Path(project_env["hooks_dir"])
hooks_dir.mkdir(parents=True, exist_ok=True)
(hooks_dir / "post-receive").write_text("#!/bin/sh\necho custom\n", encoding="utf-8")
assert install(project_env["project"], "post-receive.notify") == HookStatus.DIFFERENT
assert (hooks_dir / "post-receive").read_text(encoding="utf-8") == "#!/bin/sh\necho custom\n"
def test_remove_uninstalls_only_when_content_matches(self, project_env: dict[str, str]) -> None:
install(project_env["project"], "post-receive.notify")
assert remove(project_env["project"], "post-receive.notify") == HookStatus.NOT_INSTALLED
assert not (Path(project_env["hooks_dir"]) / "post-receive").exists()
def test_remove_preserves_custom_hook(self, project_env: dict[str, str]) -> None:
hooks_dir: Path = Path(project_env["hooks_dir"])
hooks_dir.mkdir(parents=True, exist_ok=True)
custom: str = "#!/bin/sh\necho custom\n"
(hooks_dir / "post-receive").write_text(custom, encoding="utf-8")
assert remove(project_env["project"], "post-receive.notify") == HookStatus.DIFFERENT
assert (hooks_dir / "post-receive").read_text(encoding="utf-8") == custom
def test_unknown_sample_raises(self, project_env: dict[str, str]) -> None:
with pytest.raises(KeyError):
status(project_env["project"], "no-such.sample")
with pytest.raises(KeyError):
install(project_env["project"], "no-such.sample")
with pytest.raises(KeyError):
remove(project_env["project"], "no-such.sample")
class TestBundle:
def test_bundle_status_starts_not_installed(self, project_env: dict[str, str]) -> None:
assert bundle_status(project_env["project"], "update") == HookStatus.NOT_INSTALLED
def test_install_bundle_installs_all_members(self, project_env: dict[str, str]) -> None:
assert install_bundle(project_env["project"], "update") == HookStatus.INSTALLED
assert is_installed(project_env["project"], "post-commit.notify")
assert is_installed(project_env["project"], "post-receive.notify")
def test_partial_install_reports_not_installed(self, project_env: dict[str, str]) -> None:
install(project_env["project"], "post-commit.notify")
assert bundle_status(project_env["project"], "update") == HookStatus.NOT_INSTALLED
def test_install_bundle_completes_partial(self, project_env: dict[str, str]) -> None:
install(project_env["project"], "post-commit.notify")
assert install_bundle(project_env["project"], "update") == HookStatus.INSTALLED
assert is_installed(project_env["project"], "post-receive.notify")
def test_bundle_refuses_when_member_has_custom_hook(self, project_env: dict[str, str]) -> None:
hooks_dir: Path = Path(project_env["hooks_dir"])
hooks_dir.mkdir(parents=True, exist_ok=True)
(hooks_dir / "post-commit").write_text("#!/bin/sh\necho custom\n", encoding="utf-8")
assert bundle_status(project_env["project"], "update") == HookStatus.DIFFERENT
assert install_bundle(project_env["project"], "update") == HookStatus.DIFFERENT
assert remove_bundle(project_env["project"], "update") == HookStatus.DIFFERENT
assert (hooks_dir / "post-commit").read_text(encoding="utf-8") == "#!/bin/sh\necho custom\n"
def test_remove_bundle_uninstalls_only_managed_members(self, project_env: dict[str, str]) -> None:
install_bundle(project_env["project"], "update")
assert remove_bundle(project_env["project"], "update") == HookStatus.NOT_INSTALLED
assert not (Path(project_env["hooks_dir"]) / "post-commit").exists()
assert not (Path(project_env["hooks_dir"]) / "post-receive").exists()
def test_unknown_bundle_raises(self, project_env: dict[str, str]) -> None:
with pytest.raises(KeyError):
bundle_status(project_env["project"], "no-such-bundle")
with pytest.raises(KeyError):
install_bundle(project_env["project"], "no-such-bundle")
with pytest.raises(KeyError):
remove_bundle(project_env["project"], "no-such-bundle")
def _client() -> TestClient:
return TestClient(app)
class TestHookRoute:
def test_sample_check_then_add_then_remove_round_trip(self, project_env: dict[str, str]) -> None:
project: str = project_env["project"]
with _client() as client:
check_resp = client.post(
f"/project/{project}/hook",
params={"name": "post-receive.notify", "op": "check"},
)
assert check_resp.status_code == 200
body = check_resp.json()
assert body["installed"] is False
assert body["kind"] == "sample"
add_resp = client.post(
f"/project/{project}/hook",
params={"name": "post-receive.notify", "op": "add"},
)
assert add_resp.status_code == 200
body = add_resp.json()
assert body["installed"] is True
assert body["target"] == "post-receive"
assert body["label"] == "Post-receive Notify"
remove_resp = client.post(
f"/project/{project}/hook",
params={"name": "post-receive.notify", "op": "remove"},
)
assert remove_resp.status_code == 200
assert remove_resp.json()["installed"] is False
def test_bundle_check_then_add_then_remove_round_trip(self, project_env: dict[str, str]) -> None:
project: str = project_env["project"]
hooks_dir: Path = Path(project_env["hooks_dir"])
with _client() as client:
check_resp = client.post(
f"/project/{project}/hook",
params={"name": "update", "op": "check"},
)
assert check_resp.status_code == 200
body = check_resp.json()
assert body["installed"] is False
assert body["kind"] == "bundle"
assert body["label"] == "Update Hook"
assert set(body["members"]) == {"post-commit.notify", "post-receive.notify"}
add_resp = client.post(
f"/project/{project}/hook",
params={"name": "update", "op": "add"},
)
assert add_resp.status_code == 200
assert add_resp.json()["installed"] is True
assert (hooks_dir / "post-commit").is_file()
assert (hooks_dir / "post-receive").is_file()
remove_resp = client.post(
f"/project/{project}/hook",
params={"name": "update", "op": "remove"},
)
assert remove_resp.status_code == 200
assert remove_resp.json()["installed"] is False
assert not (hooks_dir / "post-commit").exists()
assert not (hooks_dir / "post-receive").exists()
def test_bundle_add_refuses_when_member_has_custom_hook(self, project_env: dict[str, str]) -> None:
hooks_dir: Path = Path(project_env["hooks_dir"])
hooks_dir.mkdir(parents=True, exist_ok=True)
(hooks_dir / "post-commit").write_text("#!/bin/sh\necho custom\n", encoding="utf-8")
with _client() as client:
resp = client.post(
f"/project/{project_env['project']}/hook",
params={"name": "update", "op": "add"},
)
assert resp.status_code == 409
def test_invalid_op_returns_422(self, project_env: dict[str, str]) -> None:
with _client() as client:
resp = client.post(
f"/project/{project_env['project']}/hook",
params={"name": "post-receive.notify", "op": "noop"},
)
assert resp.status_code == 422
def test_unknown_name_returns_404(self, project_env: dict[str, str]) -> None:
with _client() as client:
resp = client.post(
f"/project/{project_env['project']}/hook",
params={"name": "no-such.thing", "op": "check"},
)
assert resp.status_code == 404
def test_add_refuses_to_overwrite_custom_hook(self, project_env: dict[str, str]) -> None:
hooks_dir: Path = Path(project_env["hooks_dir"])
hooks_dir.mkdir(parents=True, exist_ok=True)
(hooks_dir / "post-receive").write_text("#!/bin/sh\necho custom\n", encoding="utf-8")
with _client() as client:
resp = client.post(
f"/project/{project_env['project']}/hook",
params={"name": "post-receive.notify", "op": "add"},
)
assert resp.status_code == 409
def test_hooks_list_endpoint_returns_status_for_samples_and_bundles(self, project_env: dict[str, str]) -> None:
with _client() as client:
resp = client.get(f"/project/{project_env['project']}/hooks")
assert resp.status_code == 200
body = resp.json()
assert body["project"] == project_env["project"]
sample_names: set[str] = {entry["name"] for entry in body["hooks"]}
assert {"post-commit.notify", "post-receive.notify"} <= sample_names
bundle_names: set[str] = {entry["name"] for entry in body["bundles"]}
assert "update" in bundle_names
def test_summary_renders_install_dropdown_when_no_hooks_installed(self, project_env: dict[str, str]) -> None:
with _client() as client:
resp = client.get(f"/project/{project_env['project']}")
assert resp.status_code == 200
assert 'id="hook-install-select"' in resp.text
assert "hook-install-btn" in resp.text
assert "Post-receive Notify" in resp.text
assert "No hooks installed." in resp.text
def test_summary_renders_uninstall_buttons_for_installed_hooks(self, project_env: dict[str, str]) -> None:
install(project_env["project"], "post-receive.notify")
install(project_env["project"], "pre-commit.ruff")
with _client() as client:
resp = client.get(f"/project/{project_env['project']}")
assert resp.status_code == 200
assert resp.text.count("Uninstall") == 2
assert 'data-sample="post-receive.notify"' in resp.text
assert 'data-sample="pre-commit.ruff"' in resp.text
assert ">v1<" in resp.text or "v1" in resp.text
def test_summary_hides_installed_hooks_from_install_dropdown(self, project_env: dict[str, str]) -> None:
install(project_env["project"], "post-commit.notify")
with _client() as client:
resp = client.get(f"/project/{project_env['project']}")
assert resp.status_code == 200
assert 'data-sample="post-commit.notify"' in resp.text
assert 'value="post-commit.notify"' not in resp.text
def test_summary_shows_custom_hook_conflict(self, project_env: dict[str, str]) -> None:
hooks_dir: Path = Path(project_env["hooks_dir"])
hooks_dir.mkdir(parents=True, exist_ok=True)
(hooks_dir / "post-receive").write_text("#!/bin/sh\necho custom\n", encoding="utf-8")
with _client() as client:
resp = client.get(f"/project/{project_env['project']}")
assert resp.status_code == 200
assert "custom hook installed; refusing to manage" in resp.text
class TestHookVersion:
def test_read_hook_version_from_sample(self) -> None:
sample = get_sample("post-receive.notify")
assert sample is not None
assert read_hook_version(Path(sample["source"])) == "1"
def test_get_installed_version_reads_installed_file(self, project_env: dict[str, str]) -> None:
install(project_env["project"], "post-receive.notify")
assert get_installed_version(project_env["project"], "post-receive.notify") == "1"
def test_list_installed_hooks_includes_only_present_hooks(self, project_env: dict[str, str]) -> None:
install(project_env["project"], "post-commit.notify")
installed = list_installed_hooks(project_env["project"])
assert {entry["name"] for entry in installed} == {"post-commit.notify"}
assert installed[0]["version"] == "1"
def test_list_installable_hooks_excludes_installed(self, project_env: dict[str, str]) -> None:
install(project_env["project"], "post-commit.notify")
installable = list_installable_hooks(project_env["project"])
assert all(entry["name"] != "post-commit.notify" for entry in installable)
class TestSummaryRefSwitcherStaticScript:
def test_static_script_subscribes_via_updates_endpoint(self) -> None:
with _client() as client:
resp = client.get("/static/summary-ref-switcher.js")
assert resp.status_code == 200
body: str = resp.text
assert "a=heads&updates=true" in body
assert "subscribeToUpdates" in body
assert "AbortController" in body
assert "loadOptions" in body
assert "loadState" in body