Repository: duosecurity/duo_client_python

Issue:
Update comments to indicate "visual_style" parameter is unused
The comments for "visual_style" in the Admin API client indicates the user should see the API docs for information.  In fact, this parameter is unused and deprecated - the comments should be updated to reflect this.

https://github.com/duosecurity/duo_client_python/blob/5ed620100b3eccf485794d6ea068fc3caa525ff9/duo_client/admin.py#L142

Discussion:
@AaronAtDuo: Solved by https://github.com/duosecurity/duo_client_python/pull/283

Solution patch:
diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml
index 8201eb3..40c1843 100644
--- a/.github/workflows/python-ci.yml
+++ b/.github/workflows/python-ci.yml
@@ -14,7 +14,7 @@ jobs:
 
     strategy:
       matrix:
-        python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
+        python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
 
     steps:
       - name: Checkout
diff --git a/duo_client/admin.py b/duo_client/admin.py
index 95c1d40..7c3d316 100644
--- a/duo_client/admin.py
+++ b/duo_client/admin.py
@@ -139,10 +139,9 @@
      'notes': <str:notes>,
      'secret_key': <str:secret key>,
      'type': <str:integration type>,
-     'visual_style': <str:visual style>}
+     'visual_style': Deprecated; ignored if specified.}
 
-See the adminapi docs for possible values for enroll_policy, visual_style, ip_whitelist,
-and type.
+See the adminapi docs for possible values for enroll_policy, ip_whitelist, and type.
 
 
 ADMINISTRATIVE UNITS
@@ -2529,7 +2528,7 @@ def get_integrations_generator(self):
         """
         return self.json_paging_api_call(
             'GET',
-            '/admin/v2/integrations',
+            '/admin/v3/integrations',
             {},
         )
 
@@ -2549,7 +2548,7 @@ def get_integrations(self, limit=None, offset=0):
         if limit:
             return self.json_api_call(
                 'GET',
-                '/admin/v2/integrations',
+                '/admin/v3/integrations',
                 {'limit': limit, 'offset': offset},
             )
 
@@ -2568,7 +2567,7 @@ def get_integration(self, integration_key):
         params = {}
         response = self.json_api_call(
             'GET',
-            '/admin/v2/integrations/' + integration_key,
+            '/admin/v3/integrations/' + integration_key,
             params,
         )
         return response
@@ -2593,14 +2592,14 @@ def create_integration(self,
                            ip_whitelist_enroll_policy=None,
                            groups_allowed=None,
                            self_service_allowed=None,
-                           sso=None):
+                           sso=None,
+                           user_access=None):
         """Creates a new integration.
 
         name - The name of the integration (required)
         integration_type - <str: integration type constant> (required)
                            See adminapi docs for possible values.
-        visual_style - <str:visual style constant> (optional, default 'default')
-                       See adminapi docs for possible values.
+        visual_style - Deprecated; ignored if specified.
         greeting - <str:Voice greeting> (optional, default '')
         notes - <str:internal use> (optional, uses default setting)
         enroll_policy - <str:'enroll'|'allow'|'deny'> (optional, default 'enroll')
@@ -2671,8 +2670,11 @@ def create_integration(self,
             params['self_service_allowed'] = '1' if self_service_allowed else '0'
         if sso is not None:
             params['sso'] = sso
+        if user_access is not None:
+            params['user_access'] = user_access
+
         response = self.json_api_call('POST',
-                                      '/admin/v2/integrations',
+                                      '/admin/v3/integrations',
                                       params,
         )
         return response
@@ -2766,7 +2768,7 @@ def delete_integration(self, integration_key):
 
         """
         integration_key = urllib.parse.quote_plus(str(integration_key))
-        path = '/admin/v2/integrations/%s' % integration_key
+        path = '/admin/v3/integrations/%s' % integration_key
         return self.json_api_call(
             'DELETE',
             path,
@@ -2794,13 +2796,14 @@ def update_integration(self,
                            ip_whitelist_enroll_policy=None,
                            groups_allowed=None,
                            self_service_allowed=None,
-                           sso=None):
+                           sso=None,
+                           user_access=None
+                           ):
         """Updates an integration.
 
         integration_key - The key of the integration to update. (required)
         name - The name of the integration (optional)
-        visual_style - (optional, default 'default')
-                       See adminapi docs for possible values.
+        visual_style - Deprecated; ignored if specified.
         greeting - Voice greeting (optional, default '')
         notes - internal use (optional, uses default setting)
         enroll_policy - <'enroll'|'allow'|'deny'> (optional, default 'enroll')
@@ -2833,7 +2836,7 @@ def update_integration(self,
 
         """
         integration_key = urllib.parse.quote_plus(str(integration_key))
-        path = '/admin/v2/integrations/%s' % integration_key
+        path = '/admin/v3/integrations/%s' % integration_key
         params = {}
         if name is not None:
             params['name'] = name
@@ -2877,6 +2880,8 @@ def update_integration(self,
             params['self_service_allowed'] = '1' if self_service_allowed else '0'
         if sso is not None:
             params['sso'] = sso
+        if user_access is not None:
+            params['user_access'] = user_access
 
         if not params:
             raise TypeError("No new values were provided")


Test patch:
diff --git a/tests/admin/test_integration.py b/tests/admin/test_integration.py
index 4a63528..4f1aca5 100644
--- a/tests/admin/test_integration.py
+++ b/tests/admin/test_integration.py
@@ -16,7 +16,7 @@ def test_get_integration(self):
         (uri, args) = response['uri'].split('?')
 
         self.assertEqual(response['method'], 'GET')
-        self.assertEqual(uri, '/admin/v2/integrations/{}'.format(self.integration_key))
+        self.assertEqual(uri, '/admin/v3/integrations/{}'.format(self.integration_key))
         self.assertEqual(util.params_to_dict(args), {'account_id': [self.client.account_id]})
 
     def test_delete_integration(self):
@@ -24,7 +24,7 @@ def test_delete_integration(self):
         (uri, args) = response['uri'].split('?')
 
         self.assertEqual(response['method'], 'DELETE')
-        self.assertEqual(uri, '/admin/v2/integrations/{}'.format(self.integration_key))
+        self.assertEqual(uri, '/admin/v3/integrations/{}'.format(self.integration_key))
         self.assertEqual(util.params_to_dict(args), {'account_id': [self.client.account_id]})
 
     def test_create_integration(self):
@@ -38,7 +38,7 @@ def test_create_integration(self):
         )
 
         self.assertEqual(response['method'], 'POST')
-        self.assertEqual(response['uri'], '/admin/v2/integrations')
+        self.assertEqual(response['uri'], '/admin/v3/integrations')
         self.assertEqual(json.loads(response['body']),
             {
                 "account_id": self.client.account_id,
@@ -63,7 +63,7 @@ def test_update_integration_success(self):
         )
 
         self.assertEqual(response['method'], 'POST')
-        self.assertEqual(response['uri'], '/admin/v2/integrations/{}'.format(self.integration_key))
+        self.assertEqual(response['uri'], '/admin/v3/integrations/{}'.format(self.integration_key))
         self.assertEqual(json.loads(response['body']),
             {
                 "account_id": self.client.account_id,
diff --git a/tests/admin/test_integrations.py b/tests/admin/test_integrations.py
index 59052c2..c0d061a 100644
--- a/tests/admin/test_integrations.py
+++ b/tests/admin/test_integrations.py
@@ -11,7 +11,7 @@ def test_get_integrations_generator(self):
         response = next(generator)
         self.assertEqual(response['method'], 'GET')
         (uri, args) = response['uri'].split('?')
-        self.assertEqual(uri, '/admin/v2/integrations')
+        self.assertEqual(uri, '/admin/v3/integrations')
         self.assertEqual(
             util.params_to_dict(args),
             {
@@ -27,7 +27,7 @@ def test_get_integrations(self):
         response = response[0]
         self.assertEqual(response['method'], 'GET')
         (uri, args) = response['uri'].split('?')
-        self.assertEqual(uri, '/admin/v2/integrations')
+        self.assertEqual(uri, '/admin/v3/integrations')
         self.assertEqual(
             util.params_to_dict(args),
             {
@@ -43,7 +43,7 @@ def test_get_integrations_with_limit(self):
         response = response[0]
         self.assertEqual(response['method'], 'GET')
         (uri, args) = response['uri'].split('?')
-        self.assertEqual(uri, '/admin/v2/integrations')
+        self.assertEqual(uri, '/admin/v3/integrations')
         self.assertEqual(
             util.params_to_dict(args),
             {
@@ -59,7 +59,7 @@ def test_get_integrations_with_limit_offset(self):
         response = response[0]
         self.assertEqual(response['method'], 'GET')
         (uri, args) = response['uri'].split('?')
-        self.assertEqual(uri, '/admin/v2/integrations')
+        self.assertEqual(uri, '/admin/v3/integrations')
         self.assertEqual(
             util.params_to_dict(args),
             {
@@ -75,7 +75,7 @@ def test_get_integrations_with_offset(self):
         response = response[0]
         self.assertEqual(response['method'], 'GET')
         (uri, args) = response['uri'].split('?')
-        self.assertEqual(uri, '/admin/v2/integrations')
+        self.assertEqual(uri, '/admin/v3/integrations')
         self.assertEqual(
             util.params_to_dict(args),
             {