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,137 @@
|
|
|
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 import FastAPI
|
|
10
|
+
from fastapi.testclient import TestClient
|
|
11
|
+
|
|
12
|
+
from pygittools.tasks import BOARD_REF_PREFIX, Board
|
|
13
|
+
from pygitweb.auth_config import AuthConfig, LocalUser
|
|
14
|
+
from pygitweb.config import settings
|
|
15
|
+
from pygitweb.conftest import client_as_user
|
|
16
|
+
from pygitweb.permissions import Permission, PermissionsMap, permission_key
|
|
17
|
+
from pygitweb.tasks import EMPTY_TREE_OID, task_router
|
|
18
|
+
|
|
19
|
+
_TASKS_AUTH = AuthConfig(
|
|
20
|
+
auth_mode="local",
|
|
21
|
+
local_users=[LocalUser(user="admin", password="secret"), LocalUser(user="viewer", password="secret")],
|
|
22
|
+
oauth_permissions=PermissionsMap.model_validate({
|
|
23
|
+
"*": [],
|
|
24
|
+
permission_key(Permission.TASKS, "demo"): ["admin"],
|
|
25
|
+
}),
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _build_client() -> TestClient:
|
|
30
|
+
app = FastAPI()
|
|
31
|
+
app.include_router(task_router, prefix="/tasks")
|
|
32
|
+
return TestClient(app)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _create_initial_commit(repo: pygit2.Repository, repo_dir: Path) -> None:
|
|
36
|
+
(repo_dir / "README.md").write_text("seed\n", encoding="utf-8")
|
|
37
|
+
index = repo.index
|
|
38
|
+
index.add("README.md")
|
|
39
|
+
index.write()
|
|
40
|
+
tree = index.write_tree()
|
|
41
|
+
sig = pygit2.Signature("tester", "tester@example.com")
|
|
42
|
+
repo.create_commit("HEAD", sig, sig, "initial", tree, [])
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@pytest.fixture
|
|
46
|
+
def task_env(tmp_path: Path) -> Generator[dict[str, str | TestClient], None, None]:
|
|
47
|
+
repo_dir = tmp_path / "demo"
|
|
48
|
+
repo = pygit2.init_repository(str(repo_dir), bare=False)
|
|
49
|
+
_create_initial_commit(repo, repo_dir)
|
|
50
|
+
board = Board(EMPTY_TREE_OID, f"{BOARD_REF_PREFIX}Tasks", tagger="", description="")
|
|
51
|
+
board.write(repo)
|
|
52
|
+
with (
|
|
53
|
+
patch.object(settings, "PROJECTROOT", str(tmp_path)),
|
|
54
|
+
patch.object(settings, "PROJECTS_LIST", str(tmp_path)),
|
|
55
|
+
patch.object(settings, "STRICT_EXPORT", False),
|
|
56
|
+
patch.object(settings, "EXPORT_OK", ""),
|
|
57
|
+
patch.object(settings, "LIST_ALL", True),
|
|
58
|
+
patch("pygitweb.auth_config.auth_config", _TASKS_AUTH),
|
|
59
|
+
patch("pygitweb.auth.auth_config", _TASKS_AUTH),
|
|
60
|
+
):
|
|
61
|
+
yield {"client": _build_client(), "project": "demo", "board": "Tasks"}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_task_post_open_when_auth_disabled(task_env: dict[str, str | TestClient]) -> None:
|
|
65
|
+
client = task_env["client"]
|
|
66
|
+
assert isinstance(client, TestClient)
|
|
67
|
+
with patch.object(settings, "AUTH", False):
|
|
68
|
+
r = client.post(
|
|
69
|
+
"/tasks/create",
|
|
70
|
+
params={
|
|
71
|
+
"project": task_env["project"],
|
|
72
|
+
"board": task_env["board"],
|
|
73
|
+
"title": "New task",
|
|
74
|
+
},
|
|
75
|
+
)
|
|
76
|
+
assert r.status_code == 200
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_task_create_401_without_login(task_env: dict[str, str | TestClient]) -> None:
|
|
80
|
+
client = task_env["client"]
|
|
81
|
+
assert isinstance(client, TestClient)
|
|
82
|
+
with patch.object(settings, "AUTH", True):
|
|
83
|
+
r = client.post(
|
|
84
|
+
"/tasks/create",
|
|
85
|
+
params={
|
|
86
|
+
"project": task_env["project"],
|
|
87
|
+
"board": task_env["board"],
|
|
88
|
+
"title": "New task",
|
|
89
|
+
},
|
|
90
|
+
)
|
|
91
|
+
assert r.status_code == 401
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_task_create_allowed_with_grant(task_env: dict[str, str | TestClient]) -> None:
|
|
95
|
+
client = task_env["client"]
|
|
96
|
+
assert isinstance(client, TestClient)
|
|
97
|
+
with patch.object(settings, "AUTH", True), client_as_user(client, "admin"):
|
|
98
|
+
r = client.post(
|
|
99
|
+
"/tasks/create",
|
|
100
|
+
params={
|
|
101
|
+
"project": task_env["project"],
|
|
102
|
+
"board": task_env["board"],
|
|
103
|
+
"title": "Granted task",
|
|
104
|
+
},
|
|
105
|
+
)
|
|
106
|
+
assert r.status_code == 200
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_task_create_forbidden_without_grant(task_env: dict[str, str | TestClient]) -> None:
|
|
110
|
+
client = task_env["client"]
|
|
111
|
+
assert isinstance(client, TestClient)
|
|
112
|
+
with patch.object(settings, "AUTH", True), client_as_user(client, "viewer"):
|
|
113
|
+
r = client.post(
|
|
114
|
+
"/tasks/create",
|
|
115
|
+
params={
|
|
116
|
+
"project": task_env["project"],
|
|
117
|
+
"board": task_env["board"],
|
|
118
|
+
"title": "Denied task",
|
|
119
|
+
},
|
|
120
|
+
)
|
|
121
|
+
assert r.status_code == 403
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def test_task_update_forbidden_without_grant(task_env: dict[str, str | TestClient]) -> None:
|
|
125
|
+
client = task_env["client"]
|
|
126
|
+
assert isinstance(client, TestClient)
|
|
127
|
+
with patch.object(settings, "AUTH", True), client_as_user(client, "viewer"):
|
|
128
|
+
r = client.post(
|
|
129
|
+
"/tasks/update",
|
|
130
|
+
params={
|
|
131
|
+
"project": task_env["project"],
|
|
132
|
+
"board": task_env["board"],
|
|
133
|
+
"task": "refs/tags/tasks/task_missing",
|
|
134
|
+
},
|
|
135
|
+
json={"title": "nope"},
|
|
136
|
+
)
|
|
137
|
+
assert r.status_code == 403
|
|
@@ -0,0 +1,430 @@
|
|
|
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 import FastAPI
|
|
10
|
+
from fastapi.testclient import TestClient
|
|
11
|
+
from pygit2 import Oid
|
|
12
|
+
|
|
13
|
+
from pygittools.tasks import BOARD_REF_PREFIX, TASK_REF_PREFIX, Board, Comment, Task, get_board
|
|
14
|
+
from pygitweb.config import settings
|
|
15
|
+
from pygitweb.tasks import EMPTY_TREE_OID, board_router, comment_router, task_router
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _build_client() -> TestClient:
|
|
19
|
+
app = FastAPI()
|
|
20
|
+
app.include_router(board_router, prefix="/board")
|
|
21
|
+
app.include_router(task_router, prefix="/tasks")
|
|
22
|
+
app.include_router(comment_router, prefix="/comments")
|
|
23
|
+
return TestClient(app)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _create_initial_commit(repo: pygit2.Repository, repo_dir: Path) -> None:
|
|
27
|
+
(repo_dir / "README.md").write_text("seed\n", encoding="utf-8")
|
|
28
|
+
index = repo.index
|
|
29
|
+
index.add("README.md")
|
|
30
|
+
index.write()
|
|
31
|
+
tree = index.write_tree()
|
|
32
|
+
sig = pygit2.Signature("tester", "tester@example.com")
|
|
33
|
+
repo.create_commit("HEAD", sig, sig, "initial", tree, [])
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class TestTaskRoutes:
|
|
37
|
+
class TestReadonlyResponses:
|
|
38
|
+
@pytest.fixture(scope="class")
|
|
39
|
+
def seeded_env(
|
|
40
|
+
self, tmp_path_factory: pytest.TempPathFactory
|
|
41
|
+
) -> Generator[dict[str, str | TestClient], None, None]:
|
|
42
|
+
root: Path = tmp_path_factory.mktemp("tasks-routes-readonly")
|
|
43
|
+
repo_dir: Path = root / "demo"
|
|
44
|
+
repo = pygit2.init_repository(str(repo_dir), bare=False)
|
|
45
|
+
project: str = "demo"
|
|
46
|
+
|
|
47
|
+
board_ref: str = f"{BOARD_REF_PREFIX}Tasks"
|
|
48
|
+
board = Board(EMPTY_TREE_OID, board_ref, tagger="", description="Main board")
|
|
49
|
+
|
|
50
|
+
task_a_ref: str = f"{TASK_REF_PREFIX}task_a"
|
|
51
|
+
task_a = Task(
|
|
52
|
+
EMPTY_TREE_OID,
|
|
53
|
+
task_a_ref,
|
|
54
|
+
tagger="",
|
|
55
|
+
title="Alpha",
|
|
56
|
+
description="Alpha description",
|
|
57
|
+
status=Task.Status.TODO,
|
|
58
|
+
priority=Task.Priority.HIGH,
|
|
59
|
+
assignee="alice",
|
|
60
|
+
)
|
|
61
|
+
task_a_oid = task_a.write(repo)
|
|
62
|
+
comment_a = Comment(task_a_oid, "alice <alice@example.com>", "alpha note")
|
|
63
|
+
comment_a_oid = comment_a.write(repo)
|
|
64
|
+
task_a.comments = [str(comment_a_oid)]
|
|
65
|
+
task_a.update_message()
|
|
66
|
+
task_a_oid = task_a.write(repo)
|
|
67
|
+
|
|
68
|
+
task_b_ref: str = f"{TASK_REF_PREFIX}task_b"
|
|
69
|
+
task_b = Task(
|
|
70
|
+
EMPTY_TREE_OID,
|
|
71
|
+
task_b_ref,
|
|
72
|
+
tagger="",
|
|
73
|
+
title="Bravo",
|
|
74
|
+
description="",
|
|
75
|
+
status=Task.Status.IN_PROGRESS,
|
|
76
|
+
priority=Task.Priority.MEDIUM,
|
|
77
|
+
assignee=None,
|
|
78
|
+
)
|
|
79
|
+
task_b_oid = task_b.write(repo)
|
|
80
|
+
|
|
81
|
+
board.tasks = [str(task_a_oid), str(task_b_oid)]
|
|
82
|
+
board.update_message()
|
|
83
|
+
board.write(repo)
|
|
84
|
+
|
|
85
|
+
with (
|
|
86
|
+
patch.object(settings, "PROJECTROOT", str(root)),
|
|
87
|
+
patch.object(settings, "PROJECTS_LIST", str(root)),
|
|
88
|
+
patch.object(settings, "STRICT_EXPORT", False),
|
|
89
|
+
patch.object(settings, "EXPORT_OK", ""),
|
|
90
|
+
patch.object(settings, "AUTH", False),
|
|
91
|
+
):
|
|
92
|
+
client = _build_client()
|
|
93
|
+
yield {
|
|
94
|
+
"client": client,
|
|
95
|
+
"project": project,
|
|
96
|
+
"board": "Tasks",
|
|
97
|
+
"task_a_ref": task_a_ref,
|
|
98
|
+
"task_b_ref": task_b_ref,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
def test_board_list_response_format(self, seeded_env: dict[str, str | TestClient]) -> None:
|
|
102
|
+
client = seeded_env["client"]
|
|
103
|
+
assert isinstance(client, TestClient)
|
|
104
|
+
resp = client.get("/board/list", params={"project": seeded_env["project"]})
|
|
105
|
+
assert resp.status_code == 200
|
|
106
|
+
body = resp.json()
|
|
107
|
+
assert isinstance(body, list)
|
|
108
|
+
assert body == [{"name": "Tasks", "description": "Main board", "task_count": 2}]
|
|
109
|
+
|
|
110
|
+
def test_task_list_response_format(self, seeded_env: dict[str, str | TestClient]) -> None:
|
|
111
|
+
client = seeded_env["client"]
|
|
112
|
+
assert isinstance(client, TestClient)
|
|
113
|
+
resp = client.get(
|
|
114
|
+
"/tasks/list",
|
|
115
|
+
params={"project": seeded_env["project"], "board": seeded_env["board"]},
|
|
116
|
+
)
|
|
117
|
+
assert resp.status_code == 200
|
|
118
|
+
body = resp.json()
|
|
119
|
+
assert isinstance(body, list)
|
|
120
|
+
tasks_by_ref = {entry["ref"]: entry for entry in body}
|
|
121
|
+
alpha = tasks_by_ref[str(seeded_env["task_a_ref"])]
|
|
122
|
+
assert alpha["title"] == "Alpha"
|
|
123
|
+
assert alpha["description"] == "Alpha description"
|
|
124
|
+
assert alpha["status"] == "TODO"
|
|
125
|
+
assert alpha["priority"] == "HIGH"
|
|
126
|
+
assert alpha["assignee"] == "alice"
|
|
127
|
+
assert alpha["comments_count"] == 1
|
|
128
|
+
assert alpha["created_at"] is not None
|
|
129
|
+
assert alpha["updated_at"] is not None
|
|
130
|
+
|
|
131
|
+
def test_comment_list_response_format(self, seeded_env: dict[str, str | TestClient]) -> None:
|
|
132
|
+
client = seeded_env["client"]
|
|
133
|
+
assert isinstance(client, TestClient)
|
|
134
|
+
resp = client.get(
|
|
135
|
+
"/comments/list",
|
|
136
|
+
params={
|
|
137
|
+
"project": seeded_env["project"],
|
|
138
|
+
"board": seeded_env["board"],
|
|
139
|
+
"task": seeded_env["task_a_ref"],
|
|
140
|
+
},
|
|
141
|
+
)
|
|
142
|
+
assert resp.status_code == 200
|
|
143
|
+
body = resp.json()
|
|
144
|
+
assert isinstance(body, list)
|
|
145
|
+
assert len(body) == 1
|
|
146
|
+
comment = body[0]
|
|
147
|
+
assert comment["content"] == "alpha note"
|
|
148
|
+
assert comment["tagger"] == "alice <alice@example.com>"
|
|
149
|
+
assert comment["author"] == "alice"
|
|
150
|
+
assert comment["gravatar_url"] is not None
|
|
151
|
+
assert comment["created_at"] is not None
|
|
152
|
+
assert comment["edited_at"] is not None
|
|
153
|
+
|
|
154
|
+
class TestMutableResponses:
|
|
155
|
+
@pytest.fixture(scope="class")
|
|
156
|
+
def mutable_env(
|
|
157
|
+
self, tmp_path_factory: pytest.TempPathFactory
|
|
158
|
+
) -> Generator[dict[str, str | TestClient], None, None]:
|
|
159
|
+
root: Path = tmp_path_factory.mktemp("tasks-routes-mutable")
|
|
160
|
+
repo_dir: Path = root / "demo"
|
|
161
|
+
repo = pygit2.init_repository(str(repo_dir), bare=False)
|
|
162
|
+
_create_initial_commit(repo, repo_dir)
|
|
163
|
+
project: str = "demo"
|
|
164
|
+
board_name: str = "Tasks"
|
|
165
|
+
board_ref: str = f"{BOARD_REF_PREFIX}{board_name}"
|
|
166
|
+
board = Board(EMPTY_TREE_OID, board_ref, tagger="", description="Mutable board")
|
|
167
|
+
task_ref = f"{TASK_REF_PREFIX}mutable_seed"
|
|
168
|
+
task = Task(
|
|
169
|
+
EMPTY_TREE_OID,
|
|
170
|
+
task_ref,
|
|
171
|
+
tagger="",
|
|
172
|
+
title="Mutable seed",
|
|
173
|
+
description="",
|
|
174
|
+
status=Task.Status.TODO,
|
|
175
|
+
priority=Task.Priority.LOW,
|
|
176
|
+
)
|
|
177
|
+
task_oid = task.write(repo)
|
|
178
|
+
comment = Comment(task_oid, "seed <seed@example.com>", "seed comment")
|
|
179
|
+
comment_oid = comment.write(repo)
|
|
180
|
+
task.comments = [str(comment_oid)]
|
|
181
|
+
task.update_message()
|
|
182
|
+
task_oid = task.write(repo)
|
|
183
|
+
board.tasks = [str(task_oid)]
|
|
184
|
+
board.update_message()
|
|
185
|
+
board.write(repo)
|
|
186
|
+
|
|
187
|
+
with (
|
|
188
|
+
patch.object(settings, "PROJECTROOT", str(root)),
|
|
189
|
+
patch.object(settings, "PROJECTS_LIST", str(root)),
|
|
190
|
+
patch.object(settings, "STRICT_EXPORT", False),
|
|
191
|
+
patch.object(settings, "EXPORT_OK", ""),
|
|
192
|
+
patch.object(settings, "AUTH", False),
|
|
193
|
+
):
|
|
194
|
+
client = _build_client()
|
|
195
|
+
yield {
|
|
196
|
+
"client": client,
|
|
197
|
+
"project": project,
|
|
198
|
+
"board": board_name,
|
|
199
|
+
"seed_comment_oid": str(comment_oid),
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
def test_task_create_and_update_response_format(self, mutable_env: dict[str, str | TestClient]) -> None:
|
|
203
|
+
client = mutable_env["client"]
|
|
204
|
+
assert isinstance(client, TestClient)
|
|
205
|
+
create_resp = client.post(
|
|
206
|
+
"/tasks/create",
|
|
207
|
+
params={
|
|
208
|
+
"project": mutable_env["project"],
|
|
209
|
+
"board": mutable_env["board"],
|
|
210
|
+
"title": "Created Task",
|
|
211
|
+
"description": "Needs updates",
|
|
212
|
+
"status": "TODO",
|
|
213
|
+
"priority": "LOW",
|
|
214
|
+
},
|
|
215
|
+
)
|
|
216
|
+
assert create_resp.status_code == 200
|
|
217
|
+
created = create_resp.json()
|
|
218
|
+
assert set(created.keys()) == {"task_id", "ref", "oid"}
|
|
219
|
+
assert str(created["ref"]).startswith("refs/tags/tasks/task_")
|
|
220
|
+
|
|
221
|
+
update_resp = client.post(
|
|
222
|
+
"/tasks/update",
|
|
223
|
+
params={
|
|
224
|
+
"project": mutable_env["project"],
|
|
225
|
+
"board": mutable_env["board"],
|
|
226
|
+
"task": created["ref"],
|
|
227
|
+
},
|
|
228
|
+
json={
|
|
229
|
+
"title": "Updated Task",
|
|
230
|
+
"description": "Updated description",
|
|
231
|
+
"status": "IN_REVIEW",
|
|
232
|
+
"priority": "CRITICAL",
|
|
233
|
+
"assignee": "bob",
|
|
234
|
+
"due_date": "2025-02-03T04:05:06+00:00",
|
|
235
|
+
},
|
|
236
|
+
)
|
|
237
|
+
assert update_resp.status_code == 200
|
|
238
|
+
updated = update_resp.json()
|
|
239
|
+
assert updated["ref"] == created["ref"]
|
|
240
|
+
assert updated["title"] == "Updated Task"
|
|
241
|
+
assert updated["description"] == "Updated description"
|
|
242
|
+
assert updated["status"] == "IN_REVIEW"
|
|
243
|
+
assert updated["priority"] == "CRITICAL"
|
|
244
|
+
assert updated["assignee"] == "bob"
|
|
245
|
+
assert updated["due_date"] == "2025-02-03T04:05:06+00:00"
|
|
246
|
+
|
|
247
|
+
def test_task_status_update_moves_oid_to_end_of_board_list(
|
|
248
|
+
self, mutable_env: dict[str, str | TestClient]
|
|
249
|
+
) -> None:
|
|
250
|
+
client = mutable_env["client"]
|
|
251
|
+
assert isinstance(client, TestClient)
|
|
252
|
+
a_ref = client.post(
|
|
253
|
+
"/tasks/create",
|
|
254
|
+
params={
|
|
255
|
+
"project": mutable_env["project"],
|
|
256
|
+
"board": mutable_env["board"],
|
|
257
|
+
"title": "First",
|
|
258
|
+
"status": "TODO",
|
|
259
|
+
},
|
|
260
|
+
).json()["ref"]
|
|
261
|
+
b_ref = client.post(
|
|
262
|
+
"/tasks/create",
|
|
263
|
+
params={
|
|
264
|
+
"project": mutable_env["project"],
|
|
265
|
+
"board": mutable_env["board"],
|
|
266
|
+
"title": "Second",
|
|
267
|
+
"status": "TODO",
|
|
268
|
+
},
|
|
269
|
+
).json()["ref"]
|
|
270
|
+
list_before = client.get(
|
|
271
|
+
"/tasks/list",
|
|
272
|
+
params={"project": mutable_env["project"], "board": mutable_env["board"]},
|
|
273
|
+
).json()
|
|
274
|
+
refs_before = [t["ref"] for t in list_before if t["title"] in ("First", "Second")]
|
|
275
|
+
assert refs_before.index(a_ref) < refs_before.index(b_ref)
|
|
276
|
+
client.post(
|
|
277
|
+
"/tasks/update",
|
|
278
|
+
params={
|
|
279
|
+
"project": mutable_env["project"],
|
|
280
|
+
"board": mutable_env["board"],
|
|
281
|
+
"task": a_ref,
|
|
282
|
+
},
|
|
283
|
+
json={"status": "IN_PROGRESS"},
|
|
284
|
+
)
|
|
285
|
+
repo = pygit2.Repository(str(Path(settings.PROJECTROOT) / str(mutable_env["project"])))
|
|
286
|
+
loaded = get_board(repo, f"{BOARD_REF_PREFIX}{mutable_env['board']}")
|
|
287
|
+
assert loaded is not None
|
|
288
|
+
a_oid = str(repo.references[a_ref].resolve().target)
|
|
289
|
+
b_oid = str(repo.references[b_ref].resolve().target)
|
|
290
|
+
assert loaded.tasks.index(b_oid) < loaded.tasks.index(a_oid)
|
|
291
|
+
|
|
292
|
+
def test_task_list_resolves_stale_board_task_oid(self, mutable_env: dict[str, str | TestClient]) -> None:
|
|
293
|
+
"""Board may still list an old task OID after the ref was rewritten."""
|
|
294
|
+
client = mutable_env["client"]
|
|
295
|
+
assert isinstance(client, TestClient)
|
|
296
|
+
repo = pygit2.Repository(str(Path(settings.PROJECTROOT) / str(mutable_env["project"])))
|
|
297
|
+
board = get_board(repo, f"{BOARD_REF_PREFIX}{mutable_env['board']}")
|
|
298
|
+
assert board is not None
|
|
299
|
+
stale_oid = board.tasks[0]
|
|
300
|
+
task_ref = f"{TASK_REF_PREFIX}mutable_seed"
|
|
301
|
+
t = Task(
|
|
302
|
+
EMPTY_TREE_OID,
|
|
303
|
+
task_ref,
|
|
304
|
+
tagger="",
|
|
305
|
+
title="Stale board entry",
|
|
306
|
+
description="",
|
|
307
|
+
status=Task.Status.TODO,
|
|
308
|
+
priority=Task.Priority.LOW,
|
|
309
|
+
)
|
|
310
|
+
t.comments = ["abc123"]
|
|
311
|
+
t.update_message()
|
|
312
|
+
current_oid = t.write(repo)
|
|
313
|
+
board.tasks = [stale_oid]
|
|
314
|
+
board.update_message()
|
|
315
|
+
board.write(repo)
|
|
316
|
+
|
|
317
|
+
list_resp = client.get(
|
|
318
|
+
"/tasks/list",
|
|
319
|
+
params={"project": mutable_env["project"], "board": mutable_env["board"]},
|
|
320
|
+
)
|
|
321
|
+
assert list_resp.status_code == 200
|
|
322
|
+
by_ref = {entry["ref"]: entry for entry in list_resp.json()}
|
|
323
|
+
assert by_ref[task_ref]["comments_count"] == 1
|
|
324
|
+
reloaded = get_board(repo, f"{BOARD_REF_PREFIX}{mutable_env['board']}")
|
|
325
|
+
assert reloaded is not None
|
|
326
|
+
assert str(current_oid) in reloaded.tasks
|
|
327
|
+
|
|
328
|
+
def test_comment_create_updates_task_list_comments_count(
|
|
329
|
+
self, mutable_env: dict[str, str | TestClient]
|
|
330
|
+
) -> None:
|
|
331
|
+
client = mutable_env["client"]
|
|
332
|
+
assert isinstance(client, TestClient)
|
|
333
|
+
create_resp = client.post(
|
|
334
|
+
"/tasks/create",
|
|
335
|
+
params={
|
|
336
|
+
"project": mutable_env["project"],
|
|
337
|
+
"board": mutable_env["board"],
|
|
338
|
+
"title": "Comment count task",
|
|
339
|
+
},
|
|
340
|
+
)
|
|
341
|
+
assert create_resp.status_code == 200
|
|
342
|
+
task_ref: str = create_resp.json()["ref"]
|
|
343
|
+
comment_resp = client.post(
|
|
344
|
+
"/comments/create",
|
|
345
|
+
params={
|
|
346
|
+
"project": mutable_env["project"],
|
|
347
|
+
"board": mutable_env["board"],
|
|
348
|
+
"task": task_ref,
|
|
349
|
+
"content": "board count note",
|
|
350
|
+
},
|
|
351
|
+
)
|
|
352
|
+
assert comment_resp.status_code == 200
|
|
353
|
+
assert comment_resp.json()["comments_count"] == 1
|
|
354
|
+
list_resp = client.get(
|
|
355
|
+
"/tasks/list",
|
|
356
|
+
params={"project": mutable_env["project"], "board": mutable_env["board"]},
|
|
357
|
+
)
|
|
358
|
+
assert list_resp.status_code == 200
|
|
359
|
+
by_ref = {entry["ref"]: entry for entry in list_resp.json()}
|
|
360
|
+
assert by_ref[task_ref]["comments_count"] == 1
|
|
361
|
+
repo = pygit2.Repository(str(Path(settings.PROJECTROOT) / str(mutable_env["project"])))
|
|
362
|
+
board = get_board(repo, f"{BOARD_REF_PREFIX}{mutable_env['board']}")
|
|
363
|
+
assert board is not None
|
|
364
|
+
ref_oid = str(repo.references[task_ref].resolve().target)
|
|
365
|
+
assert ref_oid in board.tasks
|
|
366
|
+
|
|
367
|
+
def test_comment_delete_response_format(self, mutable_env: dict[str, str | TestClient]) -> None:
|
|
368
|
+
client = mutable_env["client"]
|
|
369
|
+
assert isinstance(client, TestClient)
|
|
370
|
+
delete_comment_resp = client.post(
|
|
371
|
+
"/comments/delete",
|
|
372
|
+
params={
|
|
373
|
+
"project": mutable_env["project"],
|
|
374
|
+
"board": mutable_env["board"],
|
|
375
|
+
"comment": mutable_env["seed_comment_oid"],
|
|
376
|
+
},
|
|
377
|
+
)
|
|
378
|
+
assert delete_comment_resp.status_code == 200
|
|
379
|
+
assert delete_comment_resp.json() == {"message": "Comment deleted"}
|
|
380
|
+
|
|
381
|
+
def test_task_delete_response_format(self, mutable_env: dict[str, str | TestClient]) -> None:
|
|
382
|
+
client = mutable_env["client"]
|
|
383
|
+
assert isinstance(client, TestClient)
|
|
384
|
+
create_resp = client.post(
|
|
385
|
+
"/tasks/create",
|
|
386
|
+
params={
|
|
387
|
+
"project": mutable_env["project"],
|
|
388
|
+
"board": mutable_env["board"],
|
|
389
|
+
"title": "To delete",
|
|
390
|
+
},
|
|
391
|
+
)
|
|
392
|
+
task_ref: str = create_resp.json()["ref"]
|
|
393
|
+
delete_resp = client.post(
|
|
394
|
+
"/tasks/delete",
|
|
395
|
+
params={
|
|
396
|
+
"project": mutable_env["project"],
|
|
397
|
+
"board": mutable_env["board"],
|
|
398
|
+
"task": task_ref,
|
|
399
|
+
},
|
|
400
|
+
)
|
|
401
|
+
assert delete_resp.status_code == 200
|
|
402
|
+
assert delete_resp.json() == {"message": "Task deleted"}
|
|
403
|
+
|
|
404
|
+
repo = pygit2.Repository(str(Path(settings.PROJECTROOT) / str(mutable_env["project"])))
|
|
405
|
+
assert task_ref not in repo.references
|
|
406
|
+
|
|
407
|
+
def test_board_create_and_delete_response_format(self, mutable_env: dict[str, str | TestClient]) -> None:
|
|
408
|
+
client = mutable_env["client"]
|
|
409
|
+
assert isinstance(client, TestClient)
|
|
410
|
+
create_resp = client.post(
|
|
411
|
+
"/board/create",
|
|
412
|
+
params={
|
|
413
|
+
"project": mutable_env["project"],
|
|
414
|
+
"name": "temp-board",
|
|
415
|
+
"description": "Temporary",
|
|
416
|
+
},
|
|
417
|
+
)
|
|
418
|
+
assert create_resp.status_code == 200
|
|
419
|
+
created = create_resp.json()
|
|
420
|
+
assert set(created.keys()) == {"name", "ref", "oid"}
|
|
421
|
+
assert created["name"] == "temp-board"
|
|
422
|
+
assert created["ref"] == f"{BOARD_REF_PREFIX}temp-board"
|
|
423
|
+
Oid(hex=created["oid"])
|
|
424
|
+
|
|
425
|
+
delete_resp = client.post(
|
|
426
|
+
"/board/delete",
|
|
427
|
+
params={"project": mutable_env["project"], "name": "temp-board"},
|
|
428
|
+
)
|
|
429
|
+
assert delete_resp.status_code == 200
|
|
430
|
+
assert delete_resp.json() == {"message": "Board deleted"}
|