def test_video_subtitle_key(): """ Tests that the correct subtitle key is returned for a language""" video = VideoFactory(key="8494dafc-3665-4960-8e00-9790574ec93a") now = datetime.now(tz=pytz.UTC) assert (re.fullmatch( "subtitles/8494dafc366549608e009790574ec93a/subtitles_8494dafc366549608e009790574ec93a_{}_en.vtt" .format(now.strftime("%Y%m%d%H%M%S")), video.subtitle_key(now, "en"), ) is not None)
def test_youtube_video_delete_signal(mocker): """ Tests that a video's YouTubeVideo object is deleted after changing from public to private""" mock_task = mocker.patch("ui.signals.remove_youtube_video.delay") video = VideoFactory(is_public=True) yt_video = YouTubeVideoFactory(video=video) youtube_id = yt_video.id video.is_public = False video.save() mock_task.assert_called_once_with(youtube_id)
def test_schedule_retranscodes_error(mocker, mocked_celery): """ Test that schedule_retranscodes logs an error if it occurs """ mock_error_log = mocker.patch("cloudsync.tasks.log.exception") mocker.patch("cloudsync.tasks.retranscode_video.si", side_effect=ClientError) VideoFactory.create_batch(5, schedule_retranscode=True) schedule_retranscodes.delay() mock_error_log.assert_called_with("schedule_retranscodes threw an error")
def test_refresh_status_video_job_othererror(mocker, status): """ Verify that refresh_status does not raise ClientError """ video = VideoFactory(status=status) EncodeJobFactory(video=video) video.status = VideoStatus.TRANSCODING mocker.patch("ui.utils.boto3", MockBoto) error = Exception("unexpected exception") mocker.patch("ui.utils.get_transcoder_client", return_value=MockClientET(error=error)) with pytest.raises(Exception): api.refresh_status(video)
def test_transcode_job(mocker, status, expected_status): """ Test that video status is updated properly after a transcode job is successfully created """ video = VideoFactory.create(status=status) videofile = VideoFileFactory.create(video=video) prefix = RETRANSCODE_FOLDER if status == VideoStatus.RETRANSCODE_SCHEDULED else "" preset = { "Key": f"{prefix}transcoded/" + video.hexkey + "/video_1351620000001-000040", "PresetId": "1351620000001-000040", "SegmentDuration": "10.0", } if status != VideoStatus.RETRANSCODE_SCHEDULED: preset["ThumbnailPattern"] = ("thumbnails/" + video.hexkey + "/video_thumbnail_{count}") mocker.patch.multiple( "cloudsync.tasks.settings", ET_PRESET_IDS=("1351620000001-000040", "1351620000001-000020"), AWS_REGION="us-east-1", ET_PIPELINE_ID="foo", ENVIRONMENT="test", ) mock_encoder = mocker.patch("cloudsync.api.VideoTranscoder.encode") mock_delete_objects = mocker.patch("cloudsync.api.delete_s3_objects") mocker.patch("ui.models.tasks") api.transcode_video(video, videofile) # pylint: disable=no-value-for-parameter mock_encoder.assert_called_once_with( {"Key": videofile.s3_object_key}, [ preset, { "Key": f"{prefix}transcoded/" + video.hexkey + "/video_1351620000001-000020", "PresetId": "1351620000001-000020", "SegmentDuration": "10.0", }, ], Playlists=[{ "Format": "HLSv3", "Name": f"{prefix}transcoded/" + video.hexkey + "/video__index", "OutputKeys": [ f"{prefix}transcoded/" + video.hexkey + "/video_1351620000001-000040", f"{prefix}transcoded/" + video.hexkey + "/video_1351620000001-000020", ], }], UserMetadata={"pipeline": "odl-video-service-test"}, ) assert len(video.encode_jobs.all()) == 1 assert mock_delete_objects.call_count == ( 1 if status == VideoStatus.RETRANSCODE_SCHEDULED else 0) assert Video.objects.get(id=video.id).status == expected_status
def test_video_ordering(): """ Tests that videos are sorted by reverse creation date or forward custom order """ collection = CollectionFactory.create() VideoFactory.create_batch(10, collection=collection) # Should be sorted by reverse creation date videos = collection.videos.all() for (idx, video) in enumerate(videos): if idx > len(videos) - 1: assert video.created_at >= videos[idx + 1].created_at videos[idx].custom_order = len(videos) - idx - 1 videos[idx].save() # Should be sorted by custom_order resorted_videos = Collection.objects.get(id=collection.id).videos.all() for (idx, video) in enumerate(resorted_videos): assert video.custom_order == idx
def test_async_send_notification_email_no_video(mocker): """ Tests async_send_notification_email for a video_id that does not exist """ mocked_send_email = mocker.patch("mail.tasks.send_notification_email", autospec=True) video = VideoFactory(status=VideoStatus.COMPLETE) tasks.async_send_notification_email.delay(video.id + 10000) assert mocked_send_email.call_count == 0
def test_schedule_retranscodes(mocker, mock_transcode, mock_successful_encode_job, mocked_celery): """ Test that schedule_retranscodes triggers retranscode_video tasks for each scheduled video """ retranscode_video_mock = mocker.patch("cloudsync.tasks.retranscode_video", autospec=True) collection = CollectionFactory.create(schedule_retranscode=True) scheduled_videos = VideoFactory.create_batch(5, schedule_retranscode=True) VideoFactory.create_batch(3, schedule_retranscode=False) with pytest.raises(mocked_celery.replace_exception_class): schedule_retranscodes.delay() assert mocked_celery.group.call_count == 1 assert retranscode_video_mock.si.call_count == len(scheduled_videos) for video in scheduled_videos: retranscode_video_mock.si.assert_any_call(video.id) assert Collection.objects.get( id=collection.id).schedule_retranscode is False
def test_async_send_notification_email_happy_path(mocker): """ Tests async_send_notification_email with happy path """ mocked_send_email = mocker.patch("mail.tasks.send_notification_email", autospec=True) video = VideoFactory(status=VideoStatus.COMPLETE) tasks.async_send_notification_email.delay(video.id) mocked_send_email.assert_called_once_with(video)
def test_video_sources_hls(): """ Tests that the video sources property returns the expected result for HLS """ video = VideoFactory(key="8494dafc-3665-4960-8e00-9790574ec93a") videofile = VideoFileFactory(video=video, encoding=EncodingNames.HLS) assert video.sources == [{ "src": videofile.cloudfront_url, "label": EncodingNames.HLS, "type": "application/x-mpegURL", }]
def test_download_mp4(encodings, download): """ Tests that video.download returns the most appropriate file for download """ video = VideoFactory() for encoding in encodings: VideoFileFactory(video=video, s3_object_key="{}.mp4".format(encoding), encoding=encoding) if not download: assert video.download is None else: assert video.download.encoding == download
def test_send_notification_email_wrong_status(mocker): """ Tests send_notification_email with a status that does not require sending an email """ mocked_mailgun = mocker.patch("mail.api.MailgunClient", autospec=True) mocked__get_recipients_for_video = mocker.patch( "mail.tasks._get_recipients_for_video", autospec=True) assert VideoStatus.UPLOADING not in tasks.STATUS_TO_NOTIFICATION video = VideoFactory(status=VideoStatus.UPLOADING) tasks.send_notification_email(video) assert mocked_mailgun.send_individual_email.call_count == 0 assert mocked__get_recipients_for_video.call_count == 0
def test_original_video(): """ Tests that the original_video property returns the VideoFile with 'original' encoding """ video = VideoFactory(key="8494dafc-3665-4960-8e00-9790574ec93a") videofiles = [ VideoFileFactory(video=video, s3_object_key="original.mp4", encoding=EncodingNames.ORIGINAL), VideoFileFactory(video=video, s3_object_key="transcoded.hls", encoding=EncodingNames.HLS), ] assert video.original_video == videofiles[0]
def test_collection_serializer_private_video(mocker, is_admin, is_superuser): """ Test that a private video is not included in the serializer unless the user is super/admin """ has_permission = is_superuser or is_admin mocker.patch("ui.permissions.has_admin_permission", return_value=has_permission) mocked_request = mocker.MagicMock() mocked_request.user = UserFactory.create(is_superuser=is_superuser) mocker.patch("ui.serializers.has_common_lists", return_value=is_admin) collection = factories.CollectionFactory( admin_lists=[MoiraListFactory.create()]) VideoFactory.create(collection=collection) VideoFactory.create(is_private=True, collection=collection) serialized_data = serializers.CollectionSerializer( collection, context=dict(request=mocked_request)).data assert len(serialized_data["videos"]) == (2 if has_permission else 1)
def test_send_notification_email_no_recipients(mocker): """ Tests send_notification_email for a video that has no recipients """ mocked_mailgun = mocker.patch("mail.api.MailgunClient", autospec=True) mocked__get_recipients_for_video = mocker.patch( "mail.tasks._get_recipients_for_video", autospec=True, return_value=[]) assert VideoStatus.COMPLETE in tasks.STATUS_TO_NOTIFICATION video = VideoFactory(status=VideoStatus.COMPLETE) tasks.send_notification_email(video) assert mocked_mailgun.send_individual_email.call_count == 0 mocked__get_recipients_for_video.assert_called_once_with(video)
def test_sends_debug_emails(mocker, status): """ Tests send_notification_email with statuses that should trigger sending a separate email to support. """ mocked_mailgun = mocker.patch("mail.api.MailgunClient", autospec=True) mocked_send_debug_email = mocker.patch("mail.tasks._send_debug_email", autospec=True) video = VideoFactory(status=status) tasks.send_notification_email(video) mocked_send_debug_email.assert_called_once_with( video=video, email_kwargs=mocked_mailgun.send_batch.call_args[1])
def test_transcoded_hls_video(): """ Tests that Video.transcoded_videos returns transcoded HLS videofile""" video = VideoFactory() videofiles = [ VideoFileFactory(video=video, s3_object_key="original.mp4", encoding=EncodingNames.ORIGINAL), VideoFileFactory(video=video, s3_object_key="video.m3u8", encoding=EncodingNames.HLS), ] assert len(video.transcoded_videos) == 1 assert video.transcoded_videos[0] == videofiles[1]
def test_upload_youtube_videos_error(mocker): """ Test that the YoutubeVideo object is deleted if an error occurs during upload, and all videos are processed """ videos = VideoFactory.create_batch(3, is_public=True, status=VideoStatus.COMPLETE) mock_uploader = mocker.patch("cloudsync.tasks.YouTubeApi.upload_video", side_effect=OSError) upload_youtube_videos() assert mock_uploader.call_count == 3 for video in videos: assert YouTubeVideo.objects.filter(video=video).first() is None
def test_send_notification_email_no_mail_template(mocker): """ Tests send_notification_email for a video with a status not correspondent to a email template """ mocked_mailgun = mocker.patch("mail.api.MailgunClient", autospec=True) mock_log = mocker.patch("mail.tasks.log.error") video = VideoFactory(status=VideoStatus.RETRANSCODING) tasks.send_notification_email(video) assert mocked_mailgun.send_individual_email.call_count == 0 mock_log.assert_called_once_with( "Unexpected video status", video_hexkey=video.hexkey, video_status="Retranscoding", )
def test_upload_youtube_videos(mocker, source, max_uploads): """ Test that the upload_youtube_videos task calls YouTubeApi.upload_video & creates a YoutubeVideo object for each public video, up to the max daily limit """ settings.YT_UPLOAD_LIMIT = max_uploads private_videos = VideoFactory.create_batch(2, is_public=False, status=VideoStatus.COMPLETE) VideoFactory.create_batch( 3, collection=CollectionFactory(stream_source=source), is_public=True, status=VideoStatus.COMPLETE, ) mock_uploader = mocker.patch( "cloudsync.tasks.YouTubeApi.upload_video", return_value={ "id": "".join([random.choice(string.ascii_lowercase) for n in range(8)]), "status": { "uploadStatus": "uploaded" }, }, ) upload_youtube_videos() assert mock_uploader.call_count == (min( 3, max_uploads) if source != StreamSource.CLOUDFRONT else 0) for video in Video.objects.filter( is_public=True).order_by("-created_at")[:settings.YT_UPLOAD_LIMIT]: if video.collection.stream_source != StreamSource.CLOUDFRONT: assert YouTubeVideo.objects.filter(video=video).first() is not None else: assert YouTubeVideo.objects.filter(video=video).first() is None for video in private_videos: assert YouTubeVideo.objects.filter(video=video).first() is None
def test_render_email_templates(self, _): """Test render_email_templates""" video = VideoFactory.create() context = context_for_video(video) subject, text_body, html_body = render_email_templates("sample", context) assert subject == f'Your video "{video.title}" is ready' assert text_body == f"html link ({context['video_url']})" assert html_body == ( '<style type="text/css">\n' "a {\n" " color: red;\n" "}\n" "</style>\n" f'<a href="{context["video_url"]}">html link</a>\n' )
def test_video_sources_mp4(): """ Tests that the video sources property returns the expected sorted results for MP4 """ video = VideoFactory(key="8494dafc-3665-4960-8e00-9790574ec93a") videofiles = [ VideoFileFactory(video=video, s3_object_key="medium.mp4", encoding=EncodingNames.MEDIUM), VideoFileFactory(video=video, s3_object_key="small.mp4", encoding=EncodingNames.SMALL), VideoFileFactory(video=video, s3_object_key="large.mp4", encoding=EncodingNames.LARGE), VideoFileFactory(video=video, s3_object_key="basic.mp4", encoding=EncodingNames.BASIC), VideoFileFactory(video=video, s3_object_key="hd.mp4", encoding=EncodingNames.HD), ] assert video.sources == [ { "src": videofiles[4].cloudfront_url, "label": EncodingNames.HD, "type": "video/mp4", }, { "src": videofiles[2].cloudfront_url, "label": EncodingNames.LARGE, "type": "video/mp4", }, { "src": videofiles[0].cloudfront_url, "label": EncodingNames.MEDIUM, "type": "video/mp4", }, { "src": videofiles[3].cloudfront_url, "label": EncodingNames.BASIC, "type": "video/mp4", }, { "src": videofiles[1].cloudfront_url, "label": EncodingNames.SMALL, "type": "video/mp4", }, ]
def test_refresh_status_video_job_status_complete(mocker, status): """ Verify that Video.job_status property returns the status of its encoding job """ video = VideoFactory(status=status) encodejob = EncodeJobFactory(video=video) MockClientET.job = { "Job": { "Id": "1498220566931-qtmtcu", "Status": "Complete" } } mocker.patch("ui.utils.boto3", MockBoto) mocker.patch("cloudsync.api.process_transcode_results") mocker.patch("ui.models.tasks") api.refresh_status(video, encodejob) assert video.status == VideoStatus.COMPLETE
def test_upload_youtube_quota_exceeded(mocker, msg): """ Test that the YoutubeVideo object is deleted if an error occurs during upload, and the loop is halted if the quota is exceeded. """ videos = VideoFactory.create_batch(3, is_public=True, status=VideoStatus.COMPLETE) mock_uploader = mocker.patch( "cloudsync.tasks.YouTubeApi.upload_video", side_effect=ResumableUploadError(MockHttpErrorResponse(403), str.encode(msg, "utf-8")), ) upload_youtube_videos() assert mock_uploader.call_count == (1 if msg == API_QUOTA_ERROR_MSG else 3) for video in videos: assert YouTubeVideo.objects.filter(video=video).first() is None
def test_upload_video_long_fields(mocker): """ Test that the upload_youtube_video task truncates title and description if too long """ title = "".join(random.choice(string.ascii_lowercase) for c in range(105)) desc = "".join(random.choice(string.ascii_lowercase) for c in range(5005)) video = VideoFactory.create(title=title, description=desc, is_public=True, status=VideoStatus.COMPLETE) VideoFileFactory(video=video) mocker.patch("cloudsync.youtube.resumable_upload") youtube_mocker = mocker.patch("cloudsync.youtube.build") mock_upload = youtube_mocker().videos.return_value.insert YouTubeApi().upload_video(video) called_args, called_kwargs = mock_upload.call_args assert called_kwargs["body"]["snippet"]["title"] == title[:100] assert called_kwargs["body"]["snippet"]["description"] == desc[:5000]
def test_get_recipients_for_video(mocker): """ Tests the _get_recipients_for_video api """ mock_client = mocker.patch("mail.tasks.get_moira_client") lists = MoiraListFactory.create_batch(3) video = VideoFactory(collection__admin_lists=lists) list_attributes = [[{"mailList": False}], [{"mailList": True}], None] list_emails = ["{}@mit.edu".format(lists[1].name)] mocker.patch("mail.tasks.has_common_lists", return_value=False) mock_client( ).client.service.getListAttributes.side_effect = list_attributes assert tasks._get_recipients_for_video( video) == list_emails + [video.collection.owner.email] mocker.patch("mail.tasks.has_common_lists", return_value=True) mock_client( ).client.service.getListAttributes.side_effect = list_attributes assert tasks._get_recipients_for_video(video) == list_emails
def test_send_notification_email_happy_path(mocker): """ Tests send_notification_email with happy path """ mocked_mailgun = mocker.patch("mail.api.MailgunClient", autospec=True) assert VideoStatus.COMPLETE in tasks.STATUS_TO_NOTIFICATION video = VideoFactory(status=VideoStatus.COMPLETE) subject, text, html = render_email_templates( STATUS_TO_NOTIFICATION[VideoStatus.COMPLETE], context_for_video(video)) tasks.send_notification_email(video) mocked_mailgun.send_batch.assert_called_once_with( **{ "subject": subject, "html_body": html, "text_body": text, "recipients": [(video.collection.owner.email, {})], "sender_address": settings.EMAIL_SUPPORT, "raise_for_status": True, })
def test_send_debug_email(mocker): """ Tests sends debug email to support. """ mocked_mailgun = mocker.patch("mail.api.MailgunClient", autospec=True) mocked_generate_debug_email_body = mocker.patch( "mail.tasks._generate_debug_email_body") mock_email_kwargs = defaultdict(mocker.MagicMock) video = VideoFactory() tasks._send_debug_email(video=video, email_kwargs=mock_email_kwargs) mocked_generate_debug_email_body.assert_called_once_with( video=video, email_kwargs=mock_email_kwargs) mocked_mailgun.send_individual_email.assert_called_once_with( **{ "subject": "DEBUG:{}".format(mock_email_kwargs["subject"]), "html_body": None, "text_body": mocked_generate_debug_email_body.return_value, "recipient": settings.EMAIL_SUPPORT, })
def test_transcoded_mp4_video(): """ Tests that Video.transcoded_videos returns transcoded MP4 videos in the correct order""" video = VideoFactory() videofiles = [ VideoFileFactory(video=video, s3_object_key="original.mp4", encoding=EncodingNames.ORIGINAL), VideoFileFactory(video=video, s3_object_key="small.mp4", encoding=EncodingNames.SMALL), VideoFileFactory(video=video, s3_object_key="basic.mp4", encoding=EncodingNames.BASIC), VideoFileFactory(video=video, s3_object_key="HD.mp4", encoding=EncodingNames.HD), ] assert len(video.transcoded_videos) == 3 assert video.transcoded_videos[0] == videofiles[3] assert video.transcoded_videos[1] == videofiles[2] assert video.transcoded_videos[2] == videofiles[1]
def test_youtube_video_permissions_signal(mocker): """ Tests that a video's public permissions are removed if it's subtitle is deleted """ mock_delete_video = mocker.patch("ui.signals.remove_youtube_video.delay") mock_delete_caption = mocker.patch("ui.signals.remove_youtube_caption.delay") mocker.patch("ui.models.VideoSubtitle.delete_from_s3") video = VideoFactory(is_public=True) YouTubeVideoFactory(video=video) VideoSubtitleFactory(video=video) VideoSubtitleFactory(video=video, language="fr") video.videosubtitle_set.get(language="fr").delete() # video's public status should not be changed as long as 1 subtitle still exists assert video.is_public is True assert mock_delete_caption.call_count == 1 video.videosubtitle_set.first().delete() # If no subtitles exists, video should be made non-public and deleted from youtube assert mock_delete_video.call_count == 1 assert not video.is_public caption = VideoSubtitleFactory(video=video) mock_video_save = mocker.patch("ui.models.Video.save") caption.delete() # If video is not public, no change to it should be saved after a caption is deleted. assert mock_video_save.call_count == 0