diff --git a/common/djangoapps/course_modes/tests/test_views.py b/common/djangoapps/course_modes/tests/test_views.py index b4b4350aba1a..e707723ad23b 100644 --- a/common/djangoapps/course_modes/tests/test_views.py +++ b/common/djangoapps/course_modes/tests/test_views.py @@ -10,7 +10,6 @@ import ddt import freezegun -import httpretty from django.conf import settings from django.test import override_settings from django.urls import reverse @@ -22,7 +21,6 @@ from common.djangoapps.util.testing import UrlResetMixin from common.djangoapps.util.tests.mixins.discovery import CourseCatalogServiceMockMixin from lms.djangoapps.commerce.tests import test_utils as ecomm_test_utils -from lms.djangoapps.commerce.tests.mocks import mock_payment_processors from lms.djangoapps.verify_student.services import IDVerificationService from openedx.core.djangoapps.catalog.tests.mixins import CatalogIntegrationMixin from openedx.core.djangoapps.embargo.test_utils import restrict_course @@ -63,7 +61,6 @@ def setUp(self): self.client.login(username=self.user.username, password="edx") @skip_unless_lms - @httpretty.activate @ddt.data( # is_active?, enrollment_mode, redirect?, has_started (True, 'verified', True, False), @@ -148,7 +145,6 @@ def test_no_id_redirect_otto(self): self.assertRedirects(response, '/test_basket/add/?sku=TEST', fetch_redirect_response=False) ecomm_test_utils.update_commerce_config(enabled=False) - @httpretty.activate @ddt.data( '', '1,,2', @@ -301,8 +297,7 @@ def test_choose_mode_redirect(self, course_mode, expected_redirect): else: self.fail("Must provide a valid redirect URL name") - with mock_payment_processors(expect_called=None): - self.assertRedirects(response, redirect_url, fetch_redirect_response=False,) + self.assertRedirects(response, redirect_url, fetch_redirect_response=False) def test_choose_mode_audit_enroll_on_post(self): audit_mode = 'audit' @@ -456,7 +451,6 @@ def test_multiple_mode_creation(self): @skip_unless_lms @with_comprehensive_theme("edx.org") - @httpretty.activate def test_hide_nav(self): # Create the course modes for mode in ["honor", "verified"]: @@ -688,7 +682,6 @@ def test_embargo_restrict(self): response = self.client.get(self.url) self.assertRedirects(response, redirect_url) - @httpretty.activate def test_embargo_allow(self): response = self.client.get(self.url) assert response.status_code == 200 diff --git a/common/djangoapps/student/tests/test_refunds.py b/common/djangoapps/student/tests/test_refunds.py index 2b1bf8c8ab4c..bb775a0132ee 100644 --- a/common/djangoapps/student/tests/test_refunds.py +++ b/common/djangoapps/student/tests/test_refunds.py @@ -9,7 +9,7 @@ from zoneinfo import ZoneInfo import ddt -import httpretty +import responses # Explicitly import the cache from ConfigurationModel so we can reset it after each test from config_models.models import cache @@ -129,7 +129,7 @@ def test_refundable_with_cutoff_date(self, cutoff_date): (timedelta(days=2), timedelta(days=1), timedelta(days=2), 1), ) @ddt.unpack - @httpretty.activate + @responses.activate @override_settings(ECOMMERCE_API_URL=TEST_API_URL) def test_refund_cutoff_date(self, order_date_delta, course_start_delta, expected_date_delta, days): """ @@ -143,11 +143,11 @@ def test_refund_cutoff_date(self, order_date_delta, course_start_delta, expected date_placed = order_date.strftime(ECOMMERCE_DATE_FORMAT) expected_content = f'{{"date_placed": "{date_placed}"}}' - httpretty.register_uri( - httpretty.GET, + responses.add( + responses.GET, f'{TEST_API_URL}/orders/{self.ORDER_NUMBER}/', status=200, body=expected_content, - adding_headers={'Content-Type': JSON} + content_type=JSON ) self.enrollment.course_overview.start = course_start @@ -178,7 +178,7 @@ def test_refund_cutoff_date(self, order_date_delta, course_start_delta, expected (datetime.now(ZoneInfo("UTC")) - timedelta(minutes=5), False), ) @ddt.unpack - @httpretty.activate + @responses.activate @override_settings(ECOMMERCE_API_URL=TEST_API_URL) def test_is_order_voucher_refundable(self, voucher_expiration_date, expected): """ @@ -186,11 +186,11 @@ def test_is_order_voucher_refundable(self, voucher_expiration_date, expected): """ voucher_expiration_date_str = voucher_expiration_date.strftime(ECOMMERCE_DATE_FORMAT) response = json.dumps({"vouchers": [{"end_datetime": voucher_expiration_date_str}]}) - httpretty.register_uri( - httpretty.GET, + responses.add( + responses.GET, f'{TEST_API_URL}/orders/{self.ORDER_NUMBER}/', status=200, body=response, - adding_headers={'Content-Type': JSON} + content_type=JSON ) self.enrollment.attributes.create( @@ -205,7 +205,7 @@ def test_refund_cutoff_date_no_attributes(self): """ Assert that the None is returned when no order number attribute is found.""" assert self.enrollment.refund_cutoff_date() is None - @httpretty.activate + @responses.activate @override_settings(ECOMMERCE_API_URL=TEST_API_URL) def test_is_order_voucher_refundable_no_attributes(self, ): """ Assert that False is returned when no order number or vouchers attribute is found in response.""" @@ -214,11 +214,11 @@ def test_is_order_voucher_refundable_no_attributes(self, ): # no voucher information in orders api response response = json.dumps({"vouchers": []}) - httpretty.register_uri( - httpretty.GET, + responses.add( + responses.GET, f'{TEST_API_URL}/orders/{self.ORDER_NUMBER}/', status=200, body=response, - adding_headers={'Content-Type': JSON} + content_type=JSON ) self.enrollment.attributes.create( @@ -230,11 +230,11 @@ def test_is_order_voucher_refundable_no_attributes(self, ): assert self.enrollment.is_order_voucher_refundable() is False response = json.dumps({"vouchers": None}) - httpretty.register_uri( - httpretty.GET, + responses.add( + responses.GET, f'{TEST_API_URL}/orders/{self.ORDER_NUMBER}/', status=200, body=response, - adding_headers={'Content-Type': JSON} + content_type=JSON ) assert self.enrollment.is_order_voucher_refundable() is False @@ -279,7 +279,7 @@ def test_refund_cutoff_date_with_date_placed_attr(self, mock_ecommerce_api_clien assert self.enrollment.refund_cutoff_date() == (order_date + refund_config.refund_window) mock_ecommerce_api_client.assert_not_called() - @httpretty.activate + @responses.activate @override_settings(ECOMMERCE_API_URL=TEST_API_URL) def test_multiple_refunds_dashbaord_page_error(self): """ Order with mutiple refunds will not throw 500 error when dashboard page will access.""" @@ -287,11 +287,11 @@ def test_multiple_refunds_dashbaord_page_error(self): order_date = now + timedelta(days=1) expected_content = f'{{"date_placed": "{order_date.strftime(ECOMMERCE_DATE_FORMAT)}"}}' - httpretty.register_uri( - httpretty.GET, + responses.add( + responses.GET, f'{TEST_API_URL}/orders/{self.ORDER_NUMBER}/', status=200, body=expected_content, - adding_headers={'Content-Type': JSON} + content_type=JSON ) # creating multiple attributes for same order. diff --git a/common/djangoapps/third_party_auth/tests/specs/test_testshib.py b/common/djangoapps/third_party_auth/tests/specs/test_testshib.py index 8819de5deff8..c4082356a19d 100644 --- a/common/djangoapps/third_party_auth/tests/specs/test_testshib.py +++ b/common/djangoapps/third_party_auth/tests/specs/test_testshib.py @@ -11,7 +11,7 @@ from urllib.parse import parse_qs, quote, urlparse import ddt -import httpretty +import responses from django.contrib import auth from freezegun import freeze_time from social_core import actions @@ -38,6 +38,13 @@ TESTSHIB_SSO_URL = "https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO" +def _body_bytes(request): + """Return the mocked request's body as bytes, whichever form the client sent it in.""" + body = request.body or b"" + return body if isinstance(body, bytes) else body.encode("utf-8") + + + class SamlIntegrationTestUtilities: """ Class contains methods particular to SAML integration testing so that they @@ -61,34 +68,34 @@ def setUp(self): entity_id="https://saml.example.none", ) # Mock out HTTP requests that may be made to TestShib: - httpretty.enable() - httpretty.reset() - self.addCleanup(httpretty.reset) # pylint: disable=no-member - self.addCleanup(httpretty.disable) # pylint: disable=no-member + responses.start() + responses.reset() + self.addCleanup(responses.reset) # pylint: disable=no-member + self.addCleanup(responses.stop) # pylint: disable=no-member - def metadata_callback(_request, _uri, headers): + def metadata_callback(_request): """Return a cached copy of TestShib's metadata by reading it from disk""" return ( 200, - headers, + {}, self.read_data_file("testshib_metadata.xml"), ) # pylint: disable=no-member - httpretty.register_uri(httpretty.GET, TESTSHIB_METADATA_URL, content_type="text/xml", body=metadata_callback) + responses.add_callback(responses.GET, TESTSHIB_METADATA_URL, callback=metadata_callback, content_type="text/xml") - def cache_duration_metadata_callback(_request, _uri, headers): + def cache_duration_metadata_callback(_request): """Return a cached copy of TestShib's metadata with a cacheDuration attribute""" return ( 200, - headers, + {}, self.read_data_file("testshib_metadata_with_cache_duration.xml"), ) # pylint: disable=no-member - httpretty.register_uri( - httpretty.GET, + responses.add_callback( + responses.GET, TESTSHIB_METADATA_URL_WITH_CACHE_DURATION, + callback=cache_duration_metadata_callback, content_type="text/xml", - body=cache_duration_metadata_callback, ) # Configure the SAML library to use the same request ID for every request. @@ -123,7 +130,6 @@ def _configure_testshib_provider(self, **kwargs): saml_provider = self.configure_saml_provider(**kwargs) # pylint: disable=no-member if fetch_metadata: - assert httpretty.is_enabled() # pylint: disable=no-member num_total, num_skipped, num_attempted, num_updated, num_failed, failure_messages = fetch_saml_metadata() if assert_metadata_updates: assert num_total == 1 # pylint: disable=no-member @@ -375,7 +381,6 @@ def test_configure_testshib_provider_with_cache_duration(self): kwargs.setdefault("icon_class", "fa-university") kwargs.setdefault("attr_email", "urn:oid:1.3.6.1.4.1.5923.1.1.1.6") # eduPersonPrincipalName self.configure_saml_provider(**kwargs) - assert httpretty.is_enabled() num_total, num_skipped, num_attempted, num_updated, num_failed, failure_messages = fetch_saml_metadata() assert num_total == 1 assert num_skipped == 0 @@ -420,62 +425,63 @@ class SuccessFactorsIntegrationTest(SamlIntegrationTestUtilities, IntegrationTes def setUp(self): """ - Mock out HTTP calls to various endpoints using httpretty. + Mock out HTTP calls to various endpoints using responses. """ super().setUp() # Mock the call to the SAP SuccessFactors assertion endpoint SAPSF_ASSERTION_URL = "http://successfactors.com/oauth/idp" - def assertion_callback(_request, _uri, headers): + def assertion_callback(_request): """ Return a fake assertion after checking that the input is what we expect. """ - assert b"private_key=fake_private_key_here" in _request.body - assert b"user_id=myself" in _request.body - assert b"token_url=http%3A%2F%2Fsuccessfactors.com%2Foauth%2Ftoken" in _request.body - assert b"client_id=TatVotSEiCMteSNWtSOnLanCtBGwNhGB" in _request.body - return (200, headers, "fake_saml_assertion") + assert b"private_key=fake_private_key_here" in _body_bytes(_request) + assert b"user_id=myself" in _body_bytes(_request) + assert b"token_url=http%3A%2F%2Fsuccessfactors.com%2Foauth%2Ftoken" in _body_bytes(_request) + assert b"client_id=TatVotSEiCMteSNWtSOnLanCtBGwNhGB" in _body_bytes(_request) + return (200, {}, "fake_saml_assertion") - httpretty.register_uri(httpretty.POST, SAPSF_ASSERTION_URL, content_type="text/plain", body=assertion_callback) + responses.add_callback(responses.POST, SAPSF_ASSERTION_URL, callback=assertion_callback, content_type="text/plain") SAPSF_BAD_ASSERTION_URL = "http://successfactors.com/oauth-fake/idp" - def bad_callback(_request, _uri, headers): + def bad_callback(_request): """ Return a 404 error when someone tries to call the URL. """ - return (404, headers, "NOT AN ASSERTION") + return (404, {}, "NOT AN ASSERTION") - httpretty.register_uri(httpretty.POST, SAPSF_BAD_ASSERTION_URL, content_type="text/plain", body=bad_callback) + responses.add_callback(responses.POST, SAPSF_BAD_ASSERTION_URL, callback=bad_callback, content_type="text/plain") # Mock the call to the SAP SuccessFactors token endpoint SAPSF_TOKEN_URL = "http://successfactors.com/oauth/token" - def token_callback(_request, _uri, headers): + def token_callback(_request): """ Return a fake assertion after checking that the input is what we expect. """ - assert b"assertion=fake_saml_assertion" in _request.body - assert b"company_id=NCC1701D" in _request.body - assert b"grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Asaml2-bearer" in _request.body - assert b"client_id=TatVotSEiCMteSNWtSOnLanCtBGwNhGB" in _request.body - return (200, headers, '{"access_token": "faketoken"}') + assert b"assertion=fake_saml_assertion" in _body_bytes(_request) + assert b"company_id=NCC1701D" in _body_bytes(_request) + assert b"grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Asaml2-bearer" in _body_bytes(_request) + assert b"client_id=TatVotSEiCMteSNWtSOnLanCtBGwNhGB" in _body_bytes(_request) + return (200, {}, '{"access_token": "faketoken"}') - httpretty.register_uri(httpretty.POST, SAPSF_TOKEN_URL, content_type="application/json", body=token_callback) + responses.add_callback(responses.POST, SAPSF_TOKEN_URL, callback=token_callback, content_type="application/json") # Mock the call to the SAP SuccessFactors OData user endpoint ODATA_USER_URL = ( "http://api.successfactors.com/odata/v2/User(userId='myself')" "?$select=firstName,lastName,defaultFullName,email" ) + self.odata_user_url = ODATA_USER_URL - def user_callback(request, _uri, headers): + def user_callback(request): auth_header = request.headers.get("Authorization") assert auth_header == "Bearer faketoken" return ( 200, - headers, + {}, json.dumps( { "d": { @@ -490,28 +496,32 @@ def user_callback(request, _uri, headers): ), ) - httpretty.register_uri(httpretty.GET, ODATA_USER_URL, content_type="application/json", body=user_callback) + responses.add_callback(responses.GET, ODATA_USER_URL, callback=user_callback, content_type="application/json") def _mock_odata_api_for_error(self, odata_api_root_url, username): """ Mock an error response when calling the OData API for user details. """ - def callback(request, uri, headers): # pylint: disable=unused-argument + def callback(request): """ Return a 500 error when someone tries to call the URL. """ - headers["CorrelationId"] = "aefd38b7-c92c-445a-8c7a-487a3f0c7a9d" - headers["RequestNo"] = "[787177]" # This is the format SAPSF returns for the transaction request number + headers = { + "CorrelationId": "aefd38b7-c92c-445a-8c7a-487a3f0c7a9d", + "RequestNo": "[787177]", # This is the format SAPSF returns for the transaction request number + } return 500, headers, "Failure!" + # Replace the default OData user mock from setUp; responses serves registrations in order. + responses.remove(responses.GET, self.odata_user_url) fields = ",".join(SapSuccessFactorsIdentityProvider.default_field_mapping.copy()) url = "{root_url}User(userId='{user_id}')?$select={fields}".format( # noqa: UP032 root_url=odata_api_root_url, user_id=username, fields=fields, ) - httpretty.register_uri(httpretty.GET, url, body=callback, content_type="application/json") + responses.add_callback(responses.GET, url, callback=callback, content_type="application/json") return url def test_register_insufficient_sapsf_metadata(self): @@ -571,12 +581,12 @@ def test_register_sapsf_with_value_default(self): "?$select=firstName,country,lastName,defaultFullName,email" ) - def user_callback(request, _uri, headers): + def user_callback(request): auth_header = request.headers.get("Authorization") assert auth_header == "Bearer faketoken" return ( 200, - headers, + {}, json.dumps( { "d": { @@ -590,7 +600,9 @@ def user_callback(request, _uri, headers): ), ) - httpretty.register_uri(httpretty.GET, ODATA_USER_URL, content_type="application/json", body=user_callback) + # Replace the default OData user mock from setUp; responses serves registrations in order. + responses.remove(responses.GET, self.odata_user_url) + responses.add_callback(responses.GET, ODATA_USER_URL, callback=user_callback, content_type="application/json") provider_settings = { "sapsf_oauth_root_url": "http://successfactors.com/oauth/", diff --git a/common/djangoapps/third_party_auth/tests/utils.py b/common/djangoapps/third_party_auth/tests/utils.py index f47761386c30..61dce8fa7a9b 100644 --- a/common/djangoapps/third_party_auth/tests/utils.py +++ b/common/djangoapps/third_party_auth/tests/utils.py @@ -4,7 +4,7 @@ import json from base64 import b64encode -import httpretty +import responses from oauth2_provider.models import Application from onelogin.saml2.utils import OneLogin_Saml2_Utils from social_core.backends.facebook import API_VERSION as FACEBOOK_API_VERSION @@ -16,7 +16,6 @@ from .testutil import ThirdPartyAuthTestMixin -@httpretty.activate class ThirdPartyOAuthTestMixin(ThirdPartyAuthTestMixin): """ Mixin with tests for third party oauth views. A TestCase that includes @@ -34,6 +33,9 @@ class ThirdPartyOAuthTestMixin(ThirdPartyAuthTestMixin): def setUp(self): # pylint: disable=arguments-differ super().setUp() + responses.start() + self.addCleanup(responses.stop) + self.addCleanup(responses.reset) if self.CREATE_USER: self.user = UserFactory.create(password='secret') UserSocialAuth.objects.create(user=self.user, provider=self.BACKEND, uid=self.social_uid) @@ -81,8 +83,8 @@ def _setup_provider_response_with_body(self, status, body): """ Register a mock response for the third party user information endpoint with given status and body. """ - httpretty.register_uri( - httpretty.GET, + responses.add( + responses.GET, self.USER_URL, body=body, status=status, diff --git a/common/djangoapps/util/tests/mixins/discovery.py b/common/djangoapps/util/tests/mixins/discovery.py index c673047d5e72..eff6a9a1d289 100644 --- a/common/djangoapps/util/tests/mixins/discovery.py +++ b/common/djangoapps/util/tests/mixins/discovery.py @@ -5,7 +5,7 @@ import json -import httpretty +import responses from django.conf import settings from django.core.cache import cache @@ -37,9 +37,9 @@ def mock_course_discovery_api_for_catalog_contains(self, catalog_id=1, course_ru self.COURSE_DISCOVERY_CATALOGS_URL, catalog_id, ','.join(course_run_ids) ) - httpretty.register_uri( - method=httpretty.GET, - uri=catalog_contains_uri, + responses.add( + method=responses.GET, + url=catalog_contains_uri, body=course_discovery_api_response_json, content_type='application/json' ) diff --git a/lms/djangoapps/commerce/api/v0/tests/test_views.py b/lms/djangoapps/commerce/api/v0/tests/test_views.py index 5462ea6d1574..43b82f05a229 100644 --- a/lms/djangoapps/commerce/api/v0/tests/test_views.py +++ b/lms/djangoapps/commerce/api/v0/tests/test_views.py @@ -9,6 +9,7 @@ import ddt import pytz +import responses from django.test import TestCase from django.test.utils import override_settings from django.urls import reverse, reverse_lazy @@ -290,6 +291,7 @@ def setUp(self): super().setUp() self._login() + @responses.activate def test_order_found(self): """ If the order is located, the view should pass the data from the API. """ @@ -300,6 +302,7 @@ def test_order_found(self): actual = json.loads(response.content.decode('utf-8')) assert actual == self.MOCK_ORDER + @responses.activate def test_order_not_found(self): """ If the order is not found, the view should return a 404. """ with mock_basket_order(basket_id=1, status=404): diff --git a/lms/djangoapps/commerce/api/v1/tests/test_views.py b/lms/djangoapps/commerce/api/v1/tests/test_views.py index 2dd7120e3c7d..44aca58110c3 100644 --- a/lms/djangoapps/commerce/api/v1/tests/test_views.py +++ b/lms/djangoapps/commerce/api/v1/tests/test_views.py @@ -7,6 +7,7 @@ import ddt import pytz +import responses from django.conf import settings from django.contrib.auth.models import Permission from django.test import TestCase @@ -456,6 +457,7 @@ def setUp(self): super().setUp() self._login() + @responses.activate def test_order_found(self): """ If the order is located, the view should pass the data from the API. """ with mock_order_endpoint(order_number=self.ORDER_NUMBER, response=self.MOCK_ORDER): @@ -465,6 +467,7 @@ def test_order_found(self): actual = json.loads(response.content.decode('utf-8')) assert actual == self.MOCK_ORDER + @responses.activate def test_order_not_found(self): """ If the order is not found, the view should return a 404. """ with mock_order_endpoint(order_number=self.ORDER_NUMBER, status=404): diff --git a/lms/djangoapps/commerce/tests/__init__.py b/lms/djangoapps/commerce/tests/__init__.py index 633af5ac4c2b..bc9385ca7a5e 100644 --- a/lms/djangoapps/commerce/tests/__init__.py +++ b/lms/djangoapps/commerce/tests/__init__.py @@ -6,8 +6,8 @@ from urllib.parse import urljoin import ddt -import httpretty import requests # noqa: F401 +import responses from django.conf import settings from django.test import TestCase from edx_rest_api_client.auth import JwtAuth @@ -138,7 +138,7 @@ def setUp(self): self.user = UserFactory() self.base_url = get_ecommerce_api_base_url() - @httpretty.activate + @responses.activate def test_client_unicode(self): """ The client should handle json responses properly when they contain unicode character data. @@ -146,11 +146,11 @@ def test_client_unicode(self): Regression test for ECOM-1606. """ expected_content = '{"result": "Préparatoire"}' - httpretty.register_uri( - httpretty.GET, + responses.add( + responses.GET, f"{settings.ECOMMERCE_API_URL.strip('/')}/baskets/1/order/", status=200, body=expected_content, - adding_headers={'Content-Type': JSON}, + content_type=JSON, ) api_url = urljoin(f"{self.base_url}/", "baskets/1/order/") actual_object = get_ecommerce_api_client(self.user).get(api_url).json() diff --git a/lms/djangoapps/commerce/tests/mocks.py b/lms/djangoapps/commerce/tests/mocks.py index 4e6669cf27fb..c612a5192223 100644 --- a/lms/djangoapps/commerce/tests/mocks.py +++ b/lms/djangoapps/commerce/tests/mocks.py @@ -4,7 +4,7 @@ import json -import httpretty +import responses from django.conf import settings from . import factories @@ -14,20 +14,30 @@ class mock_ecommerce_api_endpoint: """ Base class for contextmanagers used to mock calls to api endpoints. - The contextmanager internally activates and deactivates httpretty as - required, therefore it is not advised to use this mock endpoint in - test cases where httpretty is being used directly. + The contextmanager registers its response with ``responses`` on entry and + checks on exit whether the endpoint was called, so the test (or its setUp) + must already have ``responses`` active, e.g. via ``@responses.activate`` + or ``responses.start()``. + + These used to activate the HTTP mocking library (httpretty) themselves on + entry and deactivate it on exit. That meant a test using one of these + did not need to activate anything, but it also meant that nesting two of + them, or using one inside a test that was already mocking HTTP, tore + down the outer mocks on exit; the old ``reset_on_exit`` flag existed only + to work around that. Keeping activation with the test avoids the + problem, at the cost of every test that uses one of these needing + ``@responses.activate`` (or ``responses.start()`` in setUp). """ # override this in subclasses. default_response = None - # override this in subclasses, using one of httpretty's method constants + # override this in subclasses, using one of responses' method constants method = None host = settings.ECOMMERCE_API_URL.strip('/') - def __init__(self, response=None, status=200, expect_called=True, exception=None, reset_on_exit=True): + def __init__(self, response=None, status=200, expect_called=True, exception=None): """ Keyword Arguments: response: a JSON-serializable Python type representing the desired response body. @@ -36,23 +46,22 @@ def __init__(self, response=None, status=200, expect_called=True, exception=None to False if we should ensure that no request arrived, or None to skip checking if the request arrived exception: raise this exception instead of returning an HTTP response when called. - reset_on_exit (bool): Indicates if `httpretty` should be reset after the decorator exits. """ self.response = response or self.default_response self.status = status self.expect_called = expect_called self.exception = exception - self.reset_on_exit = reset_on_exit + self._calls_on_enter = 0 def get_uri(self): """ - Returns the uri to register with httpretty for this contextmanager. + Returns the uri to register with responses for this contextmanager. """ return self.host + '/' + self.get_path().lstrip('/') def get_path(self): """ - Returns the path of the URI to register with httpretty for this contextmanager. + Returns the path of the URI to register with responses for this contextmanager. Subclasses must override this method. @@ -61,38 +70,42 @@ def get_path(self): """ raise NotImplementedError - def _exception_body(self, request, uri, headers): - """Helper used to create callbacks in order to have httpretty raise Exceptions.""" - raise self.exception + def _matching_calls(self): + """Count the requests made so far to this contextmanager's endpoint.""" + uri = self.get_uri() + return sum( + 1 for call in responses.calls + if call.request.method == self.method and call.request.url.split('?', 1)[0] == uri + ) def __enter__(self): - httpretty.enable() - httpretty.register_uri( + self._calls_on_enter = self._matching_calls() + if self.exception is not None: + # responses raises an Exception instance given as the body instead of returning a response. + body = self.exception + else: + body = json.dumps(self.response) + responses.add( self.method, self.get_uri(), status=self.status, - body=self._exception_body if self.exception is not None else json.dumps(self.response), - adding_headers={'Content-Type': 'application/json'}, + body=body, + content_type='application/json', ) + return self def __exit__(self, exc_type, exc_val, exc_tb): if self.expect_called is None: - called_if_expected = True - else: - called_if_expected = self.expect_called == (bool(httpretty.last_request().headers)) - httpretty.disable() - - if self.reset_on_exit: - httpretty.reset() - - assert called_if_expected + return + called = self._matching_calls() > self._calls_on_enter + assert called == self.expect_called class mock_basket_order(mock_ecommerce_api_endpoint): """ Mocks calls to E-Commerce API client basket order method. """ default_response = {'number': 1} - method = httpretty.GET + method = responses.GET def __init__(self, basket_id, **kwargs): super().__init__(**kwargs) @@ -106,7 +119,7 @@ class mock_create_refund(mock_ecommerce_api_endpoint): """ Mocks calls to E-Commerce API client refund creation method. """ default_response = [] - method = httpretty.POST + method = responses.POST def get_path(self): return '/refunds/' @@ -118,7 +131,7 @@ class mock_payment_processors(mock_ecommerce_api_endpoint): """ default_response = ['foo', 'bar'] - method = httpretty.GET + method = responses.GET def get_path(self): return "/payment/processors/" @@ -128,7 +141,7 @@ class mock_process_refund(mock_ecommerce_api_endpoint): """ Mocks calls to E-Commerce API client refund process method. """ default_response = [] - method = httpretty.PUT + method = responses.PUT def __init__(self, refund_id, **kwargs): super().__init__(**kwargs) @@ -142,7 +155,7 @@ class mock_order_endpoint(mock_ecommerce_api_endpoint): """ Mocks calls to E-Commerce API client basket order method. """ default_response = {'number': 'EDX-100001'} - method = httpretty.GET + method = responses.GET def __init__(self, order_number, **kwargs): super().__init__(**kwargs) @@ -190,7 +203,7 @@ class mock_get_orders(mock_ecommerce_api_endpoint): ), ] } - method = httpretty.GET + method = responses.GET def get_path(self): return '/orders/' diff --git a/lms/djangoapps/commerce/tests/test_signals.py b/lms/djangoapps/commerce/tests/test_signals.py index 2dc3e2e77bbb..30047612d8d9 100644 --- a/lms/djangoapps/commerce/tests/test_signals.py +++ b/lms/djangoapps/commerce/tests/test_signals.py @@ -9,8 +9,8 @@ from urllib.parse import urljoin import ddt -import httpretty import pytest +import responses from django.conf import settings from django.contrib.auth.models import AnonymousUser from django.test.utils import override_settings @@ -42,6 +42,9 @@ class TestRefundSignal(ModuleStoreTestCase): def setUp(self): super().setUp() + responses.start() + self.addCleanup(responses.stop) + self.addCleanup(responses.reset) # Ensure the E-Commerce service user exists UserFactory(username=settings.ECOMMERCE_SERVICE_WORKER_USERNAME, is_staff=True) @@ -152,8 +155,8 @@ def test_notification_when_approval_fails(self, mock_send_notification): failed_refund_id = 2 with mock_create_refund(status=201, response=[refund_id, failed_refund_id]): - with mock_process_refund(refund_id, reset_on_exit=False): - with mock_process_refund(failed_refund_id, status=500, reset_on_exit=False): + with mock_process_refund(refund_id): + with mock_process_refund(failed_refund_id, status=500): self.send_signal() assert mock_send_notification.called mock_send_notification.assert_called_with(self.course_enrollment.user, [failed_refund_id]) @@ -180,12 +183,12 @@ def test_no_notification_after_approval(self, mock_send_notification): refund_id = 1 with mock_create_refund(status=201, response=[refund_id]): - with mock_process_refund(refund_id, reset_on_exit=False): + with mock_process_refund(refund_id): self.send_signal() assert not mock_send_notification.called - last_request = httpretty.last_request() - self.assertDictEqual(json.loads(last_request.body.decode('utf8')), {'action': 'approve_payment_only'}) # noqa: PT009 # pylint: disable=line-too-long + last_request = responses.calls[-1].request + self.assertDictEqual(json.loads(last_request.body), {'action': 'approve_payment_only'}) # noqa: PT009 # pylint: disable=line-too-long @mock.patch('lms.djangoapps.commerce.utils._send_refund_notification') def test_notification_no_refund(self, mock_send_notification): @@ -261,7 +264,7 @@ def test_send_refund_notification(self, student_email, mock_zendesk): def _mock_zendesk_api(self, status=201): """ Mock Zendesk's ticket creation API. """ - httpretty.register_uri(httpretty.POST, urljoin(ZENDESK_URL, '/api/v2/tickets.json'), status=status, + responses.add(responses.POST, urljoin(ZENDESK_URL, '/api/v2/tickets.json'), status=status, body='{}', content_type=JSON) def call_create_zendesk_ticket(self, name='Test user', email='user@example.com', subject='Test Ticket', @@ -289,7 +292,7 @@ def test_create_zendesk_ticket_request_error(self): assert not success assert mock_post.called - @httpretty.activate + @responses.activate def test_create_zendesk_ticket(self): """ Verify the Zendesk API is called. """ self._mock_zendesk_api() @@ -301,7 +304,7 @@ def test_create_zendesk_ticket(self): tags = ['auto_refund'] ticket_created = self.call_create_zendesk_ticket(name, email, subject, body, tags) assert ticket_created - last_request = httpretty.last_request() + last_request = responses.calls[-1].request # Verify the headers expected = { @@ -324,7 +327,7 @@ def test_create_zendesk_ticket(self): 'tags': ['LMS'] + tags } } - response_dict = json.loads(last_request.body.decode('utf8')) + response_dict = json.loads(last_request.body) response_dict['ticket']['tags'].sort() expected['ticket']['tags'].sort() self.assertDictEqual(response_dict, expected) # noqa: PT009 diff --git a/lms/djangoapps/commerce/tests/test_utils.py b/lms/djangoapps/commerce/tests/test_utils.py index 264fc1a87482..af64998b75fc 100644 --- a/lms/djangoapps/commerce/tests/test_utils.py +++ b/lms/djangoapps/commerce/tests/test_utils.py @@ -6,7 +6,7 @@ from urllib.parse import urlencode import ddt -import httpretty +import responses from django.conf import settings from django.test import TestCase from django.test.client import RequestFactory @@ -207,10 +207,10 @@ def test_ecommerce_service_not_configured(self, mock_commerce_configured): assert mock_commerce_configured.is_called assert not refund_success - @httpretty.activate + @responses.activate def test_no_ecommerce_connection_and_failure(self): - httpretty.register_uri( - httpretty.POST, + responses.add( + responses.POST, settings.ECOMMERCE_API_URL + 'refunds/', status=404, body='{}', @@ -220,17 +220,17 @@ def test_no_ecommerce_connection_and_failure(self): refund_success = refund_entitlement(course_entitlement) assert not refund_success - @httpretty.activate + @responses.activate def test_ecommerce_successful_refund(self): - httpretty.register_uri( - httpretty.POST, + responses.add( + responses.POST, settings.ECOMMERCE_API_URL + 'refunds/', status=201, body='[1]', content_type='application/json' ) - httpretty.register_uri( - httpretty.PUT, + responses.add( + responses.PUT, settings.ECOMMERCE_API_URL + 'refunds/1/process/', status=200, body=json.dumps({ @@ -249,18 +249,18 @@ def test_ecommerce_successful_refund(self): refund_success = refund_entitlement(course_entitlement) assert refund_success - @httpretty.activate + @responses.activate @patch('lms.djangoapps.commerce.utils._send_refund_notification', return_value=True) def test_ecommerce_refund_failed_process_notification_sent(self, mock_send_notification): - httpretty.register_uri( - httpretty.POST, + responses.add( + responses.POST, settings.ECOMMERCE_API_URL + 'refunds/', status=201, body='[1]', content_type='application/json' ) - httpretty.register_uri( - httpretty.PUT, + responses.add( + responses.PUT, settings.ECOMMERCE_API_URL + 'refunds/1/process/', status=400, body='{}', @@ -273,7 +273,7 @@ def test_ecommerce_refund_failed_process_notification_sent(self, mock_send_notif assert call_args[0] == (course_entitlement.user, [1]) assert refund_success - @httpretty.activate + @responses.activate @patch('lms.djangoapps.commerce.utils._send_refund_notification', return_value=True) def test_ecommerce_refund_not_verified_notification_for_entitlement(self, mock_send_notification): """ @@ -283,15 +283,15 @@ def test_ecommerce_refund_not_verified_notification_for_entitlement(self, mock_s PROFESSIONAL mode is used here although we never auto approve PROFESSIONAL refunds right now """ - httpretty.register_uri( - httpretty.POST, + responses.add( + responses.POST, settings.ECOMMERCE_API_URL + 'refunds/', status=201, body='[1]', content_type='application/json' ) - httpretty.register_uri( - httpretty.PUT, + responses.add( + responses.PUT, settings.ECOMMERCE_API_URL + 'refunds/1/process/', status=400, body='{}', @@ -304,18 +304,18 @@ def test_ecommerce_refund_not_verified_notification_for_entitlement(self, mock_s assert call_args[0] == (course_entitlement.user, [1]) assert refund_success - @httpretty.activate + @responses.activate @patch('lms.djangoapps.commerce.utils._send_refund_notification', return_value=True) def test_ecommerce_refund_send_notification_failed(self, mock_send_notification): - httpretty.register_uri( - httpretty.POST, + responses.add( + responses.POST, settings.ECOMMERCE_API_URL + 'refunds/', status=201, body='[1]', content_type='application/json' ) - httpretty.register_uri( - httpretty.PUT, + responses.add( + responses.PUT, settings.ECOMMERCE_API_URL + 'refunds/1/process/', status=400, body='{}', @@ -330,7 +330,7 @@ def test_ecommerce_refund_send_notification_failed(self, mock_send_notification) assert call_args[0] == (course_entitlement.user, [1]) assert not refund_success - @httpretty.activate + @responses.activate @ddt.data( (["verified", "audit"], "audit"), (["professional"], "professional"), @@ -350,15 +350,15 @@ def test_mode_change_after_refund_seat(self, course_modes, new_mode): mode_display_name=course_mode, ) - httpretty.register_uri( - httpretty.POST, + responses.add( + responses.POST, settings.ECOMMERCE_API_URL + 'refunds/', status=201, body='[1]', content_type='application/json' ) - httpretty.register_uri( - httpretty.PUT, + responses.add( + responses.PUT, settings.ECOMMERCE_API_URL + 'refunds/1/process/', status=200, body=json.dumps({ diff --git a/lms/djangoapps/discussion/rest_api/tests/test_api_v2.py b/lms/djangoapps/discussion/rest_api/tests/test_api_v2.py index 22449eaac14c..a311d3932b14 100644 --- a/lms/djangoapps/discussion/rest_api/tests/test_api_v2.py +++ b/lms/djangoapps/discussion/rest_api/tests/test_api_v2.py @@ -59,7 +59,6 @@ from lms.djangoapps.discussion.rest_api.tests.utils import ( ForumMockUtilsMixin, make_paginated_api_response, - parsed_body, # noqa: F401 ) from lms.djangoapps.discussion.tests.utils import make_minimal_cs_comment, make_minimal_cs_thread from openedx.core.djangoapps.course_groups.models import CourseUserGroupPartitionGroup diff --git a/lms/djangoapps/discussion/rest_api/tests/test_serializers_v2.py b/lms/djangoapps/discussion/rest_api/tests/test_serializers_v2.py index 106494f80b1a..17f8a1ca9938 100644 --- a/lms/djangoapps/discussion/rest_api/tests/test_serializers_v2.py +++ b/lms/djangoapps/discussion/rest_api/tests/test_serializers_v2.py @@ -7,7 +7,6 @@ from unittest import mock import ddt -import httpretty from django.test import override_settings from django.test.client import RequestFactory @@ -52,10 +51,6 @@ def tearDownClass(cls): def setUp(self): super().setUp() - httpretty.reset() - httpretty.enable() - self.addCleanup(httpretty.reset) - self.addCleanup(httpretty.disable) patcher = mock.patch( "openedx.core.djangoapps.django_comment_common.comment_client.models.forum_api.get_course_id_by_comment" ) @@ -407,10 +402,6 @@ def tearDownClass(cls): def setUp(self): super().setUp() - httpretty.reset() - httpretty.enable() - self.addCleanup(httpretty.reset) - self.addCleanup(httpretty.disable) self.user = UserFactory.create() self.register_get_user_response(self.user) self.request = RequestFactory().get("/dummy") @@ -551,10 +542,6 @@ def tearDownClass(cls): def setUp(self): super().setUp() - httpretty.reset() - httpretty.enable() - self.addCleanup(httpretty.reset) - self.addCleanup(httpretty.disable) patcher = mock.patch( "openedx.core.djangoapps.django_comment_common.comment_client.models.forum_api.get_course_id_by_comment" ) diff --git a/lms/djangoapps/discussion/rest_api/tests/test_utils.py b/lms/djangoapps/discussion/rest_api/tests/test_utils.py index 7f67804f77aa..827a6850fec0 100644 --- a/lms/djangoapps/discussion/rest_api/tests/test_utils.py +++ b/lms/djangoapps/discussion/rest_api/tests/test_utils.py @@ -11,7 +11,6 @@ from common.djangoapps.student.roles import CourseInstructorRole, CourseStaffRole from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, UserFactory from lms.djangoapps.discussion.django_comment_client.tests.factories import RoleFactory -from lms.djangoapps.discussion.rest_api.tests.utils import CommentsServiceMockMixin from lms.djangoapps.discussion.rest_api.utils import ( discussion_open_for_user, get_archived_topics, @@ -181,7 +180,7 @@ def test_remove_empty_sequentials(self): @ddt.ddt -class TestBlackoutDates(CommentsServiceMockMixin, ModuleStoreTestCase): +class TestBlackoutDates(ModuleStoreTestCase): """ Test for the is_posting_allowed function """ diff --git a/lms/djangoapps/discussion/rest_api/tests/utils.py b/lms/djangoapps/discussion/rest_api/tests/utils.py index d4dda7ea4cec..07069284252f 100644 --- a/lms/djangoapps/discussion/rest_api/tests/utils.py +++ b/lms/djangoapps/discussion/rest_api/tests/utils.py @@ -5,12 +5,9 @@ import hashlib import json -import re from contextlib import closing from datetime import datetime -from urllib.parse import parse_qs -import httpretty from PIL import Image from pytz import UTC @@ -21,38 +18,6 @@ from openedx.core.djangoapps.user_api.accounts.image_helpers import get_profile_image_names, set_has_profile_image -def _get_thread_callback(thread_data): - """ - Get a callback function that will return POST/PUT data overridden by - response_overrides. - """ - def callback(request, _uri, headers): - """ - Simulate the thread creation or update endpoint by returning the provided - data along with the data from response_overrides and dummy values for any - additional required fields. - """ - response_data = make_minimal_cs_thread(thread_data) - original_data = response_data.copy() - for key, val_list in parsed_body(request).items(): - val = val_list[0] - if key in ["anonymous", "anonymous_to_peers", "closed", "pinned"]: - response_data[key] = val == "True" - elif key == "edit_reason_code": - response_data["edit_history"] = [ - { - "original_body": original_data["body"], - "author": thread_data.get('username'), - "reason_code": val, - }, - ] - else: - response_data[key] = val - return (200, headers, json.dumps(response_data)) - - return callback - - def make_thread_callback(thread_data): """ Returns a function that simulates thread creation/update behavior, @@ -81,41 +46,6 @@ def callback(*args, **kwargs): return callback -def _get_comment_callback(comment_data, thread_id, parent_id): - """ - Get a callback function that will return a comment containing the given data - plus necessary dummy data, overridden by the content of the POST/PUT - request. - """ - def callback(request, _uri, headers): - """ - Simulate the comment creation or update endpoint as described above. - """ - response_data = make_minimal_cs_comment(comment_data) - original_data = response_data.copy() - # thread_id and parent_id are not included in request payload but - # are returned by the comments service - response_data["thread_id"] = thread_id - response_data["parent_id"] = parent_id - for key, val_list in parsed_body(request).items(): - val = val_list[0] - if key in ["anonymous", "anonymous_to_peers", "endorsed"]: - response_data[key] = val == "True" - elif key == "edit_reason_code": - response_data["edit_history"] = [ - { - "original_body": original_data["body"], - "author": comment_data.get('username'), - "reason_code": val, - }, - ] - else: - response_data[key] = val - return (200, headers, json.dumps(response_data)) - - return callback - - def make_comment_callback(comment_data, thread_id, parent_id): """ Returns a callable that mimics comment creation or update behavior, @@ -158,441 +88,6 @@ def callback(*args, **kwargs): return callback -class CommentsServiceMockMixin: - """Mixin with utility methods for mocking the comments service""" - - def register_get_threads_response(self, threads, page, num_pages): - """Register a mock response for GET on the CS thread list endpoint""" - assert httpretty.is_enabled(), 'httpretty must be enabled to mock calls.' - - httpretty.register_uri( - httpretty.GET, - "http://localhost:4567/api/v1/threads", - body=json.dumps({ - "collection": threads, - "page": page, - "num_pages": num_pages, - "thread_count": len(threads), - }), - status=200 - ) - - def register_get_course_commentable_counts_response(self, course_id, thread_counts): - """Register a mock response for GET on the CS thread list endpoint""" - assert httpretty.is_enabled(), 'httpretty must be enabled to mock calls.' - - httpretty.register_uri( - httpretty.GET, - f"http://localhost:4567/api/v1/commentables/{course_id}/counts", - body=json.dumps(thread_counts), - status=200 - ) - - def register_get_threads_search_response(self, threads, rewrite, num_pages=1): - """Register a mock response for GET on the CS thread search endpoint""" - assert httpretty.is_enabled(), 'httpretty must be enabled to mock calls.' - httpretty.register_uri( - httpretty.GET, - "http://localhost:4567/api/v1/search/threads", - body=json.dumps({ - "collection": threads, - "page": 1, - "num_pages": num_pages, - "corrected_text": rewrite, - "thread_count": len(threads), - }), - status=200 - ) - - def register_post_thread_response(self, thread_data): - """Register a mock response for POST on the CS commentable endpoint""" - assert httpretty.is_enabled(), 'httpretty must be enabled to mock calls.' - httpretty.register_uri( - httpretty.POST, - re.compile(r"http://localhost:4567/api/v1/(\w+)/threads"), - body=_get_thread_callback(thread_data) - ) - - def register_put_thread_response(self, thread_data): - """ - Register a mock response for PUT on the CS endpoint for the given - thread_id. - """ - assert httpretty.is_enabled(), 'httpretty must be enabled to mock calls.' - httpretty.register_uri( - httpretty.PUT, - "http://localhost:4567/api/v1/threads/{}".format(thread_data["id"]), - body=_get_thread_callback(thread_data) - ) - - def register_get_thread_error_response(self, thread_id, status_code): - """Register a mock error response for GET on the CS thread endpoint.""" - assert httpretty.is_enabled(), 'httpretty must be enabled to mock calls.' - httpretty.register_uri( - httpretty.GET, - f"http://localhost:4567/api/v1/threads/{thread_id}", - body="", - status=status_code - ) - - def register_get_thread_response(self, thread): - """ - Register a mock response for GET on the CS thread instance endpoint. - """ - assert httpretty.is_enabled(), 'httpretty must be enabled to mock calls.' - httpretty.register_uri( - httpretty.GET, - "http://localhost:4567/api/v1/threads/{id}".format(id=thread["id"]), - body=json.dumps(thread), - status=200 - ) - - def register_get_comments_response(self, comments, page, num_pages): - """Register a mock response for GET on the CS comments list endpoint""" - assert httpretty.is_enabled(), 'httpretty must be enabled to mock calls.' - - httpretty.register_uri( - httpretty.GET, - "http://localhost:4567/api/v1/comments", - body=json.dumps({ - "collection": comments, - "page": page, - "num_pages": num_pages, - "comment_count": len(comments), - }), - status=200 - ) - - def register_post_comment_response(self, comment_data, thread_id, parent_id=None): - """ - Register a mock response for POST on the CS comments endpoint for the - given thread or parent; exactly one of thread_id and parent_id must be - specified. - """ - if parent_id: - url = f"http://localhost:4567/api/v1/comments/{parent_id}" - else: - url = f"http://localhost:4567/api/v1/threads/{thread_id}/comments" - - assert httpretty.is_enabled(), 'httpretty must be enabled to mock calls.' - httpretty.register_uri( - httpretty.POST, - url, - body=_get_comment_callback(comment_data, thread_id, parent_id) - ) - - def register_put_comment_response(self, comment_data): - """ - Register a mock response for PUT on the CS endpoint for the given - comment data (which must include the key "id"). - """ - thread_id = comment_data["thread_id"] - parent_id = comment_data.get("parent_id") - assert httpretty.is_enabled(), 'httpretty must be enabled to mock calls.' - httpretty.register_uri( - httpretty.PUT, - "http://localhost:4567/api/v1/comments/{}".format(comment_data["id"]), - body=_get_comment_callback(comment_data, thread_id, parent_id) - ) - - def register_get_comment_error_response(self, comment_id, status_code): - """ - Register a mock error response for GET on the CS comment instance - endpoint. - """ - assert httpretty.is_enabled(), 'httpretty must be enabled to mock calls.' - httpretty.register_uri( - httpretty.GET, - f"http://localhost:4567/api/v1/comments/{comment_id}", - body="", - status=status_code - ) - - def register_get_comment_response(self, response_overrides): - """ - Register a mock response for GET on the CS comment instance endpoint. - """ - comment = make_minimal_cs_comment(response_overrides) - assert httpretty.is_enabled(), 'httpretty must be enabled to mock calls.' - httpretty.register_uri( - httpretty.GET, - "http://localhost:4567/api/v1/comments/{id}".format(id=comment["id"]), - body=json.dumps(comment), - status=200 - ) - - def register_get_user_response(self, user, subscribed_thread_ids=None, upvoted_ids=None): - """Register a mock response for GET on the CS user instance endpoint""" - assert httpretty.is_enabled(), 'httpretty must be enabled to mock calls.' - httpretty.register_uri( - httpretty.GET, - f"http://localhost:4567/api/v1/users/{user.id}", - body=json.dumps({ - "id": str(user.id), - "subscribed_thread_ids": subscribed_thread_ids or [], - "upvoted_ids": upvoted_ids or [], - }), - status=200 - ) - - def register_get_user_retire_response(self, user, status=200, body=""): - """Register a mock response for GET on the CS user retirement endpoint""" - assert httpretty.is_enabled(), 'httpretty must be enabled to mock calls.' - httpretty.register_uri( - httpretty.POST, - f"http://localhost:4567/api/v1/users/{user.id}/retire", - body=body, - status=status - ) - - def register_get_username_replacement_response(self, user, status=200, body=""): - assert httpretty.is_enabled(), 'httpretty must be enabled to mock calls.' - httpretty.register_uri( - httpretty.POST, - f"http://localhost:4567/api/v1/users/{user.id}/replace_username", - body=body, - status=status - ) - - def register_subscribed_threads_response(self, user, threads, page, num_pages): - """Register a mock response for GET on the CS user instance endpoint""" - assert httpretty.is_enabled(), 'httpretty must be enabled to mock calls.' - httpretty.register_uri( - httpretty.GET, - f"http://localhost:4567/api/v1/users/{user.id}/subscribed_threads", - body=json.dumps({ - "collection": threads, - "page": page, - "num_pages": num_pages, - "thread_count": len(threads), - }), - status=200 - ) - - def register_course_stats_response(self, course_key, stats, page, num_pages): - """Register a mock response for GET on the CS user course stats instance endpoint""" - assert httpretty.is_enabled(), 'httpretty must be enabled to mock calls.' - httpretty.register_uri( - httpretty.GET, - f"http://localhost:4567/api/v1/users/{course_key}/stats", - body=json.dumps({ - "user_stats": stats, - "page": page, - "num_pages": num_pages, - "count": len(stats), - }), - status=200 - ) - - def register_subscription_response(self, user): - """ - Register a mock response for POST and DELETE on the CS user subscription - endpoint - """ - assert httpretty.is_enabled(), 'httpretty must be enabled to mock calls.' - for method in [httpretty.POST, httpretty.DELETE]: - httpretty.register_uri( - method, - f"http://localhost:4567/api/v1/users/{user.id}/subscriptions", - body=json.dumps({}), # body is unused - status=200 - ) - - def register_thread_votes_response(self, thread_id): - """ - Register a mock response for PUT and DELETE on the CS thread votes - endpoint - """ - assert httpretty.is_enabled(), 'httpretty must be enabled to mock calls.' - for method in [httpretty.PUT, httpretty.DELETE]: - httpretty.register_uri( - method, - f"http://localhost:4567/api/v1/threads/{thread_id}/votes", - body=json.dumps({}), # body is unused - status=200 - ) - - def register_comment_votes_response(self, comment_id): - """ - Register a mock response for PUT and DELETE on the CS comment votes - endpoint - """ - assert httpretty.is_enabled(), 'httpretty must be enabled to mock calls.' - for method in [httpretty.PUT, httpretty.DELETE]: - httpretty.register_uri( - method, - f"http://localhost:4567/api/v1/comments/{comment_id}/votes", - body=json.dumps({}), # body is unused - status=200 - ) - - def register_flag_response(self, content_type, content_id): - """Register a mock response for PUT on the CS flag endpoints""" - assert httpretty.is_enabled(), 'httpretty must be enabled to mock calls.' - for path in ["abuse_flag", "abuse_unflag"]: - httpretty.register_uri( - "PUT", - "http://localhost:4567/api/v1/{content_type}s/{content_id}/{path}".format( # noqa: UP032 - content_type=content_type, - content_id=content_id, - path=path - ), - body=json.dumps({}), # body is unused - status=200 - ) - - def register_read_response(self, user, content_type, content_id): - """ - Register a mock response for POST on the CS 'read' endpoint - """ - assert httpretty.is_enabled(), 'httpretty must be enabled to mock calls.' - httpretty.register_uri( - httpretty.POST, - f"http://localhost:4567/api/v1/users/{user.id}/read", - params={'source_type': content_type, 'source_id': content_id}, - body=json.dumps({}), # body is unused - status=200 - ) - - def register_thread_flag_response(self, thread_id): - """Register a mock response for PUT on the CS thread flag endpoints""" - self.register_flag_response("thread", thread_id) - - def register_comment_flag_response(self, comment_id): - """Register a mock response for PUT on the CS comment flag endpoints""" - self.register_flag_response("comment", comment_id) - - def register_delete_thread_response(self, thread_id): - """ - Register a mock response for DELETE on the CS thread instance endpoint - """ - assert httpretty.is_enabled(), 'httpretty must be enabled to mock calls.' - httpretty.register_uri( - httpretty.DELETE, - f"http://localhost:4567/api/v1/threads/{thread_id}", - body=json.dumps({}), # body is unused - status=200 - ) - - def register_delete_comment_response(self, comment_id): - """ - Register a mock response for DELETE on the CS comment instance endpoint - """ - assert httpretty.is_enabled(), 'httpretty must be enabled to mock calls.' - httpretty.register_uri( - httpretty.DELETE, - f"http://localhost:4567/api/v1/comments/{comment_id}", - body=json.dumps({}), # body is unused - status=200 - ) - - def register_user_active_threads(self, user_id, response): - """ - Register a mock response for GET on the CS comment active threads endpoint - """ - assert httpretty.is_enabled(), 'httpretty must be enabled to mock calls.' - httpretty.register_uri( - httpretty.GET, - f"http://localhost:4567/api/v1/users/{user_id}/active_threads", - body=json.dumps(response), - status=200 - ) - - def register_get_subscriptions(self, thread_id, response): - """ - Register a mock response for GET on the CS comment active threads endpoint - """ - assert httpretty.is_enabled(), 'httpretty must be enabled to mock calls.' - httpretty.register_uri( - httpretty.GET, - f"http://localhost:4567/api/v1/threads/{thread_id}/subscriptions", - body=json.dumps(response), - status=200 - ) - - def assert_query_params_equal(self, httpretty_request, expected_params): - """ - Assert that the given mock request had the expected query parameters - """ - actual_params = dict(querystring(httpretty_request)) - actual_params.pop("request_id") # request_id is random - assert actual_params == expected_params - - def assert_last_query_params(self, expected_params): - """ - Assert that the last mock request had the expected query parameters - """ - self.assert_query_params_equal(httpretty.last_request(), expected_params) - - def request_patch(self, request_data): - """ - make a request to PATCH endpoint and return response - """ - return self.client.patch( - self.url, - json.dumps(request_data), - content_type="application/merge-patch+json" - ) - - def expected_thread_data(self, overrides=None): - """ - Returns expected thread data in API response - """ - response_data = { - "anonymous": False, - "anonymous_to_peers": False, - "author": self.user.username, - "author_label": None, - "created_at": "1970-01-01T00:00:00Z", - "updated_at": "1970-01-01T00:00:00Z", - "raw_body": "Test body", - "rendered_body": "
Test body
", - "preview_body": "Test body", - "abuse_flagged": False, - "abuse_flagged_count": None, - "voted": False, - "vote_count": 0, - "editable_fields": [ - "abuse_flagged", - "anonymous", - "copy_link", - "following", - "raw_body", - "read", - "title", - "topic_id", - "type", - ], - "course_id": str(self.course.id), - "topic_id": "test_topic", - "group_id": None, - "group_name": None, - "title": "Test Title", - "pinned": False, - "closed": False, - "can_delete": True, - "following": False, - "comment_count": 1, - "unread_comment_count": 0, - "comment_list_url": "http://testserver/api/discussion/v1/comments/?thread_id=test_thread", - "endorsed_comment_list_url": None, - "non_endorsed_comment_list_url": None, - "read": False, - "has_endorsed": False, - "id": "test_thread", - "type": "discussion", - "response_count": 0, - "last_edit": None, - "edit_by_label": None, - "closed_by": None, - "closed_by_label": None, - "close_reason": None, - "close_reason_code": None, - } - response_data.update(overrides or {}) - return response_data - - class ForumMockUtilsMixin(MockForumApiMixin): """Mixin with utility methods for mocking the comments service""" @@ -747,20 +242,6 @@ def register_comment_flag_response(self, comment_id): """Register a mock response for PUT on the CS comment flag endpoints""" self.register_flag_response("comment", comment_id) - def assert_query_params_equal(self, httpretty_request, expected_params): - """ - Assert that the given mock request had the expected query parameters - """ - actual_params = dict(querystring(httpretty_request)) - actual_params.pop("request_id") # request_id is random - assert actual_params == expected_params - - def assert_last_query_params(self, expected_params): - """ - Assert that the last mock request had the expected query parameters - """ - self.assert_query_params_equal(httpretty.last_request(), expected_params) - def request_patch(self, request_data): """ make a request to PATCH endpoint and return response @@ -965,20 +446,6 @@ def get_expected_user_profile(self, username): } -def parsed_body(request): - """Returns a parsed dictionary version of a request body""" - # This could just be HTTPrettyRequest.parsed_body, but that method double-decodes '%2B' -> '+' -> ' '. - # You can just remove this method when this issue is fixed: https://github.com/gabrielfalcao/HTTPretty/issues/240 - return parse_qs(request.body.decode('utf8')) - - -def querystring(request): - """Returns a parsed dictionary version of a query string""" - # This could just be HTTPrettyRequest.querystring, but that method double-decodes '%2B' -> '+' -> ' '. - # You can just remove this method when this issue is fixed: https://github.com/gabrielfalcao/HTTPretty/issues/240 - return parse_qs(request.path.split('?', 1)[-1]) - - class ThreadMock(object): # noqa: UP004 """ A mock thread object diff --git a/lms/djangoapps/verify_student/tests/test_views.py b/lms/djangoapps/verify_student/tests/test_views.py index cb951e28ea2f..0712594b6c12 100644 --- a/lms/djangoapps/verify_student/tests/test_views.py +++ b/lms/djangoapps/verify_student/tests/test_views.py @@ -11,7 +11,7 @@ from uuid import uuid4 import ddt -import httpretty +import responses import simplejson as json from bs4 import BeautifulSoup from django.conf import settings @@ -102,10 +102,10 @@ def _mock_payment_processors(): """ Mock out the payment processors endpoint, since we don't run ecommerce for unit tests here. Used in tests where ``mock_payment_processors`` can't be easily used, for example the whole - test is an httpretty context or the mock may or may not be called depending on ddt parameters. + test is a responses context or the mock may or may not be called depending on ddt parameters. """ - httpretty.register_uri( - httpretty.GET, + responses.add( + responses.GET, f"{TEST_API_URL}/payment/processors/", body=json.dumps(['foo', 'bar']), content_type="application/json", @@ -154,6 +154,9 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase, XssTestMixin, Tes def setUp(self): super().setUp() + responses.start() + self.addCleanup(responses.stop) + self.addCleanup(responses.reset) self.user = UserFactory.create(username=self.USERNAME, password=self.PASSWORD) result = self.client.login(username=self.USERNAME, password=self.PASSWORD) assert result, 'Could not log in' @@ -892,7 +895,6 @@ def _set_contribution(self, amount, course_id): } session.save() - @httpretty.activate @override_settings(ECOMMERCE_API_URL=TEST_API_URL) def _get_page(self, url_name, course_key, expected_status_code=200, skip_first_step=False, assert_headers=False): """Retrieve one of the verification pages. """ @@ -909,7 +911,7 @@ def _get_page(self, url_name, course_key, expected_status_code=200, skip_first_s # ensure the mock api call was made. NOTE: the following line # approximates the check - if the headers were empty it means # there was no last request. - assert httpretty.last_request().headers + assert responses.calls return response def _assert_displayed_mode(self, response, expected_mode): @@ -1300,11 +1302,11 @@ def test_submit_photos_error_does_not_send_email(self): }, "DAYS_GOOD_FOR": 10, }) - @httpretty.activate + @responses.activate def test_submit_photos_for_reverification(self): - httpretty.register_uri( - httpretty.POST, settings.VERIFY_STUDENT["SOFTWARE_SECURE"]["API_URL"], - status=200, body={}, + responses.add( + responses.POST, settings.VERIFY_STUDENT["SOFTWARE_SECURE"]["API_URL"], + status=200, json={}, content_type='application/json' ) @@ -1453,7 +1455,7 @@ def _assert_user_name(self, full_name, equality=True): def _get_post_data(self): """Retrieve POST data from the last request. """ - last_request = httpretty.last_request() + last_request = responses.calls[-1].request return json.loads(last_request.body) diff --git a/openedx/core/djangoapps/api_admin/tests/test_views.py b/openedx/core/djangoapps/api_admin/tests/test_views.py index d30f12e17838..77af4da54905 100644 --- a/openedx/core/djangoapps/api_admin/tests/test_views.py +++ b/openedx/core/djangoapps/api_admin/tests/test_views.py @@ -2,9 +2,10 @@ import json +from urllib.parse import urlparse import ddt -import httpretty +import responses from django.conf import settings from django.test import TestCase from django.test.utils import override_settings @@ -231,15 +232,14 @@ def setUp(self): self.user = UserFactory(password=password, is_staff=True) self.client.login(username=self.user.username, password=password) - def mock_catalog_endpoint(self, data, catalog_id=None, method=httpretty.GET, status_code=200): + def mock_catalog_endpoint(self, data, catalog_id=None, method=responses.GET, status_code=200): """ Mock the Course Catalog API's catalog endpoint. """ - assert httpretty.is_enabled(), 'httpretty must be enabled to mock Catalog API calls.' url = '{root}/catalogs/'.format(root=settings.COURSE_CATALOG_API_URL.rstrip('/')) if catalog_id: url += f'{catalog_id}/' - httpretty.register_uri( + responses.add( method, url, body=json.dumps(data), @@ -261,7 +261,7 @@ def test_get(self): response = self.client.get(self.url) assert response.status_code == 200 - @httpretty.activate + @responses.activate def test_post(self): catalog_user = UserFactory() self.mock_catalog_endpoint({'results': []}) @@ -283,21 +283,21 @@ def setUp(self): self.catalog_user = UserFactory() self.url = reverse('api_admin:catalog-list', kwargs={'username': self.catalog_user.username}) - @httpretty.activate + @responses.activate def test_get(self): catalog = CatalogFactory(viewers=[self.catalog_user.username]) self.mock_catalog_endpoint({'results': [catalog.attributes]}) response = self.client.get(self.url) self.assertContains(response, catalog.name) - @httpretty.activate + @responses.activate def test_get_no_catalogs(self): """Verify that the view works when no catalogs are set up.""" self.mock_catalog_endpoint({}, status_code=404) response = self.client.get(self.url) assert response.status_code == 200 - @httpretty.activate + @responses.activate def test_post(self): catalog_data = { 'name': 'test-catalog', @@ -305,13 +305,13 @@ def test_post(self): 'viewers': [self.catalog_user.username] } catalog_id = 123 - self.mock_catalog_endpoint(dict(catalog_data, id=catalog_id), method=httpretty.POST) + self.mock_catalog_endpoint(dict(catalog_data, id=catalog_id), method=responses.POST) response = self.client.post(self.url, catalog_data) - assert httpretty.last_request().method == 'POST' + assert responses.calls[-1].request.method == 'POST' self.mock_catalog_endpoint(CatalogFactory().attributes, catalog_id=catalog_id) self.assertRedirects(response, reverse('api_admin:catalog-edit', kwargs={'catalog_id': catalog_id})) - @httpretty.activate + @responses.activate def test_post_invalid(self): catalog = CatalogFactory(viewers=[self.catalog_user.username]) self.mock_catalog_endpoint({'results': [catalog.attributes]}) @@ -322,7 +322,7 @@ def test_post_invalid(self): }) assert response.status_code == 400 # Assert that no POST was made to the catalog API - assert len([r for r in httpretty.httpretty.latest_requests if r.method == 'POST']) == 0 + assert len([r for r in responses.calls if r.request.method == 'POST']) == 0 @skip_unless_lms @@ -336,43 +336,43 @@ def setUp(self): self.catalog = CatalogFactory(viewers=[self.catalog_user.username]) self.url = reverse('api_admin:catalog-edit', kwargs={'catalog_id': self.catalog.id}) - @httpretty.activate + @responses.activate def test_get(self): self.mock_catalog_endpoint(self.catalog.attributes, catalog_id=self.catalog.id) response = self.client.get(self.url) self.assertContains(response, self.catalog.name) - @httpretty.activate + @responses.activate def test_delete(self): self.mock_catalog_endpoint( self.catalog.attributes, - method=httpretty.DELETE, + method=responses.DELETE, catalog_id=self.catalog.id ) response = self.client.post(self.url, {'delete-catalog': 'on'}) self.assertRedirects(response, reverse('api_admin:catalog-search')) - assert httpretty.last_request().method == 'DELETE' # pylint: disable=no-member - assert httpretty.last_request().path == \ - f'/api/v1/catalogs/{self.catalog.id}/' # pylint: disable=no-member - assert len(httpretty.httpretty.latest_requests) == 1 + assert responses.calls[-1].request.method == 'DELETE' # pylint: disable=no-member + assert urlparse(responses.calls[-1].request.url).path == \ + f'/api/v1/catalogs/{self.catalog.id}/' + assert len(responses.calls) == 1 - @httpretty.activate + @responses.activate def test_edit(self): # Mock both PATCH and GET endpoints before making the POST request - self.mock_catalog_endpoint(self.catalog.attributes, method=httpretty.PATCH, catalog_id=self.catalog.id) + self.mock_catalog_endpoint(self.catalog.attributes, method=responses.PATCH, catalog_id=self.catalog.id) new_attributes = dict(self.catalog.attributes, **{'delete-catalog': 'off', 'name': 'changed'}) self.mock_catalog_endpoint(new_attributes, catalog_id=self.catalog.id) response = self.client.post(self.url, new_attributes) self.assertRedirects(response, reverse('api_admin:catalog-edit', kwargs={'catalog_id': self.catalog.id})) - @httpretty.activate + @responses.activate def test_edit_invalid(self): self.mock_catalog_endpoint(self.catalog.attributes, catalog_id=self.catalog.id) new_attributes = dict(self.catalog.attributes, **{'delete-catalog': 'off', 'name': ''}) response = self.client.post(self.url, new_attributes) assert response.status_code == 400 # Assert that no PATCH was made to the Catalog API - assert len([r for r in httpretty.httpretty.latest_requests if r.method == 'PATCH']) == 0 + assert len([r for r in responses.calls if r.request.method == 'PATCH']) == 0 @skip_unless_lms @@ -384,11 +384,11 @@ def setUp(self): super().setUp() self.url = reverse('api_admin:catalog-preview') - @httpretty.activate + @responses.activate def test_get(self): data = {'count': 1, 'results': ['test data'], 'next': None, 'prev': None} - httpretty.register_uri( - httpretty.GET, + responses.add( + responses.GET, '{root}/courses/'.format(root=settings.COURSE_CATALOG_API_URL.rstrip('/')), body=json.dumps(data), content_type='application/json' diff --git a/openedx/core/djangoapps/auth_exchange/tests/test_forms.py b/openedx/core/djangoapps/auth_exchange/tests/test_forms.py index 5682b6f8bb57..9062e0cd9300 100644 --- a/openedx/core/djangoapps/auth_exchange/tests/test_forms.py +++ b/openedx/core/djangoapps/auth_exchange/tests/test_forms.py @@ -6,7 +6,6 @@ import unittest -import httpretty import social_django.utils as social_utils from django.contrib.sessions.middleware import SessionMiddleware from django.test import TestCase @@ -54,7 +53,6 @@ def _assert_success(self, data, expected_scopes, expected_logged_in_user=None): # This is necessary because cms does not implement third party auth @unittest.skipUnless(TPA_FEATURE_ENABLED, TPA_FEATURES_KEY + " not enabled") -@httpretty.activate class DOTAccessTokenExchangeFormTestFacebook( DOTAdapterMixin, AccessTokenExchangeFormTest, @@ -70,7 +68,6 @@ class DOTAccessTokenExchangeFormTestFacebook( # This is necessary because cms does not implement third party auth @unittest.skipUnless(TPA_FEATURE_ENABLED, TPA_FEATURES_KEY + " not enabled") -@httpretty.activate class DOTAccessTokenExchangeFormTestGoogle( DOTAdapterMixin, AccessTokenExchangeFormTest, diff --git a/openedx/core/djangoapps/auth_exchange/tests/test_views.py b/openedx/core/djangoapps/auth_exchange/tests/test_views.py index 2c34b17681c8..067f29077fc1 100644 --- a/openedx/core/djangoapps/auth_exchange/tests/test_views.py +++ b/openedx/core/djangoapps/auth_exchange/tests/test_views.py @@ -9,7 +9,6 @@ from datetime import datetime, timedelta import ddt -import httpretty from django.test import TestCase from django.test.utils import override_settings from django.urls import reverse @@ -115,7 +114,6 @@ def test_disabled_user(self): @unittest.skipUnless(TPA_FEATURE_ENABLED, TPA_FEATURES_KEY + " not enabled") -@httpretty.activate class DOTAccessTokenExchangeViewTestFacebook( DOTAdapterMixin, AccessTokenExchangeViewTest, @@ -130,7 +128,6 @@ class DOTAccessTokenExchangeViewTestFacebook( # This is necessary because cms does not implement third party auth @unittest.skipUnless(TPA_FEATURE_ENABLED, TPA_FEATURES_KEY + " not enabled") -@httpretty.activate class DOTAccessTokenExchangeViewTestGoogle( DOTAdapterMixin, AccessTokenExchangeViewTest, diff --git a/openedx/core/djangoapps/catalog/management/commands/tests/test_cache_programs.py b/openedx/core/djangoapps/catalog/management/commands/tests/test_cache_programs.py index b981b3d4f4ac..7038df5e1c3d 100644 --- a/openedx/core/djangoapps/catalog/management/commands/tests/test_cache_programs.py +++ b/openedx/core/djangoapps/catalog/management/commands/tests/test_cache_programs.py @@ -3,11 +3,13 @@ """ import json +from urllib.parse import parse_qs, urlparse -import httpretty import pytest +import responses from django.core.cache import cache from django.core.management import call_command +from responses import matchers from common.djangoapps.student.tests.factories import UserFactory from openedx.core.djangoapps.catalog.cache import ( @@ -28,7 +30,6 @@ @skip_unless_lms -@httpretty.activate class TestCachePrograms(CatalogIntegrationMixin, CacheIsolationTestCase, SiteMixin): """ Defines tests for the ``cache_programs`` management command. @@ -37,8 +38,10 @@ class TestCachePrograms(CatalogIntegrationMixin, CacheIsolationTestCase, SiteMix def setUp(self): super().setUp() + responses.start() + self.addCleanup(responses.stop) + self.addCleanup(responses.reset) - httpretty.httpretty.reset() self.catalog_integration = self.create_catalog_integration() self.site_domain = 'testsite.com' @@ -97,47 +100,45 @@ def setUp(self): def mock_list(self, site=""): """ Mock the data returned by the program listing API endpoint. """ - # pylint: disable=unused-argument - def list_callback(request, uri, headers): + def list_callback(request): """ The mock listing callback. """ expected = { 'exclude_utm': ['1'], 'status': ['active', 'retired'], 'uuids_only': ['1'] } - assert request.querystring == expected + assert parse_qs(urlparse(request.url).query) == expected uuids = self.uuids[self.site_domain2] if site else self.uuids[self.site_domain] - return (200, headers, json.dumps(uuids)) + return (200, {}, json.dumps(uuids)) - httpretty.register_uri( - httpretty.GET, + responses.add_callback( + responses.GET, self.list_url, - body=list_callback, - content_type='application/json' + callback=list_callback, + content_type='application/json', ) def mock_detail(self, uuid, program): """ Mock the data returned by the program detail API endpoint. """ - # pylint: disable=unused-argument - def detail_callback(request, uri, headers): + def detail_callback(request): """ The mock detail callback. """ expected = { 'exclude_utm': ['1'], } - assert request.querystring == expected + assert parse_qs(urlparse(request.url).query) == expected - return (200, headers, json.dumps(program)) + return (200, {}, json.dumps(program)) - httpretty.register_uri( - httpretty.GET, + responses.add_callback( + responses.GET, self.detail_tpl.format(uuid=uuid), - body=detail_callback, - content_type='application/json' + callback=detail_callback, + content_type='application/json', ) def mock_pathways(self, pathways, page_number=1, final=True): """ Mock the data for discovery's credit pathways endpoint. """ - def pathways_callback(request, uri, headers): # pylint: disable=unused-argument + def pathways_callback(request): """ Mocks the pathways response. """ expected = { @@ -145,7 +146,7 @@ def pathways_callback(request, uri, headers): # pylint: disable=unused-argument 'page': [str(page_number)], 'status': ['published'] } - assert request.querystring == expected + assert parse_qs(urlparse(request.url).query) == expected body = { 'count': len(pathways), @@ -154,14 +155,15 @@ def pathways_callback(request, uri, headers): # pylint: disable=unused-argument 'results': pathways } - return (200, headers, json.dumps(body)) + return (200, {}, json.dumps(body)) - httpretty.register_uri( - httpretty.GET, - self.pathway_url + f'?status=published&exclude_utm=1&page={page_number}', - body=pathways_callback, + # Each page is a separate mock on the same path, so match on the query parameters. + responses.add_callback( + responses.GET, + self.pathway_url, + callback=pathways_callback, content_type='application/json', - match_querystring=True, + match=[matchers.query_param_matcher({'status': 'published', 'exclude_utm': '1', 'page': str(page_number)})], ) def test_handle_domain(self): diff --git a/openedx/core/djangoapps/credit/tests/test_api.py b/openedx/core/djangoapps/credit/tests/test_api.py index 666b744499e6..9563b993a020 100644 --- a/openedx/core/djangoapps/credit/tests/test_api.py +++ b/openedx/core/djangoapps/credit/tests/test_api.py @@ -9,8 +9,8 @@ from zoneinfo import ZoneInfo import ddt -import httpretty import pytest +import responses from django.contrib.auth.models import User # pylint: disable=imported-auth-user from django.core import mail from django.db import connection @@ -199,9 +199,9 @@ def enroll(self, user, course_id, mode): def _mock_ecommerce_courses_api(self, course_key, body, status=200): """ Mock GET requests to the ecommerce course API endpoint. """ - httpretty.reset() - httpretty.register_uri( - httpretty.GET, f'{TEST_API_URL}/courses/{str(course_key)}/?include_products=1', + responses.reset() + responses.add( + responses.GET, f'{TEST_API_URL}/courses/{str(course_key)}/?include_products=1', status=status, body=json.dumps(body), content_type='application/json', ) @@ -619,7 +619,7 @@ def test_remove_credit_requirement_status_req_not_configured(self): req_status = api.get_credit_requirement_status(self.course_key, "bob", namespace="grade", name="grade") assert len(req_status) == 0 - @httpretty.activate + @responses.activate @override_settings( ECOMMERCE_API_URL=TEST_API_URL, ECOMMERCE_SERVICE_WORKER_USERNAME=TEST_ECOMMERCE_WORKER @@ -1218,7 +1218,7 @@ def setUp(self): ) assert not hasattr(self.worker_user, 'profile') - @httpretty.activate + @responses.activate def test_get_credit_provider_display_names_method(self): """Verify that parsed providers list is returns after getting course production information.""" self._mock_ecommerce_courses_api(self.course_key, self.COURSE_API_RESPONSE) @@ -1235,7 +1235,7 @@ def test_get_credit_provider_display_names_method_with_exception(self, mock_get_ assert mock_get_client.called assert response is None - @httpretty.activate + @responses.activate def test_get_credit_provider_display_names_caching(self): """Verify that providers list is cached.""" assert self.credit_config.is_cache_enabled @@ -1250,9 +1250,9 @@ def test_get_credit_provider_display_names_caching(self): self.assertListEqual(self.PROVIDERS_LIST, response_providers) # noqa: PT009 # Verify only one request was made. - assert len(httpretty.httpretty.latest_requests) == 1 + assert len(responses.calls) == 1 - @httpretty.activate + @responses.activate def test_get_credit_provider_display_names_without_caching(self): """Verify that providers list is not cached.""" self.credit_config.cache_ttl = 0 @@ -1267,9 +1267,9 @@ def test_get_credit_provider_display_names_without_caching(self): response_providers = get_credit_provider_attribute_values(self.course_key, 'display_name') self.assertListEqual(self.PROVIDERS_LIST, response_providers) # noqa: PT009 - assert len(httpretty.httpretty.latest_requests) == 2 + assert len(responses.calls) == 2 - @httpretty.activate + @responses.activate @ddt.data( (None, None), ({'products': []}, []), @@ -1304,7 +1304,7 @@ def test_get_provider_api_with_multiple_data(self, data, expected_data): response_providers = get_credit_provider_attribute_values(self.course_key, 'display_name') assert expected_data == response_providers - @httpretty.activate + @responses.activate def test_get_credit_provider_display_names_without_providers(self): """Verify that if all providers are in-active than method return empty list.""" self._mock_ecommerce_courses_api(self.course_key, self.COURSE_API_RESPONSE) diff --git a/openedx/core/djangoapps/oauth_dispatch/tests/test_views.py b/openedx/core/djangoapps/oauth_dispatch/tests/test_views.py index 457837623e6e..4e1287c5caf8 100644 --- a/openedx/core/djangoapps/oauth_dispatch/tests/test_views.py +++ b/openedx/core/djangoapps/oauth_dispatch/tests/test_views.py @@ -7,7 +7,6 @@ from unittest.mock import call, patch import ddt -import httpretty from django.conf import settings from django.test import RequestFactory, TestCase from django.urls import reverse @@ -405,7 +404,6 @@ def test_disable_jwt_waffle_switch_disabled_mobile(self, mock_is_request_from_mo @ddt.ddt -@httpretty.activate class TestAccessTokenExchangeView(ThirdPartyOAuthTestMixinGoogle, ThirdPartyOAuthTestMixin, _DispatchingViewTestCase, mixins.AccessTokenMixin): """ diff --git a/openedx/core/djangoapps/programs/tests/test_tasks.py b/openedx/core/djangoapps/programs/tests/test_tasks.py index 881f9e657fa7..d28f0f69ef6a 100644 --- a/openedx/core/djangoapps/programs/tests/test_tasks.py +++ b/openedx/core/djangoapps/programs/tests/test_tasks.py @@ -9,9 +9,9 @@ from zoneinfo import ZoneInfo import ddt -import httpretty import pytest import requests +import responses from celery.exceptions import MaxRetriesExceededError from django.conf import settings from django.test import TestCase, override_settings @@ -83,7 +83,7 @@ class AwardProgramCertificateTestCase(TestCase): Test the award_program_certificate function """ - @httpretty.activate + @responses.activate @mock.patch("openedx.core.djangoapps.programs.tasks.get_credentials_api_base_url") def test_award_program_certificate(self, mock_get_api_base_url): """ @@ -95,8 +95,8 @@ def test_award_program_certificate(self, mock_get_api_base_url): test_client = requests.Session() test_client.auth = SuppliedJwtAuth("test-token") - httpretty.register_uri( - httpretty.POST, + responses.add( + responses.POST, "http://test-server/credentials/", ) @@ -110,7 +110,7 @@ def test_award_program_certificate(self, mock_get_api_base_url): "type": tasks.PROGRAM_CERTIFICATE, }, } - last_request_body = httpretty.last_request().body.decode("utf-8") + last_request_body = responses.calls[-1].request.body assert json.loads(last_request_body) == expected_body @@ -473,7 +473,7 @@ def setUp(self): # pylint: disable=super-method-not-called status="downloadable", ) - @httpretty.activate + @responses.activate @mock.patch("openedx.core.djangoapps.programs.tasks.get_credentials_api_base_url") def test_post_course_certificate(self, mock_get_api_base_url): """ @@ -483,8 +483,8 @@ def test_post_course_certificate(self, mock_get_api_base_url): test_client = requests.Session() test_client.auth = SuppliedJwtAuth("test-token") - httpretty.register_uri( - httpretty.POST, + responses.add( + responses.POST, "http://test-server/credentials/", ) @@ -500,7 +500,7 @@ def test_post_course_certificate(self, mock_get_api_base_url): }, "date_override": None, } - last_request_body = httpretty.last_request().body.decode("utf-8") + last_request_body = responses.calls[-1].request.body assert json.loads(last_request_body) == expected_body @@ -665,7 +665,7 @@ class RevokeProgramCertificateTestCase(TestCase): Test the revoke_program_certificate function """ - @httpretty.activate + @responses.activate @mock.patch("openedx.core.djangoapps.programs.tasks.get_credentials_api_base_url") def test_revoke_program_certificate(self, mock_get_api_base_url): """ @@ -676,8 +676,8 @@ def test_revoke_program_certificate(self, mock_get_api_base_url): test_client = requests.Session() test_client.auth = SuppliedJwtAuth("test-token") - httpretty.register_uri( - httpretty.POST, + responses.add( + responses.POST, "http://test-server/credentials/", ) @@ -691,7 +691,7 @@ def test_revoke_program_certificate(self, mock_get_api_base_url): "type": tasks.PROGRAM_CERTIFICATE, }, } - last_request_body = httpretty.last_request().body.decode("utf-8") + last_request_body = responses.calls[-1].request.body assert json.loads(last_request_body) == expected_body @@ -986,7 +986,7 @@ def setUp(self): "course_id": "testCourse", } - @httpretty.activate + @responses.activate @mock.patch("openedx.core.djangoapps.programs.tasks.get_credentials_api_base_url") def test_post_course_certificate_configuration(self, mock_get_api_base_url): """ @@ -996,8 +996,8 @@ def test_post_course_certificate_configuration(self, mock_get_api_base_url): test_client = requests.Session() test_client.auth = SuppliedJwtAuth("test-token") - httpretty.register_uri( - httpretty.POST, + responses.add( + responses.POST, "http://test-server/course_certificates/", ) @@ -1011,7 +1011,7 @@ def test_post_course_certificate_configuration(self, mock_get_api_base_url): "certificate_available_date": available_date, "is_active": True, } - last_request_body = httpretty.last_request().body.decode("utf-8") + last_request_body = responses.calls[-1].request.body assert json.loads(last_request_body) == expected_body diff --git a/openedx/core/djangoapps/programs/tests/test_utils.py b/openedx/core/djangoapps/programs/tests/test_utils.py index cf81a40820cd..000f2fe27fcb 100644 --- a/openedx/core/djangoapps/programs/tests/test_utils.py +++ b/openedx/core/djangoapps/programs/tests/test_utils.py @@ -6,10 +6,11 @@ from collections import namedtuple from copy import deepcopy from unittest import mock +from urllib.parse import parse_qs, urlparse from zoneinfo import ZoneInfo import ddt -import httpretty +import responses from django.conf import settings from django.test import TestCase from django.test.utils import override_settings @@ -59,6 +60,12 @@ LOGGER_NAME = 'openedx.core.djangoapps.programs.utils' +def _last_request_query(): + """Return the parsed query string of the most recent mocked request.""" + return parse_qs(urlparse(responses.calls[-1].request.url).query) + + + @ddt.ddt @skip_unless_lms @mock.patch("lms.djangoapps.certificates.api.auto_certificate_generation_enabled", mock.Mock(return_value=True)) @@ -1567,7 +1574,7 @@ def test_can_enroll(self, can_enroll, mock_has_perm): assert data['courses'][0]['course_runs'][0]['can_enroll'] == can_enroll - @httpretty.activate + @responses.activate def test_fetching_program_discounted_price(self): """ Authenticated users eligible for one click purchase should see the purchase button @@ -1580,8 +1587,8 @@ def test_fetching_program_discounted_price(self): 'currency': 'USD', 'total_incl_tax': 50.0 } - httpretty.register_uri( - httpretty.GET, + responses.add( + responses.GET, self.ECOMMERCE_CALCULATE_DISCOUNT_ENDPOINT, body=json.dumps(mock_discount_data), content_type='application/json' @@ -1590,11 +1597,11 @@ def test_fetching_program_discounted_price(self): data = ProgramMarketingDataExtender(self.program, self.user).extend() self._update_discount_data(mock_discount_data) - assert httpretty.last_request().querystring.get('username')[0] == self.user.username # pylint: disable=no-member, line-too-long + assert _last_request_query().get('username')[0] == self.user.username assert data['skus'] == [course['course_runs'][0]['seats'][0]['sku'] for course in self.program['courses']] assert data['discount_data'] == mock_discount_data - @httpretty.activate + @responses.activate @override_waffle_switch(ALWAYS_CALCULATE_PROGRAM_PRICE_AS_ANONYMOUS_USER, True) def test_fetching_program_price_when_forced_as_anonymous_user(self): """ @@ -1607,16 +1614,16 @@ def test_fetching_program_price_when_forced_as_anonymous_user(self): 'currency': 'USD', 'total_incl_tax': 50.0 } - httpretty.register_uri( - httpretty.GET, + responses.add( + responses.GET, self.ECOMMERCE_CALCULATE_DISCOUNT_ENDPOINT, body=json.dumps(mock_discount_data), content_type='application/json' ) ProgramMarketingDataExtender(self.program, self.user).extend() - assert httpretty.last_request().querystring.get('is_anonymous')[0] == 'True' # pylint: disable=no-member, line-too-long + assert _last_request_query().get('is_anonymous')[0] == 'True' - @httpretty.activate + @responses.activate def test_fetching_program_discounted_price_as_anonymous_user(self): """ Anonymous users should see the purchase button same way the authenticated users do @@ -1628,8 +1635,8 @@ def test_fetching_program_discounted_price_as_anonymous_user(self): 'currency': 'USD', 'total_incl_tax': 50.0 } - httpretty.register_uri( - httpretty.GET, + responses.add( + responses.GET, self.ECOMMERCE_CALCULATE_DISCOUNT_ENDPOINT, body=json.dumps(mock_discount_data), content_type='application/json' @@ -1639,7 +1646,7 @@ def test_fetching_program_discounted_price_as_anonymous_user(self): data = ProgramMarketingDataExtender(self.program, user).extend() self._update_discount_data(mock_discount_data) - assert httpretty.last_request().querystring.get('is_anonymous', None) is not None # pylint: disable=no-member, line-too-long + assert _last_request_query().get('is_anonymous', None) is not None assert data['skus'] == [course['course_runs'][0]['seats'][0]['sku'] for course in self.program['courses']] assert data['discount_data'] == mock_discount_data @@ -1652,15 +1659,15 @@ def test_fetching_program_discounted_price_no_applicable_seats(self): assert len(data['skus']) == 0 - @httpretty.activate + @responses.activate def test_fetching_program_discounted_price_api_exception_caught(self): """ User should be able to do a one click purchase of a program even if the ecommerce API throws an exception during the calculation of program discounted price. """ self._prepare_program_for_discounted_price_calculation_endpoint() - httpretty.register_uri( - httpretty.GET, + responses.add( + responses.GET, self.ECOMMERCE_CALCULATE_DISCOUNT_ENDPOINT, status=400, content_type='application/json' diff --git a/openedx/core/djangoapps/user_authn/views/tests/test_register.py b/openedx/core/djangoapps/user_authn/views/tests/test_register.py index 009509a6a064..a3606d661c8e 100644 --- a/openedx/core/djangoapps/user_authn/views/tests/test_register.py +++ b/openedx/core/djangoapps/user_authn/views/tests/test_register.py @@ -6,7 +6,6 @@ from zoneinfo import ZoneInfo import ddt -import httpretty from django.conf import settings from django.contrib.auth.models import User # pylint: disable=imported-auth-user from django.core import mail @@ -2392,7 +2391,6 @@ def test_registration_allowed_when_embargo_disabled(self): self.assertEqual(response.status_code, 200) # noqa: PT009 -@httpretty.activate @ddt.ddt class ThirdPartyRegistrationTestMixin( OpenEdxEventsTestMixin, ThirdPartyOAuthTestMixin, CacheIsolationTestCase diff --git a/openedx/core/lib/tests/test_edx_api_utils.py b/openedx/core/lib/tests/test_edx_api_utils.py index 725645408d9f..35cc71e62297 100644 --- a/openedx/core/lib/tests/test_edx_api_utils.py +++ b/openedx/core/lib/tests/test_edx_api_utils.py @@ -5,7 +5,7 @@ from unittest import mock from urllib.parse import urljoin -import httpretty +import responses from django.core.cache import cache from common.djangoapps.student.tests.factories import UserFactory @@ -21,7 +21,6 @@ @skip_unless_lms -@httpretty.activate class TestGetEdxApiData( CatalogIntegrationMixin, CredentialsApiConfigMixin, CacheIsolationTestCase ): @@ -33,29 +32,29 @@ class TestGetEdxApiData( def setUp(self): super().setUp() + responses.start() + self.addCleanup(responses.stop) + self.addCleanup(responses.reset) self.user = UserFactory() self.base_api_url = ( CatalogIntegration.current().get_internal_api_url().strip("/") ) - httpretty.httpretty.reset() cache.clear() - def _mock_catalog_api(self, responses, url=None): - assert ( - httpretty.is_enabled() - ), "httpretty must be enabled to mock Catalog API calls." - + def _mock_catalog_api(self, mock_responses, url=None): + """Register one mocked response per dict of ``responses.add`` kwargs, returned in order.""" url = url if url else urljoin(f"{self.base_api_url}/", "programs/") - httpretty.register_uri(httpretty.GET, url, responses=responses) + for kwargs in mock_responses: + responses.add(responses.GET, url, content_type="application/json", **kwargs) def _assert_num_requests(self, count): """ DRY helper for verifying request counts. """ - assert len(httpretty.httpretty.latest_requests) == count + assert len(responses.calls) == count def test_get_unpaginated_data(self): """ @@ -71,7 +70,7 @@ def test_get_unpaginated_data(self): } self._mock_catalog_api( - [httpretty.Response(body=json.dumps(data), content_type="application/json")] + [{"body": json.dumps(data)}] ) with mock.patch("requests.Session") as mock_init: @@ -99,7 +98,7 @@ def test_get_paginated_data(self): expected_collection = ["some", "test", "data"] url = urljoin(f"{self.base_api_url}/", "/programs/?page={}") - responses = [] + mock_responses = [] for page, record in enumerate(expected_collection, start=1): data = { "next": url.format(page + 1) @@ -109,11 +108,11 @@ def test_get_paginated_data(self): } body = json.dumps(data) - responses.append( - httpretty.Response(body=body, content_type="application/json") + mock_responses.append( + {"body": body} ) - self._mock_catalog_api(responses) + self._mock_catalog_api(mock_responses) actual_collection = get_api_data( catalog_integration, @@ -133,7 +132,7 @@ def test_get_paginated_data_do_not_traverse_pagination(self): api = get_catalog_api_client(self.user) url = urljoin(f"{self.base_api_url}/", "/programs/?page={}") - responses = [ + mock_responses = [ { "next": url.format(2), "results": ["some"], @@ -143,14 +142,12 @@ def test_get_paginated_data_do_not_traverse_pagination(self): "results": ["test"], }, ] - expected_response = responses[0] + expected_response = mock_responses[0] self._mock_catalog_api( [ - httpretty.Response( - body=json.dumps(body), content_type="application/json" - ) - for body in responses + {"body": json.dumps(body)} + for body in mock_responses ] ) @@ -178,9 +175,7 @@ def test_get_specific_resource(self): self._mock_catalog_api( [ - httpretty.Response( - body=json.dumps(expected_resource), content_type="application/json" - ) + {"body": json.dumps(expected_resource)} ], url=url, ) @@ -216,9 +211,7 @@ def test_get_specific_resource_with_falsey_id(self): self._mock_catalog_api( [ - httpretty.Response( - body=json.dumps(expected_resource), content_type="application/json" - ) + {"body": json.dumps(expected_resource)} ], url=url, ) @@ -251,9 +244,7 @@ def test_get_specific_fields_from_cache_response(self): self._mock_catalog_api( [ - httpretty.Response( - body=json.dumps(response), content_type="application/json" - ) + {"body": json.dumps(response)} ], url=url, ) @@ -303,9 +294,7 @@ def test_cache_utilization(self): self._mock_catalog_api( [ - httpretty.Response( - body=json.dumps(data), content_type="application/json" - ) + {"body": json.dumps(data)} ], ) @@ -316,9 +305,7 @@ def test_cache_utilization(self): self._mock_catalog_api( [ - httpretty.Response( - body=json.dumps(expected_resource), content_type="application/json" - ) + {"body": json.dumps(expected_resource)} ], url=url, ) @@ -392,9 +379,7 @@ def test_data_retrieval_failure(self, mock_exception): self._mock_catalog_api( [ - httpretty.Response( - body="clunk", content_type="application/json", status_code=500 - ) + {"body": "clunk", "status": 500} ] ) @@ -437,9 +422,7 @@ def test_data_retrieval_failure_with_id(self, mock_exception): self._mock_catalog_api( [ - httpretty.Response( - body="clunk", content_type="application/json", status_code=500 - ) + {"body": "clunk", "status": 500} ] ) @@ -466,13 +449,9 @@ def test_data_retrieval_raise_on_error(self): self._mock_catalog_api( [ - httpretty.Response( - body="clunk", - content_type="application/json", - status_code=500, - url=url, - ) - ] + {"body": "clunk", "status": 500} + ], + url=url, ) with self.assertRaises(Exception): # noqa: B017, PT027 diff --git a/openedx/features/enterprise_support/tests/mixins/enterprise.py b/openedx/features/enterprise_support/tests/mixins/enterprise.py index e16b95e619ec..5360ef791f07 100644 --- a/openedx/features/enterprise_support/tests/mixins/enterprise.py +++ b/openedx/features/enterprise_support/tests/mixins/enterprise.py @@ -5,7 +5,7 @@ import json -import httpretty +import responses from django.conf import settings from django.core.cache import cache @@ -31,9 +31,9 @@ def mock_get_enterprise_customer(self, uuid, response, status): Helper to mock the HTTP call to the /enterprise-customer/uuid endpoint """ body = json.dumps(response) - httpretty.register_uri( - method=httpretty.GET, - uri=(self.get_enterprise_url('enterprise-customer') + uuid + '/'), + responses.add( + method=responses.GET, + url=(self.get_enterprise_url('enterprise-customer') + uuid + '/'), body=body, content_type='application/json', status=status, @@ -54,9 +54,9 @@ def mock_enterprise_course_enrollment_post_api( # pylint: disable=invalid-name consent_granted: consent_granted, } api_response_json = json.dumps(api_response) - httpretty.register_uri( - method=httpretty.POST, - uri=self.get_enterprise_url('enterprise-course-enrollment'), + responses.add( + method=responses.POST, + url=self.get_enterprise_url('enterprise-course-enrollment'), body=api_response_json, content_type='application/json' ) @@ -65,9 +65,9 @@ def mock_enterprise_course_enrollment_post_api_failure(self): # pylint: disable """ Helper method to register the enterprise course enrollment API endpoint for a failure. """ - httpretty.register_uri( - method=httpretty.POST, - uri=self.get_enterprise_url('enterprise-course-enrollment'), + responses.add( + method=responses.POST, + url=self.get_enterprise_url('enterprise-course-enrollment'), body='{}', content_type='application/json', status=500 @@ -78,7 +78,7 @@ def mock_consent_response( # pylint: disable=missing-function-docstring username, course_id, ec_uuid, - method=httpretty.GET, + method=responses.GET, granted=True, required=False, exists=True, @@ -92,9 +92,9 @@ def mock_consent_response( # pylint: disable=missing-function-docstring 'consent_required': required, 'exists': exists, } - httpretty.register_uri( + responses.add( method=method, - uri=self.consent_url, + url=self.consent_url, content_type='application/json', body=json.dumps(response_body), status=response_code or 200, @@ -105,7 +105,7 @@ def mock_consent_post(self, username, course_id, ec_uuid): username, course_id, ec_uuid, - method=httpretty.POST, + method=responses.POST, granted=True, exists=True, ) @@ -271,9 +271,9 @@ def mock_enterprise_learner_api( } enterprise_learner_api_response_json = json.dumps(enterprise_learner_api_response) - httpretty.register_uri( - method=httpretty.GET, - uri=self.get_enterprise_url('enterprise-learner'), + responses.add( + method=responses.GET, + url=self.get_enterprise_url('enterprise-learner'), body=enterprise_learner_api_response_json, content_type='application/json' ) diff --git a/openedx/features/enterprise_support/tests/test_api.py b/openedx/features/enterprise_support/tests/test_api.py index e1008cce2054..51343c90bdbc 100644 --- a/openedx/features/enterprise_support/tests/test_api.py +++ b/openedx/features/enterprise_support/tests/test_api.py @@ -4,8 +4,8 @@ from unittest import mock import ddt -import httpretty import pytest +import responses from consent.models import DataSharingConsent from django.conf import settings from django.contrib.auth.models import User # pylint: disable=imported-auth-user @@ -130,7 +130,7 @@ def _assert_get_enterprise_customer_with_cache(self, api_client, enterprise_cust cached_enterprise_customer = cache.get(cache_key) assert cached_enterprise_customer == enterprise_customer - @httpretty.activate + @responses.activate @mock.patch('openedx.features.enterprise_support.api.create_jwt_for_user') def test_enterprise_api_client_with_service_user(self, mock_jwt_builder): """ @@ -151,7 +151,7 @@ def test_enterprise_api_client_with_service_user(self, mock_jwt_builder): enterprise_api_data_for_mock_2 = {'name': 'dummy-enterprise-customer-2', 'uuid': 'enterprise-uuid-2'} self._assert_get_enterprise_customer(enterprise_api_client, enterprise_api_data_for_mock_2) - @httpretty.activate + @responses.activate @mock.patch('openedx.features.enterprise_support.api.create_jwt_for_user') def test_enterprise_api_client_with_user(self, mock_jwt_builder): """ @@ -161,7 +161,7 @@ def test_enterprise_api_client_with_user(self, mock_jwt_builder): self._assert_api_client_with_user(EnterpriseApiClient, mock_jwt_builder) @ddt.data(True, False) - @httpretty.activate + @responses.activate @mock.patch('openedx.features.enterprise_support.api.create_jwt_for_user') def test_enterprise_api_client_with_user_post_enrollment(self, should_raise_http_error, mock_jwt_builder): """ @@ -201,7 +201,7 @@ def test_enterprise_customer_from_api_cache_miss(self, mock_client_class, mock_u assert actual_result is None assert not mock_client_class.called - @httpretty.activate + @responses.activate @mock.patch('openedx.features.enterprise_support.api.create_jwt_for_user') def test_enterprise_consent_api_client_with_service_user(self, mock_jwt_builder): """ @@ -210,7 +210,7 @@ def test_enterprise_consent_api_client_with_service_user(self, mock_jwt_builder) """ self._assert_api_service_client(ConsentApiServiceClient, mock_jwt_builder) - @httpretty.activate + @responses.activate @mock.patch('openedx.features.enterprise_support.api.create_jwt_for_user') def test_enterprise_consent_api_client_with_user(self, mock_jwt_builder): """ @@ -349,7 +349,7 @@ def test_get_enterprise_course_enrollments(self, is_enterprise_enabled, mock_ent assert len(enterprise_course_enrollments) == 1 assert enterprise_course_enrollments[0].id == ece.id - @httpretty.activate + @responses.activate @mock.patch('openedx.features.enterprise_support.api.get_enterprise_learner_data_from_db') @mock.patch('openedx.features.enterprise_support.api.EnterpriseCustomer') @mock.patch('openedx.features.enterprise_support.api.get_partial_pipeline') @@ -379,7 +379,7 @@ def mock_get_enterprise_customer(**kwargs): enterprise_customer = enterprise_customer_for_request(dummy_request) assert enterprise_customer == {'real': 'enterprisecustomer'} - httpretty.reset() + responses.reset() # Verify that the method `enterprise_customer_for_request` returns no # enterprise customer if the enterprise customer API throws 404. @@ -388,7 +388,7 @@ def mock_get_enterprise_customer(**kwargs): enterprise_customer = enterprise_customer_for_request(dummy_request) assert enterprise_customer is None - httpretty.reset() + responses.reset() # Verify that the method `enterprise_customer_for_request` returns # expected enterprise customer against the requesting user even if diff --git a/pyproject.toml b/pyproject.toml index 68ab6af4e578..8c343b2804fd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -180,7 +180,6 @@ testing = [ "edx-lint", # pylint extensions for Open edX repositories "factory-boy", # Library for creating test fixtures, used in many tests "freezegun", # Pinning the freezegun version because 0.3.13 is causing failures which have also been reported on the git repo by public.; Allows tests to mock the output of assorted datetime module functions - "httpretty", # Library for mocking HTTP requests, used in many tests "import-linter", # Tool for making assertions about which modules can import which others "mock", # Deprecated alias to standard library `unittest.mock` "ruff", # Fast Python linter and formatter @@ -195,6 +194,7 @@ testing = [ "pytest-randomly", # pytest plugin to randomly order tests "pytest-reportlog", # Per-test timing data including setup/teardown (used for shard rebalancing) "pytest-xdist[psutil]", # Parallel execution of tests on multiple CPU cores or hosts + "responses", # Mocks HTTP responses at the requests library level, used in many tests "singledispatch", # Backport of functools.singledispatch from Python 3.4+, used in tests of XBlock rendering "testfixtures", # Provides a LogCapture utility used by several tests "unidiff", # Required by coverage_pytest_plugin diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt index d0514d0ca271..7aedf41ce7fd 100644 --- a/requirements/edx/development.txt +++ b/requirements/edx/development.txt @@ -729,7 +729,6 @@ html5lib==1.1 # xblocks-contrib httpcore==1.0.9 # via httpx -httpretty==1.1.4 httpx==0.28.1 # via # firebase-admin @@ -1230,6 +1229,7 @@ pyyaml==6.0.3 # edx-i18n-tools # jsondiff # openedx-platform + # responses # sphinx-autoapi # sphinxcontrib-openapi # swagger-spec-validator @@ -1270,6 +1270,7 @@ requests==2.34.2 # pylti1p3 # python-swiftclient # requests-oauthlib + # responses # sailthru-client # slumber # snowflake-connector-python @@ -1281,6 +1282,7 @@ requests-oauthlib==2.0.0 # via # openedx-platform # social-auth-core +responses==0.26.3 rich==15.0.0 # via import-linter roman-numerals==4.1.0 @@ -1525,6 +1527,7 @@ urllib3==2.8.0 # botocore # elasticsearch # requests + # responses # types-requests uv==0.13.1 # via tox-uv diff --git a/uv.lock b/uv.lock index bf4108f1ef28..baa9c749ea79 100644 --- a/uv.lock +++ b/uv.lock @@ -3283,12 +3283,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, ] -[[package]] -name = "httpretty" -version = "1.1.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6e/19/850b7ed736319d0c4088581f4fc34f707ef14461947284026664641e16d4/httpretty-1.1.4.tar.gz", hash = "sha256:20de0e5dd5a18292d36d928cc3d6e52f8b2ac73daec40d41eb62dee154933b68", size = 442389, upload-time = "2021-08-16T19:35:31.4Z" } - [[package]] name = "httpx" version = "0.28.1" @@ -4886,7 +4880,6 @@ development = [ { name = "factory-boy" }, { name = "freezegun" }, { name = "gitpython" }, - { name = "httpretty" }, { name = "import-linter" }, { name = "libsass" }, { name = "mailsnake" }, @@ -4911,6 +4904,7 @@ development = [ { name = "pytest-xdist", extra = ["psutil"] }, { name = "pywatchman" }, { name = "recommender-xblock" }, + { name = "responses" }, { name = "ruff" }, { name = "singledispatch" }, { name = "sphinx" }, @@ -4948,7 +4942,6 @@ django42 = [ { name = "enterprise-integrated-channels" }, { name = "factory-boy" }, { name = "freezegun" }, - { name = "httpretty" }, { name = "import-linter" }, { name = "mailsnake" }, { name = "mock" }, @@ -4969,6 +4962,7 @@ django42 = [ { name = "pytest-reportlog" }, { name = "pytest-xdist", extra = ["psutil"], marker = "extra == 'group-16-openedx-platform-django42'" }, { name = "recommender-xblock" }, + { name = "responses" }, { name = "ruff" }, { name = "singledispatch" }, { name = "staff-graded-xblock" }, @@ -4996,7 +4990,6 @@ django52 = [ { name = "enterprise-integrated-channels" }, { name = "factory-boy" }, { name = "freezegun" }, - { name = "httpretty" }, { name = "import-linter" }, { name = "mailsnake" }, { name = "mock" }, @@ -5017,6 +5010,7 @@ django52 = [ { name = "pytest-reportlog" }, { name = "pytest-xdist", extra = ["psutil"], marker = "extra == 'group-16-openedx-platform-django52'" }, { name = "recommender-xblock" }, + { name = "responses" }, { name = "ruff" }, { name = "singledispatch" }, { name = "staff-graded-xblock" }, @@ -5069,7 +5063,6 @@ testing = [ { name = "enterprise-integrated-channels" }, { name = "factory-boy" }, { name = "freezegun" }, - { name = "httpretty" }, { name = "import-linter" }, { name = "mailsnake" }, { name = "mock" }, @@ -5090,6 +5083,7 @@ testing = [ { name = "pytest-reportlog" }, { name = "pytest-xdist", extra = ["psutil"] }, { name = "recommender-xblock" }, + { name = "responses" }, { name = "ruff" }, { name = "singledispatch" }, { name = "staff-graded-xblock" }, @@ -5292,7 +5286,6 @@ development = [ { name = "factory-boy" }, { name = "freezegun" }, { name = "gitpython" }, - { name = "httpretty" }, { name = "import-linter" }, { name = "libsass" }, { name = "mailsnake" }, @@ -5317,6 +5310,7 @@ development = [ { name = "pytest-xdist", extras = ["psutil"] }, { name = "pywatchman" }, { name = "recommender-xblock" }, + { name = "responses" }, { name = "ruff" }, { name = "singledispatch" }, { name = "sphinx" }, @@ -5354,7 +5348,6 @@ django42 = [ { name = "enterprise-integrated-channels" }, { name = "factory-boy" }, { name = "freezegun" }, - { name = "httpretty" }, { name = "import-linter" }, { name = "mailsnake" }, { name = "mock" }, @@ -5375,6 +5368,7 @@ django42 = [ { name = "pytest-reportlog" }, { name = "pytest-xdist", extras = ["psutil"] }, { name = "recommender-xblock" }, + { name = "responses" }, { name = "ruff" }, { name = "singledispatch" }, { name = "staff-graded-xblock" }, @@ -5402,7 +5396,6 @@ django52 = [ { name = "enterprise-integrated-channels" }, { name = "factory-boy" }, { name = "freezegun" }, - { name = "httpretty" }, { name = "import-linter" }, { name = "mailsnake" }, { name = "mock" }, @@ -5423,6 +5416,7 @@ django52 = [ { name = "pytest-reportlog" }, { name = "pytest-xdist", extras = ["psutil"] }, { name = "recommender-xblock" }, + { name = "responses" }, { name = "ruff" }, { name = "singledispatch" }, { name = "staff-graded-xblock" }, @@ -5475,7 +5469,6 @@ testing = [ { name = "enterprise-integrated-channels" }, { name = "factory-boy" }, { name = "freezegun" }, - { name = "httpretty" }, { name = "import-linter" }, { name = "mailsnake" }, { name = "mock" }, @@ -5496,6 +5489,7 @@ testing = [ { name = "pytest-reportlog" }, { name = "pytest-xdist", extras = ["psutil"] }, { name = "recommender-xblock" }, + { name = "responses" }, { name = "ruff" }, { name = "singledispatch" }, { name = "staff-graded-xblock" }, @@ -7011,6 +7005,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179, upload-time = "2024-03-22T20:32:28.055Z" }, ] +[[package]] +name = "responses" +version = "0.26.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, + { name = "requests" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/47/f216a33221db8eff328987661cf18371afee89c62a62b434b963d6b509c9/responses-0.26.3.tar.gz", hash = "sha256:b0c11ca8131b8b227b8d5108e6ed39772222bd5aab030ed430e8f99057c4c409", size = 86335, upload-time = "2026-08-26T19:17:24.373Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/86/ca7958de70cb0752350575e98229368a3a2f746a2942034b3364e17312bb/responses-0.26.3-py3-none-any.whl", hash = "sha256:74474f799334ac4f37d93b6437ecc3bb1bb5c77a8d31780a338643be2dce0af8", size = 36289, upload-time = "2026-08-26T19:17:23.176Z" }, +] + [[package]] name = "rich" version = "15.0.0"