Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add transcript credentials api #210

Merged
merged 5 commits into from
Apr 1, 2020
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion edxval/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,15 @@
EncodedVideo,
Profile,
ThirdPartyTranscriptCredentialsState,
TranscriptCredentials,
TranscriptPreference,
TranscriptProviderType,
Video,
VideoImage,
VideoTranscript,
)
from edxval.serializers import TranscriptPreferenceSerializer, TranscriptSerializer, VideoSerializer
from edxval.transcript_utils import Transcript
from edxval.transcript_utils import Transcript, validate_transcript_credentials
from edxval.utils import THIRD_PARTY_TRANSCRIPTION_PLANS, TranscriptFormat, create_file_in_fs, get_transcript_format

logger = logging.getLogger(__name__) # pylint: disable=C0103
Expand Down Expand Up @@ -1230,3 +1231,20 @@ def create_transcript_objects(xml, edx_video_id, resource_fs, static_dir, extern
resource_fs=file_system,
static_dir=static_dir
)


def create_or_update_transcript_credentials(**credentials):
"""
Internal API method to create or update transcript credentials.
"""
provider = credentials.pop('provider', None)
error_type, error_message, validated_credentials = validate_transcript_credentials(
provider=provider, **credentials
)
if not error_message:
TranscriptCredentials.objects.update_or_create(
org=validated_credentials.pop('org'), provider=provider, defaults=validated_credentials
)
error_type = None

return dict(error_type=error_type, message=error_message)
12 changes: 12 additions & 0 deletions edxval/enum.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"""
Module containing all the Enumerations used in the API.
"""


class TranscriptionProviderErrorType:
"""
Transcription provider's errors enumeration.
"""
INVALID_CREDENTIALS = 1
INVALID_PROVIDER = 2
MISSING_REQUIRED_ATTRIBUTES = 3
8 changes: 8 additions & 0 deletions edxval/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,3 +179,11 @@

# Set this value in the environment-specific files (e.g. local.py, production.py, test.py)
FERNET_KEYS = ['insecure-ferent-key']

# Transcript provider settings variables, which will be overridden at deployment if needed
# NOTE: These settings must be added in edx-platform settings to allow deployment override and val integration
CIELO24_SETTINGS = dict(
CIELO24_API_VERSION=1,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: since these settings will live both on val/platform and vem, we should add a comment suggesting to change the version, whenever we do, in both places (VEM and VAL/Platform) or keep it in some config repo to update it in one place

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These settings aren't utilized by edxval as edxval is to be plugged in edx-platform. I will add the comment when adding the settings in platform.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added the settings here to show what set of values are present in it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okay

CIELO24_BASE_API_URL="https://sandbox.cielo24.com/api",
CIELO24_LOGIN_URL="https://sandbox.cielo24.com/api/account/login"
)
49 changes: 49 additions & 0 deletions edxval/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from tempfile import mkdtemp

import mock
import responses
import six
from ddt import data, ddt, unpack
from django.conf import settings
Expand Down Expand Up @@ -45,6 +46,7 @@
EncodedVideo,
Profile,
ThirdPartyTranscriptCredentialsState,
TranscriptCredentials,
TranscriptPreference,
TranscriptProviderType,
Video,
Expand Down Expand Up @@ -3073,3 +3075,50 @@ def test_get_credentials_state(self, org, provider, result):
"""
credentials_state = api.get_transcript_credentials_state_for_org(org=org, provider=provider)
self.assertEqual(credentials_state, result)


@ddt
class CreateUpdateTranscriptCredentialsTest(TestCase):
"""
Test Suite for transcript credentials create or update internal API.
"""
CIELO24_LOGIN_URL = "https://sandbox.cielo24.com/api/account/login"

@data(
{
'org': 'test',
'provider': TranscriptProviderType.CIELO24,
'api_key': 'test-api-key',
'username': 'test-cielo-user'
},
{
'org': 'test',
'provider': TranscriptProviderType.THREE_PLAY_MEDIA,
'api_key': 'test-api-key',
'api_secret_key': 'test-secret-key'
}
)
@responses.activate
def test_transcript_credentials_success(self, credentials):
"""
Test that creating credentials works as expected with correct set of data.
"""
responses.add(
responses.GET,
self.CIELO24_LOGIN_URL,
body='{"ApiToken": "cielo-api-token"}',
status=status.HTTP_200_OK
)

transcript_credentials = TranscriptCredentials.objects.filter(
org=credentials.get('org'),
provider=credentials.get('provider')
)
self.assertFalse(transcript_credentials.exists())

_ = api.create_or_update_transcript_credentials(**credentials)
transcript_credentials = TranscriptCredentials.objects.filter(
org=credentials.get('org'),
provider=credentials.get('provider')
)
self.assertTrue(transcript_credentials.exists())
113 changes: 106 additions & 7 deletions edxval/tests/test_transcript_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,21 @@

import json
import textwrap
import unittest

import ddt
import responses
from ddt import data, ddt, unpack
from django.test import TestCase
from mock import patch
from rest_framework import status

from edxval.enum import TranscriptionProviderErrorType
from edxval.exceptions import TranscriptsGenerationException
from edxval.transcript_utils import Transcript
from edxval.models import TranscriptProviderType
from edxval.transcript_utils import Transcript, validate_transcript_credentials


@ddt.ddt
class TestTranscriptUtils(unittest.TestCase):
@ddt
class TestTranscriptUtils(TestCase):
"""
Tests transcripts conversion util.
"""
Expand Down Expand Up @@ -50,12 +55,12 @@ def setUp(self):
}
""").encode('utf8')

@ddt.data(
@data(
('invalid_input_format', 'sjson'),
('sjson', 'invalid_output_format'),
('invalid_input_format', 'invalid_output_format')
)
@ddt.unpack
@unpack
def test_invalid_transcript_format(self, input_format, output_format):
"""
Tests that transcript conversion raises `AssertionError` on invalid input/output formats.
Expand Down Expand Up @@ -95,3 +100,97 @@ def test_convert_invalid_srt_to_sjson(self):
invalid_srt_transcript = b'invalid SubRip file content'
with self.assertRaises(TranscriptsGenerationException):
Transcript.convert(invalid_srt_transcript, 'srt', 'sjson')


@ddt
class TestCredentialsUtils(TestCase):
"""
Test Suite for various transcript credential utilities.
"""

CIELO24_LOGIN_URL = "https://sandbox.cielo24.com/api/account/login"

@patch('edxval.transcript_utils.LOGGER')
@responses.activate
def test_cielo24_error(self, mock_logger):
"""
Test that when invalid cielo credentials are supplied, we get correct error response.
"""
expected_error_message = 'Invalid credentials supplied.'
responses.add(
responses.GET,
self.CIELO24_LOGIN_URL,
body=json.dumps({'error': expected_error_message}),
status=status.HTTP_400_BAD_REQUEST
)

credentials = {
'org': 'test',
'provider': TranscriptProviderType.CIELO24,
'api_key': 'test-api-key',
'username': 'test-cielo-user',
'api_secret_key': ''
}
error_type, error_message, _ = validate_transcript_credentials(**credentials)
self.assertEqual(error_type, TranscriptionProviderErrorType.INVALID_CREDENTIALS)
self.assertEqual(error_message, expected_error_message)

mock_logger.warning.assert_called_with(
'[Transcript Credentials] Unable to get api token -- response %s -- status %s.',
json.dumps({'error': error_message}),
status.HTTP_400_BAD_REQUEST
)

@data(
{
'provider': 'unsupported-provider'
},
{
'org': 'test',
'api_key': 'test-api-key'
}
)
def test_transcript_credentials_invalid_provider(self, credentials):
"""
Test that validating credentials gives proper error in case of invalid provider.
"""
provider = credentials.pop('provider', '')
error_type, error_message, _ = validate_transcript_credentials(provider, **credentials)
self.assertEqual(error_type, TranscriptionProviderErrorType.INVALID_PROVIDER)
self.assertEqual(error_message, 'Invalid provider {provider}.'.format(provider=provider))

@data(
(
{'provider': TranscriptProviderType.CIELO24},
'org and api_key and username'
),
(
{'provider': TranscriptProviderType.THREE_PLAY_MEDIA},
'org and api_key and api_secret_key'
),
(
{'provider': TranscriptProviderType.CIELO24, 'org': 'test-org'},
'api_key and username'
),
(
{'provider': TranscriptProviderType.CIELO24, 'org': 'test-org', 'api_key': 'test-api-key'},
'username'
),
(
{'org': 'test', 'provider': TranscriptProviderType.THREE_PLAY_MEDIA, 'api_key': 'test-api-key'},
'api_secret_key'
)
)
@unpack
def test_transcript_credentials_error(self, credentials, missing_keys):
"""
Test that validating credentials gives proper error in case of invalid input.
"""
provider = credentials.pop('provider')
expected_error_message = '{missing} must be specified for {provider}.'.format(
provider=provider,
missing=missing_keys
)
error_type, error_message, _ = validate_transcript_credentials(provider, **credentials)
self.assertEqual(error_type, TranscriptionProviderErrorType.MISSING_REQUIRED_ATTRIBUTES)
self.assertEqual(error_message, expected_error_message)
88 changes: 87 additions & 1 deletion edxval/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,15 @@
from django.urls import reverse
from rest_framework import status

from edxval.models import CourseVideo, EncodedVideo, Profile, TranscriptProviderType, Video, VideoTranscript
from edxval.models import (
CourseVideo,
EncodedVideo,
Profile,
TranscriptCredentials,
TranscriptProviderType,
Video,
VideoTranscript,
)
from edxval.serializers import TranscriptSerializer
from edxval.tests import APIAuthTestCase, constants
from edxval.utils import TranscriptFormat
Expand Down Expand Up @@ -1054,3 +1062,81 @@ def test_update_hls_encodes_for_video(self):
self.assertEqual(actual_encoded_video.url, expected_data['encode_data']['url'])
self.assertEqual(actual_encoded_video.file_size, expected_data['encode_data']['file_size'])
self.assertEqual(actual_encoded_video.bitrate, expected_data['encode_data']['bitrate'])


@ddt
class TranscriptCredentialsTest(APIAuthTestCase):
"""
Transcript credentials tests.
"""

def test_transcript_credentials_unauthorized(self):
"""
Tests that if user is not logged in we get Unauthorized response.
"""
self.client.logout()
url = reverse('transcript-credentials', kwargs={'org': 'test', 'provider': 'provider'})
response = self.client.get(
url,
content_type='application/json'
)
response_status_code = response.status_code
self.assertEqual(response_status_code, status.HTTP_401_UNAUTHORIZED)

@data(
({'org': '', 'provider': ''}, 'org and provider must be specified.'),
({'org': 'org', 'provider': ''}, 'provider must be specified.'),
({'org': '', 'provider': 'provider'}, 'org must be specified.')
)
@unpack
def test_fetch_credentials_missing_parameters(self, query_params, error_message):
"""
Verify that if query parameters are missing, then request fails with missing params error.
"""
url = reverse('transcript-credentials', kwargs=query_params)
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)

response = json.loads(response.content.decode('utf-8'))
self.assertDictEqual(response, {
'message': error_message,
})

def test_get_non_existent_credentials(self):
"""
Test that fetching a non-existing set of credentials results in failure.
"""
provider, org = "provider", "org"
query_params = {'provider': provider, 'org': org}
url = reverse('transcript-credentials', kwargs=query_params)
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)

response = json.loads(response.content.decode('utf-8'))
self.assertDictEqual(response, {
'message': "Credentials not found for provider {provider} & organization {org}".format(
provider=provider,
org=org
),
})

def test_successful_fetch(self):
"""
Verify that persistent credentials are returned for correct query params.
"""
provider, org = "provider", "org"
credentials_dict = dict(
provider=provider,
org=org,
api_key='key',
api_secret='secret'
)
TranscriptCredentials(**credentials_dict).save()
query_params = {'provider': provider, 'org': org}
url = reverse('transcript-credentials', kwargs=query_params)
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)

response = json.loads(response.content.decode('utf-8'))
self.assertEqual(response['api_key'], credentials_dict['api_key'])
self.assertEqual(response['api_secret_key'], credentials_dict['api_secret'])
Loading