1
1
from __future__ import annotations
2
2
3
3
from datetime import datetime , timezone
4
- from typing import TYPE_CHECKING , Dict , Union
4
+ from random import randint
5
+ from typing import TYPE_CHECKING , Dict , List , Union
5
6
from unittest .mock import Mock , patch
6
7
7
8
import pytest
8
9
from httpx import Response
9
10
from pydantic import ValidationError
10
11
from toggl_python .exceptions import BadRequest
11
12
from toggl_python .schemas .time_entry import (
13
+ BulkEditTimeEntriesFieldNames ,
14
+ BulkEditTimeEntriesOperation ,
15
+ BulkEditTimeEntriesOperations ,
16
+ BulkEditTimeEntriesResponse ,
12
17
MeTimeEntryResponse ,
13
18
MeTimeEntryWithMetaResponse ,
14
19
MeWebTimerResponse ,
15
20
)
16
21
17
22
from tests .responses .me_get import ME_WEB_TIMER_RESPONSE
18
23
from tests .responses .time_entry_get import ME_TIME_ENTRY_RESPONSE , ME_TIME_ENTRY_WITH_META_RESPONSE
24
+ from tests .responses .time_entry_put_and_patch import BULK_EDIT_TIME_ENTRIES_RESPONSE
19
25
20
26
21
27
if TYPE_CHECKING :
22
28
from respx import MockRouter
23
29
from toggl_python .entities .user import CurrentUser
30
+ from toggl_python .entities .workspace import Workspace
24
31
25
32
26
33
def test_get_time_entry__without_query_params (
@@ -111,7 +118,7 @@ def test_get_time_entries__with_meta_query_param(
111
118
assert result == expected_result
112
119
113
120
114
- @patch ("toggl_python.schemas.time_entry .datetime" )
121
+ @patch ("toggl_python.schemas.base .datetime" )
115
122
@pytest .mark .parametrize (
116
123
argnames = "query_params, method_kwargs" ,
117
124
argvalues = (
@@ -190,7 +197,7 @@ def test_get_time_entries__invalid_query_params(
190
197
_ = authed_current_user .get_time_entries (** query_params )
191
198
192
199
193
- @patch ("toggl_python.schemas.time_entry .datetime" )
200
+ @patch ("toggl_python.schemas.base .datetime" )
194
201
def test_get_time_entries__too_old_since_value (
195
202
mocked_datetime : Mock , authed_current_user : CurrentUser
196
203
) -> None :
@@ -212,3 +219,182 @@ def test_get_web_timer__ok(response_mock: MockRouter, authed_current_user: Curre
212
219
213
220
assert mocked_route .called is True
214
221
assert result == expected_result
222
+
223
+
224
+ @pytest .mark .parametrize (
225
+ argnames = ("field_name" , "field_value" ),
226
+ argvalues = [
227
+ ("billable" , True ),
228
+ ("description" , "updated description" ),
229
+ ("duration" , - 1 ),
230
+ ("project_id" , 757542305 ),
231
+ ("shared_with_user_ids" , [1243543643 , 676586868 ]),
232
+ ("start" , "2020-11-11T09:30:00-04:00" ),
233
+ ("stop" , "2010-01-29T19:50:00+02:00" ),
234
+ ("tag_ids" , [24032 , 354742502 ]),
235
+ ("tags" , ["new tag" ]),
236
+ ("task_id" , 1593268409 ),
237
+ ("user_id" , 573250897 ),
238
+ ],
239
+ )
240
+ def test_workspace_update_time_entry__ok (
241
+ field_name : str ,
242
+ field_value : Union [bool , str , int , List [int ]],
243
+ response_mock : MockRouter ,
244
+ authed_workspace : Workspace ,
245
+ ) -> None :
246
+ workspace_id = 123
247
+ time_entry_id = 98765
248
+ payload = {field_name : field_value }
249
+ fake_response = ME_TIME_ENTRY_RESPONSE .copy ()
250
+ fake_response .update (** payload )
251
+ mocked_route = response_mock .put (
252
+ f"/workspaces/{ workspace_id } /time_entries/{ time_entry_id } "
253
+ ).mock (
254
+ return_value = Response (status_code = 200 , json = fake_response ),
255
+ )
256
+ expected_result = MeTimeEntryResponse .model_validate (fake_response )
257
+
258
+ result = authed_workspace .update_time_entry (workspace_id , time_entry_id , ** payload )
259
+
260
+ assert mocked_route .called is True
261
+ assert result == expected_result
262
+
263
+
264
+ def test_update_time_entry__user_cannot_access_project (
265
+ response_mock : MockRouter , authed_workspace : Workspace
266
+ ) -> None :
267
+ workspace_id = 123
268
+ time_entry_id = 98765
269
+ error_message = "User cannot access the selected project"
270
+ mocked_route = response_mock .put (
271
+ f"/workspaces/{ workspace_id } /time_entries/{ time_entry_id } "
272
+ ).mock (
273
+ return_value = Response (status_code = 400 , text = error_message ),
274
+ )
275
+
276
+ with pytest .raises (BadRequest , match = error_message ):
277
+ _ = authed_workspace .update_time_entry (workspace_id , time_entry_id , project_id = 125872350 )
278
+
279
+ assert mocked_route .called is True
280
+
281
+
282
+ def test_delete_time_entry__ok (response_mock : MockRouter , authed_workspace : Workspace ) -> None :
283
+ workspace_id = 123
284
+ time_entry_id = 98765
285
+ mocked_route = response_mock .delete (
286
+ f"/workspaces/{ workspace_id } /time_entries/{ time_entry_id } "
287
+ ).mock (
288
+ return_value = Response (status_code = 200 ),
289
+ )
290
+
291
+ result = authed_workspace .delete_time_entry (workspace_id , time_entry_id )
292
+
293
+ assert mocked_route .called is True
294
+ assert result is True
295
+
296
+
297
+ def test_bulk_edit_time_entries__too_much_ids (authed_workspace : Workspace ) -> None :
298
+ workspace_id = 123
299
+ time_entry_ids = [randint (100000 , 999999 ) for _ in range (101 )] # noqa: S311
300
+ error_message = "Limit to max TimeEntry IDs exceeded. "
301
+
302
+ with pytest .raises (ValueError , match = error_message ):
303
+ _ = authed_workspace .bulk_edit_time_entries (workspace_id , time_entry_ids , operations = [])
304
+
305
+
306
+ def test_bulk_edit_time_entries__empty_time_entry_ids (authed_workspace : Workspace ) -> None :
307
+ workspace_id = 123
308
+ error_message = "Specify at least one TimeEntry ID"
309
+
310
+ with pytest .raises (ValueError , match = error_message ):
311
+ _ = authed_workspace .bulk_edit_time_entries (workspace_id , time_entry_ids = [], operations = [])
312
+
313
+
314
+ def test_bulk_edit_time_entries__empty_operations (authed_workspace : Workspace ) -> None :
315
+ workspace_id = 123
316
+ time_entry_ids = [12345677 ]
317
+ error_message = "Specify at least one edit operation"
318
+
319
+ with pytest .raises (ValueError , match = error_message ):
320
+ _ = authed_workspace .bulk_edit_time_entries (workspace_id , time_entry_ids , operations = [])
321
+
322
+
323
+ @pytest .mark .parametrize (
324
+ argnames = ("operation" ), argvalues = [item .value for item in BulkEditTimeEntriesOperations ]
325
+ )
326
+ @pytest .mark .parametrize (
327
+ argnames = ("field_name" , "field_value" ),
328
+ argvalues = [
329
+ (BulkEditTimeEntriesFieldNames .billable .value , True ),
330
+ (BulkEditTimeEntriesFieldNames .description .value , "updated description" ),
331
+ (BulkEditTimeEntriesFieldNames .duration .value , - 1 ),
332
+ (BulkEditTimeEntriesFieldNames .project_id .value , 757542305 ),
333
+ (BulkEditTimeEntriesFieldNames .shared_with_user_ids .value , [1243543643 , 676586868 ]),
334
+ (BulkEditTimeEntriesFieldNames .start .value , datetime (2024 , 5 , 10 , tzinfo = timezone .utc )),
335
+ (BulkEditTimeEntriesFieldNames .stop .value , datetime (2022 , 4 , 15 , tzinfo = timezone .utc )),
336
+ (BulkEditTimeEntriesFieldNames .tag_ids .value , [24032 , 354742502 ]),
337
+ (BulkEditTimeEntriesFieldNames .tags .value , ["new tag" ]),
338
+ (BulkEditTimeEntriesFieldNames .task_id .value , 1593268409 ),
339
+ (BulkEditTimeEntriesFieldNames .user_id .value , 573250897 ),
340
+ ],
341
+ )
342
+ def test_bulk_edit_time_entries__ok (
343
+ field_name : BulkEditTimeEntriesFieldNames ,
344
+ field_value : Union [str , int ],
345
+ operation : BulkEditTimeEntriesOperations ,
346
+ response_mock : MockRouter ,
347
+ authed_workspace : Workspace ,
348
+ ) -> None :
349
+ workspace_id = 123
350
+ time_entry_ids = [98765 , 43210 ]
351
+ edit_operation = BulkEditTimeEntriesOperation (
352
+ operation = operation , field_name = field_name , field_value = field_value
353
+ )
354
+ mocked_route = response_mock .patch (
355
+ f"/workspaces/{ workspace_id } /time_entries/{ time_entry_ids } "
356
+ ).mock (
357
+ return_value = Response (status_code = 200 , json = BULK_EDIT_TIME_ENTRIES_RESPONSE ),
358
+ )
359
+ expected_result = BulkEditTimeEntriesResponse .model_validate (BULK_EDIT_TIME_ENTRIES_RESPONSE )
360
+
361
+ result = authed_workspace .bulk_edit_time_entries (
362
+ workspace_id , time_entry_ids , operations = [edit_operation ]
363
+ )
364
+
365
+ assert mocked_route .called is True
366
+ assert result == expected_result
367
+
368
+
369
+ def test_stop_time_entry__ok (response_mock : MockRouter , authed_workspace : Workspace ) -> None :
370
+ workspace_id = 123
371
+ time_entry_id = 98765
372
+ mocked_route = response_mock .patch (
373
+ f"/workspaces/{ workspace_id } /time_entries/{ time_entry_id } /stop"
374
+ ).mock (
375
+ return_value = Response (status_code = 200 , json = ME_TIME_ENTRY_RESPONSE ),
376
+ )
377
+ expected_result = MeTimeEntryResponse .model_validate (ME_TIME_ENTRY_RESPONSE )
378
+
379
+ result = authed_workspace .stop_time_entry (workspace_id , time_entry_id )
380
+
381
+ assert mocked_route .called is True
382
+ assert result == expected_result
383
+
384
+
385
+ def test_stop_time_entry__already_stopped (
386
+ response_mock : MockRouter , authed_workspace : Workspace
387
+ ) -> None :
388
+ workspace_id = 123
389
+ time_entry_id = 98765
390
+ error_message = "Time entry already stopped"
391
+ mocked_route = response_mock .patch (
392
+ f"/workspaces/{ workspace_id } /time_entries/{ time_entry_id } /stop"
393
+ ).mock (
394
+ return_value = Response (status_code = 409 , text = error_message ),
395
+ )
396
+
397
+ with pytest .raises (BadRequest , match = error_message ):
398
+ _ = authed_workspace .stop_time_entry (workspace_id , time_entry_id )
399
+
400
+ assert mocked_route .called is True
0 commit comments