From f01455411aa6f2cba28db0ec7e946381be1ab0a5 Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Thu, 2 Oct 2025 01:54:31 +0200 Subject: [PATCH] move tests to api mocking to remove jitter in ci (#10447) closes #10446 --- .../plugin/base/integration/test_mixins.py | 91 +++++++++++-------- .../plugin/samples/integration/api_caller.py | 2 +- .../samples/integration/test_api_caller.py | 18 ++-- src/backend/requirements-dev.in | 1 + src/backend/requirements-dev.txt | 29 ++++++ 5 files changed, 90 insertions(+), 51 deletions(-) diff --git a/src/backend/InvenTree/plugin/base/integration/test_mixins.py b/src/backend/InvenTree/plugin/base/integration/test_mixins.py index e0251f4805..a5495fb04f 100644 --- a/src/backend/InvenTree/plugin/base/integration/test_mixins.py +++ b/src/backend/InvenTree/plugin/base/integration/test_mixins.py @@ -1,11 +1,11 @@ """Unit tests for base mixins for plugins.""" -import os - from django.conf import settings from django.test import TestCase from django.urls import include, path, re_path +import requests_mock + from InvenTree.unit_test import InvenTreeTestCase from plugin import InvenTreePlugin from plugin.helpers import MixinNotImplementedError @@ -205,12 +205,12 @@ class APICallMixinTest(BaseMixinDefinition, TestCase): 'API_TOKEN': { 'name': 'API Token', 'protected': True, - 'default': 'reqres-free-v1', + 'default': 'sample-free-v1', }, 'API_URL': { 'name': 'External URL', 'description': 'Where is your API located?', - 'default': 'https://api.github.com', + 'default': 'https://api.example.com', }, } @@ -221,7 +221,7 @@ class APICallMixinTest(BaseMixinDefinition, TestCase): @property def api_url(self): """Override API URL for this test.""" - return 'https://api.github.com' + return 'https://api.example.com' def get_external_url(self, simple: bool = True): """Returns data from the sample endpoint.""" @@ -229,13 +229,6 @@ class APICallMixinTest(BaseMixinDefinition, TestCase): self.mixin = MixinCls() - # If running in github workflow, make use of GITHUB_TOKEN - if settings.TESTING: - token = os.getenv('GITHUB_TOKEN', None) - - if token: - self.mixin.set_setting('API_TOKEN', token) - class WrongCLS(APICallMixin, InvenTreePlugin): pass @@ -251,7 +244,7 @@ class APICallMixinTest(BaseMixinDefinition, TestCase): # check init self.assertTrue(self.mixin.has_api_call) # api_url - self.assertEqual('https://api.github.com', self.mixin.api_url) + self.assertEqual('https://api.example.com', self.mixin.api_url) # api_headers headers = self.mixin.api_headers @@ -273,9 +266,34 @@ class APICallMixinTest(BaseMixinDefinition, TestCase): result = self.mixin.api_build_url_args({'a': 'b', 'c': ['d', 'efgh', 1337]}) self.assertEqual(result, '?a=b&c=d,efgh,1337') - def test_api_call(self): + @requests_mock.Mocker() + def test_api_call(self, m: requests_mock.Mocker): """Test that api calls work.""" - import time + # Set up mock responses + m.get( + 'https://api.example.com/orgs/inventree', + json={ + 'login': 'inventree', + 'email': 'inventree', + 'name': 'InvenTree', + 'twitter_username': 'inventree', + }, + status_code=200, + ) + m.post( + 'https://api.example.com/users/', + json={'name': 'morpheus', 'job': 'leader'}, + status_code=201, + headers={ + 'Authorization': 'x-api-key sample-free-v1', + 'Content-Type': 'application/json', + }, + ) + m.get( + 'https://api.example.com/repos/inventree/InvenTree/stargazers?page=2', + json={'sample': True}, + status_code=200, + ) # api_call result = self.mixin.get_external_url() @@ -287,27 +305,17 @@ class APICallMixinTest(BaseMixinDefinition, TestCase): # api_call without json conversion result = self.mixin.get_external_url(False) self.assertTrue(result) - self.assertEqual(result.reason, 'OK') + self.assertTrue(result.ok) # Set API TOKEN - self.mixin.set_setting('API_TOKEN', 'reqres-free-v1') + self.mixin.set_setting('API_TOKEN', 'sample-free-v1') # api_call with post and data - - # Try multiple times, account for the rate limit - result = None - - for _ in range(5): - try: - result = self.mixin.api_call( - 'https://reqres.in/api/users/', - json={'name': 'morpheus', 'job': 'leader'}, - method='POST', - endpoint_is_url=True, - timeout=5000, - ) - break - except Exception: - time.sleep(1) + result = self.mixin.api_call( + 'https://api.example.com/users/', + json={'name': 'morpheus', 'job': 'leader'}, + method='POST', + endpoint_is_url=True, + ) self.assertTrue(result) self.assertNotIn('error', result) @@ -317,16 +325,21 @@ class APICallMixinTest(BaseMixinDefinition, TestCase): # api_call with endpoint with leading slash result = self.mixin.api_call('/orgs/inventree', simple_response=False) self.assertTrue(result) - self.assertEqual(result.reason, 'OK') + self.assertTrue(result.ok) - # api_call with filter + # api_call with filter - this errors out the mocker if not created correctly result = self.mixin.api_call( 'repos/inventree/InvenTree/stargazers', url_args={'page': '2'} ) self.assertTrue(result) - def test_function_errors(self): + @requests_mock.Mocker() + def test_function_errors(self, m: requests_mock.Mocker): """Test function errors.""" + # Set up mock responses + m.get('https://api.example.com/orgs/inventree', status_code=404) + m.post('https://api.example.com/api/users/', status_code=400) + # wrongly defined plugins should not load with self.assertRaises(MixinNotImplementedError): self.mixin_wrong.has_api_call() @@ -338,12 +351,12 @@ class APICallMixinTest(BaseMixinDefinition, TestCase): # Too many data arguments with self.assertRaises(ValueError): self.mixin.api_call( - 'https://reqres.in/api/users/', json={'a': 1}, data={'a': 1} + 'https://api.example.com/api/users/', json={'a': 1}, data={'a': 1} ) - # Sending a request with a wrong data format should result in 40 + # Sending a request with a wrong data format should result in 400 result = self.mixin.api_call( - 'https://reqres.in/api/users/', + 'https://api.example.com/api/users/', data={'name': 'morpheus', 'job': 'leader'}, method='POST', endpoint_is_url=True, diff --git a/src/backend/InvenTree/plugin/samples/integration/api_caller.py b/src/backend/InvenTree/plugin/samples/integration/api_caller.py index 759a02c88b..6e2d5da4be 100644 --- a/src/backend/InvenTree/plugin/samples/integration/api_caller.py +++ b/src/backend/InvenTree/plugin/samples/integration/api_caller.py @@ -18,7 +18,7 @@ class SampleApiCallerPlugin(APICallMixin, SettingsMixin, InvenTreePlugin): 'API_URL': { 'name': 'External URL', 'description': 'Where is your API located?', - 'default': 'reqres.in', + 'default': 'api.example.com', }, } API_URL_SETTING = 'API_URL' diff --git a/src/backend/InvenTree/plugin/samples/integration/test_api_caller.py b/src/backend/InvenTree/plugin/samples/integration/test_api_caller.py index 89132be43f..783ae6370b 100644 --- a/src/backend/InvenTree/plugin/samples/integration/test_api_caller.py +++ b/src/backend/InvenTree/plugin/samples/integration/test_api_caller.py @@ -2,15 +2,19 @@ from django.test import TestCase +import requests_mock + from plugin import registry class SampleApiCallerPluginTests(TestCase): """Tests for SampleApiCallerPluginTests.""" - def test_return(self): + @requests_mock.Mocker() + def test_return(self, m): """Check if the external api call works.""" - import time + # Set up mock responses + m.get('https://api.example.com/api/users/2', json={'data': 'sample'}) # The plugin should be defined self.assertIn('sample-api-caller', registry.plugins) @@ -18,15 +22,7 @@ class SampleApiCallerPluginTests(TestCase): self.assertTrue(plg) # do an api call - # Note: rate limits may apply in CI - result = False - - for _i in range(5): - result = plg.get_external_url() - if result: - break - else: - time.sleep(1) + result = plg.get_external_url() self.assertTrue(result) self.assertIn('data', result) diff --git a/src/backend/requirements-dev.in b/src/backend/requirements-dev.in index d9782f1a19..686068e18d 100644 --- a/src/backend/requirements-dev.in +++ b/src/backend/requirements-dev.in @@ -12,3 +12,4 @@ pdfminer.six # PDF validation ty # type checking django-types # typing django-stubs # typing +requests-mock # Mock requests for unit tests diff --git a/src/backend/requirements-dev.txt b/src/backend/requirements-dev.txt index 69969e1c69..3594d4cbd0 100644 --- a/src/backend/requirements-dev.txt +++ b/src/backend/requirements-dev.txt @@ -11,6 +11,12 @@ build==1.3.0 \ --hash=sha256:698edd0ea270bde950f53aed21f3a0135672206f3911e0176261a31e0e07b397 \ --hash=sha256:7145f0b5061ba90a1500d60bd1b13ca0a8a4cebdd0cc16ed8adf1c0e739f43b4 # via pip-tools +certifi==2025.8.3 \ + --hash=sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407 \ + --hash=sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5 + # via + # -c src/backend/requirements.txt + # requests cffi==1.17.1 \ --hash=sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8 \ --hash=sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2 \ @@ -182,6 +188,7 @@ charset-normalizer==3.4.2 \ # via # -c src/backend/requirements.txt # pdfminer-six + # requests click==8.1.8 \ --hash=sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2 \ --hash=sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a @@ -359,6 +366,12 @@ identify==2.6.12 \ --hash=sha256:ad9672d5a72e0d2ff7c5c8809b62dfa60458626352fb0eb7b55e69bdc45334a2 \ --hash=sha256:d8de45749f1efb108badef65ee8386f0f7bb19a7f26185f74de6367bffbaf0e6 # via pre-commit +idna==3.10 \ + --hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9 \ + --hash=sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3 + # via + # -c src/backend/requirements.txt + # requests importlib-metadata==8.7.0 \ --hash=sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000 \ --hash=sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd @@ -470,6 +483,16 @@ pyyaml==6.0.2 \ # via # -c src/backend/requirements.txt # pre-commit +requests==2.32.5 \ + --hash=sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6 \ + --hash=sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf + # via + # -c src/backend/requirements.txt + # requests-mock +requests-mock==1.12.1 \ + --hash=sha256:b1e37054004cdd5e56c84454cc7df12b25f90f382159087f4b6915aaeef39563 \ + --hash=sha256:e9e12e333b525156e82a3c852f22016b9158220d2f47454de9cae8a77d371401 + # via -r src/backend/requirements-dev.in setuptools==80.9.0 \ --hash=sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922 \ --hash=sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c @@ -559,6 +582,12 @@ typing-extensions==4.14.1 \ # django-stubs # django-stubs-ext # django-test-migrations +urllib3==1.26.20 \ + --hash=sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e \ + --hash=sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32 + # via + # -c src/backend/requirements.txt + # requests virtualenv==20.33.1 \ --hash=sha256:07c19bc66c11acab6a5958b815cbcee30891cd1c2ccf53785a28651a0d8d8a67 \ --hash=sha256:1b44478d9e261b3fb8baa5e74a0ca3bc0e05f21aa36167bf9cbf850e542765b8