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
from __future__ import annotations
import asyncio
from collections.abc import Generator
from pathlib import Path
from unittest.mock import patch
import httpx
import pygit2
import pytest
from httpx import ASGITransport
from pygitweb.change_queue import CHANGE_QUEUE
from pygitweb.config import settings
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(scope="module")
def updates_env(tmp_path_factory: pytest.TempPathFactory) -> Generator[dict[str, str], None, None]:
root: Path = tmp_path_factory.mktemp("updates-routes")
alpha_dir: Path = root / "alpha"
alpha_dir.mkdir()
_create_initial_commit(pygit2.init_repository(str(alpha_dir), bare=False), alpha_dir)
beta_dir: Path = root / "group" / "beta"
beta_dir.mkdir(parents=True)
_create_initial_commit(pygit2.init_repository(str(beta_dir), bare=False), beta_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", None),
patch.object(settings, "MAXLOAD", None),
):
yield {"root": str(root), "alpha": "alpha", "beta": "group/beta"}
def _client() -> httpx.AsyncClient:
return httpx.AsyncClient(transport=ASGITransport(app=app), base_url="http://test")
class TestUpdatesLongPoll:
def test_updates_returns_304_after_timeout(self, updates_env: dict[str, str]) -> None:
async def scenario() -> int:
async with _client() as client:
with patch.object(CHANGE_QUEUE, "DEFAULT_TIMEOUT_SECONDS", 0.05):
resp = await client.get(
f"/project/{updates_env['alpha']}",
params={"a": "log", "updates": "true"},
)
return resp.status_code
assert asyncio.run(scenario()) == 304
def test_updates_returns_data_after_notify(self, updates_env: dict[str, str]) -> None:
async def scenario() -> tuple[int, str]:
async with _client() as client:
poll: asyncio.Task[httpx.Response] = asyncio.create_task(
client.get(
f"/project/{updates_env['alpha']}",
params={"a": "log", "updates": "true"},
)
)
await asyncio.sleep(0.05)
notify_resp = await client.post(
"/_internal/notify",
params={"project": updates_env["alpha"]},
)
assert notify_resp.status_code == 200
assert notify_resp.json()["waiters_woken"] == 1
resp: httpx.Response = await poll
return resp.status_code, resp.text
status, body = asyncio.run(scenario())
assert status == 200
assert "Log" in body
def test_updates_with_pf_wakes_on_any_matching_project(self, updates_env: dict[str, str]) -> None:
async def scenario() -> int:
async with _client() as client:
poll: asyncio.Task[httpx.Response] = asyncio.create_task(
client.get(
f"/project/{updates_env['alpha']}",
params={"a": "heads", "updates": "true", "pf": "group"},
)
)
await asyncio.sleep(0.05)
notify_resp = await client.post(
"/_internal/notify",
params={"project": updates_env["beta"]},
)
assert notify_resp.status_code == 200
assert notify_resp.json()["waiters_woken"] == 1
return (await poll).status_code
assert asyncio.run(scenario()) == 200
def test_updates_rejected_for_unsupported_action(self, updates_env: dict[str, str]) -> None:
async def scenario() -> int:
async with _client() as client:
resp = await client.get(
f"/project/{updates_env['alpha']}",
params={"a": "tree", "updates": "true"},
)
return resp.status_code
assert asyncio.run(scenario()) == 400
def test_notify_unknown_project_returns_404(self, updates_env: dict[str, str]) -> None:
async def scenario() -> int:
async with _client() as client:
resp = await client.post("/_internal/notify", params={"project": "no-such-project"})
return resp.status_code
assert asyncio.run(scenario()) == 404