From 8e962c0c59980a7382d9282d2a863ed05e148b1c Mon Sep 17 00:00:00 2001
From: Matthias <matthias.mair@oewf.org>
Date: Mon, 13 Dec 2021 08:03:19 +0100
Subject: [PATCH 01/24] add mixin to consum a single API

---
 .../plugin/builtin/integration/mixins.py      | 72 +++++++++++++++++++
 InvenTree/plugin/mixins/__init__.py           |  4 +-
 2 files changed, 74 insertions(+), 2 deletions(-)

diff --git a/InvenTree/plugin/builtin/integration/mixins.py b/InvenTree/plugin/builtin/integration/mixins.py
index 3a6b558db7..70345172ce 100644
--- a/InvenTree/plugin/builtin/integration/mixins.py
+++ b/InvenTree/plugin/builtin/integration/mixins.py
@@ -1,4 +1,7 @@
 """default mixins for IntegrationMixins"""
+import json
+import requests
+
 from django.conf.urls import url, include
 
 from plugin.urls import PLUGIN_BASE
@@ -167,3 +170,72 @@ class AppMixin:
         this plugin is always an app with this plugin
         """
         return True
+
+
+class APICallMixin:
+    """Mixin that enables easier API calls for a plugin
+    
+    1. Add this mixin
+    2. Add two global settings for the required url and token/passowrd (use `GlobalSettingsMixin`)
+    3. Save the references to `API_URL_SETTING` and `API_PASSWORD_SETTING`
+    4. Set `API_TOKEN` to the name required for the token / password by the external API
+    5. (Optional) Override the `api_url` property method if some part of the APIs url is static
+    6. (Optional) Override `api_headers` to add extra headers (by default the token/password and Content-Type are contained)
+    6. Access the API in you plugin code via `api_call`
+    """
+    API_METHOD = 'https'
+    API_URL_SETTING = None
+    API_PASSWORD_SETTING = None
+
+    API_TOKEN = 'Bearer'
+
+    class MixinMeta:
+        """meta options for this mixin"""
+        MIXIN_NAME = 'external API usage'
+
+    def __init__(self):
+        super().__init__()
+        self.add_mixin('api_call', 'has_api_call', __class__)
+
+    @property
+    def has_api_call(self):
+        """Is the mixin ready to call external APIs?"""
+        # TODO check if settings are set
+        return True
+
+    @property
+    def api_url(self):
+        return f'{self.API_METHOD}://{self.get_globalsetting(self.API_URL_SETTING)}'
+
+    @property
+    def api_headers(self):
+        return {self.API_TOKEN: self.get_globalsetting(self.API_PASSWORD_SETTING), 'Content-Type': 'application/json'}
+
+    def api_build_url_args(self, arguments):
+        groups = []
+        for key, val in arguments.items():
+            groups.append(f'{key}={",".join([str(a) for a in val])}')
+        return f'?{"&".join(groups)}'
+
+    def api_call(self, endpoint, method: str='GET', url_args=None, data=None, headers=None, simple_response: bool = True):
+        if url_args:
+            endpoint += self.api_build_url_args(url_args)
+
+        if headers is None:
+            headers = self.api_headers
+
+        # build kwargs for call
+        kwargs = {
+            'url': f'{self.api_url}/{endpoint}',
+            'headers': headers,
+        }
+        if data:
+            kwargs['data'] = json.dumps(data)
+
+        # run command
+        response = requests.request(method, **kwargs)
+
+        # return
+        if simple_response:
+            return response.json()
+        return response
diff --git a/InvenTree/plugin/mixins/__init__.py b/InvenTree/plugin/mixins/__init__.py
index feb6bc3466..d63bae097b 100644
--- a/InvenTree/plugin/mixins/__init__.py
+++ b/InvenTree/plugin/mixins/__init__.py
@@ -1,6 +1,6 @@
 """utility class to enable simpler imports"""
-from ..builtin.integration.mixins import AppMixin, GlobalSettingsMixin, UrlsMixin, NavigationMixin
+from ..builtin.integration.mixins import AppMixin, GlobalSettingsMixin, UrlsMixin, NavigationMixin, APICallMixin
 
 __all__ = [
-    'AppMixin', 'GlobalSettingsMixin', 'UrlsMixin', 'NavigationMixin',
+    'AppMixin', 'GlobalSettingsMixin', 'UrlsMixin', 'NavigationMixin', 'APICallMixin',
 ]

From 251fdeb69e161d1c03fb7751fcdab61c49319471 Mon Sep 17 00:00:00 2001
From: Matthias <matthias.mair@oewf.org>
Date: Mon, 13 Dec 2021 18:01:20 +0100
Subject: [PATCH 02/24] PEP fixes

---
 InvenTree/plugin/builtin/integration/mixins.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/InvenTree/plugin/builtin/integration/mixins.py b/InvenTree/plugin/builtin/integration/mixins.py
index 70345172ce..932588c281 100644
--- a/InvenTree/plugin/builtin/integration/mixins.py
+++ b/InvenTree/plugin/builtin/integration/mixins.py
@@ -174,7 +174,7 @@ class AppMixin:
 
 class APICallMixin:
     """Mixin that enables easier API calls for a plugin
-    
+
     1. Add this mixin
     2. Add two global settings for the required url and token/passowrd (use `GlobalSettingsMixin`)
     3. Save the references to `API_URL_SETTING` and `API_PASSWORD_SETTING`
@@ -217,7 +217,7 @@ class APICallMixin:
             groups.append(f'{key}={",".join([str(a) for a in val])}')
         return f'?{"&".join(groups)}'
 
-    def api_call(self, endpoint, method: str='GET', url_args=None, data=None, headers=None, simple_response: bool = True):
+    def api_call(self, endpoint, method: str = 'GET', url_args=None, data=None, headers=None, simple_response: bool = True):
         if url_args:
             endpoint += self.api_build_url_args(url_args)
 

From a2871ccb45beebf61d54ed30df91722646fa600d Mon Sep 17 00:00:00 2001
From: Matthias <matthias.mair@oewf.org>
Date: Mon, 13 Dec 2021 18:40:24 +0100
Subject: [PATCH 03/24] update database images before running

---
 .github/workflows/qc_checks.yaml | 1 +
 1 file changed, 1 insertion(+)

diff --git a/.github/workflows/qc_checks.yaml b/.github/workflows/qc_checks.yaml
index 929a299e93..4491ba7815 100644
--- a/.github/workflows/qc_checks.yaml
+++ b/.github/workflows/qc_checks.yaml
@@ -282,6 +282,7 @@ jobs:
           cache: 'pip'
       - name: Install Dependencies
         run: |
+          sudo apt-get update
           sudo apt-get install mysql-server libmysqlclient-dev
           pip3 install invoke
           pip3 install mysqlclient

From 62394c4a826b06eaf37dfea8e90cc21021f507e8 Mon Sep 17 00:00:00 2001
From: Matthias <matthias.mair@oewf.org>
Date: Sat, 8 Jan 2022 21:54:42 +0100
Subject: [PATCH 04/24] small reformat

---
 InvenTree/plugin/builtin/integration/mixins.py | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/InvenTree/plugin/builtin/integration/mixins.py b/InvenTree/plugin/builtin/integration/mixins.py
index 457f86fb80..4f2d35268e 100644
--- a/InvenTree/plugin/builtin/integration/mixins.py
+++ b/InvenTree/plugin/builtin/integration/mixins.py
@@ -342,7 +342,10 @@ class APICallMixin:
 
     @property
     def api_headers(self):
-        return {self.API_TOKEN: self.get_globalsetting(self.API_PASSWORD_SETTING), 'Content-Type': 'application/json'}
+        return {
+            self.API_TOKEN: self.get_globalsetting(self.API_PASSWORD_SETTING),
+            'Content-Type': 'application/json'
+        }
 
     def api_build_url_args(self, arguments):
         groups = []

From f59b59401fb66374a60555308b249411750b205e Mon Sep 17 00:00:00 2001
From: Matthias <matthias.mair@oewf.org>
Date: Sat, 8 Jan 2022 21:58:44 +0100
Subject: [PATCH 05/24] refactor setting

---
 InvenTree/plugin/builtin/integration/mixins.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/InvenTree/plugin/builtin/integration/mixins.py b/InvenTree/plugin/builtin/integration/mixins.py
index 4f2d35268e..590c7615d6 100644
--- a/InvenTree/plugin/builtin/integration/mixins.py
+++ b/InvenTree/plugin/builtin/integration/mixins.py
@@ -318,7 +318,7 @@ class APICallMixin:
     """
     API_METHOD = 'https'
     API_URL_SETTING = None
-    API_PASSWORD_SETTING = None
+    API_TOKEN_SETTING = None
 
     API_TOKEN = 'Bearer'
 
@@ -343,7 +343,7 @@ class APICallMixin:
     @property
     def api_headers(self):
         return {
-            self.API_TOKEN: self.get_globalsetting(self.API_PASSWORD_SETTING),
+            self.API_TOKEN: self.get_globalsetting(self.API_TOKEN_SETTING),
             'Content-Type': 'application/json'
         }
 

From 3aea1bb7ba1c44c813ba8cf099d2ac43b91c5f62 Mon Sep 17 00:00:00 2001
From: Matthias <matthias.mair@oewf.org>
Date: Sat, 8 Jan 2022 21:59:02 +0100
Subject: [PATCH 06/24] made docstring clearer

---
 InvenTree/plugin/builtin/integration/mixins.py | 15 ++++++++-------
 1 file changed, 8 insertions(+), 7 deletions(-)

diff --git a/InvenTree/plugin/builtin/integration/mixins.py b/InvenTree/plugin/builtin/integration/mixins.py
index 590c7615d6..4001a100e3 100644
--- a/InvenTree/plugin/builtin/integration/mixins.py
+++ b/InvenTree/plugin/builtin/integration/mixins.py
@@ -306,14 +306,15 @@ class AppMixin:
 
 
 class APICallMixin:
-    """Mixin that enables easier API calls for a plugin
+    """
+    Mixin that enables easier API calls for a plugin
 
-    1. Add this mixin
-    2. Add two global settings for the required url and token/passowrd (use `GlobalSettingsMixin`)
-    3. Save the references to `API_URL_SETTING` and `API_PASSWORD_SETTING`
-    4. Set `API_TOKEN` to the name required for the token / password by the external API
-    5. (Optional) Override the `api_url` property method if some part of the APIs url is static
-    6. (Optional) Override `api_headers` to add extra headers (by default the token/password and Content-Type are contained)
+    1. Add this mixin before (left of) SettingsMixin and PluginBase
+    2. Add two settings for the required url and token/passowrd (use `SettingsMixin`)
+    3. Save the references to keys of the settings in `API_URL_SETTING` and `API_TOKEN_SETTING`
+    4. (Optional) Set `API_TOKEN` to the name required for the token by the external API - Defaults to `Bearer`
+    5. (Optional) Override the `api_url` property method if the setting needs to be extended
+    6. (Optional) Override `api_headers` to add extra headers (by default the token and Content-Type are contained)
     6. Access the API in you plugin code via `api_call`
     """
     API_METHOD = 'https'

From d939107d3633f623d76e83f140ff1a9852bee84b Mon Sep 17 00:00:00 2001
From: Matthias <matthias.mair@oewf.org>
Date: Sun, 9 Jan 2022 03:01:31 +0100
Subject: [PATCH 07/24] add example

---
 .../plugin/builtin/integration/mixins.py      | 35 ++++++++++++++++++-
 1 file changed, 34 insertions(+), 1 deletion(-)

diff --git a/InvenTree/plugin/builtin/integration/mixins.py b/InvenTree/plugin/builtin/integration/mixins.py
index 4001a100e3..84d99d1bce 100644
--- a/InvenTree/plugin/builtin/integration/mixins.py
+++ b/InvenTree/plugin/builtin/integration/mixins.py
@@ -315,7 +315,40 @@ class APICallMixin:
     4. (Optional) Set `API_TOKEN` to the name required for the token by the external API - Defaults to `Bearer`
     5. (Optional) Override the `api_url` property method if the setting needs to be extended
     6. (Optional) Override `api_headers` to add extra headers (by default the token and Content-Type are contained)
-    6. Access the API in you plugin code via `api_call`
+    7. Access the API in you plugin code via `api_call`
+
+    Example:
+    ```
+    from plugin import IntegrationPluginBase
+    from plugin.mixins import APICallMixin, SettingsMixin
+
+
+    class SampleApiCallerPlugin(APICallMixin, SettingsMixin, IntegrationPluginBase):
+        '''
+        A small api call sample
+        '''
+        PLUGIN_NAME = "Sample API Caller"
+
+        SETTINGS = {
+            'API_TOKEN': {
+                'name': 'API Token',
+                'protected': True,
+            },
+            'API_URL': {
+                'name': 'External URL',
+                'description': 'Where is your API located?',
+                'default': 'https://reqres.in',
+            },
+        }
+        API_URL_SETTING = 'API_URL'
+        API_TOKEN_SETTING = 'API_TOKEN'
+
+        def get_external_url(self):
+            '''
+            returns data from the sample endpoint
+            '''
+            return self.api_call('api/users/2')
+    ```
     """
     API_METHOD = 'https'
     API_URL_SETTING = None

From 33ee7e53dbbd861d32221b31969fa80d61a93b9c Mon Sep 17 00:00:00 2001
From: Matthias <matthias.mair@oewf.org>
Date: Sun, 9 Jan 2022 03:01:50 +0100
Subject: [PATCH 08/24] append docstring

---
 InvenTree/plugin/builtin/integration/mixins.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/InvenTree/plugin/builtin/integration/mixins.py b/InvenTree/plugin/builtin/integration/mixins.py
index 84d99d1bce..d9f6bdc2d5 100644
--- a/InvenTree/plugin/builtin/integration/mixins.py
+++ b/InvenTree/plugin/builtin/integration/mixins.py
@@ -309,6 +309,7 @@ class APICallMixin:
     """
     Mixin that enables easier API calls for a plugin
 
+    Steps to set up:
     1. Add this mixin before (left of) SettingsMixin and PluginBase
     2. Add two settings for the required url and token/passowrd (use `SettingsMixin`)
     3. Save the references to keys of the settings in `API_URL_SETTING` and `API_TOKEN_SETTING`

From 19f2c44c2a77e89bd508b99ceea45a2ce9233344 Mon Sep 17 00:00:00 2001
From: Matthias <matthias.mair@oewf.org>
Date: Sun, 9 Jan 2022 03:02:19 +0100
Subject: [PATCH 09/24] change mixin name

---
 InvenTree/plugin/builtin/integration/mixins.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/InvenTree/plugin/builtin/integration/mixins.py b/InvenTree/plugin/builtin/integration/mixins.py
index d9f6bdc2d5..22ba6de27d 100644
--- a/InvenTree/plugin/builtin/integration/mixins.py
+++ b/InvenTree/plugin/builtin/integration/mixins.py
@@ -359,7 +359,7 @@ class APICallMixin:
 
     class MixinMeta:
         """meta options for this mixin"""
-        MIXIN_NAME = 'external API usage'
+        MIXIN_NAME = 'API calls'
 
     def __init__(self):
         super().__init__()

From 61b21d1ec14e0be683f8da2b92b3ca2aa9fdcf59 Mon Sep 17 00:00:00 2001
From: Matthias <matthias.mair@oewf.org>
Date: Sun, 9 Jan 2022 03:03:05 +0100
Subject: [PATCH 10/24] add sample for api caller

---
 .../plugin/samples/integration/api_caller.py  | 34 +++++++++++++++++++
 1 file changed, 34 insertions(+)
 create mode 100644 InvenTree/plugin/samples/integration/api_caller.py

diff --git a/InvenTree/plugin/samples/integration/api_caller.py b/InvenTree/plugin/samples/integration/api_caller.py
new file mode 100644
index 0000000000..eaef93a4b6
--- /dev/null
+++ b/InvenTree/plugin/samples/integration/api_caller.py
@@ -0,0 +1,34 @@
+"""
+Sample plugin for calling an external API
+"""
+from django.utils.translation import ugettext_lazy as _
+
+from plugin import IntegrationPluginBase
+from plugin.mixins import APICallMixin, SettingsMixin
+
+
+class SampleApiCallerPlugin(APICallMixin, SettingsMixin, IntegrationPluginBase):
+    """
+    A small api call sample
+    """
+    PLUGIN_NAME = "Sample API Caller"
+
+    SETTINGS = {
+        'API_TOKEN': {
+            'name': 'API Token',
+            'protected': True,
+        },
+        'API_URL': {
+            'name': 'External URL',
+            'description': 'Where is your API located?',
+            'default': 'https://reqres.in',
+        },
+    }
+    API_URL_SETTING = 'API_URL'
+    API_TOKEN_SETTING = 'API_TOKEN'
+
+    def get_external_url(self):
+        """
+        returns data from the sample endpoint
+        """
+        return self.api_call('api/users/2')

From ed193e9e90304c04ed8047a2ffc41f8926808e0e Mon Sep 17 00:00:00 2001
From: Matthias <matthias.mair@oewf.org>
Date: Sun, 9 Jan 2022 03:04:00 +0100
Subject: [PATCH 11/24] docstring for plugin base import class

---
 InvenTree/plugin/__init__.py | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/InvenTree/plugin/__init__.py b/InvenTree/plugin/__init__.py
index b3dc3a2fd0..86f65919c4 100644
--- a/InvenTree/plugin/__init__.py
+++ b/InvenTree/plugin/__init__.py
@@ -1,3 +1,7 @@
+"""
+Utility file to enable simper imports
+"""
+
 from .registry import plugin_registry
 from .plugin import InvenTreePlugin
 from .integration import IntegrationPluginBase

From ea8fd21af09933526a5930cce7c70b7e2b69eda4 Mon Sep 17 00:00:00 2001
From: Matthias <matthias.mair@oewf.org>
Date: Sun, 9 Jan 2022 03:10:23 +0100
Subject: [PATCH 12/24] pip fix

---
 InvenTree/plugin/samples/integration/api_caller.py | 2 --
 1 file changed, 2 deletions(-)

diff --git a/InvenTree/plugin/samples/integration/api_caller.py b/InvenTree/plugin/samples/integration/api_caller.py
index eaef93a4b6..7e5c883961 100644
--- a/InvenTree/plugin/samples/integration/api_caller.py
+++ b/InvenTree/plugin/samples/integration/api_caller.py
@@ -1,8 +1,6 @@
 """
 Sample plugin for calling an external API
 """
-from django.utils.translation import ugettext_lazy as _
-
 from plugin import IntegrationPluginBase
 from plugin.mixins import APICallMixin, SettingsMixin
 

From b48e9bcac9cf8417cf51ba44f3f0577d2cc1fa89 Mon Sep 17 00:00:00 2001
From: Matthias <matthias.mair@oewf.org>
Date: Sun, 9 Jan 2022 03:33:47 +0100
Subject: [PATCH 13/24] fix settings call

---
 InvenTree/plugin/builtin/integration/mixins.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/InvenTree/plugin/builtin/integration/mixins.py b/InvenTree/plugin/builtin/integration/mixins.py
index 22ba6de27d..c8bb9f7f9e 100644
--- a/InvenTree/plugin/builtin/integration/mixins.py
+++ b/InvenTree/plugin/builtin/integration/mixins.py
@@ -373,12 +373,12 @@ class APICallMixin:
 
     @property
     def api_url(self):
-        return f'{self.API_METHOD}://{self.get_globalsetting(self.API_URL_SETTING)}'
+        return f'{self.API_METHOD}://{self.get_setting(self.API_URL_SETTING)}'
 
     @property
     def api_headers(self):
         return {
-            self.API_TOKEN: self.get_globalsetting(self.API_TOKEN_SETTING),
+            self.API_TOKEN: self.get_setting(self.API_TOKEN_SETTING),
             'Content-Type': 'application/json'
         }
 

From cc8948c708cdb00c28341e42c7b321f0ffa4d37b Mon Sep 17 00:00:00 2001
From: Matthias <matthias.mair@oewf.org>
Date: Sun, 9 Jan 2022 03:34:08 +0100
Subject: [PATCH 14/24] fix sample url

---
 InvenTree/plugin/builtin/integration/mixins.py     | 2 +-
 InvenTree/plugin/samples/integration/api_caller.py | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/InvenTree/plugin/builtin/integration/mixins.py b/InvenTree/plugin/builtin/integration/mixins.py
index c8bb9f7f9e..66676d0520 100644
--- a/InvenTree/plugin/builtin/integration/mixins.py
+++ b/InvenTree/plugin/builtin/integration/mixins.py
@@ -338,7 +338,7 @@ class APICallMixin:
             'API_URL': {
                 'name': 'External URL',
                 'description': 'Where is your API located?',
-                'default': 'https://reqres.in',
+                'default': 'reqres.in',
             },
         }
         API_URL_SETTING = 'API_URL'
diff --git a/InvenTree/plugin/samples/integration/api_caller.py b/InvenTree/plugin/samples/integration/api_caller.py
index 7e5c883961..36e1583ba0 100644
--- a/InvenTree/plugin/samples/integration/api_caller.py
+++ b/InvenTree/plugin/samples/integration/api_caller.py
@@ -19,7 +19,7 @@ class SampleApiCallerPlugin(APICallMixin, SettingsMixin, IntegrationPluginBase):
         'API_URL': {
             'name': 'External URL',
             'description': 'Where is your API located?',
-            'default': 'https://reqres.in',
+            'default': 'reqres.in',
         },
     }
     API_URL_SETTING = 'API_URL'

From f9742ab41d6f75d44f853b7767b37f30fb289512 Mon Sep 17 00:00:00 2001
From: Matthias <matthias.mair@oewf.org>
Date: Sun, 9 Jan 2022 03:34:27 +0100
Subject: [PATCH 15/24] add integration test for plugin

---
 .../samples/integration/test_api_caller.py    | 20 +++++++++++++++++++
 1 file changed, 20 insertions(+)
 create mode 100644 InvenTree/plugin/samples/integration/test_api_caller.py

diff --git a/InvenTree/plugin/samples/integration/test_api_caller.py b/InvenTree/plugin/samples/integration/test_api_caller.py
new file mode 100644
index 0000000000..7db431c3ee
--- /dev/null
+++ b/InvenTree/plugin/samples/integration/test_api_caller.py
@@ -0,0 +1,20 @@
+""" Unit tests for action caller sample"""
+
+from django.test import TestCase
+
+from plugin import plugin_registry
+
+class SampleApiCallerPluginTests(TestCase):
+    """ Tests for SampleApiCallerPluginTests """
+
+    def test_return(self):
+        """check if the external api call works"""
+        # The plugin should be defined
+        self.assertIn('sample-api-caller', plugin_registry.plugins)
+        plg = plugin_registry.plugins['sample-api-caller']
+        self.assertTrue(plg)
+
+        # do an api call
+        result = plg.get_external_url()
+        self.assertTrue(result)
+        self.assertIn('data', result,)

From ad9a9da656f7686dde4bd23133e2ea7b4cf63c50 Mon Sep 17 00:00:00 2001
From: Matthias <matthias.mair@oewf.org>
Date: Sun, 9 Jan 2022 03:35:29 +0100
Subject: [PATCH 16/24] PEP fix

---
 InvenTree/plugin/samples/integration/test_api_caller.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/InvenTree/plugin/samples/integration/test_api_caller.py b/InvenTree/plugin/samples/integration/test_api_caller.py
index 7db431c3ee..e15edfad94 100644
--- a/InvenTree/plugin/samples/integration/test_api_caller.py
+++ b/InvenTree/plugin/samples/integration/test_api_caller.py
@@ -4,6 +4,7 @@ from django.test import TestCase
 
 from plugin import plugin_registry
 
+
 class SampleApiCallerPluginTests(TestCase):
     """ Tests for SampleApiCallerPluginTests """
 

From c699ced34ad263fdb0cdc6372d73fa2e5a7ac7a3 Mon Sep 17 00:00:00 2001
From: Matthias <matthias.mair@oewf.org>
Date: Sun, 9 Jan 2022 22:16:19 +0100
Subject: [PATCH 17/24] make general mixin tests multi mixin enabled

---
 InvenTree/plugin/test_integration.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/InvenTree/plugin/test_integration.py b/InvenTree/plugin/test_integration.py
index 3d88fed4dd..abddc9ee59 100644
--- a/InvenTree/plugin/test_integration.py
+++ b/InvenTree/plugin/test_integration.py
@@ -15,9 +15,9 @@ from plugin.urls import PLUGIN_BASE
 class BaseMixinDefinition:
     def test_mixin_name(self):
         # mixin name
-        self.assertEqual(self.mixin.registered_mixins[0]['key'], self.MIXIN_NAME)
+        self.assertIn(self.MIXIN_NAME, [item['key'] for item in self.mixin.registered_mixins])
         # human name
-        self.assertEqual(self.mixin.registered_mixins[0]['human_name'], self.MIXIN_HUMAN_NAME)
+        self.assertIn(self.MIXIN_HUMAN_NAME, [item['human_name'] for item in self.mixin.registered_mixins])
 
 
 class SettingsMixinTest(BaseMixinDefinition, TestCase):

From 31d587a9b190ef7eb0c7546fa6e4ed25e477da3c Mon Sep 17 00:00:00 2001
From: Matthias <matthias.mair@oewf.org>
Date: Sun, 9 Jan 2022 22:19:01 +0100
Subject: [PATCH 18/24] unittests fdor ApiCallMixin

---
 InvenTree/plugin/test_integration.py | 46 +++++++++++++++++++++++++++-
 1 file changed, 45 insertions(+), 1 deletion(-)

diff --git a/InvenTree/plugin/test_integration.py b/InvenTree/plugin/test_integration.py
index abddc9ee59..eb55f984d3 100644
--- a/InvenTree/plugin/test_integration.py
+++ b/InvenTree/plugin/test_integration.py
@@ -8,7 +8,7 @@ from django.contrib.auth import get_user_model
 from datetime import datetime
 
 from plugin import IntegrationPluginBase
-from plugin.mixins import AppMixin, SettingsMixin, UrlsMixin, NavigationMixin
+from plugin.mixins import AppMixin, SettingsMixin, UrlsMixin, NavigationMixin, APICallMixin
 from plugin.urls import PLUGIN_BASE
 
 
@@ -142,6 +142,50 @@ class NavigationMixinTest(BaseMixinDefinition, TestCase):
         self.assertEqual(self.nothing_mixin.navigation_name, '')
 
 
+class APICallMixinTest(BaseMixinDefinition, TestCase):
+    MIXIN_HUMAN_NAME = 'API calls'
+    MIXIN_NAME = 'api_call'
+    MIXIN_ENABLE_CHECK = 'has_api_call'
+
+    def setUp(self):
+        class MixinCls(APICallMixin, SettingsMixin, IntegrationPluginBase):
+            PLUGIN_NAME = "Sample API Caller"
+
+            SETTINGS = {
+                'API_TOKEN': {
+                    'name': 'API Token',
+                    'protected': True,
+                },
+                'API_URL': {
+                    'name': 'External URL',
+                    'description': 'Where is your API located?',
+                    'default': 'reqres.in',
+                },
+            }
+            API_URL_SETTING = 'API_URL'
+            API_TOKEN_SETTING = 'API_TOKEN'
+
+            def get_external_url(self):
+                '''
+                returns data from the sample endpoint
+                '''
+                return self.api_call('api/users/2')
+        self.mixin = MixinCls()
+    def test_function(self):
+        # api_url
+        self.assertEqual('https://reqres.in', self.mixin.api_url)
+
+        # api_headers
+        headers = self.mixin.api_headers
+        self.assertEqual(headers, {'Bearer': '', 'Content-Type': 'application/json'})
+
+        # api_build_url_args
+        # api_call
+        result = self.mixin.get_external_url()
+        self.assertTrue(result)
+        self.assertIn('data', result,)
+
+
 class IntegrationPluginBaseTests(TestCase):
     """ Tests for IntegrationPluginBase """
 

From e889f487f010be2b59a0961e051ce93e54db40de Mon Sep 17 00:00:00 2001
From: Matthias <matthias.mair@oewf.org>
Date: Sun, 9 Jan 2022 22:27:50 +0100
Subject: [PATCH 19/24] added a check for the required constants

---
 InvenTree/plugin/builtin/integration/mixins.py | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/InvenTree/plugin/builtin/integration/mixins.py b/InvenTree/plugin/builtin/integration/mixins.py
index 66676d0520..7d257dbee5 100644
--- a/InvenTree/plugin/builtin/integration/mixins.py
+++ b/InvenTree/plugin/builtin/integration/mixins.py
@@ -368,7 +368,10 @@ class APICallMixin:
     @property
     def has_api_call(self):
         """Is the mixin ready to call external APIs?"""
-        # TODO check if settings are set
+        if not bool(self.API_URL_SETTING):
+            raise ValueError("API_URL_SETTING must be defined")
+        if not bool(self.API_TOKEN_SETTING):
+            raise ValueError("API_TOKEN_SETTING must be defined")
         return True
 
     @property

From c8599039a25cb17535f41149d588ff3acb4836af Mon Sep 17 00:00:00 2001
From: Matthias <matthias.mair@oewf.org>
Date: Sun, 9 Jan 2022 22:33:14 +0100
Subject: [PATCH 20/24] added test for wrong config

---
 InvenTree/plugin/test_integration.py | 9 +++++++++
 1 file changed, 9 insertions(+)

diff --git a/InvenTree/plugin/test_integration.py b/InvenTree/plugin/test_integration.py
index eb55f984d3..48533a70e2 100644
--- a/InvenTree/plugin/test_integration.py
+++ b/InvenTree/plugin/test_integration.py
@@ -171,6 +171,11 @@ class APICallMixinTest(BaseMixinDefinition, TestCase):
                 '''
                 return self.api_call('api/users/2')
         self.mixin = MixinCls()
+
+        class WrongCLS(APICallMixin, IntegrationPluginBase):
+            pass
+        self.mixin_nothing = WrongCLS()
+
     def test_function(self):
         # api_url
         self.assertEqual('https://reqres.in', self.mixin.api_url)
@@ -185,6 +190,10 @@ class APICallMixinTest(BaseMixinDefinition, TestCase):
         self.assertTrue(result)
         self.assertIn('data', result,)
 
+        # wrongly defined plugins should not load
+        with self.assertRaises(ValueError):
+            self.mixin_nothing.has_api_call()
+
 
 class IntegrationPluginBaseTests(TestCase):
     """ Tests for IntegrationPluginBase """

From 2c05b858a4afa2542a20556b6a5f3b6c8ea6f9e0 Mon Sep 17 00:00:00 2001
From: Matthias <matthias.mair@oewf.org>
Date: Sun, 9 Jan 2022 22:34:02 +0100
Subject: [PATCH 21/24] renmae var

---
 InvenTree/plugin/test_integration.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/InvenTree/plugin/test_integration.py b/InvenTree/plugin/test_integration.py
index 48533a70e2..a65fc85708 100644
--- a/InvenTree/plugin/test_integration.py
+++ b/InvenTree/plugin/test_integration.py
@@ -174,7 +174,7 @@ class APICallMixinTest(BaseMixinDefinition, TestCase):
 
         class WrongCLS(APICallMixin, IntegrationPluginBase):
             pass
-        self.mixin_nothing = WrongCLS()
+        self.mixin_wrong = WrongCLS()
 
     def test_function(self):
         # api_url
@@ -192,7 +192,7 @@ class APICallMixinTest(BaseMixinDefinition, TestCase):
 
         # wrongly defined plugins should not load
         with self.assertRaises(ValueError):
-            self.mixin_nothing.has_api_call()
+            self.mixin_wrong.has_api_call()
 
 
 class IntegrationPluginBaseTests(TestCase):

From afada6b7598787f5fd27de1a186bbd390f7af923 Mon Sep 17 00:00:00 2001
From: Matthias <matthias.mair@oewf.org>
Date: Sun, 9 Jan 2022 22:56:14 +0100
Subject: [PATCH 22/24] test the url arg building

---
 InvenTree/plugin/test_integration.py | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/InvenTree/plugin/test_integration.py b/InvenTree/plugin/test_integration.py
index a65fc85708..5831e50af2 100644
--- a/InvenTree/plugin/test_integration.py
+++ b/InvenTree/plugin/test_integration.py
@@ -185,6 +185,16 @@ class APICallMixinTest(BaseMixinDefinition, TestCase):
         self.assertEqual(headers, {'Bearer': '', 'Content-Type': 'application/json'})
 
         # api_build_url_args
+        # 1 arg
+        result = self.mixin.api_build_url_args({'a': 'b'})
+        self.assertEqual(result, '?a=b')
+        # more args
+        result = self.mixin.api_build_url_args({'a': 'b', 'c': 'd'})
+        self.assertEqual(result, '?a=b&c=d')
+        # list args
+        result = self.mixin.api_build_url_args({'a': 'b', 'c': ['d', 'e', 'f', ]})
+        self.assertEqual(result, '?a=b&c=d,e,f')
+
         # api_call
         result = self.mixin.get_external_url()
         self.assertTrue(result)

From bf7af8f72ab64956396e358df9dc20e2ef4ea0ee Mon Sep 17 00:00:00 2001
From: Matthias <matthias.mair@oewf.org>
Date: Sun, 9 Jan 2022 23:01:38 +0100
Subject: [PATCH 23/24] cover another missing setting

---
 InvenTree/plugin/test_integration.py | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/InvenTree/plugin/test_integration.py b/InvenTree/plugin/test_integration.py
index 5831e50af2..7ce581e68b 100644
--- a/InvenTree/plugin/test_integration.py
+++ b/InvenTree/plugin/test_integration.py
@@ -176,7 +176,13 @@ class APICallMixinTest(BaseMixinDefinition, TestCase):
             pass
         self.mixin_wrong = WrongCLS()
 
+        class WrongCLS2(APICallMixin, IntegrationPluginBase):
+            API_URL_SETTING = 'test'
+        self.mixin_wrong2 = WrongCLS2()
+
     def test_function(self):
+        # check init
+        self.assertTrue(self.mixin.has_api_call())
         # api_url
         self.assertEqual('https://reqres.in', self.mixin.api_url)
 
@@ -204,6 +210,10 @@ class APICallMixinTest(BaseMixinDefinition, TestCase):
         with self.assertRaises(ValueError):
             self.mixin_wrong.has_api_call()
 
+        # cover wrong token setting
+        with self.assertRaises(ValueError):
+            self.mixin_wrong.has_api_call()
+
 
 class IntegrationPluginBaseTests(TestCase):
     """ Tests for IntegrationPluginBase """

From 6af2267e3df77509b726158d189079f7ceb9d024 Mon Sep 17 00:00:00 2001
From: Matthias <matthias.mair@oewf.org>
Date: Sun, 9 Jan 2022 23:05:51 +0100
Subject: [PATCH 24/24] fix test

---
 InvenTree/plugin/test_integration.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/InvenTree/plugin/test_integration.py b/InvenTree/plugin/test_integration.py
index 7ce581e68b..dbc77f7cd0 100644
--- a/InvenTree/plugin/test_integration.py
+++ b/InvenTree/plugin/test_integration.py
@@ -182,7 +182,7 @@ class APICallMixinTest(BaseMixinDefinition, TestCase):
 
     def test_function(self):
         # check init
-        self.assertTrue(self.mixin.has_api_call())
+        self.assertTrue(self.mixin.has_api_call)
         # api_url
         self.assertEqual('https://reqres.in', self.mixin.api_url)