Repository: saleor/saleor

Issue:
Braintree payment (mis)configuration 
### What I'm trying to achieve
Configure braintree payment configuration

### Steps to reproduce the problem
1. Go to dashboard payments configuration
2. Configure braintree gateway with invalid data
3. Save configuration
4. User is redirected to /dashboard/payments/ where the server returns 500 with AuthenticationError exception and cannot access the braintree configuration anymore to change the config again (Back button works).

### What I expected to happen
1. AuthenticationError should be raised as part of the form validation so that a save should not be permitted
and/or
2. get_payment_config() should fail gracefully (?)

### Screenshots
<!-- If applicable, add screenshots to help explain your problem. -->

**System information**
Operating system: Windows 10 Pro
Browser: Firefox 69



Discussion:
Hi, unfortunately, I wasn't able to reproduce this issue. Could you tell me what permissions does your user have? Do you use saleor-storefront?
Hi,

The user has superuser rights. I'm using the template based UI, not the graphql storefront. My understanding is that the view calls get_extensions_manager().list_payment_gateways() to get the list of gateways to show on screen. This calls the plugin's get_payment_config() which in the case of braintree also requests a token from braintree and thus raises an exception (since the config is invalid) that goes unhandled.

Check the full stack trace below:

INFO django.server "POST /dashboard/payments/configure/Braintree HTTP/1.1" 302 0 [PID:20176:Thread-30]
ERROR django.request Internal Server Error: /dashboard/payments/ [PID:20176:Thread-32]
Traceback (most recent call last):
  File "C:\[redacted]\saleor\.env\lib\site-packages\django\core\handlers\exception.py", line 34, in inner
    response = get_response(request)
  File "C:\[redacted]\saleor\.env\lib\site-packages\django\core\handlers\base.py", line 115, in _get_response
    response = self.process_exception_by_middleware(e, request)
  File "C:\[redacted]\saleor\.env\lib\site-packages\django\core\handlers\base.py", line 113, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "C:\[redacted]\saleor\.env\lib\site-packages\django\contrib\auth\decorators.py", line 21, in _wrapped_view
    return view_func(request, *args, **kwargs)
  File "C:\[redacted]\saleor\.env\lib\site-packages\django\contrib\auth\decorators.py", line 21, in _wrapped_view
    return view_func(request, *args, **kwargs)
  File "C:\[redacted]\saleor\saleor\dashboard\payment\views.py", line 18, in index
    for gateway in get_extensions_manager().list_payment_gateways(active_only=False)
  File "C:\[redacted]\saleor\saleor\extensions\manager.py", line 280, in list_payment_gateways
    for plugin in plugins
  File "C:\[redacted]\saleor\saleor\extensions\manager.py", line 281, in <listcomp>
    if payment_method in type(plugin).__dict__
  File "C:\[redacted]\saleor\saleor\extensions\manager.py", line 294, in __get_payment_config
    return self.__run_method_on_single_plugin(gtw, method_name, default_value)
  File "C:\[redacted]\saleor\saleor\extensions\manager.py", line 62, in __run_method_on_single_plugin
    returned_value = plugin_method(*args, **kwargs, previous_value=previous_value)
  File "C:\[redacted]\saleor\saleor\payment\gateways\braintree\plugin.py", line 33, in wrapped
    return fn(self, *args, **kwargs)
  File "C:\[redacted]\saleor\saleor\payment\gateways\braintree\plugin.py", line 213, in get_payment_config
    {"field": "client_token", "value": get_client_token(config=config)},
  File "C:\[redacted]\saleor\saleor\payment\gateways\braintree\__init__.py", line 119, in get_client_token
    return gateway.client_token.generate()
  File "C:\[redacted]\saleor\.env\lib\site-packages\braintree\client_token_gateway.py", line 24, in generate
    response = self.config.http().post(self.config.base_merchant_path() + "/client_token", params)
  File "C:\[redacted]\saleor\.env\lib\site-packages\braintree\util\http.py", line 58, in post
    return self._make_request("POST", path, Http.ContentType.Xml, params)
  File "C:\[redacted]\saleor\.env\lib\site-packages\braintree\util\http.py", line 88, in _make_request
    Http.raise_exception_from_status(status)
  File "C:\[redacted]\saleor\.env\lib\site-packages\braintree\util\http.py", line 37, in raise_exception_from_status
    raise AuthenticationError()
braintree.exceptions.authentication_error.AuthenticationError
ERROR django.request Internal Server Error: /dashboard/payments/ [PID:20176:Thread-32]
Traceback (most recent call last):
  File "C:\[redacted]\saleor\.env\lib\site-packages\django\core\handlers\exception.py", line 34, in inner
    response = get_response(request)
  File "C:\[redacted]\saleor\.env\lib\site-packages\django\core\handlers\base.py", line 115, in _get_response
    response = self.process_exception_by_middleware(e, request)
  File "C:\[redacted]\saleor\.env\lib\site-packages\django\core\handlers\base.py", line 113, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "C:\[redacted]\saleor\.env\lib\site-packages\django\contrib\auth\decorators.py", line 21, in _wrapped_view
    return view_func(request, *args, **kwargs)
  File "C:\[redacted]\saleor\.env\lib\site-packages\django\contrib\auth\decorators.py", line 21, in _wrapped_view
    return view_func(request, *args, **kwargs)
  File "C:\[redacted]\saleor\saleor\dashboard\payment\views.py", line 18, in index
    for gateway in get_extensions_manager().list_payment_gateways(active_only=False)
  File "C:\[redacted]\saleor\saleor\extensions\manager.py", line 280, in list_payment_gateways
    for plugin in plugins
  File "C:\[redacted]\saleor\saleor\extensions\manager.py", line 281, in <listcomp>
    if payment_method in type(plugin).__dict__
  File "C:\[redacted]\saleor\saleor\extensions\manager.py", line 294, in __get_payment_config
    return self.__run_method_on_single_plugin(gtw, method_name, default_value)
  File "C:\[redacted]\saleor\saleor\extensions\manager.py", line 62, in __run_method_on_single_plugin
    returned_value = plugin_method(*args, **kwargs, previous_value=previous_value)
  File "C:\[redacted]\saleor\saleor\payment\gateways\braintree\plugin.py", line 33, in wrapped
    return fn(self, *args, **kwargs)
  File "C:\[redacted]\saleor\saleor\payment\gateways\braintree\plugin.py", line 213, in get_payment_config
    {"field": "client_token", "value": get_client_token(config=config)},
  File "C:\[redacted]\saleor\saleor\payment\gateways\braintree\__init__.py", line 119, in get_client_token
    return gateway.client_token.generate()
  File "C:\[redacted]\saleor\.env\lib\site-packages\braintree\client_token_gateway.py", line 24, in generate
    response = self.config.http().post(self.config.base_merchant_path() + "/client_token", params)
  File "C:\[redacted]\saleor\.env\lib\site-packages\braintree\util\http.py", line 58, in post
    return self._make_request("POST", path, Http.ContentType.Xml, params)
  File "C:\[redacted]\saleor\.env\lib\site-packages\braintree\util\http.py", line 88, in _make_request
    Http.raise_exception_from_status(status)
  File "C:\[redacted]\saleor\.env\lib\site-packages\braintree\util\http.py", line 37, in raise_exception_from_status
    raise AuthenticationError()
braintree.exceptions.authentication_error.AuthenticationError
ERROR django.server "GET /dashboard/payments/ HTTP/1.1" 500 151147 [PID:20176:Thread-32]
ERROR django.server "GET /dashboard/payments/ HTTP/1.1" 500 151147 [PID:20176:Thread-32]
ERROR django.server "GET /dashboard/payments/ HTTP/1.1" 500 151147 [PID:20176:Thread-32]




Resolving patch:
diff --git a/saleor/dashboard/payment/views.py b/saleor/dashboard/payment/views.py
--- a/saleor/dashboard/payment/views.py
+++ b/saleor/dashboard/payment/views.py
@@ -13,11 +13,7 @@
 @staff_member_required
 @permission_required("extensions.manage_plugins")
 def index(request: "HttpRequest") -> TemplateResponse:
-    payment_gateways = [
-        gateway["name"]
-        for gateway in get_extensions_manager().list_payment_gateways(active_only=False)
-    ]
-    ctx = {"payment_gateways": payment_gateways}
+    ctx = {"payment_gateways": get_extensions_manager().list_payment_plugin_names()}
     return TemplateResponse(request, "dashboard/payments/index.html", ctx)
 
 
diff --git a/saleor/extensions/manager.py b/saleor/extensions/manager.py
--- a/saleor/extensions/manager.py
+++ b/saleor/extensions/manager.py
@@ -271,24 +271,33 @@ def list_payment_sources(
             )
         raise Exception(f"Payment plugin {gateway} is inaccessible!")
 
-    def list_payment_gateways(self, active_only: bool = True) -> List[dict]:
+    def get_active_plugins(self, plugins=None) -> List["BasePlugin"]:
+        if plugins is None:
+            plugins = self.plugins
+        return [
+            plugin
+            for plugin in plugins
+            if self.get_plugin_configuration(plugin.PLUGIN_NAME).active
+        ]
+
+    def list_payment_plugin_names(self, active_only: bool = False) -> List[str]:
         payment_method = "process_payment"
         plugins = self.plugins
         if active_only:
-            plugins = [
-                plugin
-                for plugin in plugins
-                if self.get_plugin_configuration(plugin.PLUGIN_NAME).active
-            ]
+            plugins = self.get_active_plugins()
         return [
-            {
-                "name": plugin.PLUGIN_NAME,
-                "config": self.__get_payment_config(plugin.PLUGIN_NAME),
-            }
+            plugin.PLUGIN_NAME
             for plugin in plugins
             if payment_method in type(plugin).__dict__
         ]
 
+    def list_payment_gateways(self, active_only: bool = True) -> List[dict]:
+        payment_plugins = self.list_payment_plugin_names(active_only=active_only)
+        return [
+            {"name": plugin_name, "config": self.__get_payment_config(plugin_name)}
+            for plugin_name in payment_plugins
+        ]
+
     def get_payment_template(self, gateway: str) -> str:
         method_name = "get_payment_template"
         default_value = None


Test patch: