Passed
Pull Request — master (#44)
by Paolo
05:38
created

RetrievalCompleteTaskTestCase.test_need_revision()   A

Complexity

Conditions 1

Size

Total Lines 19
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 11
dl 0
loc 19
rs 9.85
c 0
b 0
f 0
cc 1
nop 1
1
#!/usr/bin/env python3
2
# -*- coding: utf-8 -*-
3
"""
4
Created on Tue Oct  9 14:51:13 2018
5
6
@author: Paolo Cozzi <[email protected]>
7
"""
8
9
from pytest import raises
10
from collections import Counter
11
from unittest.mock import patch, Mock
12
from datetime import timedelta
13
14
from celery.exceptions import Retry
15
16
from django.test import TestCase
17
from django.core import mail
18
from django.utils import timezone
19
20
from common.constants import (
21
    LOADED, ERROR, READY, NEED_REVISION, SUBMITTED, COMPLETED, STATUSES)
22
from common.tests import WebSocketMixin
23
from image_app.models import Submission, Name
24
25
from ..tasks.retrieval import (
26
    FetchStatusTask, FetchStatusHelper, RetrievalCompleteTask)
27
from ..models import ManagedTeam, Submission as USISubmission
28
29
30
class FetchMixin():
31
    """Mixin for fetching status"""
32
33
    fixtures = [
34
        'biosample/account',
35
        'biosample/managedteam',
36
        'biosample/submission',
37
        'biosample/submissiondata',
38
        'image_app/animal',
39
        'image_app/dictbreed',
40
        'image_app/dictcountry',
41
        'image_app/dictrole',
42
        'image_app/dictsex',
43
        'image_app/dictspecie',
44
        'image_app/dictstage',
45
        'image_app/dictuberon',
46
        'image_app/name',
47
        'image_app/organization',
48
        'image_app/publication',
49
        'image_app/sample',
50
        'image_app/submission',
51
        'image_app/user'
52
    ]
53
54
    @classmethod
55
    def setUpClass(cls):
56
        # calling my base class setup
57
        super().setUpClass()
58
59
        unmanaged = ManagedTeam.objects.get(pk=2)
60
        unmanaged.delete()
61
62
        # starting mocked objects
63
        cls.mock_root_patcher = patch('pyUSIrest.client.Root')
64
        cls.mock_root = cls.mock_root_patcher.start()
65
66
        cls.mock_auth_patcher = patch('biosample.helpers.Auth')
67
        cls.mock_auth = cls.mock_auth_patcher.start()
68
69
    @classmethod
70
    def tearDownClass(cls):
71
        cls.mock_root_patcher.stop()
72
        cls.mock_auth_patcher.stop()
73
74
        # calling base method
75
        super().tearDownClass()
76
77
    def setUp(self):
78
        # calling my base setup
79
        super().setUp()
80
81
        # get a submission object
82
        self.submission_obj = Submission.objects.get(pk=1)
83
84
        # set a status which I can fetch_status
85
        self.submission_obj.status = SUBMITTED
86
        self.submission_obj.message = "Waiting for biosample validation"
87
        self.submission_obj.save()
88
89
        # set status for names, like submittask does. Only sample not unknown
90
        # are submitted
91
        self.name_qs = Name.objects.exclude(name__contains="unknown")
92
        self.name_qs.update(status=SUBMITTED)
93
94
        # count number of names in UID for such submission (exclude
95
        # unknown animals)
96
        self.n_to_submit = self.name_qs.count()
97
98
        # track submission ID
99
        self.submission_obj_id = self.submission_obj.id
100
101
        # start root object
102
        self.my_root = self.mock_root.return_value
103
104
105
class FetchStatusHelperMixin(FetchMixin):
106
    """Test class for FetchStatusHelper"""
107
108
    def setUp(self):
109
        # calling my base setup
110
        super().setUp()
111
112
        # define a biosample submission object
113
        self.my_submission = Mock()
114
        self.my_submission.name = "test-fetch"
115
116
        # passing submission to Mocked Root
117
        self.my_root.get_submission_by_name.return_value = self.my_submission
118
119
        # get a biosample.model.Submission and update object
120
        self.usi_submission = USISubmission.objects.get(pk=1)
121
        self.usi_submission.usi_submission_name = self.my_submission.name
122
        self.usi_submission.status = SUBMITTED
123
        self.usi_submission.save()
124
125
        # ok setup the object
126
        self.status_helper = FetchStatusHelper(self.usi_submission)
127
128
        # track names
129
        self.animal_name = Name.objects.get(pk=3)
130
        self.sample_name = Name.objects.get(pk=4)
131
132
    def common_tests(self):
133
        """Assert stuff for each test"""
134
135
        # call stuff
136
        self.status_helper.check_submission_status()
137
138
        # UID submission status remain the same
139
        self.submission_obj.refresh_from_db()
140
        self.assertEqual(self.submission_obj.status, SUBMITTED)
141
142
        self.assertTrue(self.mock_auth.called)
143
        self.assertTrue(self.mock_root.called)
144
        self.assertTrue(self.my_root.get_submission_by_name.called)
145
146
147
class FetchIgnoreTestCase(FetchStatusHelperMixin, TestCase):
148
    """a submission that could be ignored"""
149
150
    def setUp(self):
151
        # calling my base setup
152
        super().setUp()
153
154
        # an unmanaged status
155
        self.my_submission.status = 'Unmanaged'
156
157
    def common_tests(self, status):
158
        """Override default common tests. Status is the status the
159
        submission is supposed to remain"""
160
161
        # assert auth, root and get_submission by name called
162
        super().common_tests()
163
164
        # USI submission status did't changed
165
        self.usi_submission.refresh_from_db()
166
        self.assertEqual(self.usi_submission.status, status)
167
168
        # check name status didn't changed
169
        qs = Name.objects.filter(status=SUBMITTED)
170
        self.assertEqual(len(qs), self.n_to_submit)
171
172
    def test_fetch_unmanaged_submission_status(self):
173
        """Test fetch status for an unmanaged submission"""
174
175
        # assert my common tests
176
        self.common_tests(status=SUBMITTED)
177
178
    def test_fetch_not_submitted(self):
179
        """Ignore a submission with a status different from SUBMITTED"""
180
181
        self.usi_submission.status = COMPLETED
182
        self.usi_submission.save()
183
184
        self.common_tests(status=COMPLETED)
185
186
187
class FetchCompletedTestCase(FetchStatusHelperMixin, TestCase):
188
    """a completed submission with two samples"""
189
190
    def setUp(self):
191
        # calling my base setup
192
        super().setUp()
193
194
        # a completed submission with two samples
195
        self.my_submission.status = 'Completed'
196
197
    def test_fetch_status(self):
198
        """Test fetch status for a complete submission"""
199
200
        # Add samples
201
        my_sample1 = Mock()
202
        my_sample1.name = "test-animal"
203
        my_sample1.alias = "IMAGEA000000001"
204
        my_sample1.accession = "SAMEA0000001"
205
        my_sample2 = Mock()
206
        my_sample2.name = "test-sample"
207
        my_sample2.alias = "IMAGES000000001"
208
        my_sample2.accession = "SAMEA0000002"
209
        self.my_submission.get_samples.return_value = [my_sample1, my_sample2]
210
211
        # assert auth, root and get_submission by name called
212
        self.common_tests()
213
214
        # USI submission status changed
215
        self.usi_submission.refresh_from_db()
216
        self.assertEqual(self.usi_submission.status, COMPLETED)
217
218
        # check name status changed
219
        qs = Name.objects.filter(status=COMPLETED)
220
        self.assertEqual(len(qs), 2)
221
222
        # fetch two name objects
223
        self.animal_name.refresh_from_db()
224
        self.assertEqual(self.animal_name.biosample_id, "SAMEA0000001")
225
226
        self.sample_name.refresh_from_db()
227
        self.assertEqual(self.sample_name.biosample_id, "SAMEA0000002")
228
229
    def test_fetch_status_no_accession(self):
230
        """Test fetch status for a submission which doens't send accession
231
        no updates in such case"""
232
233
        # Add samples
234
        my_sample1 = Mock()
235
        my_sample1.name = "test-animal"
236
        my_sample1.alias = "IMAGEA000000001"
237
        my_sample1.accession = None
238
        my_sample2 = Mock()
239
        my_sample2.name = "test-sample"
240
        my_sample2.alias = "IMAGES000000001"
241
        my_sample2.accession = None
242
        self.my_submission.get_samples.return_value = [my_sample1, my_sample2]
243
244
        # assert auth, root and get_submission by name called
245
        self.common_tests()
246
247
        # USI submission status didn't change
248
        self.usi_submission.refresh_from_db()
249
        self.assertEqual(self.usi_submission.status, SUBMITTED)
250
251
        # check name status didn't changed
252
        qs = Name.objects.filter(status=SUBMITTED)
253
        self.assertEqual(len(qs), self.n_to_submit)
254
255
256
class FetchWithErrorsTestCase(FetchStatusHelperMixin, TestCase):
257
    """Test a submission with errors for biosample"""
258
259
    def setUp(self):
260
        # calling my base setup
261
        super().setUp()
262
263
        # a draft submission with errors
264
        self.my_submission.status = 'Draft'
265
        self.my_submission.has_errors.return_value = Counter(
266
            {True: 1, False: 1})
267
        self.my_submission.get_status.return_value = Counter({'Complete': 2})
268
269
        # Add samples. Suppose that first failed, second is ok
270
        my_validation_result1 = Mock()
271
        my_validation_result1.errorMessages = {
272
            'Ena': [
273
                'a sample message',
274
            ]
275
        }
276
277
        my_sample1 = Mock()
278
        my_sample1.name = "test-animal"
279
        my_sample1.alias = "IMAGEA000000001"
280
        my_sample1.has_errors.return_value = True
281
        my_sample1.get_validation_result.return_value = my_validation_result1
282
283
        # sample2 is ok
284
        my_validation_result2 = Mock()
285
        my_validation_result2.errorMessages = None
286
287
        my_sample2 = Mock()
288
        my_sample2.name = "test-sample"
289
        my_sample2.alias = "IMAGES000000001"
290
        my_sample2.has_errors.return_value = False
291
        my_sample2.get_validation_result.return_value = my_validation_result2
292
293
        # simulate that IMAGEA000000001 has errors
294
        self.my_submission.get_samples.return_value = [my_sample1, my_sample2]
295
296
        # track other objects
297
        self.my_sample1 = my_sample1
298
        self.my_sample2 = my_sample2
299
300
    def common_tests(self):
301
        # assert auth, root and get_submission by name called
302
        super().common_tests()
303
304
        # assert custom mock attributes called
305
        self.assertTrue(self.my_sample1.has_errors.called)
306
        self.assertTrue(self.my_sample1.get_validation_result.called)
307
308
        # if sample has no errors, no all methods will be called
309
        self.assertTrue(self.my_sample2.has_errors.called)
310
        self.assertFalse(self.my_sample2.get_validation_result.called)
311
312
    def test_fetch_status(self):
313
        # assert tmock methods called
314
        self.common_tests()
315
316
        # USI submission changed
317
        self.usi_submission.refresh_from_db()
318
        self.assertEqual(self.usi_submission.status, NEED_REVISION)
319
320
        # check name status changed only for animal (not sample)
321
        self.animal_name.refresh_from_db()
322
        self.assertEqual(self.animal_name.status, NEED_REVISION)
323
324
        self.sample_name.refresh_from_db()
325
        self.assertEqual(self.sample_name.status, SUBMITTED)
326
327
328
class FetchDraftTestCase(FetchStatusHelperMixin, TestCase):
329
    """a draft submission without errors"""
330
331
    def common_tests(self):
332
        # assert auth, root and get_submission by name called
333
        super().common_tests()
334
335
        # USI submission status didn't change
336
        self.usi_submission.refresh_from_db()
337
        self.assertEqual(self.usi_submission.status, SUBMITTED)
338
339
    def test_fetch_status(self):
340
        # a draft submission without errors
341
        self.my_submission.status = 'Draft'
342
        self.my_submission.has_errors.return_value = Counter({False: 2})
343
        self.my_submission.get_status.return_value = Counter({'Complete': 2})
344
345
        # assert mock methods called
346
        self.common_tests()
347
348
        # testing a finalized biosample condition
349
        self.assertTrue(self.my_submission.finalize.called)
350
351
    def test_fetch_status_pending(self):
352
        """Testing status with pending validation"""
353
354
        # a draft submission without errors
355
        self.my_submission.status = 'Draft'
356
        self.my_submission.has_errors.return_value = Counter({False: 2})
357
        self.my_submission.get_status.return_value = Counter({'Pending': 2})
358
359
        # assert mock methods called
360
        self.common_tests()
361
362
        # testing a not finalized biosample condition
363
        self.assertFalse(self.my_submission.finalize.called)
364
365
    def test_fetch_status_submitted(self):
366
        """Testing status during biosample submission"""
367
368
        # a draft submission without errors
369
        self.my_submission.status = 'Submitted'
370
        self.my_submission.has_errors.return_value = Counter({False: 2})
371
        self.my_submission.get_status.return_value = Counter({'Complete': 2})
372
373
        # assert mock methods called
374
        self.common_tests()
375
376
        # testing a not finalized biosample condition
377
        self.assertFalse(self.my_submission.finalize.called)
378
379
380
class FetchLongStatusTestCase(FetchStatusHelperMixin, TestCase):
381
    """A submission wich remain in the same status for a long time"""
382
383
    def setUp(self):
384
        # calling my base setup
385
        super().setUp()
386
387
        # make "now" 2 months ago
388
        testtime = timezone.now() - timedelta(days=60)
389
390
        # https://devblog.kogan.com/blog/testing-auto-now-datetime-fields-in-django
391
        with patch('django.utils.timezone.now') as mock_now:
392
            mock_now.return_value = testtime
393
394
            # update submission updated time with an older date than now
395
            self.usi_submission.updated_at = testtime
396
            self.usi_submission.save()
397
398
    def common_tests(self):
399
        # assert auth, root and get_submission by name called
400
        super().common_tests()
401
402
        # biosample.models.Submission status changed
403
        self.assertEqual(self.usi_submission.status, ERROR)
404
        self.assertIn(
405
            "Biosample submission '{}' remained with the same status".format(
406
                self.my_submission.name),
407
            self.usi_submission.message
408
        )
409
410
    def test_error_in_submitted_status(self):
411
        # a still running submission
412
        self.my_submission.status = 'Submitted'
413
414
        # assert mock methods called
415
        self.common_tests()
416
417
    def test_error_in_draft_status(self):
418
        # a still running submission
419
        self.my_submission.status = 'Draft'
420
421
        # assert mock methods called
422
        self.common_tests()
423
424
425
class FetchUnsupportedStatusTestCase(FetchMixin, TestCase):
426
    """A submission object with a status I can ignore. Task will exit
427
    immediatey"""
428
429
    def setUp(self):
430
        # calling my base setup
431
        super().setUp()
432
433
        # define my task
434
        self.my_task = FetchStatusTask()
435
436
        # change lock_id (useful when running test during cron)
437
        self.my_task.lock_id = "test-FetchStatusTask"
438
439
    def update_status(self, status):
440
        # change status
441
        self.submission_obj.status = status
442
        self.submission_obj.save()
443
444
    # override FetchMixing methods
445
    def common_tests(self, status):
446
        # update submission status
447
        self.update_status(status)
448
449
        # NOTE that I'm calling the function directly, without delay
450
        # (AsyncResult). I've patched the time consuming task
451
        res = self.my_task.run()
452
453
        # assert a success with data uploading
454
        self.assertEqual(res, "success")
455
456
        self.assertFalse(self.mock_auth.called)
457
        self.assertFalse(self.mock_root.called)
458
        self.assertFalse(self.my_root.get_submission_by_name.called)
459
460
        # assert status for submissions
461
        self.submission_obj.refresh_from_db()
462
        self.assertEqual(self.submission_obj.status, status)
463
464
    def test_loaded(self):
465
        """Test fecth_status with a loaded submission"""
466
467
        # assert task and mock methods called
468
        self.common_tests(LOADED)
469
470
    def test_need_revision(self):
471
        """Test fecth_status with a need_revision submission"""
472
473
        # assert task and mock methods called
474
        self.common_tests(NEED_REVISION)
475
476
    def test_ready(self):
477
        """Test fecth_status with a ready submission"""
478
479
        # assert task and mock methods called
480
        self.common_tests(READY)
481
482
    def test_completed(self):
483
        """Test fecth_status with a completed submission"""
484
485
        # assert task and mock methods called
486
        self.common_tests(COMPLETED)
487
488
489
class FetchStatusTaskTestCase(FetchMixin, TestCase):
490
    def setUp(self):
491
        # calling my base setup
492
        super().setUp()
493
494
        # set proper status to biosample.models.Submission
495
        USISubmission.objects.update(status=SUBMITTED)
496
497
        # starting mocked objects
498
        self.mock_helper_patcher = patch(
499
            'biosample.tasks.retrieval.FetchStatusHelper')
500
        self.mock_helper = self.mock_helper_patcher.start()
501
502
        self.mock_complete_patcher = patch(
503
            'biosample.tasks.retrieval.RetrievalCompleteTask')
504
        self.mock_complete = self.mock_complete_patcher.start()
505
506
        # define my task
507
        self.my_task = FetchStatusTask()
508
509
        # change lock_id (useful when running test during cron)
510
        self.my_task.lock_id = "test-FetchStatusTask"
511
512
    def tearDown(self):
513
        self.mock_helper_patcher.stop()
514
        self.mock_complete_patcher.stop()
515
516
        # calling base method
517
        super().tearDown()
518
519
    def test_fetch_status(self):
520
        """Test fetch status task"""
521
522
        # NOTE that I'm calling the function directly, without delay
523
        # (AsyncResult). I've patched the time consuming task
524
        res = self.my_task.run()
525
526
        # assert a success with data uploading
527
        self.assertEqual(res, "success")
528
529
        # assert my objects called
530
        self.assertTrue(self.mock_helper.called)
531
        self.assertTrue(self.mock_complete.called)
532
533
        # those objects are proper of FetchStatusHelper class, no one
534
        # call them in this task itself
535
        self.assertFalse(self.mock_auth.called)
536
        self.assertFalse(self.mock_root.called)
537
538
    def test_fetch_status_all_completed(self):
539
        """Test fetch status task with completed biosample.models.Submission"""
540
541
        # simulate completed case (no more requests to biosample)
542
        USISubmission.objects.update(status=COMPLETED)
543
544
        # NOTE that I'm calling the function directly, without delay
545
        # (AsyncResult). I've patched the time consuming task
546
        res = self.my_task.run()
547
548
        # assert a success with data uploading
549
        self.assertEqual(res, "success")
550
551
        # assert no helper called for this submission
552
        self.assertFalse(self.mock_helper.called)
553
554
        # this is called if every submission is completed
555
        self.assertTrue(self.mock_complete.called)
556
557
        # those objects are proper of FetchStatusHelper class, no one
558
        # call them in this task itself
559
        self.assertFalse(self.mock_auth.called)
560
        self.assertFalse(self.mock_root.called)
561
562
    # http://docs.celeryproject.org/en/latest/userguide/testing.html#tasks-and-unit-tests
563
    @patch("biosample.tasks.FetchStatusTask.retry")
564
    @patch("biosample.tasks.FetchStatusTask.fetch_queryset")
565
    def test_fetch_status_retry(self, my_fetch, my_retry):
566
        """Test fetch status with retry"""
567
568
        # Set a side effect on the patched methods
569
        # so that they raise the errors we want.
570
        my_retry.side_effect = Retry()
571
        my_fetch.side_effect = ConnectionError()
572
573
        with raises(Retry):
574
            self.my_task.run()
575
576
        self.assertTrue(my_fetch.called)
577
        self.assertTrue(my_retry.called)
578
579
        # assert no helper called for this submission
580
        self.assertFalse(self.mock_helper.called)
581
        self.assertFalse(self.mock_complete.called)
582
583
    # Test a non blocking instance
584
    @patch("biosample.tasks.FetchStatusTask.fetch_queryset")
585
    @patch("redis.lock.Lock.acquire", return_value=False)
586
    def test_fetch_status_nb(self, my_lock, my_fetch):
587
        """Test FetchSTatus while a lock is present"""
588
589
        res = self.my_task.run()
590
591
        # assert database is locked
592
        self.assertEqual(res, "%s already running!" % (self.my_task.name))
593
        self.assertFalse(my_fetch.called)
594
595
        # assert no helper called for this submission
596
        self.assertFalse(self.mock_helper.called)
597
        self.assertFalse(self.mock_complete.called)
598
599
600
class RetrievalCompleteTaskTestCase(FetchMixin, WebSocketMixin, TestCase):
601
    """testing update status after a fetch status"""
602
603
    def setUp(self):
604
        # calling my base setup
605
        super().setUp()
606
607
        # set proper status to biosample.models.Submission
608
        USISubmission.objects.update(status=SUBMITTED)
609
610
        # define my task
611
        self.my_task = RetrievalCompleteTask()
612
613
    def updated_check(self, status, message):
614
        """Common check for tests"""
615
616
        # set proper status to biosample.models.Submission
617
        USISubmission.objects.update(status=status, message=message)
618
619
        # calling task
620
        result = self.my_task.run(
621
            uid_submission_id=self.submission_obj_id)
622
623
        # assert a success with data uploading
624
        self.assertEqual(result, "success")
625
626
        # check status and messages
627
        self.submission_obj.refresh_from_db()
628
        self.assertEqual(self.submission_obj.status, status)
629
        self.assertEqual(self.submission_obj.message, message)
630
631
        # calling a WebSocketMixin method
632
        self.check_message(
633
            STATUSES.get_value_display(status),
634
            message)
635
636
    def not_updated_check(self, status, message):
637
        """Test a submission not updated"""
638
639
        # set proper status to biosample.models.Submission
640
        USISubmission.objects.filter(pk=1).update(
641
            status=status, message=message)
642
643
        # calling task
644
        result = self.my_task.run(
645
            uid_submission_id=self.submission_obj_id)
646
647
        # assert a success with data uploading
648
        self.assertEqual(result, "success")
649
650
        # check status and messages
651
        self.submission_obj.refresh_from_db()
652
        self.assertEqual(self.submission_obj.status, SUBMITTED)
653
        self.assertEqual(
654
            self.submission_obj.message,
655
            "Waiting for biosample validation")
656
657
        # defined in websoketmixin
658
        self.check_message_not_called()
659
660
    def test_submitted(self):
661
        """Test submitted status"""
662
663
        status = SUBMITTED
664
        message = "Waiting for biosample validation"
665
666
        self.not_updated_check(
667
            status,
668
            message)
669
670
    def test_error(self):
671
        """test an error in a submission"""
672
673
        status = ERROR
674
        message = "error messages"
675
676
        self.updated_check(
677
            status,
678
            message)
679
680
        # test email sent
681
        self.assertEqual(len(mail.outbox), 1)
682
683
        # read email
684
        email = mail.outbox[0]
685
686
        self.assertEqual(
687
            "Error in biosample submission 1",
688
            email.subject)
689
690
    def test_need_revision(self):
691
        """test an issue in a submission"""
692
693
        status = NEED_REVISION
694
        message = "error messages"
695
696
        self.updated_check(
697
            status,
698
            message)
699
700
        # test email sent
701
        self.assertEqual(len(mail.outbox), 1)
702
703
        # read email
704
        email = mail.outbox[0]
705
706
        self.assertEqual(
707
            "Error in biosample submission 1",
708
            email.subject)
709
710
    def test_completed(self):
711
        """test a submission completed"""
712
713
        status = COMPLETED
714
        message = "completed messages"
715
716
        self.updated_check(
717
            status,
718
            message)
719
720
    def test_partial_submitted(self):
721
        """Having submitted in statuses will not complete the submission"""
722
723
        status = ERROR
724
        message = "error messages"
725
726
        self.not_updated_check(
727
            status,
728
            message)
729
730
        status = NEED_REVISION
731
        message = "error messages"
732
733
        self.not_updated_check(
734
            status,
735
            message)
736
737
        status = COMPLETED
738
        message = "completed messages"
739
740
        self.not_updated_check(
741
            status,
742
            message)
743