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.
Files changed (95) hide show
  1. pygitweb/__init__.py +22 -0
  2. pygitweb/__meta__.py +9 -0
  3. pygitweb/actions.py +1303 -0
  4. pygitweb/addproject_permissions_test.py +114 -0
  5. pygitweb/api/__init__.py +0 -0
  6. pygitweb/api/action.py +20 -0
  7. pygitweb/api/plugins/__init__.py +0 -0
  8. pygitweb/api/plugins/project_zip.py +71 -0
  9. pygitweb/api/project.py +67 -0
  10. pygitweb/api/subpage.py +22 -0
  11. pygitweb/auth.py +387 -0
  12. pygitweb/auth.schema.json +106 -0
  13. pygitweb/auth_config.py +243 -0
  14. pygitweb/auth_oauth.py +343 -0
  15. pygitweb/auth_test.py +390 -0
  16. pygitweb/boards_permissions_test.py +113 -0
  17. pygitweb/change_queue.py +110 -0
  18. pygitweb/change_queue_routes_test.py +146 -0
  19. pygitweb/change_queue_test.py +91 -0
  20. pygitweb/comments_permissions_test.py +140 -0
  21. pygitweb/config.py +185 -0
  22. pygitweb/conftest.py +43 -0
  23. pygitweb/dependencies.py +87 -0
  24. pygitweb/formatting.py +182 -0
  25. pygitweb/formatting_test.py +68 -0
  26. pygitweb/git_helpers.py +454 -0
  27. pygitweb/git_helpers_test.py +223 -0
  28. pygitweb/gravatar.py +16 -0
  29. pygitweb/gravatar_test.py +10 -0
  30. pygitweb/hooks_install.py +278 -0
  31. pygitweb/hooks_install_test.py +363 -0
  32. pygitweb/hooks_permissions_test.py +116 -0
  33. pygitweb/main.py +887 -0
  34. pygitweb/merge_requests.py +230 -0
  35. pygitweb/merge_requests_test.py +85 -0
  36. pygitweb/mr_permissions_test.py +148 -0
  37. pygitweb/password_hash.py +74 -0
  38. pygitweb/password_hash_test.py +20 -0
  39. pygitweb/permissions.py +209 -0
  40. pygitweb/permissions_test.py +86 -0
  41. pygitweb/plugin_loader.py +83 -0
  42. pygitweb/projects.py +213 -0
  43. pygitweb/projects_test.py +186 -0
  44. pygitweb/sessions.py +73 -0
  45. pygitweb/settings.py +412 -0
  46. pygitweb/settings.schema.json +73 -0
  47. pygitweb/settings_config.py +122 -0
  48. pygitweb/settings_config_test.py +101 -0
  49. pygitweb/settings_permissions_test.py +76 -0
  50. pygitweb/static/blob-view.js +133 -0
  51. pygitweb/static/board.js +839 -0
  52. pygitweb/static/hook-install.js +53 -0
  53. pygitweb/static/icons/cloud-down.svg +1 -0
  54. pygitweb/static/icons/cloud-exclamation.svg +1 -0
  55. pygitweb/static/icons/cloud-filled.svg +1 -0
  56. pygitweb/static/icons/cloud-storm.svg +1 -0
  57. pygitweb/static/icons/cloud-up.svg +1 -0
  58. pygitweb/static/icons/cloud.svg +1 -0
  59. pygitweb/static/logo.svg +44 -0
  60. pygitweb/static/main.css +1210 -0
  61. pygitweb/static/navbar-auth.js +82 -0
  62. pygitweb/static/readme-render.js +174 -0
  63. pygitweb/static/search-page.js +139 -0
  64. pygitweb/static/summary-ref-switcher.js +130 -0
  65. pygitweb/static/themes/solarized.css +34 -0
  66. pygitweb/tasks.py +595 -0
  67. pygitweb/tasks_permissions_test.py +137 -0
  68. pygitweb/tasks_routes_test.py +430 -0
  69. pygitweb/templates/addproject.html +174 -0
  70. pygitweb/templates/blobdiff.html +57 -0
  71. pygitweb/templates/board.html +31 -0
  72. pygitweb/templates/board_card.html +44 -0
  73. pygitweb/templates/commitdiff.html +57 -0
  74. pygitweb/templates/hooks_summary_cell.html +37 -0
  75. pygitweb/templates/login.html +37 -0
  76. pygitweb/templates/merge_request_summary_cell.html +18 -0
  77. pygitweb/templates/merge_request_tag.html +90 -0
  78. pygitweb/templates/pagination.html +44 -0
  79. pygitweb/templates/preamble.html +89 -0
  80. pygitweb/templates/project_settings.html +13 -0
  81. pygitweb/templates/pygit2_settings.html +12 -0
  82. pygitweb/templates/pygitweb_settings.html +12 -0
  83. pygitweb/templates/search.html +77 -0
  84. pygitweb/templates/settings_form_field.html +27 -0
  85. pygitweb/templates/table.html +15 -0
  86. pygitweb/templates/user_profile.html +10 -0
  87. pygitweb/templates_env.py +21 -0
  88. pygitweb/timeline_cache.py +243 -0
  89. pygitweb/validation.py +75 -0
  90. pygitweb/validation_test.py +167 -0
  91. pygitweb-0.1.0.dist-info/METADATA +165 -0
  92. pygitweb-0.1.0.dist-info/RECORD +95 -0
  93. pygitweb-0.1.0.dist-info/WHEEL +5 -0
  94. pygitweb-0.1.0.dist-info/licenses/LICENSE +21 -0
  95. 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"}