""" Unit tests for integration of the django-user-tasks app and its REST API. """ from __future__ import absolute_import, print_function, unicode_literals import logging from uuid import uuid4 import mock from boto.exception import NoAuthHandlerFound from django.conf import settings from django.contrib.auth.models import User from django.core import mail from django.core.urlresolvers import reverse from django.test import override_settings from rest_framework.test import APITestCase from user_tasks.models import UserTaskArtifact, UserTaskStatus from user_tasks.serializers import ArtifactSerializer, StatusSerializer from .signals import user_task_stopped class MockLoggingHandler(logging.Handler): """ Mock logging handler to help check for logging statements """ def __init__(self, *args, **kwargs): self.reset() logging.Handler.__init__(self, *args, **kwargs) def emit(self, record): """ Override to catch messages and store them messages in our internal dicts """ self.messages[record.levelname.lower()].append(record.getMessage()) def reset(self): """ Clear out all messages, also called to initially populate messages dict """ self.messages = { 'debug': [], 'info': [], 'warning': [], 'error': [], 'critical': [], } # Helper functions for stuff that pylint complains about without disable comments def _context(response): """ Get a context dictionary for a serializer appropriate for the given response. """ return {'request': response.wsgi_request} # pylint: disable=no-member def _data(response): """ Get the serialized data dictionary from the given REST API test response. """ return response.data # pylint: disable=no-member @override_settings(BROKER_URL='memory://localhost/') class TestUserTasks(APITestCase): """ Tests of the django-user-tasks REST API endpoints. Detailed tests of the default authorization rules are in the django-user-tasks code. These tests just verify that the API is exposed and functioning. """ @classmethod def setUpTestData(cls): cls.user = User.objects.create_user('test_user', 'test@example.com', 'password') cls.status = UserTaskStatus.objects.create( user=cls.user, task_id=str(uuid4()), task_class='test_rest_api.sample_task', name='SampleTask 2', total_steps=5) cls.artifact = UserTaskArtifact.objects.create(status=cls.status, text='Lorem ipsum') def setUp(self): super(TestUserTasks, self).setUp() self.status.refresh_from_db() self.client.force_authenticate(self.user) # pylint: disable=no-member def test_artifact_detail(self): """ Users should be able to access artifacts for tasks they triggered. """ response = self.client.get(reverse('usertaskartifact-detail', args=[self.artifact.uuid])) assert response.status_code == 200 serializer = ArtifactSerializer(self.artifact, context=_context(response)) assert _data(response) == serializer.data def test_artifact_list(self): """ Users should be able to access a list of their tasks' artifacts. """ response = self.client.get(reverse('usertaskartifact-list')) assert response.status_code == 200 serializer = ArtifactSerializer(self.artifact, context=_context(response)) assert _data(response)['results'] == [serializer.data] def test_status_cancel(self): """ Users should be able to cancel tasks they no longer wish to complete. """ response = self.client.post(reverse('usertaskstatus-cancel', args=[self.status.uuid])) assert response.status_code == 200 self.status.refresh_from_db() assert self.status.state == UserTaskStatus.CANCELED def test_status_delete(self): """ Users should be able to delete their own task status records when they're done with them. """ response = self.client.delete(reverse('usertaskstatus-detail', args=[self.status.uuid])) assert response.status_code == 204 assert not UserTaskStatus.objects.filter(pk=self.status.id).exists() def test_status_detail(self): """ Users should be able to access status records for tasks they triggered. """ response = self.client.get(reverse('usertaskstatus-detail', args=[self.status.uuid])) assert response.status_code == 200 serializer = StatusSerializer(self.status, context=_context(response)) assert _data(response) == serializer.data def test_status_list(self): """ Users should be able to access a list of their tasks' status records. """ response = self.client.get(reverse('usertaskstatus-list')) assert response.status_code == 200 serializer = StatusSerializer([self.status], context=_context(response), many=True) assert _data(response)['results'] == serializer.data @override_settings(BROKER_URL='memory://localhost/') class TestUserTaskStopped(APITestCase): """ Tests of the django-user-tasks signal handling and email integration. """ @classmethod def setUpTestData(cls): cls.user = User.objects.create_user('test_user', 'test@example.com', 'password') cls.status = UserTaskStatus.objects.create( user=cls.user, task_id=str(uuid4()), task_class='test_rest_api.sample_task', name='SampleTask 2', total_steps=5) def setUp(self): super(TestUserTaskStopped, self).setUp() self.status.refresh_from_db() self.client.force_authenticate(self.user) # pylint: disable=no-member def test_email_sent_with_site(self): """ Check the signal receiver and email sending. """ UserTaskArtifact.objects.create( status=self.status, name='BASE_URL', url='https://test.edx.org/' ) user_task_stopped.send(sender=UserTaskStatus, status=self.status) subject = "{platform_name} {studio_name}: Task Status Update".format( platform_name=settings.PLATFORM_NAME, studio_name=settings.STUDIO_NAME ) body_fragments = [ "Your {task_name} task has completed with the status".format(task_name=self.status.name.lower()), "https://test.edx.org/", reverse('usertaskstatus-detail', args=[self.status.uuid]) ] self.assertEqual(len(mail.outbox), 1) msg = mail.outbox[0] self.assertEqual(msg.subject, subject) for fragment in body_fragments: self.assertIn(fragment, msg.body) def test_email_not_sent_for_child(self): """ No email should be send for child tasks in chords, chains, etc. """ child_status = UserTaskStatus.objects.create( user=self.user, task_id=str(uuid4()), task_class='test_rest_api.sample_task', name='SampleTask 2', total_steps=5, parent=self.status) user_task_stopped.send(sender=UserTaskStatus, status=child_status) self.assertEqual(len(mail.outbox), 0) def test_email_sent_without_site(self): """ Make sure we send a generic email if the BASE_URL artifact doesn't exist """ user_task_stopped.send(sender=UserTaskStatus, status=self.status) subject = "{platform_name} {studio_name}: Task Status Update".format( platform_name=settings.PLATFORM_NAME, studio_name=settings.STUDIO_NAME ) fragments = [ "Your {task_name} task has completed with the status".format(task_name=self.status.name.lower()), "Sign in to view the details of your task or download any files created." ] self.assertEqual(len(mail.outbox), 1) msg = mail.outbox[0] self.assertEqual(msg.subject, subject) for fragment in fragments: self.assertIn(fragment, msg.body) def test_email_retries(self): """ Make sure we can succeed on retries """ with mock.patch('django.core.mail.send_mail') as mock_exception: mock_exception.side_effect = NoAuthHandlerFound() with mock.patch('cms_user_tasks.tasks.send_task_complete_email.retry') as mock_retry: user_task_stopped.send(sender=UserTaskStatus, status=self.status) self.assertTrue(mock_retry.called) def test_queue_email_failure(self): logger = logging.getLogger("cms_user_tasks.signals") hdlr = MockLoggingHandler(level="DEBUG") logger.addHandler(hdlr) with mock.patch('cms_user_tasks.tasks.send_task_complete_email.delay') as mock_delay: mock_delay.side_effect = NoAuthHandlerFound() user_task_stopped.send(sender=UserTaskStatus, status=self.status) self.assertTrue(mock_delay.called) self.assertEqual(hdlr.messages['error'][0], u'Unable to queue send_task_complete_email')