Repository: saleor/saleor

Issue:
Bug: Invalid plugin configuration for boolean type
### What are you trying to achieve?

For some boolean fields value is saved as a string instead of a boolean value. This happened when the new conf fields are added to the already configured plugin.

### Steps to reproduce the problem

1. Checkout to 3.8, and set up the db on this branch.
2. Checkout to main. Update avatax plugin configuration - set override global taxes to global.
3. Checkout to 3.8. Update the plugin again.
4. Checkout to the main branch and try to do the migration.
5. The error will be raised, as some boolean values are kept as string.

### What did you expect to happen?

The plugin configuration should be saved correctly, the mutation shouldn't fail.

### Logs

https://sentry.io/organizations/saleor/issues/3755997991/?project=6417854

### Environment

Saleor version: 3.1+



Discussion:

Resolving patch:
diff --git a/saleor/plugins/base_plugin.py b/saleor/plugins/base_plugin.py
--- a/saleor/plugins/base_plugin.py
+++ b/saleor/plugins/base_plugin.py
@@ -1017,39 +1017,55 @@ def _update_config_items(
         config_structure: dict = (
             cls.CONFIG_STRUCTURE if cls.CONFIG_STRUCTURE is not None else {}
         )
+        configuration_to_update_dict = {
+            c_field["name"]: c_field.get("value") for c_field in configuration_to_update
+        }
         for config_item in current_config:
-            for config_item_to_update in configuration_to_update:
-                config_item_name = config_item_to_update.get("name")
-                if config_item["name"] == config_item_name:
-                    new_value = config_item_to_update.get("value")
-                    item_type = config_structure.get(config_item_name, {}).get("type")
-                    if (
-                        item_type == ConfigurationTypeField.BOOLEAN
-                        and new_value
-                        and not isinstance(new_value, bool)
-                    ):
-                        new_value = new_value.lower() == "true"
-                    if item_type == ConfigurationTypeField.OUTPUT:
-                        # OUTPUT field is read only. No need to update it
-                        continue
-                    config_item.update([("value", new_value)])
+            new_value = configuration_to_update_dict.get(config_item["name"])
+            if new_value is None:
+                continue
+            item_type = config_structure.get(config_item["name"], {}).get("type")
+            new_value = cls._clean_configuration_value(item_type, new_value)
+            if new_value is not None:
+                config_item.update([("value", new_value)])
 
         # Get new keys that don't exist in current_config and extend it.
         current_config_keys = set(c_field["name"] for c_field in current_config)
-        configuration_to_update_dict = {
-            c_field["name"]: c_field["value"] for c_field in configuration_to_update
-        }
         missing_keys = set(configuration_to_update_dict.keys()) - current_config_keys
         for missing_key in missing_keys:
             if not config_structure.get(missing_key):
                 continue
+            item_type = config_structure.get(missing_key, {}).get("type")
+            new_value = cls._clean_configuration_value(
+                item_type, configuration_to_update_dict[missing_key]
+            )
+            if new_value is None:
+                continue
             current_config.append(
                 {
                     "name": missing_key,
-                    "value": configuration_to_update_dict[missing_key],
+                    "value": new_value,
                 }
             )
 
+    @classmethod
+    def _clean_configuration_value(cls, item_type, new_value):
+        """Clean the value that is saved in plugin configuration.
+
+        Change the string provided as boolean into the bool value.
+        Return None for Output type, as it's read only field.
+        """
+        if (
+            item_type == ConfigurationTypeField.BOOLEAN
+            and new_value
+            and not isinstance(new_value, bool)
+        ):
+            new_value = new_value.lower() == "true"
+        if item_type == ConfigurationTypeField.OUTPUT:
+            # OUTPUT field is read only. No need to update it
+            return
+        return new_value
+
     @classmethod
     def validate_plugin_configuration(
         cls, plugin_configuration: "PluginConfiguration", **kwargs
@@ -1082,11 +1098,12 @@ def save_plugin_configuration(
 
         cls.validate_plugin_configuration(plugin_configuration)
         cls.pre_save_plugin_configuration(plugin_configuration)
-        plugin_configuration.save()
 
         if plugin_configuration.configuration:
             # Let's add a translated descriptions and labels
             cls._append_config_structure(plugin_configuration.configuration)
+
+        plugin_configuration.save()
         return plugin_configuration
 
     @classmethod
diff --git a/saleor/tax/migrations/0002_add_default_tax_configs.py b/saleor/tax/migrations/0002_add_default_tax_configs.py
--- a/saleor/tax/migrations/0002_add_default_tax_configs.py
+++ b/saleor/tax/migrations/0002_add_default_tax_configs.py
@@ -73,10 +73,16 @@ def _get_tax_app():
         tc.tax_calculation_strategy = TaxCalculationStrategy.TAX_APP
 
         override_global_tax = config_dict.get("override_global_tax")
-        if override_global_tax:
+        # There might be a situation that the value is a string `true` or `false`,
+        # so we need to handle such situation too
+        if override_global_tax is True or override_global_tax == "true":
             include_taxes_in_prices = config_dict.get("include_taxes_in_prices")
             if include_taxes_in_prices is not None:
-                tc.prices_entered_with_tax = include_taxes_in_prices
+                tc.prices_entered_with_tax = (
+                    include_taxes_in_prices
+                    if isinstance(include_taxes_in_prices, bool)
+                    else include_taxes_in_prices.lower() == "true"
+                )
 
         avatax_tax_configs.append(tc)
     TaxConfiguration.objects.bulk_update(


Test patch:
diff --git a/saleor/graphql/plugins/tests/test_mutation_plugin_update.py b/saleor/graphql/plugins/tests/test_mutation_plugin_update.py
--- a/saleor/graphql/plugins/tests/test_mutation_plugin_update.py
+++ b/saleor/graphql/plugins/tests/test_mutation_plugin_update.py
@@ -117,6 +117,50 @@ def test_plugin_configuration_update(
     assert configuration[0]["value"] == updated_configuration_item["value"]
 
 
+def test_plugin_configuration_update_value_not_given(
+    staff_api_client_can_manage_plugins, settings
+):
+    settings.PLUGINS = ["saleor.plugins.tests.sample_plugins.PluginSample"]
+    manager = get_plugins_manager()
+    plugin = manager.get_plugin(PluginSample.PLUGIN_ID)
+    old_configuration = copy.deepcopy(plugin.configuration)
+
+    configuration_item = {"name": old_configuration[0]["name"]}
+    active = True
+
+    variables = {
+        "id": plugin.PLUGIN_ID,
+        "active": active,
+        "channel": None,
+        "configuration": [configuration_item],
+    }
+    response = staff_api_client_can_manage_plugins.post_graphql(
+        PLUGIN_UPDATE_MUTATION, variables
+    )
+    content = get_graphql_content(response)
+
+    plugin_data = content["data"]["pluginUpdate"]["plugin"]
+
+    assert plugin_data["name"] == plugin.PLUGIN_NAME
+    assert plugin_data["description"] == plugin.PLUGIN_DESCRIPTION
+
+    plugin = PluginConfiguration.objects.get(identifier=PluginSample.PLUGIN_ID)
+    assert plugin.active == active
+
+    first_configuration_item = plugin.configuration[0]
+    assert first_configuration_item["name"] == configuration_item["name"]
+    assert first_configuration_item["value"] == old_configuration[0]["value"]
+
+    second_configuration_item = plugin.configuration[1]
+    assert second_configuration_item["name"] == old_configuration[1]["name"]
+    assert second_configuration_item["value"] == old_configuration[1]["value"]
+
+    configuration = plugin_data["globalConfiguration"]["configuration"]
+    assert configuration is not None
+    assert configuration[0]["name"] == configuration_item["name"]
+    assert configuration[0]["value"] == old_configuration[0]["value"]
+
+
 @pytest.mark.parametrize(
     "active",
     [