Skip to content

Commit 97c10d7

Browse files
author
Paul Philion
committed
more tests, more coverage
1 parent cdbcf79 commit 97c10d7

File tree

6 files changed

+102
-34
lines changed

6 files changed

+102
-34
lines changed

redmine/model.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,10 @@ def set_field(self, fieldname:str, value):
254254
def asdict(self):
255255
return dataclasses.asdict(self)
256256

257+
@property
258+
def json(self):
259+
return json.dumps(self.asdict(), indent=4, default=vars)
260+
257261

258262
@dataclass
259263
class UserResult:

redmine/synctime.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,15 @@ def parse_millis(timestamp:int) -> dt.datetime:
3030
return dt.datetime.fromtimestamp(timestamp, dt.timezone.utc)
3131

3232

33+
def ago(**kwargs) -> dt.datetime:
34+
# so generate one for "three yeas ago"
35+
return now() - dt.timedelta(**kwargs)
36+
37+
3338
def epoch_datetime() -> dt.datetime:
3439
# discord API fails when using 0 as a timestamp,
35-
# so generate one for "three yeas ago"
36-
return now() - dt.timedelta(days=3*365)
40+
# so generate one for "ten years ago"
41+
return ago(days=10*365)
3742

3843

3944
def parse_str(timestamp:str) -> dt.datetime:

redmine/tickets.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -329,9 +329,16 @@ def recycle(self, ticket:Ticket, team_id: int):
329329

330330
def dusty(self) -> set[Ticket]:
331331
# tickets that are older than 7
332-
tickets = set()
333-
tickets.update(self.older_than(TICKET_DUSTY_AGE))
334-
return tickets
332+
333+
# http://localhost/projects/scn/issues.json?v[status_id][]=2&op[updated_on]=%3Ct-&v[updated_on][]=7
334+
#"status_id": "2"
335+
#query = f"/issues.json?status_id=2&op[updated_on]=%3Ct-&v[updated_on][]=7&op[priority_id]=!&v[priority_id][]=14&include=children"
336+
query = "/issues.json?set_filter=1&sort=id%3Adesc&f[]=priority_id&op[priority_id]=!&v[priority_id][]=14&f[]=status_id&op[status_id]=%3D&v[status_id][]=2&f[]=updated_on&op[updated_on]=%3Ct-&v[updated_on][]=7&f[]=&c[]=tracker&c[]=status&c[]=priority&c[]=subject&c[]=assigned_to&c[]=updated_on&group_by=&t[]="
337+
response = self.session.get(query)
338+
if response:
339+
return TicketsResult(**response).issues
340+
else:
341+
return []
335342

336343

337344
def recyclable(self) -> set[Ticket]:

tests/mock_session.py

Lines changed: 41 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from urllib.parse import urlparse
88

99
from redmine.session import RedmineSession
10-
from redmine.model import User
10+
from redmine.model import User, Ticket, TicketsResult
1111

1212
TEST_DATA = "data" # dir with test data
1313

@@ -50,10 +50,32 @@ def _get(self, resource:str) -> dict:
5050
#super().get(query, impersonate_id)
5151

5252

53+
def _cache(self, resource:str, item):
54+
self.test_cache[resource] = json.dumps(item, indent=4, default=str)
55+
56+
5357
def cache_user(self, user:User):
54-
data = {}
55-
data['user'] = user.asdict()
56-
self.test_cache[f"/users/{user.id}.json"] = json.dumps(data)
58+
data = {
59+
'user': user.asdict()
60+
}
61+
self._cache(f"/users/{user.id}.json", data)
62+
63+
64+
def cache_ticket(self, ticket:Ticket):
65+
data = {
66+
'issue': ticket.asdict()
67+
}
68+
log.info(data)
69+
self._cache(f"/issues/{ticket.id}.json", data)
70+
71+
72+
def cache_results(self, tickets:list[Ticket]):
73+
result = TicketsResult(
74+
total_count=len(tickets),
75+
limit=25,
76+
offset=0,
77+
issues=tickets)
78+
self._cache("/issues.json", result.asdict())
5779

5880

5981
def get(self, query:str, impersonate_id:str|None=None):
@@ -80,24 +102,24 @@ def put(self, resource:str, data:str, impersonate_id:str|None=None) -> None:
80102
log.debug(f"PUT {path} -> {item}")
81103

82104

83-
def post(self, resource: str, data:str, user_login: str|None = None, files: list|None = None) -> dict|None:
84-
log.info(f"POST {resource}, data={data} user_login={user_login}")
85-
item_id = self._next_id()
105+
# def post(self, resource: str, data:str, user_login: str|None = None, files: list|None = None) -> dict|None:
106+
# log.info(f"POST {resource}, data={data} user_login={user_login}")
107+
# item_id = self._next_id()
86108

87-
path = urlparse(resource).path
109+
# path = urlparse(resource).path
88110

89-
name_a = path.rsplit('.', 1)
90-
path = f"{name_a[0]}/{item_id}.{name_a[1]}"
111+
# name_a = path.rsplit('.', 1)
112+
# path = f"{name_a[0]}/{item_id}.{name_a[1]}"
91113

92-
data_dict = json.loads(data)
93-
for _, v in data_dict.items():
94-
v['id'] = item_id
95-
value = json.dumps(data_dict)
96-
log.debug(f"POST {path} -> {value}")
97-
self.test_cache[path] = value
98-
99-
return data_dict
100-
#raise RedmineException(f"POST failed, status=[{r.status_code}] {r.reason}", r.headers['X-Request-Id'])
114+
# data_dict = json.loads(data)
115+
# for _, v in data_dict.items():
116+
# v['id'] = item_id
117+
# value = json.dumps(data_dict)
118+
# log.debug(f"POST {path} -> {value}")
119+
# self.test_cache[path] = value
120+
121+
# return data_dict
122+
# #raise RedmineException(f"POST failed, status=[{r.status_code}] {r.reason}", r.headers['X-Request-Id'])
101123

102124

103125
def delete(self, resource: str) -> None:

tests/test_tickets.py

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,60 @@
11
#!/usr/bin/env python3
22
"""Redmine tickets manager test cases"""
33

4+
import datetime
45
import unittest
56
import logging
67
import json
78
from unittest.mock import MagicMock, patch
89

910
from dotenv import load_dotenv
1011

11-
from redmine.model import ParentTicket, synctime
12+
from redmine.model import ParentTicket, synctime, TicketStatus
1213
from redmine.tickets import TICKET_DUSTY_AGE, TICKET_MAX_AGE
1314

1415
from tests import test_utils
1516

16-
1717
log = logging.getLogger(__name__)
1818

1919
class TestTicketManager(test_utils.MockRedmineTestCase):
2020
"""Mocked testing of ticket manager"""
2121

2222
def test_dusty_tickets(self):
23-
for ticket in self.tickets_mgr.dusty():
23+
# to find dusty tickets, a dusty ticket needs to be created.
24+
ticket1 = self.create_ticket(
25+
status=TicketStatus(id=2,name="In Progress",is_closed=False),
26+
updated_on=synctime.ago(days=TICKET_DUSTY_AGE+1),
27+
)
28+
# cache a search result
29+
self.session.cache_results([ticket1])
30+
31+
self.assertGreaterEqual(synctime.age(ticket1.updated_on), datetime.timedelta(days=TICKET_DUSTY_AGE))
32+
33+
dusty_tickets = self.tickets_mgr.dusty()
34+
self.assertGreaterEqual(len(dusty_tickets), 1, "No dusty tickets found")
35+
for ticket in dusty_tickets:
2436
age = synctime.age(ticket.updated_on)
2537
self.assertGreaterEqual(age.days, TICKET_DUSTY_AGE)
38+
self.assertEqual(ticket.status.name, "In Progress")
39+
40+
41+
def test_recyclable_tickets(self):
42+
# to find dusty tickets, a dusty ticket needs to be created.
43+
ticket1 = self.create_ticket(
44+
status=TicketStatus(id=2,name="In Progress",is_closed=False),
45+
updated_on=synctime.ago(days=TICKET_MAX_AGE+1),
46+
)
47+
# cache a search result
48+
self.session.cache_results([ticket1])
49+
50+
self.assertGreaterEqual(synctime.age(ticket1.updated_on), datetime.timedelta(days=TICKET_DUSTY_AGE))
2651

27-
for ticket in self.tickets_mgr.recyclable():
52+
dusty_tickets = self.tickets_mgr.dusty()
53+
self.assertGreaterEqual(len(dusty_tickets), 1, "No recyclable tickets found")
54+
for ticket in dusty_tickets:
2855
age = synctime.age(ticket.updated_on)
2956
self.assertGreaterEqual(age.days, TICKET_MAX_AGE)
57+
self.assertEqual(ticket.status.name, "In Progress")
3058

3159

3260
@unittest.skip # FIXME currently breaking mock testing

tests/test_utils.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -141,14 +141,13 @@ class MockRedmineTestCase(unittest.TestCase):
141141
@classmethod
142142
def setUpClass(cls):
143143
cls.tag:str = tagstr()
144-
session = MockSession(cls.tag)
145-
cls.redmine = Client.from_session(session, default_project=1)
144+
cls.session = MockSession(cls.tag)
145+
cls.redmine = Client.from_session(cls.session, default_project=1)
146146
cls.user_mgr = cls.redmine.user_mgr
147147
cls.tickets_mgr = cls.redmine.ticket_mgr
148148

149149
cls.user:User = mock_user(cls.tag)
150-
#session.test_cache[f"/users/{cls.user.id}.json"] = json.dumps(cls.user)
151-
session.cache_user(cls.user)
150+
cls.session.cache_user(cls.user)
152151
cls.user_mgr.cache.cache_user(cls.user)
153152
log.info(f"SETUP created mock user: {cls.user}")
154153

@@ -167,8 +166,11 @@ def message_from(self, ticket: Ticket) -> Message:
167166
return message
168167

169168

170-
def create_ticket(self) -> Ticket:
171-
return self.redmine.create_ticket(self.user, self.create_message())
169+
def create_ticket(self, **kwargs) -> Ticket:
170+
# create a ticket from json template
171+
ticket = mock_ticket(**kwargs)
172+
self.session.cache_ticket(ticket)
173+
return ticket
172174

173175

174176
class MockBotTestCase(MockRedmineTestCase, unittest.IsolatedAsyncioTestCase):

0 commit comments

Comments
 (0)