2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-10-03 15:52:51 +00:00

move tests to api mocking to remove jitter in ci (#10447)

closes #10446
This commit is contained in:
Matthias Mair
2025-10-02 01:54:31 +02:00
committed by GitHub
parent 40700dfbcf
commit f01455411a
5 changed files with 90 additions and 51 deletions

View File

@@ -1,11 +1,11 @@
"""Unit tests for base mixins for plugins.""" """Unit tests for base mixins for plugins."""
import os
from django.conf import settings from django.conf import settings
from django.test import TestCase from django.test import TestCase
from django.urls import include, path, re_path from django.urls import include, path, re_path
import requests_mock
from InvenTree.unit_test import InvenTreeTestCase from InvenTree.unit_test import InvenTreeTestCase
from plugin import InvenTreePlugin from plugin import InvenTreePlugin
from plugin.helpers import MixinNotImplementedError from plugin.helpers import MixinNotImplementedError
@@ -205,12 +205,12 @@ class APICallMixinTest(BaseMixinDefinition, TestCase):
'API_TOKEN': { 'API_TOKEN': {
'name': 'API Token', 'name': 'API Token',
'protected': True, 'protected': True,
'default': 'reqres-free-v1', 'default': 'sample-free-v1',
}, },
'API_URL': { 'API_URL': {
'name': 'External URL', 'name': 'External URL',
'description': 'Where is your API located?', '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 @property
def api_url(self): def api_url(self):
"""Override API URL for this test.""" """Override API URL for this test."""
return 'https://api.github.com' return 'https://api.example.com'
def get_external_url(self, simple: bool = True): def get_external_url(self, simple: bool = True):
"""Returns data from the sample endpoint.""" """Returns data from the sample endpoint."""
@@ -229,13 +229,6 @@ class APICallMixinTest(BaseMixinDefinition, TestCase):
self.mixin = MixinCls() 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): class WrongCLS(APICallMixin, InvenTreePlugin):
pass pass
@@ -251,7 +244,7 @@ class APICallMixinTest(BaseMixinDefinition, TestCase):
# check init # check init
self.assertTrue(self.mixin.has_api_call) self.assertTrue(self.mixin.has_api_call)
# api_url # api_url
self.assertEqual('https://api.github.com', self.mixin.api_url) self.assertEqual('https://api.example.com', self.mixin.api_url)
# api_headers # api_headers
headers = self.mixin.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]}) result = self.mixin.api_build_url_args({'a': 'b', 'c': ['d', 'efgh', 1337]})
self.assertEqual(result, '?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.""" """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 # api_call
result = self.mixin.get_external_url() result = self.mixin.get_external_url()
@@ -287,27 +305,17 @@ class APICallMixinTest(BaseMixinDefinition, TestCase):
# api_call without json conversion # api_call without json conversion
result = self.mixin.get_external_url(False) result = self.mixin.get_external_url(False)
self.assertTrue(result) self.assertTrue(result)
self.assertEqual(result.reason, 'OK') self.assertTrue(result.ok)
# Set API TOKEN # 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 # api_call with post and data
result = self.mixin.api_call(
# Try multiple times, account for the rate limit 'https://api.example.com/users/',
result = None json={'name': 'morpheus', 'job': 'leader'},
method='POST',
for _ in range(5): endpoint_is_url=True,
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)
self.assertTrue(result) self.assertTrue(result)
self.assertNotIn('error', result) self.assertNotIn('error', result)
@@ -317,16 +325,21 @@ class APICallMixinTest(BaseMixinDefinition, TestCase):
# api_call with endpoint with leading slash # api_call with endpoint with leading slash
result = self.mixin.api_call('/orgs/inventree', simple_response=False) result = self.mixin.api_call('/orgs/inventree', simple_response=False)
self.assertTrue(result) 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( result = self.mixin.api_call(
'repos/inventree/InvenTree/stargazers', url_args={'page': '2'} 'repos/inventree/InvenTree/stargazers', url_args={'page': '2'}
) )
self.assertTrue(result) self.assertTrue(result)
def test_function_errors(self): @requests_mock.Mocker()
def test_function_errors(self, m: requests_mock.Mocker):
"""Test function errors.""" """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 # wrongly defined plugins should not load
with self.assertRaises(MixinNotImplementedError): with self.assertRaises(MixinNotImplementedError):
self.mixin_wrong.has_api_call() self.mixin_wrong.has_api_call()
@@ -338,12 +351,12 @@ class APICallMixinTest(BaseMixinDefinition, TestCase):
# Too many data arguments # Too many data arguments
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
self.mixin.api_call( 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( result = self.mixin.api_call(
'https://reqres.in/api/users/', 'https://api.example.com/api/users/',
data={'name': 'morpheus', 'job': 'leader'}, data={'name': 'morpheus', 'job': 'leader'},
method='POST', method='POST',
endpoint_is_url=True, endpoint_is_url=True,

View File

@@ -18,7 +18,7 @@ class SampleApiCallerPlugin(APICallMixin, SettingsMixin, InvenTreePlugin):
'API_URL': { 'API_URL': {
'name': 'External URL', 'name': 'External URL',
'description': 'Where is your API located?', 'description': 'Where is your API located?',
'default': 'reqres.in', 'default': 'api.example.com',
}, },
} }
API_URL_SETTING = 'API_URL' API_URL_SETTING = 'API_URL'

View File

@@ -2,15 +2,19 @@
from django.test import TestCase from django.test import TestCase
import requests_mock
from plugin import registry from plugin import registry
class SampleApiCallerPluginTests(TestCase): class SampleApiCallerPluginTests(TestCase):
"""Tests for SampleApiCallerPluginTests.""" """Tests for SampleApiCallerPluginTests."""
def test_return(self): @requests_mock.Mocker()
def test_return(self, m):
"""Check if the external api call works.""" """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 # The plugin should be defined
self.assertIn('sample-api-caller', registry.plugins) self.assertIn('sample-api-caller', registry.plugins)
@@ -18,15 +22,7 @@ class SampleApiCallerPluginTests(TestCase):
self.assertTrue(plg) self.assertTrue(plg)
# do an api call # do an api call
# Note: rate limits may apply in CI result = plg.get_external_url()
result = False
for _i in range(5):
result = plg.get_external_url()
if result:
break
else:
time.sleep(1)
self.assertTrue(result) self.assertTrue(result)
self.assertIn('data', result) self.assertIn('data', result)

View File

@@ -12,3 +12,4 @@ pdfminer.six # PDF validation
ty # type checking ty # type checking
django-types # typing django-types # typing
django-stubs # typing django-stubs # typing
requests-mock # Mock requests for unit tests

View File

@@ -11,6 +11,12 @@ build==1.3.0 \
--hash=sha256:698edd0ea270bde950f53aed21f3a0135672206f3911e0176261a31e0e07b397 \ --hash=sha256:698edd0ea270bde950f53aed21f3a0135672206f3911e0176261a31e0e07b397 \
--hash=sha256:7145f0b5061ba90a1500d60bd1b13ca0a8a4cebdd0cc16ed8adf1c0e739f43b4 --hash=sha256:7145f0b5061ba90a1500d60bd1b13ca0a8a4cebdd0cc16ed8adf1c0e739f43b4
# via pip-tools # via pip-tools
certifi==2025.8.3 \
--hash=sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407 \
--hash=sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5
# via
# -c src/backend/requirements.txt
# requests
cffi==1.17.1 \ cffi==1.17.1 \
--hash=sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8 \ --hash=sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8 \
--hash=sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2 \ --hash=sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2 \
@@ -182,6 +188,7 @@ charset-normalizer==3.4.2 \
# via # via
# -c src/backend/requirements.txt # -c src/backend/requirements.txt
# pdfminer-six # pdfminer-six
# requests
click==8.1.8 \ click==8.1.8 \
--hash=sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2 \ --hash=sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2 \
--hash=sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a --hash=sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a
@@ -359,6 +366,12 @@ identify==2.6.12 \
--hash=sha256:ad9672d5a72e0d2ff7c5c8809b62dfa60458626352fb0eb7b55e69bdc45334a2 \ --hash=sha256:ad9672d5a72e0d2ff7c5c8809b62dfa60458626352fb0eb7b55e69bdc45334a2 \
--hash=sha256:d8de45749f1efb108badef65ee8386f0f7bb19a7f26185f74de6367bffbaf0e6 --hash=sha256:d8de45749f1efb108badef65ee8386f0f7bb19a7f26185f74de6367bffbaf0e6
# via pre-commit # via pre-commit
idna==3.10 \
--hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9 \
--hash=sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3
# via
# -c src/backend/requirements.txt
# requests
importlib-metadata==8.7.0 \ importlib-metadata==8.7.0 \
--hash=sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000 \ --hash=sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000 \
--hash=sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd --hash=sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd
@@ -470,6 +483,16 @@ pyyaml==6.0.2 \
# via # via
# -c src/backend/requirements.txt # -c src/backend/requirements.txt
# pre-commit # 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 \ setuptools==80.9.0 \
--hash=sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922 \ --hash=sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922 \
--hash=sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c --hash=sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c
@@ -559,6 +582,12 @@ typing-extensions==4.14.1 \
# django-stubs # django-stubs
# django-stubs-ext # django-stubs-ext
# django-test-migrations # django-test-migrations
urllib3==1.26.20 \
--hash=sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e \
--hash=sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32
# via
# -c src/backend/requirements.txt
# requests
virtualenv==20.33.1 \ virtualenv==20.33.1 \
--hash=sha256:07c19bc66c11acab6a5958b815cbcee30891cd1c2ccf53785a28651a0d8d8a67 \ --hash=sha256:07c19bc66c11acab6a5958b815cbcee30891cd1c2ccf53785a28651a0d8d8a67 \
--hash=sha256:1b44478d9e261b3fb8baa5e74a0ca3bc0e05f21aa36167bf9cbf850e542765b8 --hash=sha256:1b44478d9e261b3fb8baa5e74a0ca3bc0e05f21aa36167bf9cbf850e542765b8