pygitweb 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pygitweb/__init__.py +22 -0
- pygitweb/__meta__.py +9 -0
- pygitweb/actions.py +1303 -0
- pygitweb/addproject_permissions_test.py +114 -0
- pygitweb/api/__init__.py +0 -0
- pygitweb/api/action.py +20 -0
- pygitweb/api/plugins/__init__.py +0 -0
- pygitweb/api/plugins/project_zip.py +71 -0
- pygitweb/api/project.py +67 -0
- pygitweb/api/subpage.py +22 -0
- pygitweb/auth.py +387 -0
- pygitweb/auth.schema.json +106 -0
- pygitweb/auth_config.py +243 -0
- pygitweb/auth_oauth.py +343 -0
- pygitweb/auth_test.py +390 -0
- pygitweb/boards_permissions_test.py +113 -0
- pygitweb/change_queue.py +110 -0
- pygitweb/change_queue_routes_test.py +146 -0
- pygitweb/change_queue_test.py +91 -0
- pygitweb/comments_permissions_test.py +140 -0
- pygitweb/config.py +185 -0
- pygitweb/conftest.py +43 -0
- pygitweb/dependencies.py +87 -0
- pygitweb/formatting.py +182 -0
- pygitweb/formatting_test.py +68 -0
- pygitweb/git_helpers.py +454 -0
- pygitweb/git_helpers_test.py +223 -0
- pygitweb/gravatar.py +16 -0
- pygitweb/gravatar_test.py +10 -0
- pygitweb/hooks_install.py +278 -0
- pygitweb/hooks_install_test.py +363 -0
- pygitweb/hooks_permissions_test.py +116 -0
- pygitweb/main.py +887 -0
- pygitweb/merge_requests.py +230 -0
- pygitweb/merge_requests_test.py +85 -0
- pygitweb/mr_permissions_test.py +148 -0
- pygitweb/password_hash.py +74 -0
- pygitweb/password_hash_test.py +20 -0
- pygitweb/permissions.py +209 -0
- pygitweb/permissions_test.py +86 -0
- pygitweb/plugin_loader.py +83 -0
- pygitweb/projects.py +213 -0
- pygitweb/projects_test.py +186 -0
- pygitweb/sessions.py +73 -0
- pygitweb/settings.py +412 -0
- pygitweb/settings.schema.json +73 -0
- pygitweb/settings_config.py +122 -0
- pygitweb/settings_config_test.py +101 -0
- pygitweb/settings_permissions_test.py +76 -0
- pygitweb/static/blob-view.js +133 -0
- pygitweb/static/board.js +839 -0
- pygitweb/static/hook-install.js +53 -0
- pygitweb/static/icons/cloud-down.svg +1 -0
- pygitweb/static/icons/cloud-exclamation.svg +1 -0
- pygitweb/static/icons/cloud-filled.svg +1 -0
- pygitweb/static/icons/cloud-storm.svg +1 -0
- pygitweb/static/icons/cloud-up.svg +1 -0
- pygitweb/static/icons/cloud.svg +1 -0
- pygitweb/static/logo.svg +44 -0
- pygitweb/static/main.css +1210 -0
- pygitweb/static/navbar-auth.js +82 -0
- pygitweb/static/readme-render.js +174 -0
- pygitweb/static/search-page.js +139 -0
- pygitweb/static/summary-ref-switcher.js +130 -0
- pygitweb/static/themes/solarized.css +34 -0
- pygitweb/tasks.py +595 -0
- pygitweb/tasks_permissions_test.py +137 -0
- pygitweb/tasks_routes_test.py +430 -0
- pygitweb/templates/addproject.html +174 -0
- pygitweb/templates/blobdiff.html +57 -0
- pygitweb/templates/board.html +31 -0
- pygitweb/templates/board_card.html +44 -0
- pygitweb/templates/commitdiff.html +57 -0
- pygitweb/templates/hooks_summary_cell.html +37 -0
- pygitweb/templates/login.html +37 -0
- pygitweb/templates/merge_request_summary_cell.html +18 -0
- pygitweb/templates/merge_request_tag.html +90 -0
- pygitweb/templates/pagination.html +44 -0
- pygitweb/templates/preamble.html +89 -0
- pygitweb/templates/project_settings.html +13 -0
- pygitweb/templates/pygit2_settings.html +12 -0
- pygitweb/templates/pygitweb_settings.html +12 -0
- pygitweb/templates/search.html +77 -0
- pygitweb/templates/settings_form_field.html +27 -0
- pygitweb/templates/table.html +15 -0
- pygitweb/templates/user_profile.html +10 -0
- pygitweb/templates_env.py +21 -0
- pygitweb/timeline_cache.py +243 -0
- pygitweb/validation.py +75 -0
- pygitweb/validation_test.py +167 -0
- pygitweb-0.1.0.dist-info/METADATA +165 -0
- pygitweb-0.1.0.dist-info/RECORD +95 -0
- pygitweb-0.1.0.dist-info/WHEEL +5 -0
- pygitweb-0.1.0.dist-info/licenses/LICENSE +21 -0
- pygitweb-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Generator
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from unittest.mock import patch
|
|
6
|
+
|
|
7
|
+
import pygit2
|
|
8
|
+
import pytest
|
|
9
|
+
from fastapi.testclient import TestClient
|
|
10
|
+
|
|
11
|
+
from pygitweb.config import settings
|
|
12
|
+
from pygitweb.hooks_install import (
|
|
13
|
+
HOOK_BUNDLES,
|
|
14
|
+
HookStatus,
|
|
15
|
+
bundle_status,
|
|
16
|
+
get_bundle,
|
|
17
|
+
get_installed_version,
|
|
18
|
+
get_sample,
|
|
19
|
+
install,
|
|
20
|
+
install_bundle,
|
|
21
|
+
is_installed,
|
|
22
|
+
list_bundles,
|
|
23
|
+
list_installable_hooks,
|
|
24
|
+
list_installed_hooks,
|
|
25
|
+
list_samples,
|
|
26
|
+
read_hook_version,
|
|
27
|
+
remove,
|
|
28
|
+
remove_bundle,
|
|
29
|
+
status,
|
|
30
|
+
)
|
|
31
|
+
from pygitweb.main import app
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _create_initial_commit(repo: pygit2.Repository, repo_dir: Path) -> None:
|
|
35
|
+
(repo_dir / "README.md").write_text("seed\n", encoding="utf-8")
|
|
36
|
+
index = repo.index
|
|
37
|
+
index.add("README.md")
|
|
38
|
+
index.write()
|
|
39
|
+
tree = index.write_tree()
|
|
40
|
+
sig = pygit2.Signature("tester", "tester@example.com")
|
|
41
|
+
repo.create_commit("HEAD", sig, sig, "initial", tree, [])
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@pytest.fixture
|
|
45
|
+
def project_env(tmp_path: Path) -> Generator[dict[str, str], None, None]:
|
|
46
|
+
root: Path = tmp_path / "root"
|
|
47
|
+
root.mkdir()
|
|
48
|
+
repo_dir: Path = root / "demo"
|
|
49
|
+
repo_dir.mkdir()
|
|
50
|
+
_create_initial_commit(pygit2.init_repository(str(repo_dir), bare=False), repo_dir)
|
|
51
|
+
with (
|
|
52
|
+
patch.object(settings, "PROJECTROOT", str(root)),
|
|
53
|
+
patch.object(settings, "PROJECTS_LIST", str(root)),
|
|
54
|
+
patch.object(settings, "PROJECT_MAXDEPTH", 3),
|
|
55
|
+
patch.object(settings, "STRICT_EXPORT", False),
|
|
56
|
+
patch.object(settings, "EXPORT_OK", ""),
|
|
57
|
+
patch.object(settings, "LIST_ALL", True),
|
|
58
|
+
patch.object(settings, "AUTH", False),
|
|
59
|
+
patch.object(settings, "MAXLOAD", None),
|
|
60
|
+
):
|
|
61
|
+
yield {"root": str(root), "project": "demo", "hooks_dir": str(repo_dir / ".git" / "hooks")}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class TestRegistry:
|
|
65
|
+
def test_list_samples_includes_notify_pair(self) -> None:
|
|
66
|
+
names: set[str] = {s["name"] for s in list_samples()}
|
|
67
|
+
assert {"post-commit.notify", "post-receive.notify"} <= names
|
|
68
|
+
|
|
69
|
+
def test_get_sample_target_is_filename_prefix(self) -> None:
|
|
70
|
+
sample = get_sample("post-receive.notify")
|
|
71
|
+
assert sample is not None
|
|
72
|
+
assert sample["target"] == "post-receive"
|
|
73
|
+
assert Path(sample["source"]).is_file()
|
|
74
|
+
|
|
75
|
+
def test_get_sample_unknown_returns_none(self) -> None:
|
|
76
|
+
assert get_sample("does-not-exist.foo") is None
|
|
77
|
+
|
|
78
|
+
def test_update_bundle_includes_both_notify_samples(self) -> None:
|
|
79
|
+
bundle = get_bundle("update")
|
|
80
|
+
assert bundle is not None
|
|
81
|
+
assert bundle["label"] == "Update Hook"
|
|
82
|
+
assert {m["name"] for m in bundle["members"]} == {"post-commit.notify", "post-receive.notify"}
|
|
83
|
+
|
|
84
|
+
def test_list_bundles_returns_update(self) -> None:
|
|
85
|
+
assert any(b["name"] == "update" for b in list_bundles())
|
|
86
|
+
assert "update" in HOOK_BUNDLES
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class TestStatusInstallRemove:
|
|
90
|
+
def test_status_not_installed_when_no_file(self, project_env: dict[str, str]) -> None:
|
|
91
|
+
assert status(project_env["project"], "post-receive.notify") == HookStatus.NOT_INSTALLED
|
|
92
|
+
assert is_installed(project_env["project"], "post-receive.notify") is False
|
|
93
|
+
|
|
94
|
+
def test_install_then_status_installed(self, project_env: dict[str, str]) -> None:
|
|
95
|
+
assert install(project_env["project"], "post-receive.notify") == HookStatus.INSTALLED
|
|
96
|
+
assert is_installed(project_env["project"], "post-receive.notify") is True
|
|
97
|
+
target: Path = Path(project_env["hooks_dir"]) / "post-receive"
|
|
98
|
+
assert target.is_file()
|
|
99
|
+
|
|
100
|
+
def test_install_is_idempotent(self, project_env: dict[str, str]) -> None:
|
|
101
|
+
install(project_env["project"], "post-receive.notify")
|
|
102
|
+
assert install(project_env["project"], "post-receive.notify") == HookStatus.INSTALLED
|
|
103
|
+
|
|
104
|
+
def test_install_refuses_to_overwrite_custom_hook(self, project_env: dict[str, str]) -> None:
|
|
105
|
+
hooks_dir: Path = Path(project_env["hooks_dir"])
|
|
106
|
+
hooks_dir.mkdir(parents=True, exist_ok=True)
|
|
107
|
+
(hooks_dir / "post-receive").write_text("#!/bin/sh\necho custom\n", encoding="utf-8")
|
|
108
|
+
assert install(project_env["project"], "post-receive.notify") == HookStatus.DIFFERENT
|
|
109
|
+
assert (hooks_dir / "post-receive").read_text(encoding="utf-8") == "#!/bin/sh\necho custom\n"
|
|
110
|
+
|
|
111
|
+
def test_remove_uninstalls_only_when_content_matches(self, project_env: dict[str, str]) -> None:
|
|
112
|
+
install(project_env["project"], "post-receive.notify")
|
|
113
|
+
assert remove(project_env["project"], "post-receive.notify") == HookStatus.NOT_INSTALLED
|
|
114
|
+
assert not (Path(project_env["hooks_dir"]) / "post-receive").exists()
|
|
115
|
+
|
|
116
|
+
def test_remove_preserves_custom_hook(self, project_env: dict[str, str]) -> None:
|
|
117
|
+
hooks_dir: Path = Path(project_env["hooks_dir"])
|
|
118
|
+
hooks_dir.mkdir(parents=True, exist_ok=True)
|
|
119
|
+
custom: str = "#!/bin/sh\necho custom\n"
|
|
120
|
+
(hooks_dir / "post-receive").write_text(custom, encoding="utf-8")
|
|
121
|
+
assert remove(project_env["project"], "post-receive.notify") == HookStatus.DIFFERENT
|
|
122
|
+
assert (hooks_dir / "post-receive").read_text(encoding="utf-8") == custom
|
|
123
|
+
|
|
124
|
+
def test_unknown_sample_raises(self, project_env: dict[str, str]) -> None:
|
|
125
|
+
with pytest.raises(KeyError):
|
|
126
|
+
status(project_env["project"], "no-such.sample")
|
|
127
|
+
with pytest.raises(KeyError):
|
|
128
|
+
install(project_env["project"], "no-such.sample")
|
|
129
|
+
with pytest.raises(KeyError):
|
|
130
|
+
remove(project_env["project"], "no-such.sample")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class TestBundle:
|
|
134
|
+
def test_bundle_status_starts_not_installed(self, project_env: dict[str, str]) -> None:
|
|
135
|
+
assert bundle_status(project_env["project"], "update") == HookStatus.NOT_INSTALLED
|
|
136
|
+
|
|
137
|
+
def test_install_bundle_installs_all_members(self, project_env: dict[str, str]) -> None:
|
|
138
|
+
assert install_bundle(project_env["project"], "update") == HookStatus.INSTALLED
|
|
139
|
+
assert is_installed(project_env["project"], "post-commit.notify")
|
|
140
|
+
assert is_installed(project_env["project"], "post-receive.notify")
|
|
141
|
+
|
|
142
|
+
def test_partial_install_reports_not_installed(self, project_env: dict[str, str]) -> None:
|
|
143
|
+
install(project_env["project"], "post-commit.notify")
|
|
144
|
+
assert bundle_status(project_env["project"], "update") == HookStatus.NOT_INSTALLED
|
|
145
|
+
|
|
146
|
+
def test_install_bundle_completes_partial(self, project_env: dict[str, str]) -> None:
|
|
147
|
+
install(project_env["project"], "post-commit.notify")
|
|
148
|
+
assert install_bundle(project_env["project"], "update") == HookStatus.INSTALLED
|
|
149
|
+
assert is_installed(project_env["project"], "post-receive.notify")
|
|
150
|
+
|
|
151
|
+
def test_bundle_refuses_when_member_has_custom_hook(self, project_env: dict[str, str]) -> None:
|
|
152
|
+
hooks_dir: Path = Path(project_env["hooks_dir"])
|
|
153
|
+
hooks_dir.mkdir(parents=True, exist_ok=True)
|
|
154
|
+
(hooks_dir / "post-commit").write_text("#!/bin/sh\necho custom\n", encoding="utf-8")
|
|
155
|
+
assert bundle_status(project_env["project"], "update") == HookStatus.DIFFERENT
|
|
156
|
+
assert install_bundle(project_env["project"], "update") == HookStatus.DIFFERENT
|
|
157
|
+
assert remove_bundle(project_env["project"], "update") == HookStatus.DIFFERENT
|
|
158
|
+
assert (hooks_dir / "post-commit").read_text(encoding="utf-8") == "#!/bin/sh\necho custom\n"
|
|
159
|
+
|
|
160
|
+
def test_remove_bundle_uninstalls_only_managed_members(self, project_env: dict[str, str]) -> None:
|
|
161
|
+
install_bundle(project_env["project"], "update")
|
|
162
|
+
assert remove_bundle(project_env["project"], "update") == HookStatus.NOT_INSTALLED
|
|
163
|
+
assert not (Path(project_env["hooks_dir"]) / "post-commit").exists()
|
|
164
|
+
assert not (Path(project_env["hooks_dir"]) / "post-receive").exists()
|
|
165
|
+
|
|
166
|
+
def test_unknown_bundle_raises(self, project_env: dict[str, str]) -> None:
|
|
167
|
+
with pytest.raises(KeyError):
|
|
168
|
+
bundle_status(project_env["project"], "no-such-bundle")
|
|
169
|
+
with pytest.raises(KeyError):
|
|
170
|
+
install_bundle(project_env["project"], "no-such-bundle")
|
|
171
|
+
with pytest.raises(KeyError):
|
|
172
|
+
remove_bundle(project_env["project"], "no-such-bundle")
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _client() -> TestClient:
|
|
176
|
+
return TestClient(app)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class TestHookRoute:
|
|
180
|
+
def test_sample_check_then_add_then_remove_round_trip(self, project_env: dict[str, str]) -> None:
|
|
181
|
+
project: str = project_env["project"]
|
|
182
|
+
with _client() as client:
|
|
183
|
+
check_resp = client.post(
|
|
184
|
+
f"/project/{project}/hook",
|
|
185
|
+
params={"name": "post-receive.notify", "op": "check"},
|
|
186
|
+
)
|
|
187
|
+
assert check_resp.status_code == 200
|
|
188
|
+
body = check_resp.json()
|
|
189
|
+
assert body["installed"] is False
|
|
190
|
+
assert body["kind"] == "sample"
|
|
191
|
+
|
|
192
|
+
add_resp = client.post(
|
|
193
|
+
f"/project/{project}/hook",
|
|
194
|
+
params={"name": "post-receive.notify", "op": "add"},
|
|
195
|
+
)
|
|
196
|
+
assert add_resp.status_code == 200
|
|
197
|
+
body = add_resp.json()
|
|
198
|
+
assert body["installed"] is True
|
|
199
|
+
assert body["target"] == "post-receive"
|
|
200
|
+
assert body["label"] == "Post-receive Notify"
|
|
201
|
+
|
|
202
|
+
remove_resp = client.post(
|
|
203
|
+
f"/project/{project}/hook",
|
|
204
|
+
params={"name": "post-receive.notify", "op": "remove"},
|
|
205
|
+
)
|
|
206
|
+
assert remove_resp.status_code == 200
|
|
207
|
+
assert remove_resp.json()["installed"] is False
|
|
208
|
+
|
|
209
|
+
def test_bundle_check_then_add_then_remove_round_trip(self, project_env: dict[str, str]) -> None:
|
|
210
|
+
project: str = project_env["project"]
|
|
211
|
+
hooks_dir: Path = Path(project_env["hooks_dir"])
|
|
212
|
+
with _client() as client:
|
|
213
|
+
check_resp = client.post(
|
|
214
|
+
f"/project/{project}/hook",
|
|
215
|
+
params={"name": "update", "op": "check"},
|
|
216
|
+
)
|
|
217
|
+
assert check_resp.status_code == 200
|
|
218
|
+
body = check_resp.json()
|
|
219
|
+
assert body["installed"] is False
|
|
220
|
+
assert body["kind"] == "bundle"
|
|
221
|
+
assert body["label"] == "Update Hook"
|
|
222
|
+
assert set(body["members"]) == {"post-commit.notify", "post-receive.notify"}
|
|
223
|
+
|
|
224
|
+
add_resp = client.post(
|
|
225
|
+
f"/project/{project}/hook",
|
|
226
|
+
params={"name": "update", "op": "add"},
|
|
227
|
+
)
|
|
228
|
+
assert add_resp.status_code == 200
|
|
229
|
+
assert add_resp.json()["installed"] is True
|
|
230
|
+
assert (hooks_dir / "post-commit").is_file()
|
|
231
|
+
assert (hooks_dir / "post-receive").is_file()
|
|
232
|
+
|
|
233
|
+
remove_resp = client.post(
|
|
234
|
+
f"/project/{project}/hook",
|
|
235
|
+
params={"name": "update", "op": "remove"},
|
|
236
|
+
)
|
|
237
|
+
assert remove_resp.status_code == 200
|
|
238
|
+
assert remove_resp.json()["installed"] is False
|
|
239
|
+
assert not (hooks_dir / "post-commit").exists()
|
|
240
|
+
assert not (hooks_dir / "post-receive").exists()
|
|
241
|
+
|
|
242
|
+
def test_bundle_add_refuses_when_member_has_custom_hook(self, project_env: dict[str, str]) -> None:
|
|
243
|
+
hooks_dir: Path = Path(project_env["hooks_dir"])
|
|
244
|
+
hooks_dir.mkdir(parents=True, exist_ok=True)
|
|
245
|
+
(hooks_dir / "post-commit").write_text("#!/bin/sh\necho custom\n", encoding="utf-8")
|
|
246
|
+
with _client() as client:
|
|
247
|
+
resp = client.post(
|
|
248
|
+
f"/project/{project_env['project']}/hook",
|
|
249
|
+
params={"name": "update", "op": "add"},
|
|
250
|
+
)
|
|
251
|
+
assert resp.status_code == 409
|
|
252
|
+
|
|
253
|
+
def test_invalid_op_returns_422(self, project_env: dict[str, str]) -> None:
|
|
254
|
+
with _client() as client:
|
|
255
|
+
resp = client.post(
|
|
256
|
+
f"/project/{project_env['project']}/hook",
|
|
257
|
+
params={"name": "post-receive.notify", "op": "noop"},
|
|
258
|
+
)
|
|
259
|
+
assert resp.status_code == 422
|
|
260
|
+
|
|
261
|
+
def test_unknown_name_returns_404(self, project_env: dict[str, str]) -> None:
|
|
262
|
+
with _client() as client:
|
|
263
|
+
resp = client.post(
|
|
264
|
+
f"/project/{project_env['project']}/hook",
|
|
265
|
+
params={"name": "no-such.thing", "op": "check"},
|
|
266
|
+
)
|
|
267
|
+
assert resp.status_code == 404
|
|
268
|
+
|
|
269
|
+
def test_add_refuses_to_overwrite_custom_hook(self, project_env: dict[str, str]) -> None:
|
|
270
|
+
hooks_dir: Path = Path(project_env["hooks_dir"])
|
|
271
|
+
hooks_dir.mkdir(parents=True, exist_ok=True)
|
|
272
|
+
(hooks_dir / "post-receive").write_text("#!/bin/sh\necho custom\n", encoding="utf-8")
|
|
273
|
+
with _client() as client:
|
|
274
|
+
resp = client.post(
|
|
275
|
+
f"/project/{project_env['project']}/hook",
|
|
276
|
+
params={"name": "post-receive.notify", "op": "add"},
|
|
277
|
+
)
|
|
278
|
+
assert resp.status_code == 409
|
|
279
|
+
|
|
280
|
+
def test_hooks_list_endpoint_returns_status_for_samples_and_bundles(self, project_env: dict[str, str]) -> None:
|
|
281
|
+
with _client() as client:
|
|
282
|
+
resp = client.get(f"/project/{project_env['project']}/hooks")
|
|
283
|
+
assert resp.status_code == 200
|
|
284
|
+
body = resp.json()
|
|
285
|
+
assert body["project"] == project_env["project"]
|
|
286
|
+
sample_names: set[str] = {entry["name"] for entry in body["hooks"]}
|
|
287
|
+
assert {"post-commit.notify", "post-receive.notify"} <= sample_names
|
|
288
|
+
bundle_names: set[str] = {entry["name"] for entry in body["bundles"]}
|
|
289
|
+
assert "update" in bundle_names
|
|
290
|
+
|
|
291
|
+
def test_summary_renders_install_dropdown_when_no_hooks_installed(self, project_env: dict[str, str]) -> None:
|
|
292
|
+
with _client() as client:
|
|
293
|
+
resp = client.get(f"/project/{project_env['project']}")
|
|
294
|
+
assert resp.status_code == 200
|
|
295
|
+
assert 'id="hook-install-select"' in resp.text
|
|
296
|
+
assert "hook-install-btn" in resp.text
|
|
297
|
+
assert "Post-receive Notify" in resp.text
|
|
298
|
+
assert "No hooks installed." in resp.text
|
|
299
|
+
|
|
300
|
+
def test_summary_renders_uninstall_buttons_for_installed_hooks(self, project_env: dict[str, str]) -> None:
|
|
301
|
+
install(project_env["project"], "post-receive.notify")
|
|
302
|
+
install(project_env["project"], "pre-commit.ruff")
|
|
303
|
+
with _client() as client:
|
|
304
|
+
resp = client.get(f"/project/{project_env['project']}")
|
|
305
|
+
assert resp.status_code == 200
|
|
306
|
+
assert resp.text.count("Uninstall") == 2
|
|
307
|
+
assert 'data-sample="post-receive.notify"' in resp.text
|
|
308
|
+
assert 'data-sample="pre-commit.ruff"' in resp.text
|
|
309
|
+
assert ">v1<" in resp.text or "v1" in resp.text
|
|
310
|
+
|
|
311
|
+
def test_summary_hides_installed_hooks_from_install_dropdown(self, project_env: dict[str, str]) -> None:
|
|
312
|
+
install(project_env["project"], "post-commit.notify")
|
|
313
|
+
with _client() as client:
|
|
314
|
+
resp = client.get(f"/project/{project_env['project']}")
|
|
315
|
+
assert resp.status_code == 200
|
|
316
|
+
assert 'data-sample="post-commit.notify"' in resp.text
|
|
317
|
+
assert 'value="post-commit.notify"' not in resp.text
|
|
318
|
+
|
|
319
|
+
def test_summary_shows_custom_hook_conflict(self, project_env: dict[str, str]) -> None:
|
|
320
|
+
hooks_dir: Path = Path(project_env["hooks_dir"])
|
|
321
|
+
hooks_dir.mkdir(parents=True, exist_ok=True)
|
|
322
|
+
(hooks_dir / "post-receive").write_text("#!/bin/sh\necho custom\n", encoding="utf-8")
|
|
323
|
+
with _client() as client:
|
|
324
|
+
resp = client.get(f"/project/{project_env['project']}")
|
|
325
|
+
assert resp.status_code == 200
|
|
326
|
+
assert "custom hook installed; refusing to manage" in resp.text
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
class TestHookVersion:
|
|
330
|
+
def test_read_hook_version_from_sample(self) -> None:
|
|
331
|
+
sample = get_sample("post-receive.notify")
|
|
332
|
+
assert sample is not None
|
|
333
|
+
assert read_hook_version(Path(sample["source"])) == "1"
|
|
334
|
+
|
|
335
|
+
def test_get_installed_version_reads_installed_file(self, project_env: dict[str, str]) -> None:
|
|
336
|
+
install(project_env["project"], "post-receive.notify")
|
|
337
|
+
assert get_installed_version(project_env["project"], "post-receive.notify") == "1"
|
|
338
|
+
|
|
339
|
+
def test_list_installed_hooks_includes_only_present_hooks(self, project_env: dict[str, str]) -> None:
|
|
340
|
+
install(project_env["project"], "post-commit.notify")
|
|
341
|
+
installed = list_installed_hooks(project_env["project"])
|
|
342
|
+
assert {entry["name"] for entry in installed} == {"post-commit.notify"}
|
|
343
|
+
assert installed[0]["version"] == "1"
|
|
344
|
+
|
|
345
|
+
def test_list_installable_hooks_excludes_installed(self, project_env: dict[str, str]) -> None:
|
|
346
|
+
install(project_env["project"], "post-commit.notify")
|
|
347
|
+
installable = list_installable_hooks(project_env["project"])
|
|
348
|
+
assert all(entry["name"] != "post-commit.notify" for entry in installable)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
class TestSummaryRefSwitcherStaticScript:
|
|
352
|
+
def test_static_script_subscribes_via_updates_endpoint(self) -> None:
|
|
353
|
+
with _client() as client:
|
|
354
|
+
resp = client.get("/static/summary-ref-switcher.js")
|
|
355
|
+
assert resp.status_code == 200
|
|
356
|
+
body: str = resp.text
|
|
357
|
+
assert "a=heads&updates=true" in body
|
|
358
|
+
assert "subscribeToUpdates" in body
|
|
359
|
+
assert "AbortController" in body
|
|
360
|
+
assert "loadOptions" in body
|
|
361
|
+
assert "loadState" in body
|
|
362
|
+
assert "summary-tree-container" in body
|
|
363
|
+
assert "tree_html" in body
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Generator
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from unittest.mock import patch
|
|
6
|
+
|
|
7
|
+
import pygit2
|
|
8
|
+
import pytest
|
|
9
|
+
from fastapi.testclient import TestClient
|
|
10
|
+
|
|
11
|
+
from pygitweb.auth_config import AuthConfig, LocalUser
|
|
12
|
+
from pygitweb.config import settings
|
|
13
|
+
from pygitweb.conftest import client_as_user
|
|
14
|
+
from pygitweb.main import app
|
|
15
|
+
from pygitweb.permissions import Permission, PermissionsMap, permission_key
|
|
16
|
+
|
|
17
|
+
_HOOKS_AUTH = AuthConfig(
|
|
18
|
+
auth_mode="local",
|
|
19
|
+
local_users=[LocalUser(user="admin", password="secret"), LocalUser(user="viewer", password="secret")],
|
|
20
|
+
oauth_permissions=PermissionsMap.model_validate({
|
|
21
|
+
"*": [],
|
|
22
|
+
permission_key(Permission.HOOKS, "demo"): ["admin"],
|
|
23
|
+
}),
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _create_initial_commit(repo: pygit2.Repository, repo_dir: Path) -> None:
|
|
28
|
+
(repo_dir / "README.md").write_text("seed\n", encoding="utf-8")
|
|
29
|
+
index = repo.index
|
|
30
|
+
index.add("README.md")
|
|
31
|
+
index.write()
|
|
32
|
+
tree = index.write_tree()
|
|
33
|
+
sig = pygit2.Signature("tester", "tester@example.com")
|
|
34
|
+
repo.create_commit("HEAD", sig, sig, "initial", tree, [])
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@pytest.fixture
|
|
38
|
+
def hook_env(tmp_path: Path) -> Generator[dict[str, str], None, None]:
|
|
39
|
+
root = tmp_path / "root"
|
|
40
|
+
root.mkdir()
|
|
41
|
+
repo_dir = root / "demo"
|
|
42
|
+
repo_dir.mkdir()
|
|
43
|
+
_create_initial_commit(pygit2.init_repository(str(repo_dir), bare=False), repo_dir)
|
|
44
|
+
with (
|
|
45
|
+
patch.object(settings, "PROJECTROOT", str(root)),
|
|
46
|
+
patch.object(settings, "PROJECTS_LIST", str(root)),
|
|
47
|
+
patch.object(settings, "PROJECT_MAXDEPTH", 3),
|
|
48
|
+
patch.object(settings, "STRICT_EXPORT", False),
|
|
49
|
+
patch.object(settings, "EXPORT_OK", ""),
|
|
50
|
+
patch.object(settings, "LIST_ALL", True),
|
|
51
|
+
patch.object(settings, "MAXLOAD", None),
|
|
52
|
+
patch("pygitweb.auth_config.auth_config", _HOOKS_AUTH),
|
|
53
|
+
patch("pygitweb.auth.auth_config", _HOOKS_AUTH),
|
|
54
|
+
patch("pygitweb.main.auth_config", _HOOKS_AUTH),
|
|
55
|
+
):
|
|
56
|
+
yield {"project": "demo"}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@pytest.fixture
|
|
60
|
+
def client() -> Generator[TestClient, None, None]:
|
|
61
|
+
with TestClient(app) as c:
|
|
62
|
+
yield c
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_hook_check_open_when_auth_disabled(client: TestClient, hook_env: dict[str, str]) -> None:
|
|
66
|
+
with patch.object(settings, "AUTH", False):
|
|
67
|
+
r = client.post(
|
|
68
|
+
f"/project/{hook_env['project']}/hook",
|
|
69
|
+
params={"name": "post-receive.notify", "op": "check"},
|
|
70
|
+
)
|
|
71
|
+
assert r.status_code == 200
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_hook_add_open_when_auth_disabled(client: TestClient, hook_env: dict[str, str]) -> None:
|
|
75
|
+
with patch.object(settings, "AUTH", False):
|
|
76
|
+
r = client.post(
|
|
77
|
+
f"/project/{hook_env['project']}/hook",
|
|
78
|
+
params={"name": "post-receive.notify", "op": "add"},
|
|
79
|
+
)
|
|
80
|
+
assert r.status_code == 200
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_hook_add_401_without_login(client: TestClient, hook_env: dict[str, str]) -> None:
|
|
84
|
+
with patch.object(settings, "AUTH", True):
|
|
85
|
+
r = client.post(
|
|
86
|
+
f"/project/{hook_env['project']}/hook",
|
|
87
|
+
params={"name": "post-receive.notify", "op": "add"},
|
|
88
|
+
)
|
|
89
|
+
assert r.status_code == 401
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_hook_add_allowed_with_grant(client: TestClient, hook_env: dict[str, str]) -> None:
|
|
93
|
+
with patch.object(settings, "AUTH", True), client_as_user(client, "admin"):
|
|
94
|
+
r = client.post(
|
|
95
|
+
f"/project/{hook_env['project']}/hook",
|
|
96
|
+
params={"name": "post-receive.notify", "op": "add"},
|
|
97
|
+
)
|
|
98
|
+
assert r.status_code == 200
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_hook_add_forbidden_without_grant(client: TestClient, hook_env: dict[str, str]) -> None:
|
|
102
|
+
with patch.object(settings, "AUTH", True), client_as_user(client, "viewer"):
|
|
103
|
+
r = client.post(
|
|
104
|
+
f"/project/{hook_env['project']}/hook",
|
|
105
|
+
params={"name": "post-receive.notify", "op": "add"},
|
|
106
|
+
)
|
|
107
|
+
assert r.status_code == 403
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def test_hook_check_allowed_without_hooks_grant(client: TestClient, hook_env: dict[str, str]) -> None:
|
|
111
|
+
with patch.object(settings, "AUTH", True), client_as_user(client, "viewer"):
|
|
112
|
+
r = client.post(
|
|
113
|
+
f"/project/{hook_env['project']}/hook",
|
|
114
|
+
params={"name": "post-receive.notify", "op": "check"},
|
|
115
|
+
)
|
|
116
|
+
assert r.status_code == 200
|