From 4973169bcf3cc4368336c256461aa9d1d0ebfa57 Mon Sep 17 00:00:00 2001 From: synkd Date: Wed, 1 Jun 2022 16:57:23 -0400 Subject: [PATCH] Modify Manifester class to work with MockStub Add a class to test_manifester.py to enable requests to be sent to a MockStub version of the requests package instead of sending live requests to the API. Modify the Manifester class to use the MockStub-based requests when running unit tests. Add a helper function to generate mock HTTP response codes. --- manifester/helpers.py | 47 ++++++++++++++++++++++++++++++++++++ manifester/manifester.py | 27 +++++++++++---------- tests/test_manifester.py | 52 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 114 insertions(+), 12 deletions(-) create mode 100644 tests/test_manifester.py diff --git a/manifester/helpers.py b/manifester/helpers.py index 5849297..ad59b30 100644 --- a/manifester/helpers.py +++ b/manifester/helpers.py @@ -1,5 +1,7 @@ +import random import time +from collections import UserDict from logzero import logger @@ -12,6 +14,7 @@ def simple_retry(cmd, cmd_args=None, cmd_kwargs=None, max_timeout=240, _cur_time # with caution as some data (notably the offline token) should be treated as a secret. logger.debug(f"Sending request to endpoint {cmd_args}") response = cmd(*cmd_args, **cmd_kwargs) + breakpoint() logger.debug(f"Response status code is {response.status_code}") if response.status_code in [429, 500, 504]: new_wait = _cur_timeout * 2 @@ -21,3 +24,47 @@ def simple_retry(cmd, cmd_args=None, cmd_kwargs=None, max_timeout=240, _cur_time time.sleep(_cur_timeout) response = simple_retry(cmd, cmd_args, cmd_kwargs, max_timeout, new_wait) return response + +def fake_http_response_code(good_codes=None, bad_codes=None, fail_rate=20): + # randomish = random.random() + # print(randomish, fail_rate/100) + if random.random() > (fail_rate / 100): + return random.choice(good_codes) + else: + return random.choice(bad_codes) + + +class MockStub(UserDict): + """Test helper class. Allows for both arbitrary mocking and stubbing""" + + def __init__(self, in_dict=None): + """Initialize the class and all nested dictionaries""" + if in_dict is None: + in_dict = {} + for key, value in in_dict.items(): + if isinstance(value, dict): + setattr(self, key, MockStub(value)) + elif type(value) in (list, tuple): + setattr( + self, + key, + [MockStub(x) if isinstance(x, dict) else x for x in value], + ) + else: + setattr(self, key, value) + super().__init__(in_dict) + + def __getattr__(self, name): + return self + + def __getitem__(self, key): + if isinstance(key, str): + item = getattr(self, key, self) + try: + item = super().__getitem__(key) + except KeyError: + item = self + return item + + def __call__(self, *args, **kwargs): + return self \ No newline at end of file diff --git a/manifester/manifester.py b/manifester/manifester.py index 3d00026..eb7ac31 100644 --- a/manifester/manifester.py +++ b/manifester/manifester.py @@ -2,7 +2,6 @@ import string from pathlib import Path -import requests from logzero import logger from manifester.helpers import simple_retry @@ -13,7 +12,11 @@ class Manifester: def __init__(self, manifest_category, allocation_name=None, **kwargs): self.allocation_name = allocation_name or "".join(random.sample(string.ascii_letters, 10)) - # self.manifest_name = kwargs.get("manifest_name") + if kwargs.get("requester") is not None: + self.requester = kwargs["requester"] + else: + import requests + self.requester = requests self.offline_token = kwargs.get("offline_token", settings.offline_token) manifest_data = settings.manifest_category.get(manifest_category) self.subscription_data = manifest_data.subscription_data @@ -33,7 +36,7 @@ def access_token(self): token_request_data = {"data": self.token_request_data} logger.debug("Generating access token") token_data = simple_retry( - requests.post, + self.requester.post, cmd_args=["https://sso.redhat.com/auth/realms/redhat-external/protocol/openid-connect/token"], cmd_kwargs=token_request_data, ).json() @@ -50,7 +53,7 @@ def create_subscription_allocation(self): }, } self.allocation = simple_retry( - requests.post, + self.requester.post, cmd_args=["https://api.access.redhat.com/management/v1/allocations"], cmd_kwargs=allocation_data, ).json() @@ -69,7 +72,7 @@ def subscription_pools(self): "params": {"offset": _offset}, } self._subscription_pools = simple_retry( - requests.get, + self.requester.get, cmd_args=[f"https://api.access.redhat.com/management/v1/allocations/{self.allocation_uuid}/pools"], cmd_kwargs=data, ).json() @@ -85,7 +88,7 @@ def subscription_pools(self): "params": {"offset": _offset}, } offset_pools = simple_retry( - requests.get, + self.requester.get, cmd_args=[f"https://api.access.redhat.com/management/v1/allocations/{self.allocation_uuid}/pools"], cmd_kwargs=data, ).json() @@ -101,7 +104,7 @@ def add_entitlements_to_allocation(self, pool_id, entitlement_quantity): "params": {"pool": f"{pool_id}", "quantity": f"{entitlement_quantity}"}, } add_entitlements = simple_retry( - requests.post, + self.requester.post, cmd_args=[f"https://api.access.redhat.com/management/v1/allocations/{self.allocation_uuid}/entitlements"], cmd_kwargs=data, ) @@ -114,7 +117,7 @@ def verify_allocation_entitlements(self, entitlement_quantity, subscription_name "params": {"include": "entitlements"}, } self.entitlement_data = simple_retry( - requests.get, + self.requester.get, cmd_args=[f"https://api.access.redhat.com/management/v1/allocations/{self.allocation_uuid}"], cmd_kwargs=data, ).json() @@ -193,7 +196,7 @@ def trigger_manifest_export(self): local_file.parent.mkdir(parents=True, exist_ok=True) logger.info(f"Triggering manifest export job for subscription allocation {self.allocation_name}") trigger_export_job = simple_retry( - requests.get, + self.requester.get, cmd_args=[ f"https://api.access.redhat.com/management/v1/allocations/{self.allocation_uuid}/export" ], @@ -201,14 +204,14 @@ def trigger_manifest_export(self): ).json() export_job_id = trigger_export_job["body"]["exportJobID"] export_job = simple_retry( - requests.get, + self.requester.get, cmd_args=[f"https://api.access.redhat.com/management/v1/allocations/{self.allocation_uuid}/exportJob/{export_job_id}"], cmd_kwargs=headers, ) request_count = 1 while export_job.status_code != 200: export_job = simple_retry( - requests.get, + self.requester.get, cmd_args=[f"https://api.access.redhat.com/management/v1/allocations/{self.allocation_uuid}/exportJob/{export_job_id}"], cmd_kwargs=headers, ) @@ -227,7 +230,7 @@ def trigger_manifest_export(self): export_job = export_job.json() export_href = export_job["body"]["href"] manifest = simple_retry( - requests.get, + self.requester.get, cmd_args=[f"{export_href}"], cmd_kwargs=headers, ) diff --git a/tests/test_manifester.py b/tests/test_manifester.py new file mode 100644 index 0000000..5a69bae --- /dev/null +++ b/tests/test_manifester.py @@ -0,0 +1,52 @@ +from unittest.mock import Mock + +from requests import request +from manifester import Manifester +from manifester.settings import settings +from manifester.helpers import MockStub, fake_http_response_code +import pytest +import random + +def test_empty_init(manifest_category="golden_ticket"): + manifester_inst = Manifester(manifest_category=manifest_category) + assert isinstance(manifester_inst, Manifester) + +class RhsmApiStub(MockStub): + def __init__(self, in_dict=None, **kwargs): + self._good_codes = kwargs.get("good_codes", [200]) + self._bad_codes = kwargs.get("bad_codes", [429, 500, 504]) + self._fail_rate = kwargs.get("fail_rate", 10) + super().__init__(in_dict) + + @property + def status_code(self): + return fake_http_response_code(self._good_codes, self._bad_codes, self._fail_rate) + + def post(*args, **kwargs): + if args[0].endswith("openid-connect/token"): + return MockStub(in_dict={"access_token": "this is a simulated access token"}, status_code=200) + if args[0].endswith("allocations"): + return MockStub(in_dict={"uuid": "1234567890"}) + if args[0].endswith("entitlements"): + return MockStub(status_code=200) + + def get(*args, **kwargs): + if args[0].endswith("pools"): + # question: how to fake > 50 pools to test use of offset parameter? + return MockStub(in_dict={"pool": "this is a simulated list of dictionaries of subscription pool data"}) + if "allocations" in args[0] and not ("export" in args[0] or "pools" in args[0]): + return MockStub(in_dict={"allocation_data": "this allocation data also includes entitlement data"}) + if args[0].endswith("export"): + return MockStub(in_dict={"export_job": "Manifest export job triggered successfully"}) + if "exportJob" in args[0]: + responses = [202, 200] + return MockStub(status_code=random.choice(responses)) + if "export" in args[0] and not args[0].endswith("export"): + return MockStub(in_dict={"content": "this is a simulated manifest"}) + + +def test_create_allocation(): + manifester = Manifester(manifest_category="golden_ticket", requester=RhsmApiStub(in_dict=None, status_code=200)) + allocation_uuid = manifester.create_subscription_allocation() + breakpoint() + assert allocation_uuid == "1234567890"