Skip to content

Commit

Permalink
refactor: replace EdxRestAPIClient with OAuthAPIClient
Browse files Browse the repository at this point in the history
closes openedx/public-engineering#48

*Note:*
The httpretty library was replaced with responses because it had issues
with keeping connection with requests.Session.
  • Loading branch information
dyudyunov committed Apr 8, 2022
1 parent c1fda38 commit 704e4c2
Show file tree
Hide file tree
Showing 98 changed files with 1,419 additions and 1,425 deletions.
63 changes: 44 additions & 19 deletions e2e/api.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from urllib.parse import urljoin


from edx_rest_api_client.client import EdxRestApiClient
from edx_rest_api_client.client import OAuthAPIClient

from e2e.config import (
DISCOVERY_API_URL_ROOT,
Expand All @@ -18,19 +18,20 @@ class BaseApi:

def __init__(self):
assert self.api_url_root
access_token, __ = self.get_access_token()
self._client = EdxRestApiClient(self.api_url_root, jwt=access_token, append_slash=self.append_slash)
self._client = OAuthAPIClient(OAUTH_ACCESS_TOKEN_URL, OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET)

@staticmethod
def get_access_token():
""" Returns an access token and expiration date from the OAuth provider.
def get_api_url(self, path):
"""
Construct the full API URL using the api_url_root and path.
Returns:
(str, datetime)
Args:
path (str): API endpoint path.
"""
return EdxRestApiClient.get_oauth_access_token(
OAUTH_ACCESS_TOKEN_URL, OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET, token_type='jwt'
)
path = path.strip('/')
if self.append_slash:
path += '/'

return urljoin(f"{self.api_url_root}/", path) # type: ignore


class DiscoveryApi(BaseApi):
Expand All @@ -48,9 +49,15 @@ def get_course_runs(self, seat_type):
Returns:
list(dict)
"""
results = self._client.search.course_runs.facets.get(
selected_query_facets='availability_current', selected_facets='seat_types_exact:{}'.format(seat_type))
return results['objects']['results']
response = self._client.get(
self.get_api_url("search/course_runs/facets/"),
params={
"selected_query_facets": "availability_current",
"selected_facets": f"seat_types_exact:{seat_type}"
}
)
response.raise_for_status()
return response.json()['objects']['results']

def get_course_run(self, course_run):
""" Returns the details for a given course run.
Expand All @@ -61,7 +68,11 @@ def get_course_run(self, course_run):
Returns:
dict
"""
return self._client.course_runs(course_run).get()
response = self._client.get(
self.get_api_url(f"course_runs/{course_run}/"),
)
response.raise_for_status()
return response.json()


class EcommerceApi(BaseApi):
Expand All @@ -77,10 +88,20 @@ def create_refunds_for_course_run(self, username, course_run_id):
Returns:
str[]: List of refund IDs.
"""
return self._client.refunds.post({'username': username, 'course_id': course_run_id})
response = self._client.post(
self.get_api_url("refunds/"),
json={'username': username, 'course_id': course_run_id}
)
response.raise_for_status()
return response.json()

def process_refund(self, refund_id, action):
return self._client.refunds(refund_id).process.put({'action': action})
response = self._client.put(
self.get_api_url(f"refunds/{refund_id}/process/"),
json={'action': action}
)
response.raise_for_status()
return response.json()


class EnrollmentApi(BaseApi):
Expand All @@ -94,4 +115,8 @@ def get_enrollment(self, username, course_run_id):
username (str)
course_run_id (str)
"""
return self._client.enrollment('{},{}'.format(username, course_run_id)).get()
response = self._client.get(
self.get_api_url(f"enrollment/{username},{course_run_id}")
)
response.raise_for_status()
return response.json()
29 changes: 15 additions & 14 deletions ecommerce/core/management/commands/sync_hubspot.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@
import traceback
from datetime import datetime, timedelta
from decimal import Decimal as D
from urllib.parse import urljoin

import requests
from django.contrib.auth import get_user_model
from django.core.management.base import BaseCommand, CommandError
from django.db.models import Q
from edx_rest_api_client.client import EdxRestApiClient
from oscar.core.loading import get_class, get_model
from slumber.exceptions import HttpClientError, HttpServerError
from requests.exceptions import HTTPError, RequestException

from ecommerce.extensions.fulfillment.status import ORDER

Expand Down Expand Up @@ -174,6 +175,8 @@
DEAL = "DEAL"
BATCH_SIZE = 200

EXPECTED_METHODS = ["GET", "POST", "PUT"]


class Command(BaseCommand):
help = 'Sync Product, Orders and Lines to Hubspot server.'
Expand All @@ -189,14 +192,12 @@ def _hubspot_endpoint(self, hubspot_object, api_url, method, body=None, **kwargs
"""
This function is responsible for all the calls of hubspot.
"""
client = EdxRestApiClient('/'.join([HUBSPOT_API_BASE_URL, api_url]))
if method == "GET":
return getattr(client, hubspot_object).get(**kwargs)
if method == "POST":
return getattr(client, hubspot_object).post(**kwargs)
if method == "PUT":
return getattr(client, hubspot_object).put(body, **kwargs)
raise ValueError("Unexpected method {}".format(method))
api_url = urljoin(f"{HUBSPOT_API_BASE_URL}/", f"{api_url}/{hubspot_object}")
if method not in EXPECTED_METHODS:
raise ValueError(f"Unexpected method {method}. Allowed methods are: {EXPECTED_METHODS}")
response = requests.request(method, api_url, json=body, params=kwargs)
response.raise_for_status()
return response.json()

def _install_hubspot_ecommerce_bridge(self, site_configuration):
"""
Expand All @@ -216,7 +217,7 @@ def _install_hubspot_ecommerce_bridge(self, site_configuration):
)
)
status = True
except (HttpClientError, HttpServerError) as ex:
except (HTTPError, RequestException) as ex:
self.stderr.write(
'An error occurred while installing hubspot ecommerce bridge for site {site}, {message}'.format(
site=site_configuration.site.domain, message=ex
Expand All @@ -243,7 +244,7 @@ def _define_hubspot_ecommerce_settings(self, site_configuration):
)
)
status = True
except (HttpClientError, HttpServerError) as ex:
except (HTTPError, RequestException) as ex:
self.stderr.write(
'An error occurred while defining hubspot ecommerce settings for site {site}, {message}'.format(
site=site_configuration.site.domain, message=ex
Expand Down Expand Up @@ -416,7 +417,7 @@ def _upsert_hubspot_objects(self, object_type, objects, site_configuration):
site=site_configuration.site.domain
)
)
except (HttpClientError, HttpServerError) as ex:
except (HTTPError, RequestException) as ex:
self.stderr.write(
'An error occurred while upserting {object_type} for site {site}: {message}'.format(
object_type=object_type, site=site_configuration.site.domain, message=ex
Expand All @@ -443,7 +444,7 @@ def _call_sync_errors_messages_endpoint(self, site_configuration):
message=error.get('details')
)
)
except (HttpClientError, HttpServerError) as ex:
except (HTTPError, RequestException) as ex:
self.stderr.write(
'An error occurred while getting the error syncing message for site {site}: {message} '.format(
site=site_configuration.site.domain, message=ex
Expand Down
29 changes: 22 additions & 7 deletions ecommerce/core/management/commands/tests/test_sync_hubspot.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@
from django.core.management.base import CommandError
from factory.django import get_model
from mock import patch
from slumber.exceptions import HttpClientError
from requests.exceptions import HTTPError

from ecommerce.core.management.commands.sync_hubspot import EXPECTED_METHODS
from ecommerce.core.management.commands.sync_hubspot import Command as sync_command
from ecommerce.extensions.test.factories import create_basket, create_order
from ecommerce.tests.factories import SiteConfigurationFactory, UserFactory
Expand Down Expand Up @@ -132,7 +133,7 @@ def test_upsert_hubspot_objects(self, mocked_hubspot):
with patch.object(sync_command, '_install_hubspot_ecommerce_bridge', return_value=True), \
patch.object(sync_command, '_define_hubspot_ecommerce_settings', return_value=True):
# if _upsert_hubspot_objects raises an exception
mocked_hubspot.side_effect = HttpClientError
mocked_hubspot.side_effect = HTTPError
output = self._get_command_output(is_stderr=True)
self.assertIn('An error occurred while upserting', output)

Expand All @@ -145,7 +146,7 @@ def test_install_hubspot_ecommerce_bridge(self, mocked_hubspot):
output = self._get_command_output()
self.assertIn('Successfully installed hubspot ecommerce bridge', output)
# if _install_hubspot_ecommerce_bridge raises an exception
mocked_hubspot.side_effect = HttpClientError
mocked_hubspot.side_effect = HTTPError
output = self._get_command_output(is_stderr=True)
self.assertIn('An error occurred while installing hubspot ecommerce bridge', output)

Expand All @@ -158,7 +159,7 @@ def test_define_hubspot_ecommerce_settings(self, mocked_hubspot):
output = self._get_command_output()
self.assertIn('Successfully defined the hubspot ecommerce settings', output)
# if _define_hubspot_ecommerce_settings raises an exception
mocked_hubspot.side_effect = HttpClientError
mocked_hubspot.side_effect = HTTPError
output = self._get_command_output(is_stderr=True)
self.assertIn('An error occurred while defining hubspot ecommerce settings', output)

Expand All @@ -183,7 +184,7 @@ def test_sync_errors_messages_endpoint(self, mocked_hubspot):
output
)
# if _call_sync_errors_messages_endpoint raises an exception
mocked_hubspot.side_effect = HttpClientError
mocked_hubspot.side_effect = HTTPError
output = self._get_command_output(is_stderr=True)
self.assertIn(
'An error occurred while getting the error syncing message',
Expand All @@ -201,7 +202,7 @@ def test_hubspot_endpoint(self):
6. Upsert(LINE ITEM)
7. Sync-error
"""
with patch('ecommerce.core.management.commands.sync_hubspot.EdxRestApiClient') as mock_client:
with patch('ecommerce.core.management.commands.sync_hubspot.requests.request') as mock_client:
output = self._get_command_output()
self.assertEqual(mock_client.call_count, 7)
self.assertIn('Successfully installed hubspot ecommerce bridge', output)
Expand All @@ -215,7 +216,21 @@ def test_with_exception(self, mocked_hubspot): # pylint: disable=unused-arg
with patch.object(sync_command, '_install_hubspot_ecommerce_bridge', return_value=True), \
patch.object(sync_command, '_define_hubspot_ecommerce_settings', return_value=True), \
patch.object(sync_command, '_get_unsynced_carts') as mocked_get_unsynced_carts:
mocked_get_unsynced_carts.side_effect = HttpClientError
mocked_get_unsynced_carts.side_effect = HTTPError
with self.assertRaises(CommandError):
output = self._get_command_output(is_stderr=True)
self.assertIn('Command failed with ', output)

def test_hubspot_endpoint_unsupported_method(self):
"""
Test validation error appears when trying to use unsupported method.
"""
unsupported_method = "DELETE"
self.assertNotIn(unsupported_method, EXPECTED_METHODS)
command = sync_command()
with self.assertRaises(ValueError):
command._hubspot_endpoint(
hubspot_object="fake_obj",
api_url="fake_url",
method=unsupported_method
)
Loading

0 comments on commit 704e4c2

Please sign in to comment.