Commit f08645d2 by Simon Chen

Update the marketing api loader to login properly

ECOM-6880
parent c0b56065
...@@ -7,6 +7,7 @@ from django.utils.translation import ugettext_lazy as _ ...@@ -7,6 +7,7 @@ from django.utils.translation import ugettext_lazy as _
from course_discovery.apps.course_metadata.forms import ProgramAdminForm, CourseAdminForm from course_discovery.apps.course_metadata.forms import ProgramAdminForm, CourseAdminForm
from course_discovery.apps.course_metadata.models import * # pylint: disable=wildcard-import from course_discovery.apps.course_metadata.models import * # pylint: disable=wildcard-import
from course_discovery.apps.course_metadata.publishers import ProgramPublisherException from course_discovery.apps.course_metadata.publishers import ProgramPublisherException
from course_discovery.apps.course_metadata.utils import MarketingSiteAPIClientException
class SeatInline(admin.TabularInline): class SeatInline(admin.TabularInline):
...@@ -123,7 +124,7 @@ class ProgramAdmin(admin.ModelAdmin): ...@@ -123,7 +124,7 @@ class ProgramAdmin(admin.ModelAdmin):
try: try:
super().save_model(request, obj, form, change) super().save_model(request, obj, form, change)
self.save_error = False self.save_error = False
except ProgramPublisherException: except (ProgramPublisherException, MarketingSiteAPIClientException):
# TODO Redirect the user back to the form so that he/she can try again. # TODO Redirect the user back to the form so that he/she can try again.
logger.exception('An error occurred while publishing the program [%s] to the marketing site.', obj.uuid) logger.exception('An error occurred while publishing the program [%s] to the marketing site.', obj.uuid)
msg = _('An error occurred while publishing the program to the marketing site. Please try again. ' msg = _('An error occurred while publishing the program to the marketing site. Please try again. '
......
...@@ -7,7 +7,6 @@ from uuid import UUID ...@@ -7,7 +7,6 @@ from uuid import UUID
from dateutil import rrule from dateutil import rrule
import pytz import pytz
import requests
from django.db.models import Q from django.db.models import Q
from django.utils.functional import cached_property from django.utils.functional import cached_property
from opaque_keys import InvalidKeyError from opaque_keys import InvalidKeyError
...@@ -18,6 +17,7 @@ from course_discovery.apps.course_metadata.data_loaders import AbstractDataLoade ...@@ -18,6 +17,7 @@ from course_discovery.apps.course_metadata.data_loaders import AbstractDataLoade
from course_discovery.apps.course_metadata.models import ( from course_discovery.apps.course_metadata.models import (
Course, Organization, Person, Subject, Program, Position, LevelType, CourseRun Course, Organization, Person, Subject, Program, Position, LevelType, CourseRun
) )
from course_discovery.apps.course_metadata.utils import MarketingSiteAPIClient
from course_discovery.apps.ietf_language_tags.models import LanguageTag from course_discovery.apps.ietf_language_tags.models import LanguageTag
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
...@@ -37,24 +37,14 @@ class AbstractMarketingSiteDataLoader(AbstractDataLoader): ...@@ -37,24 +37,14 @@ class AbstractMarketingSiteDataLoader(AbstractDataLoader):
@cached_property @cached_property
def api_client(self): def api_client(self):
username = self.partner.marketing_site_api_username
# Login by posting to the login form
login_data = {
'name': username,
'pass': self.partner.marketing_site_api_password,
'form_id': 'user_login',
'op': 'Log in',
}
session = requests.Session() marketing_site_api_client = MarketingSiteAPIClient(
login_url = '{root}/user'.format(root=self.api_url) self.partner.marketing_site_api_username,
response = session.post(login_url, data=login_data) self.partner.marketing_site_api_password,
expected_url = '{root}/users/{username}'.format(root=self.api_url, username=username) self.api_url
if not (response.status_code == 200 and response.url == expected_url): )
raise Exception('Login failed!')
return session return marketing_site_api_client.api_session
def get_query_kwargs(self): def get_query_kwargs(self):
return { return {
......
...@@ -87,6 +87,20 @@ class AbstractMarketingSiteDataLoaderTestMixin(DataLoaderTestMixin): ...@@ -87,6 +87,20 @@ class AbstractMarketingSiteDataLoaderTestMixin(DataLoaderTestMixin):
responses.add(responses.POST, url, status=status, adding_headers=adding_headers) responses.add(responses.POST, url, status=status, adding_headers=adding_headers)
responses.add(responses.GET, landing_url) responses.add(responses.GET, landing_url)
responses.add(
responses.GET,
'{root}admin'.format(root=self.api_url),
status=(500 if failure else 200)
)
responses.add(
responses.GET,
'{root}restws/session/token'.format(root=self.api_url),
body='test token',
content_type='text/html',
status=200
)
def mock_api_failure(self): def mock_api_failure(self):
url = self.api_url + 'node.json' url = self.api_url + 'node.json'
responses.add(responses.GET, url, status=500) responses.add(responses.GET, url, status=500)
......
class MarketingSiteAPIClientException(Exception):
""" The exception thrown from MarketingSiteAPIClient """
pass
class ProgramPublisherException(Exception):
""" The exception thrown during the program publishing process to marketing site """
def __init__(self, message):
super(ProgramPublisherException, self).__init__(message)
suffix = 'The program data has not been saved. Please check your marketing site configuration'
self.message = '{exception_msg} {suffix}'.format(exception_msg=message, suffix=suffix)
import json import json
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
import requests
from django.utils.functional import cached_property from course_discovery.apps.course_metadata.exceptions import ProgramPublisherException
from course_discovery.apps.course_metadata.utils import MarketingSiteAPIClient
class ProgramPublisherException(Exception):
def __init__(self, message):
super(ProgramPublisherException, self).__init__(message)
suffix = 'The program data has not been saved. Please check your marketing site configuration'
self.message = '{exception_msg} {suffix}'.format(exception_msg=message, suffix=suffix)
class MarketingSiteAPIClient(object):
"""
The marketing site API client we can use to communicate with the marketing site
"""
username = None
password = None
api_url = None
def __init__(self, marketing_site_api_username, marketing_site_api_password, api_url):
if not (marketing_site_api_username and marketing_site_api_password):
raise ProgramPublisherException('Marketing Site API credentials are not properly configured!')
self.username = marketing_site_api_username
self.password = marketing_site_api_password
self.api_url = api_url.strip('/')
@cached_property
def init_session(self):
# Login to set session cookies
session = requests.Session()
login_url = '{root}/user'.format(root=self.api_url)
login_data = {
'name': self.username,
'pass': self.password,
'form_id': 'user_login',
'op': 'Log in',
}
response = session.post(login_url, data=login_data)
expected_url = '{root}/users/{username}'.format(root=self.api_url, username=self.username)
admin_url = '{root}/admin'.format(root=self.api_url)
can_access_admin = session.get(admin_url)
if not (can_access_admin.status_code == 200 and response.url == expected_url):
raise ProgramPublisherException('Marketing Site Login failed!')
return session
@cached_property
def api_session(self):
self.init_session.headers.update(self.headers)
return self.init_session
@cached_property
def csrf_token(self):
token_url = '{root}/restws/session/token'.format(root=self.api_url)
response = self.init_session.get(token_url)
if not response.status_code == 200:
raise ProgramPublisherException('Failed to retrieve Marketing Site CSRF token!')
token = response.content.decode('utf8')
return token
@cached_property
def user_id(self):
# Get a user ID
user_url = '{root}/user.json?name={username}'.format(root=self.api_url, username=self.username)
response = self.init_session.get(user_url)
if not response.status_code == 200:
raise ProgramPublisherException('Failed to retrieve Marketing site user details!')
user_id = response.json()['list'][0]['uid']
return user_id
@cached_property
def headers(self):
return {
'Content-Type': 'application/json',
'X-CSRF-Token': self.csrf_token,
}
class MarketingSitePublisher(object): class MarketingSitePublisher(object):
......
...@@ -8,92 +8,9 @@ from course_discovery.apps.course_metadata.publishers import ( ...@@ -8,92 +8,9 @@ from course_discovery.apps.course_metadata.publishers import (
ProgramPublisherException, ProgramPublisherException,
) )
from course_discovery.apps.course_metadata.tests.factories import ProgramFactory from course_discovery.apps.course_metadata.tests.factories import ProgramFactory
from course_discovery.apps.course_metadata.tests.mixins import ( from course_discovery.apps.course_metadata.tests.mixins import MarketingSitePublisherTestMixin
MarketingSiteAPIClientTestMixin,
MarketingSitePublisherTestMixin,
)
from course_discovery.apps.course_metadata.models import ProgramType
class MarketingSiteAPIClientTests(MarketingSiteAPIClientTestMixin):
"""
Unit test cases for MarketinSiteAPIClient
"""
def setUp(self):
super(MarketingSiteAPIClientTests, self).setUp()
self.api_client = MarketingSiteAPIClient(
self.username,
self.password,
self.api_root
)
@responses.activate
def test_init_session(self):
self.mock_login_response(200)
self.mock_admin_response(200)
session = self.api_client.init_session
self.assert_responses_call_count(3)
self.assertIsNotNone(session)
@responses.activate
def test_init_session_failed(self):
self.mock_login_response(500)
self.mock_admin_response(500)
with self.assertRaises(ProgramPublisherException):
self.api_client.init_session # pylint: disable=pointless-statement
@responses.activate
def test_csrf_token(self):
self.mock_login_response(200)
self.mock_admin_response(200)
self.mock_csrf_token_response(200)
csrf_token = self.api_client.csrf_token
self.assert_responses_call_count(4)
self.assertEqual(self.csrf_token, csrf_token)
@responses.activate from course_discovery.apps.course_metadata.models import ProgramType
def test_csrf_token_failed(self):
self.mock_login_response(200)
self.mock_admin_response(200)
self.mock_csrf_token_response(500)
with self.assertRaises(ProgramPublisherException):
self.api_client.csrf_token # pylint: disable=pointless-statement
@responses.activate
def test_user_id(self):
self.mock_login_response(200)
self.mock_admin_response(200)
self.mock_user_id_response(200)
user_id = self.api_client.user_id
self.assert_responses_call_count(4)
self.assertEqual(self.user_id, user_id)
@responses.activate
def test_user_id_failed(self):
self.mock_login_response(200)
self.mock_admin_response(200)
self.mock_user_id_response(500)
with self.assertRaises(ProgramPublisherException):
self.api_client.user_id # pylint: disable=pointless-statement
@responses.activate
def test_api_session(self):
self.mock_login_response(200)
self.mock_admin_response(200)
self.mock_csrf_token_response(200)
api_session = self.api_client.api_session
self.assert_responses_call_count(4)
self.assertIsNotNone(api_session)
self.assertEqual(api_session.headers.get('Content-Type'), 'application/json')
self.assertEqual(api_session.headers.get('X-CSRF-Token'), self.csrf_token)
@responses.activate
def test_api_session_failed(self):
self.mock_login_response(500)
self.mock_admin_response(500)
self.mock_csrf_token_response(500)
with self.assertRaises(ProgramPublisherException):
self.api_client.api_session # pylint: disable=pointless-statement
class MarketingSitePublisherTests(MarketingSitePublisherTestMixin): class MarketingSitePublisherTests(MarketingSitePublisherTestMixin):
......
...@@ -2,8 +2,11 @@ import os ...@@ -2,8 +2,11 @@ import os
import ddt import ddt
from django.test import TestCase from django.test import TestCase
import responses
from course_discovery.apps.course_metadata.exceptions import MarketingSiteAPIClientException
from course_discovery.apps.course_metadata.tests.factories import ProgramFactory from course_discovery.apps.course_metadata.tests.factories import ProgramFactory
from course_discovery.apps.course_metadata.tests.mixins import MarketingSiteAPIClientTestMixin
from course_discovery.apps.course_metadata import utils from course_discovery.apps.course_metadata import utils
...@@ -29,3 +32,84 @@ class UploadToFieldNamePathTests(TestCase): ...@@ -29,3 +32,84 @@ class UploadToFieldNamePathTests(TestCase):
upload_path = upload_to(self.program, 'name' + ext) upload_path = upload_to(self.program, 'name' + ext)
expected = os.path.join(path, str(getattr(self.program, field)) + ext) expected = os.path.join(path, str(getattr(self.program, field)) + ext)
self.assertEqual(upload_path, expected) self.assertEqual(upload_path, expected)
class MarketingSiteAPIClientTests(MarketingSiteAPIClientTestMixin):
"""
Unit test cases for MarketinSiteAPIClient
"""
def setUp(self):
super(MarketingSiteAPIClientTests, self).setUp()
self.api_client = utils.MarketingSiteAPIClient(
self.username,
self.password,
self.api_root
)
@responses.activate
def test_init_session(self):
self.mock_login_response(200)
self.mock_admin_response(200)
session = self.api_client.init_session
self.assert_responses_call_count(3)
self.assertIsNotNone(session)
@responses.activate
def test_init_session_failed(self):
self.mock_login_response(500)
self.mock_admin_response(500)
with self.assertRaises(MarketingSiteAPIClientException):
self.api_client.init_session # pylint: disable=pointless-statement
@responses.activate
def test_csrf_token(self):
self.mock_login_response(200)
self.mock_admin_response(200)
self.mock_csrf_token_response(200)
csrf_token = self.api_client.csrf_token
self.assert_responses_call_count(4)
self.assertEqual(self.csrf_token, csrf_token)
@responses.activate
def test_csrf_token_failed(self):
self.mock_login_response(200)
self.mock_admin_response(200)
self.mock_csrf_token_response(500)
with self.assertRaises(MarketingSiteAPIClientException):
self.api_client.csrf_token # pylint: disable=pointless-statement
@responses.activate
def test_user_id(self):
self.mock_login_response(200)
self.mock_admin_response(200)
self.mock_user_id_response(200)
user_id = self.api_client.user_id
self.assert_responses_call_count(4)
self.assertEqual(self.user_id, user_id)
@responses.activate
def test_user_id_failed(self):
self.mock_login_response(200)
self.mock_admin_response(200)
self.mock_user_id_response(500)
with self.assertRaises(MarketingSiteAPIClientException):
self.api_client.user_id # pylint: disable=pointless-statement
@responses.activate
def test_api_session(self):
self.mock_login_response(200)
self.mock_admin_response(200)
self.mock_csrf_token_response(200)
api_session = self.api_client.api_session
self.assert_responses_call_count(4)
self.assertIsNotNone(api_session)
self.assertEqual(api_session.headers.get('Content-Type'), 'application/json')
self.assertEqual(api_session.headers.get('X-CSRF-Token'), self.csrf_token)
@responses.activate
def test_api_session_failed(self):
self.mock_login_response(500)
self.mock_admin_response(500)
self.mock_csrf_token_response(500)
with self.assertRaises(MarketingSiteAPIClientException):
self.api_client.api_session # pylint: disable=pointless-statement
from django.utils.functional import cached_property
import requests
from stdimage.models import StdImageFieldFile from stdimage.models import StdImageFieldFile
from stdimage.utils import UploadTo from stdimage.utils import UploadTo
from course_discovery.apps.course_metadata.exceptions import MarketingSiteAPIClientException
RESERVED_ELASTICSEARCH_QUERY_OPERATORS = ('AND', 'OR', 'NOT', 'TO',) RESERVED_ELASTICSEARCH_QUERY_OPERATORS = ('AND', 'OR', 'NOT', 'TO',)
...@@ -63,3 +67,71 @@ def custom_render_variations(file_name, variations, storage, replace=True): ...@@ -63,3 +67,71 @@ def custom_render_variations(file_name, variations, storage, replace=True):
# to prevent default behaviour # to prevent default behaviour
return False return False
class MarketingSiteAPIClient(object):
"""
The marketing site API client we can use to communicate with the marketing site
"""
username = None
password = None
api_url = None
def __init__(self, marketing_site_api_username, marketing_site_api_password, api_url):
if not (marketing_site_api_username and marketing_site_api_password):
raise MarketingSiteAPIClientException('Marketing Site API credentials are not properly configured!')
self.username = marketing_site_api_username
self.password = marketing_site_api_password
self.api_url = api_url.strip('/')
@cached_property
def init_session(self):
# Login to set session cookies
session = requests.Session()
login_url = '{root}/user'.format(root=self.api_url)
login_data = {
'name': self.username,
'pass': self.password,
'form_id': 'user_login',
'op': 'Log in',
}
response = session.post(login_url, data=login_data)
expected_url = '{root}/users/{username}'.format(root=self.api_url, username=self.username)
admin_url = '{root}/admin'.format(root=self.api_url)
# Temporary way of checking whether the user has been logged into marketing site until
# the marketing site login flow is fixed
can_access_admin = session.get(admin_url)
if not (can_access_admin.status_code == 200 and response.url == expected_url):
raise MarketingSiteAPIClientException('Marketing Site Login failed!')
return session
@cached_property
def api_session(self):
self.init_session.headers.update(self.headers)
return self.init_session
@cached_property
def csrf_token(self):
token_url = '{root}/restws/session/token'.format(root=self.api_url)
response = self.init_session.get(token_url)
if not response.status_code == 200:
raise MarketingSiteAPIClientException('Failed to retrieve Marketing Site CSRF token!')
token = response.content.decode('utf8')
return token
@cached_property
def user_id(self):
# Get a user ID
user_url = '{root}/user.json?name={username}'.format(root=self.api_url, username=self.username)
response = self.init_session.get(user_url)
if not response.status_code == 200:
raise MarketingSiteAPIClientException('Failed to retrieve Marketing site user details!')
user_id = response.json()['list'][0]['uid']
return user_id
@cached_property
def headers(self):
return {
'Content-Type': 'application/json',
'X-CSRF-Token': self.csrf_token,
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment