diff --git a/.gitignore b/.gitignore index ad02e2d29..86f8e491d 100644 --- a/.gitignore +++ b/.gitignore @@ -58,3 +58,4 @@ venv.bak/ !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json +/.idea diff --git a/stripe/api_resources/abstract/__init__.py b/stripe/api_resources/abstract/__init__.py index 026213449..047694ae1 100644 --- a/stripe/api_resources/abstract/__init__.py +++ b/stripe/api_resources/abstract/__init__.py @@ -26,6 +26,11 @@ from stripe.api_resources.abstract.custom_method import custom_method +from stripe.api_resources.abstract.test_helpers import ( + test_helpers, + APIResourceTestHelpers, +) + from stripe.api_resources.abstract.nested_resource_class_methods import ( nested_resource_class_methods, ) diff --git a/stripe/api_resources/abstract/test_helpers.py b/stripe/api_resources/abstract/test_helpers.py new file mode 100644 index 000000000..abe9625bc --- /dev/null +++ b/stripe/api_resources/abstract/test_helpers.py @@ -0,0 +1,77 @@ +from __future__ import absolute_import, division, print_function + +from stripe import error, util, six +from stripe.six.moves.urllib.parse import quote_plus +from stripe.api_resources.abstract import APIResource + + +class APIResourceTestHelpers: + """ + The base type for the TestHelper nested classes. + Handles request URL generation for test_helper custom methods. + Should be used in combination with the @test_helpers decorator. + + @test_helpers + class Foo(APIResource): + class TestHelpers(APIResourceTestHelpers): + """ + + def __init__(self, resource): + self.resource = resource + + @classmethod + def class_url(cls): + if cls == APIResourceTestHelpers: + raise NotImplementedError( + "APIResourceTestHelpers is an abstract class. You should perform " + "actions on its subclasses (e.g. Charge, Customer)" + ) + # Namespaces are separated in object names with periods (.) and in URLs + # with forward slashes (/), so replace the former with the latter. + base = cls._resource_cls.OBJECT_NAME.replace(".", "/") + return "/v1/test_helpers/%ss" % (base,) + + def instance_url(self): + id = self.resource.get("id") + + if not isinstance(id, six.string_types): + raise error.InvalidRequestError( + "Could not determine which URL to request: %s instance " + "has invalid ID: %r, %s. ID should be of type `str` (or" + " `unicode`)" % (type(self).__name__, id, type(id)), + "id", + ) + + id = util.utf8(id) + base = self.class_url() + extn = quote_plus(id) + return "%s/%s" % (base, extn) + + +def test_helpers(cls): + """ + test_helpers decorator adds a test_helpers property and + wires the parent resource class to the nested TestHelpers class. + + Should only be used on types that inherit from APIResource. + + @test_helpers + class Foo(APIResource): + class TestHelpers(APIResourceTestHelpers): + """ + + def test_helpers_getter(self): + return self.TestHelpers(self) + + if not issubclass(cls, APIResource): + raise ValueError( + "Could not apply @test_helpers decorator to %r." + " The class should a subclass of APIResource." % cls + ) + + cls.TestHelpers._resource_cls = cls + cls.TestHelpers._static_request = cls._static_request + cls.TestHelpers._static_request_stream = cls._static_request_stream + + cls.test_helpers = property(test_helpers_getter) + return cls diff --git a/tests/api_resources/abstract/test_test_helpers_api_resource.py b/tests/api_resources/abstract/test_test_helpers_api_resource.py new file mode 100644 index 000000000..36a02f546 --- /dev/null +++ b/tests/api_resources/abstract/test_test_helpers_api_resource.py @@ -0,0 +1,66 @@ +from __future__ import absolute_import, division, print_function + +import stripe +import pytest +from stripe import util +from stripe.api_resources.abstract import APIResourceTestHelpers + + +class TestTestHelperAPIResource(object): + @stripe.api_resources.abstract.test_helpers + class MyTestHelpersResource(stripe.api_resources.abstract.APIResource): + OBJECT_NAME = "myresource" + + @stripe.api_resources.abstract.custom_method( + "do_stuff", http_verb="post", http_path="do_the_thing" + ) + class TestHelpers(APIResourceTestHelpers): + def __init__(self, resource): + self.resource = resource + + def do_stuff(self, idempotency_key=None, **params): + url = self.instance_url() + "/do_the_thing" + headers = util.populate_headers(idempotency_key) + self.resource.refresh_from( + self.resource.request("post", url, params, headers) + ) + return self.resource + + def test_call_custom_method_class(self, request_mock): + request_mock.stub_request( + "post", + "/v1/test_helpers/myresources/mid/do_the_thing", + {"id": "mid", "thing_done": True}, + rheaders={"request-id": "req_id"}, + ) + + obj = self.MyTestHelpersResource.TestHelpers.do_stuff("mid", foo="bar") + + request_mock.assert_requested( + "post", + "/v1/test_helpers/myresources/mid/do_the_thing", + {"foo": "bar"}, + ) + assert obj.thing_done is True + + def test_call_custom_method_instance_via_property(self, request_mock): + request_mock.stub_request( + "post", + "/v1/test_helpers/myresources/mid/do_the_thing", + {"id": "mid", "thing_done": True}, + rheaders={"request-id": "req_id"}, + ) + + obj = self.MyTestHelpersResource.construct_from({"id": "mid"}, "mykey") + obj.test_helpers.do_stuff(foo="bar") + + request_mock.assert_requested( + "post", + "/v1/test_helpers/myresources/mid/do_the_thing", + {"foo": "bar"}, + ) + assert obj.thing_done is True + + def test_helper_decorator_raises_for_non_resource(self): + with pytest.raises(ValueError): + stripe.api_resources.abstract.test_helpers(str) diff --git a/tox.ini b/tox.ini index 01438682f..7aece58b9 100644 --- a/tox.ini +++ b/tox.ini @@ -39,7 +39,8 @@ deps = pytest-cov >= 2.8.1, < 2.11.0 pytest-mock >= 2.0.0 pytest-xdist >= 1.31.0 -commands = pytest --cov {posargs:-n auto} +# ignore stripe directory as all tests are inside ./tests +commands = pytest --cov {posargs:-n auto} --ignore stripe # compilation flags can be useful when prebuilt wheels cannot be used, e.g. # PyPy 2 needs to compile the `cryptography` module. On macOS this can be done # by passing the following flags: