Files
edx-platform/cms/djangoapps/contentstore/tests/test_video_utils.py
Awais Qureshi bceba32f0a Upgrade django-storages to 1.14 ( max ver ) (#33312)
* feat!: upgrading `django-storages` to `1.14`
2023-09-25 15:16:59 +05:00

492 lines
18 KiB
Python

"""
Unit tests for video utils.
"""
from datetime import datetime
from unittest import TestCase
from unittest import mock
import ddt
import pytz
import requests
from django.conf import settings
from django.core.files.base import ContentFile
from django.core.files.storage import get_storage_class
from django.core.files.uploadedfile import UploadedFile
from django.test.utils import override_settings
from edxval.api import create_profile, create_video, get_course_video_image_url, update_video_image
from storages.backends.s3boto3 import S3Boto3Storage
from cms.djangoapps.contentstore.tests.utils import CourseTestCase
from cms.djangoapps.contentstore.video_utils import (
YOUTUBE_THUMBNAIL_SIZES,
download_youtube_video_thumbnail,
scrape_youtube_thumbnail,
validate_video_image
)
from openedx.core.djangoapps.profile_images.tests.helpers import make_image_file
class ValidateVideoImageTestCase(TestCase):
"""
Tests for `validate_video_image` method.
"""
def test_invalid_image_file_info(self):
"""
Test that when no file information is provided to validate_video_image, it gives proper error message.
"""
error = validate_video_image({})
self.assertEqual(error, 'The image must have name, content type, and size information.')
def test_corrupt_image_file(self):
"""
Test that when corrupt file is provided to validate_video_image, it gives proper error message.
"""
with open(settings.MEDIA_ROOT + '/test-corrupt-image.png', 'w+') as image_file:
uploaded_image_file = UploadedFile(
image_file,
content_type='image/png',
size=settings.VIDEO_IMAGE_SETTINGS['VIDEO_IMAGE_MIN_BYTES']
)
error = validate_video_image(uploaded_image_file)
self.assertEqual(error, 'There is a problem with this image file. Try to upload a different file.')
@ddt.ddt
class ScrapeVideoThumbnailsTestCase(CourseTestCase):
"""
Test cases for scraping video thumbnails from youtube.
"""
def setUp(self):
super().setUp()
course_ids = [str(self.course.id)]
profiles = ['youtube']
created = datetime.now(pytz.utc)
previous_uploads = [
{
'edx_video_id': 'test1',
'client_video_id': 'test1.mp4',
'duration': 42.0,
'status': 'upload',
'courses': course_ids,
'encoded_videos': [],
'created': created
},
{
'edx_video_id': 'test-youtube-video-1',
'client_video_id': 'test-youtube-id.mp4',
'duration': 128.0,
'status': 'file_complete',
'courses': course_ids,
'created': created,
'encoded_videos': [
{
'profile': 'youtube',
'url': '3_yD_cEKoCk',
'file_size': 1600,
'bitrate': 100,
}
],
},
{
'edx_video_id': 'test-youtube-video-2',
'client_video_id': 'test-youtube-id.mp4',
'image': 'image2.jpg',
'duration': 128.0,
'status': 'file_complete',
'courses': course_ids,
'created': created,
'encoded_videos': [
{
'profile': 'youtube',
'url': '3_yD_cEKoCk',
'file_size': 1600,
'bitrate': 100,
}
],
},
]
for profile in profiles:
create_profile(profile)
for video in previous_uploads:
create_video(video)
# Create video images.
with make_image_file() as image_file:
update_video_image(
'test-youtube-video-2', str(self.course.id), image_file, 'image.jpg'
)
def mocked_youtube_thumbnail_response(
self,
mocked_content=None,
error_response=False,
image_width=settings.VIDEO_IMAGE_MIN_WIDTH,
image_height=settings.VIDEO_IMAGE_MIN_HEIGHT
):
"""
Returns a mocked youtube thumbnail response.
"""
image_content = ''
with make_image_file(dimensions=(image_width, image_height), ) as image_file:
image_content = image_file.read()
if mocked_content or error_response:
image_content = mocked_content
mocked_response = requests.Response()
mocked_response.status_code = requests.codes.ok if image_content else requests.codes.not_found # pylint: disable=no-member
mocked_response._content = image_content # pylint: disable=protected-access
mocked_response.headers = {'content-type': 'image/jpeg'}
return mocked_response
@override_settings(AWS_ACCESS_KEY_ID='test_key_id', AWS_SECRET_ACCESS_KEY='test_secret')
@mock.patch('requests.get')
@ddt.data(
(
{
'maxresdefault': 'maxresdefault-result-image-content',
'sddefault': 'sddefault-result-image-content',
'hqdefault': 'hqdefault-result-image-content',
'mqdefault': 'mqdefault-result-image-content',
'default': 'default-result-image-content'
},
'maxresdefault-result-image-content'
),
(
{
'maxresdefault': '',
'sddefault': 'sddefault-result-image-content',
'hqdefault': 'hqdefault-result-image-content',
'mqdefault': 'mqdefault-result-image-content',
'default': 'default-result-image-content'
},
'sddefault-result-image-content'
),
(
{
'maxresdefault': '',
'sddefault': '',
'hqdefault': 'hqdefault-result-image-content',
'mqdefault': 'mqdefault-result-image-content',
'default': 'default-result-image-content'
},
'hqdefault-result-image-content'
),
(
{
'maxresdefault': '',
'sddefault': '',
'hqdefault': '',
'mqdefault': 'mqdefault-result-image-content',
'default': 'default-result-image-content'
},
'mqdefault-result-image-content'
),
(
{
'maxresdefault': '',
'sddefault': '',
'hqdefault': '',
'mqdefault': '',
'default': 'default-result-image-content'
},
'default-result-image-content'
),
)
@ddt.unpack
def test_youtube_video_thumbnail_download(
self,
thumbnail_content_data,
expected_thumbnail_content,
mocked_request
):
"""
Test that we get highest resolution video thumbnail available from youtube.
"""
# Mock get youtube thumbnail responses.
def mocked_youtube_thumbnail_responses(resolutions):
"""
Returns a list of mocked responses containing youtube thumbnails.
"""
mocked_responses = []
for resolution in YOUTUBE_THUMBNAIL_SIZES:
mocked_content = resolutions.get(resolution, '')
error_response = False if mocked_content else True # lint-amnesty, pylint: disable=simplifiable-if-expression
mocked_responses.append(self.mocked_youtube_thumbnail_response(mocked_content, error_response))
return mocked_responses
mocked_request.side_effect = mocked_youtube_thumbnail_responses(thumbnail_content_data)
thumbnail_content, thumbnail_content_type = download_youtube_video_thumbnail('test-yt-id')
# Verify that we get the expected thumbnail content.
self.assertEqual(thumbnail_content, expected_thumbnail_content)
self.assertEqual(thumbnail_content_type, 'image/jpeg')
@override_settings(AWS_ACCESS_KEY_ID='test_key_id', AWS_SECRET_ACCESS_KEY='test_secret')
@mock.patch('requests.get')
def test_scrape_youtube_thumbnail(self, mocked_request):
"""
Test that youtube thumbnails are correctly scrapped.
"""
course_id = str(self.course.id)
video1_edx_video_id = 'test-youtube-video-1'
video2_edx_video_id = 'test-youtube-video-2'
# Mock get youtube thumbnail responses.
mocked_request.side_effect = [self.mocked_youtube_thumbnail_response()]
# Verify that video1 has no image attached.
video1_image_url = get_course_video_image_url(course_id=course_id, edx_video_id=video1_edx_video_id)
self.assertIsNone(video1_image_url)
# Verify that video2 has already image attached.
video2_image_url = get_course_video_image_url(course_id=course_id, edx_video_id=video2_edx_video_id)
self.assertIsNotNone(video2_image_url)
# Scrape video thumbnails.
scrape_youtube_thumbnail(course_id, video1_edx_video_id, 'test-yt-id')
scrape_youtube_thumbnail(course_id, video2_edx_video_id, 'test-yt-id2')
# Verify that now video1 image is attached.
video1_image_url = get_course_video_image_url(course_id=course_id, edx_video_id=video1_edx_video_id)
self.assertIsNotNone(video1_image_url)
# Also verify that video2's image is not updated.
video2_image_url_latest = get_course_video_image_url(course_id=course_id, edx_video_id=video2_edx_video_id)
self.assertEqual(video2_image_url, video2_image_url_latest)
@ddt.data(
(
100,
100,
False
),
(
640,
360,
True
)
)
@override_settings(AWS_ACCESS_KEY_ID='test_key_id', AWS_SECRET_ACCESS_KEY='test_secret')
@mock.patch('cms.djangoapps.contentstore.video_utils.LOGGER')
@mock.patch('requests.get')
@ddt.unpack
def test_scrape_youtube_thumbnail_logging(
self,
image_width,
image_height,
is_success,
mocked_request,
mock_logger
):
"""
Test that we get correct logs in case of failure as well as success.
"""
course_id = str(self.course.id)
video1_edx_video_id = 'test-youtube-video-1'
mocked_request.side_effect = [
self.mocked_youtube_thumbnail_response(
image_width=image_width,
image_height=image_height
)
]
scrape_youtube_thumbnail(course_id, video1_edx_video_id, 'test-yt-id')
if is_success:
mock_logger.info.assert_called_with(
'VIDEOS: Scraping youtube video thumbnail for edx_video_id [%s] in course [%s]',
video1_edx_video_id,
course_id
)
else:
mock_logger.info.assert_called_with(
'VIDEOS: Scraping youtube video thumbnail failed for edx_video_id [%s] in course [%s] with error: %s',
video1_edx_video_id,
course_id,
'This image file must be larger than 2 KB.'
)
@ddt.data(
(
None,
'image/jpeg',
'This image file must be larger than {image_min_size}.'.format(
image_min_size=settings.VIDEO_IMAGE_MIN_FILE_SIZE_KB
)
),
(
b'dummy-content',
None,
'This image file type is not supported. Supported file types are {supported_file_formats}.'.format(
supported_file_formats=list(settings.VIDEO_IMAGE_SUPPORTED_FILE_FORMATS.keys())
)
),
(
None,
None,
'This image file type is not supported. Supported file types are {supported_file_formats}.'.format(
supported_file_formats=list(settings.VIDEO_IMAGE_SUPPORTED_FILE_FORMATS.keys())
)
),
)
@mock.patch('cms.djangoapps.contentstore.video_utils.LOGGER')
@mock.patch('cms.djangoapps.contentstore.video_utils.download_youtube_video_thumbnail')
@ddt.unpack
def test_no_video_thumbnail_downloaded(
self,
image_content,
image_content_type,
error_message,
mock_download_youtube_thumbnail,
mock_logger
):
"""
Test that when no thumbnail is downloaded, video image is not updated.
"""
mock_download_youtube_thumbnail.return_value = image_content, image_content_type
course_id = str(self.course.id)
video1_edx_video_id = 'test-youtube-video-1'
# Verify that video1 has no image attached.
video1_image_url = get_course_video_image_url(course_id=course_id, edx_video_id=video1_edx_video_id)
self.assertIsNone(video1_image_url)
# Scrape video thumbnail.
scrape_youtube_thumbnail(course_id, video1_edx_video_id, 'test-yt-id')
mock_logger.info.assert_called_with(
'VIDEOS: Scraping youtube video thumbnail failed for edx_video_id [%s] in course [%s] with error: %s',
video1_edx_video_id,
course_id,
error_message
)
# Verify that no image is attached to video1.
video1_image_url = get_course_video_image_url(course_id=course_id, edx_video_id=video1_edx_video_id)
self.assertIsNone(video1_image_url)
@ddt.ddt
class S3Boto3TestCase(TestCase):
""" verify s3boto3 returns valid backend."""
def setUp(self):
self.storage = S3Boto3Storage()
self.storage._connections.connection = mock.MagicMock() # pylint: disable=protected-access
def order_dict(self, dictionary):
"""
sorting dict key:values for tests cases.
"""
sorted_key_values = sorted(dictionary.items())
dictionary.clear()
dictionary.update(sorted_key_values)
return dictionary
def test_video_backend(self):
self.assertEqual(
S3Boto3Storage,
get_storage_class(
'storages.backends.s3boto3.S3Boto3Storage',
)(**settings.VIDEO_IMAGE_SETTINGS.get('STORAGE_KWARGS', {})).__class__
)
@override_settings(VIDEO_IMAGE_SETTINGS={
'STORAGE_CLASS': 'storages.backends.s3boto3.S3Boto3Storage',
'STORAGE_KWARGS':
{'bucket_name': 'test', 'default_acl': None, 'location': 'abc/def'}}
)
def test_boto3_backend_with_params(self):
storage = get_storage_class(
settings.VIDEO_IMAGE_SETTINGS.get('STORAGE_CLASS', {})
)(**settings.VIDEO_IMAGE_SETTINGS.get('STORAGE_KWARGS', {}))
self.assertEqual(S3Boto3Storage, storage.__class__)
def test_storage_without_global_default_acl_setting(self):
"""
In 1.9.1 package provides the default-acl=`public-read`.
AWS_DEFAULT_ACL is not defined but package will send public-read.
In 1.10.1 this test will fail because that version has no default value.
"""
name = 'test_storage_save231.txt'
content = ContentFile('new content')
storage = S3Boto3Storage(**{'bucket_name': 'test'})
storage._connections.connection = mock.MagicMock() # pylint: disable=protected-access
storage.save(name, content)
storage.bucket.Object.assert_called_once_with(name)
obj = storage.bucket.Object.return_value
obj.upload_fileobj.assert_called_with(
mock.ANY,
ExtraArgs=self.order_dict({
'ContentType': 'text/plain',
}),
Config=storage.transfer_config # pylint: disable=protected-access
)
@override_settings(AWS_DEFAULT_ACL='public-read')
@ddt.data(
('public-read', 'public-read'),
('private', 'private'),
(None, None)
)
@ddt.unpack
def test_storage_without_global_default_acl_setting_and_bucket_acls(self, default_acl, output_acl):
"""
AWS_DEFAULT_ACL set to private and let bucket level acl overrides it behaviour.
"""
name = 'test_storage_save.txt'
content = ContentFile('new content')
storage = S3Boto3Storage(**{'bucket_name': 'test', 'default_acl': default_acl})
storage._connections.connection = mock.MagicMock() # pylint: disable=protected-access
storage.save(name, content)
storage.bucket.Object.assert_called_once_with(name)
obj = storage.bucket.Object.return_value
ExtraArgs = {
'ACL': output_acl,
'ContentType': 'text/plain',
}
if default_acl is None:
del ExtraArgs['ACL']
obj.upload_fileobj.assert_called_with(
mock.ANY,
ExtraArgs=self.order_dict(ExtraArgs),
Config=storage.transfer_config # pylint: disable=protected-access
)
@ddt.data('public-read', 'private')
def test_storage_passing_default_acl_as_none(self, input_acl):
"""
check bucket-level None behaviour with different AWS_DEFAULT_ACL
"""
with override_settings(AWS_DEFAULT_ACL=input_acl):
name = 'test_storage_save231.txt'
content = ContentFile('new content')
storage = S3Boto3Storage(**{'bucket_name': 'test', 'default_acl': None})
storage._connections.connection = mock.MagicMock() # pylint: disable=protected-access
storage.save(name, content)
storage.bucket.Object.assert_called_once_with(name)
obj = storage.bucket.Object.return_value
obj.upload_fileobj.assert_called_with(
mock.ANY,
Config=storage.transfer_config, # pylint: disable=protected-access
ExtraArgs={
'ContentType': 'text/plain',
},
)