forked from AndyEverything/openproject-mcp-server
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest_sprints.py
More file actions
182 lines (152 loc) · 5.52 KB
/
Copy pathtest_sprints.py
File metadata and controls
182 lines (152 loc) · 5.52 KB
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
#!/usr/bin/env python3
"""
Unit tests for the sprint & backlog tools (network-free).
Covers status parsing (the API models status via _links.status), sprint
formatting, and the work-package filter construction (sprint "=" for a sprint,
sprint "!*" for the backlog). The HTTP client is mocked.
python test_sprints.py
"""
import asyncio
import json
import os
import sys
from unittest.mock import AsyncMock, patch
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
os.environ.setdefault("OPENPROJECT_URL", "http://localhost")
os.environ.setdefault("OPENPROJECT_API_KEY", "test-key")
from src.tools.sprints import ( # noqa: E402
list_sprints,
list_sprint_work_packages,
list_backlog_work_packages,
_sprint_status,
_format_sprint,
_norm_status,
)
def _sprint(sid, name, status_title, start="2026-09-01", finish="2026-09-14"):
return {
"id": sid,
"name": name,
"startDate": start,
"finishDate": finish,
"_links": {
"status": {"title": status_title},
"definingWorkspace": {"title": "Demo Project"},
},
}
def test_status_parsing():
print("\n[1] status is read from _links.status and normalised")
try:
assert _norm_status("In planning") == "in_planning"
assert _sprint_status(_sprint(1, "S", "Active")) == "active"
assert _sprint_status({"status": "completed"}) == "completed"
assert _sprint_status({}) == "unknown"
print("OK PASSED")
return True
except Exception as e:
print(f"FAIL {e}")
return False
def test_format_sprint():
print("\n[2] _format_sprint shows icon, name, status, dates, project")
try:
out = _format_sprint(_sprint(7, "Sprint 7", "active"))
assert "Sprint 7" in out and "(ID: 7)" in out
assert "🟢" in out and "active" in out
assert "Demo Project" in out
assert "2026-09-01 → 2026-09-14" in out
print("OK PASSED")
return True
except Exception as e:
print(f"FAIL {e}")
return False
async def _capture_wp_filters(coro_factory):
captured = {}
async def fake_get_wps(project_id=None, filters=None, **kw):
captured["filters"] = json.loads(filters) if filters else []
captured["project_id"] = project_id
return {"_embedded": {"elements": []}, "total": 0}
with patch("src.tools.sprints.get_client") as gc:
client = AsyncMock()
client.get_work_packages = AsyncMock(side_effect=fake_get_wps)
gc.return_value = client
await coro_factory()
return captured
def _by_key(filters):
return {list(f.keys())[0]: list(f.values())[0] for f in filters}
def test_sprint_wp_filter():
print("\n[3] list_sprint_work_packages filters by sprint = <id>")
try:
cap = asyncio.run(
_capture_wp_filters(lambda: list_sprint_work_packages.fn(sprint_id=12))
)
bk = _by_key(cap["filters"])
assert bk.get("sprint") == {"operator": "=", "values": ["12"]}, bk
print("OK PASSED")
return True
except Exception as e:
print(f"FAIL {e}")
return False
def test_backlog_filter():
print("\n[4] list_backlog_work_packages filters by sprint !* (+ type/priority)")
try:
cap = asyncio.run(
_capture_wp_filters(
lambda: list_backlog_work_packages.fn(
project_id=5, type_ids="1,2", priority_ids="3"
)
)
)
bk = _by_key(cap["filters"])
assert bk.get("sprint") == {"operator": "!*", "values": []}, bk
assert bk.get("type") == {"operator": "=", "values": ["1", "2"]}, bk
assert bk.get("priority") == {"operator": "=", "values": ["3"]}, bk
assert cap["project_id"] == 5
print("OK PASSED")
return True
except Exception as e:
print(f"FAIL {e}")
return False
def test_list_sprints_status_filter():
print("\n[5] list_sprints filters by status client-side")
try:
async def run():
async def fake_get_sprints(project_id=None, offset=0, page_size=25):
return {
"_embedded": {
"elements": [
_sprint(1, "Planning one", "in_planning"),
_sprint(2, "Active one", "active"),
]
},
"total": 2,
}
with patch("src.tools.sprints.get_client") as gc:
client = AsyncMock()
client.get_sprints = AsyncMock(side_effect=fake_get_sprints)
gc.return_value = client
return await list_sprints.fn(status="active")
out = asyncio.run(run())
assert "Active one" in out and "Planning one" not in out, out
# invalid status is rejected
bad = asyncio.run(list_sprints.fn(status="bogus"))
assert "Invalid status" in bad, bad
print("OK PASSED")
return True
except Exception as e:
print(f"FAIL {e}")
return False
def run_all_tests():
print("=" * 70)
print("Sprint & backlog tools - UNIT TEST SUITE")
print("=" * 70)
results = [
test_status_parsing(),
test_format_sprint(),
test_sprint_wp_filter(),
test_backlog_filter(),
test_list_sprints_status_filter(),
]
passed = sum(1 for r in results if r)
print(f"\nTotal: {passed}/{len(results)} tests passed")
return all(results)
if __name__ == "__main__":
sys.exit(0 if run_all_tests() else 1)