CVE-2024-29888: Insufficient Information (NVD-CWE-noinfo)
value: Saleor is an e-commerce platform that serves high-volume companies. When using `Pickup: Local stock only` click-and-collect as a delivery method in specific conditions the customer could overwrite the warehouse address with its own, which exposes its address as click-and-collect address. This issue has been patched in versions: `3.14.61`, `3.15.37`, `3.16.34`, `3.17.32`, `3.18.28`, `3.19.15`.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
Published: 2024-03-27T19:15Z
Repository: https://github.com/saleor/saleor
Language: Python
Files changed: saleor/checkout/error_codes.py, saleor/graphql/checkout/mutations/checkout_delivery_method_update.py, saleor/graphql/checkout/mutations/checkout_shipping_address_update.py, saleor/graphql/checkout/mutations/utils.py, saleor/graphql/checkout/tests/mutations/test_checkout_delivery_method_update.py, saleor/graphql/checkout/tests/mutations/test_checkout_shipping_address_update.py, saleor/graphql/schema.graphql
Commit date: 2024-03-01 10:02:18 +0100
Commit message: Fix  tax calculation for Click and Collect option. (#15505)


Diff: diff --git a/saleor/checkout/error_codes.py b/saleor/checkout/error_codes.py
index 9c9ccd9493..158bfc0884 100644
--- a/saleor/checkout/error_codes.py
+++ b/saleor/checkout/error_codes.py
@@ -30,10 +30,11 @@ class CheckoutErrorCode(Enum):
     EMAIL_NOT_SET = "email_not_set"
     NO_LINES = "no_lines"
     INACTIVE_PAYMENT = "inactive_payment"
     NON_EDITABLE_GIFT_LINE = "non_editable_gift_line"
     NON_REMOVABLE_GIFT_LINE = "non_removable_gift_line"
+    SHIPPING_CHANGE_FORBIDDEN = "shipping_change_forbidden"
 
 
 class OrderCreateFromCheckoutErrorCode(Enum):
     GRAPHQL_ERROR = "graphql_error"
     CHECKOUT_NOT_FOUND = "checkout_not_found"
diff --git a/saleor/graphql/checkout/mutations/checkout_delivery_method_update.py b/saleor/graphql/checkout/mutations/checkout_delivery_method_update.py
index a52752cc0a..b07aae6049 100644
--- a/saleor/graphql/checkout/mutations/checkout_delivery_method_update.py
+++ b/saleor/graphql/checkout/mutations/checkout_delivery_method_update.py
@@ -19,10 +19,11 @@ from ....checkout.utils import (
     set_external_shipping_id,
 )
 from ....shipping import interface as shipping_interface
 from ....shipping import models as shipping_models
 from ....shipping.utils import convert_to_shipping_method_data
+from ....warehouse import WarehouseClickAndCollectOption
 from ....warehouse import models as warehouse_models
 from ....webhook.const import APP_ID_PREFIX
 from ....webhook.event_types import WebhookEventAsyncType, WebhookEventSyncType
 from ...core import ResolveInfo
 from ...core.descriptions import ADDED_IN_31, ADDED_IN_34, DEPRECATED_IN_3X_INPUT
@@ -57,11 +58,13 @@ class CheckoutDeliveryMethodUpdate(BaseMutation):
         )
 
     class Meta:
         description = (
             "Updates the delivery method (shipping method or pick up point) "
-            "of the checkout." + ADDED_IN_31
+            "of the checkout. "
+            "Updates the checkout shipping_address for click and collect delivery "
+            "for a warehouse address. " + ADDED_IN_31
         )
         doc_category = DOC_CATEGORY_CHECKOUT
         error_type_class = CheckoutError
         webhook_events_info = [
             WebhookEventInfo(
@@ -236,28 +239,33 @@ class CheckoutDeliveryMethodUpdate(BaseMutation):
         *,
         shipping_method: Optional[ShippingMethod],
         external_shipping_method: Optional[shipping_interface.ShippingMethodData],
         collection_point: Optional[Warehouse],
     ) -> None:
+        checkout_fields_to_update = ["shipping_method", "collection_point"]
         checkout = checkout_info.checkout
         if external_shipping_method:
             set_external_shipping_id(
                 checkout=checkout, app_shipping_id=external_shipping_method.id
             )
         else:
             delete_external_shipping_id(checkout=checkout)
         checkout.shipping_method = shipping_method
         checkout.collection_point = collection_point
+        if (
+            collection_point is not None
+            and collection_point.click_and_collect_option
+            == WarehouseClickAndCollectOption.LOCAL_STOCK
+        ):
+            checkout.shipping_address = collection_point.address
+            checkout_info.shipping_address = collection_point.address
+            checkout_fields_to_update += ["shipping_address"]
         invalidate_prices_updated_fields = invalidate_checkout(
             checkout_info, lines, manager, save=False
         )
         checkout.save(
-            update_fields=[
-                "shipping_method",
-                "collection_point",
-            ]
-            + invalidate_prices_updated_fields
+            update_fields=checkout_fields_to_update + invalidate_prices_updated_fields
         )
         get_or_create_checkout_metadata(checkout).save()
         cls.call_event(manager.checkout_updated, checkout)
 
     @staticmethod
@@ -322,11 +330,10 @@ class CheckoutDeliveryMethodUpdate(BaseMutation):
                         code=CheckoutErrorCode.SHIPPING_NOT_REQUIRED.value,
                     )
                 }
             )
         type_name = cls._resolve_delivery_method_type(delivery_method_id)
-
         checkout_info = fetch_checkout_info(checkout, lines, manager)
         if type_name == "Warehouse":
             return cls.perform_on_collection_point(
                 info, delivery_method_id, checkout_info, lines, checkout, manager
             )
diff --git a/saleor/graphql/checkout/mutations/checkout_shipping_address_update.py b/saleor/graphql/checkout/mutations/checkout_shipping_address_update.py
index 536ba83eac..028035eb84 100644
--- a/saleor/graphql/checkout/mutations/checkout_shipping_address_update.py
+++ b/saleor/graphql/checkout/mutations/checkout_shipping_address_update.py
@@ -31,10 +31,11 @@ from ...core.utils import WebhookEventInfo
 from ...plugins.dataloaders import get_plugin_manager_promise
 from ...site.dataloaders import get_site_promise
 from ..types import Checkout
 from .checkout_create import CheckoutAddressValidationRules
 from .utils import (
+    ERROR_CC_ADDRESS_CHANGE_FORBIDDEN,
     ERROR_DOES_NOT_SHIP,
     check_lines_quantity,
     get_checkout,
     update_checkout_shipping_method_if_invalid,
 )
@@ -151,10 +152,20 @@ class CheckoutShippingAddressUpdate(AddressMetadataMixin, BaseMutation, I18nMixi
                         ERROR_DOES_NOT_SHIP,
                         code=CheckoutErrorCode.SHIPPING_NOT_REQUIRED.value,
                     )
                 }
             )
+        # prevent from changing the shipping address when click and collect is used.
+        if checkout.collection_point_id:
+            raise ValidationError(
+                {
+                    "shipping_address": ValidationError(
+                        ERROR_CC_ADDRESS_CHANGE_FORBIDDEN,
+                        code=CheckoutErrorCode.SHIPPING_CHANGE_FORBIDDEN.value,
+                    )
+                }
+            )
         address_validation_rules = validation_rules or {}
         shipping_address_instance = cls.validate_address(
             shipping_address,
             address_type=AddressType.SHIPPING,
             instance=checkout.shipping_address,
diff --git a/saleor/graphql/checkout/mutations/utils.py b/saleor/graphql/checkout/mutations/utils.py
index abd41b4991..3d1d3afa89 100644
--- a/saleor/graphql/checkout/mutations/utils.py
+++ b/saleor/graphql/checkout/mutations/utils.py
@@ -44,10 +44,14 @@ from ..types import Checkout
 if TYPE_CHECKING:
     from ...core.mutations import BaseMutation
 
 
 ERROR_DOES_NOT_SHIP = "This checkout doesn't need shipping"
+ERROR_CC_ADDRESS_CHANGE_FORBIDDEN = (
+    "Can't change shipping address manually. "
+    "For click and collect delivery, address is set to a warehouse address."
+)
 
 
 @dataclass
 class CheckoutLineData:
     variant_id: Optional[str] = None
diff --git a/saleor/graphql/checkout/tests/mutations/test_checkout_delivery_method_update.py b/saleor/graphql/checkout/tests/mutations/test_checkout_delivery_method_update.py
index 853f12c146..6a9f7f01f9 100644
--- a/saleor/graphql/checkout/tests/mutations/test_checkout_delivery_method_update.py
+++ b/saleor/graphql/checkout/tests/mutations/test_checkout_delivery_method_update.py
@@ -445,11 +445,120 @@ def test_checkout_delivery_method_update_shipping_zone_with_channel(
     assert not errors
 
     assert checkout.shipping_method == shipping_method
 
 
-@pytest.mark.parametrize("is_valid_delivery_method", [True, False])
+@pytest.mark.parametrize(
+    ("delivery_method", "node_name", "attribute_name"),
+    [
+        ("shipping_method", "ShippingMethod", "shipping_method"),
+    ],
+    indirect=("delivery_method",),
+)
+@patch(
+    "saleor.graphql.checkout.mutations.checkout_delivery_method_update."
+    "clean_delivery_method"
+)
+def test_checkout_delivery_method_update_valid_method_not_all_shipping_data(
+    mock_clean_delivery,
+    api_client,
+    delivery_method,
+    node_name,
+    attribute_name,
+    checkout_with_item_for_cc,
+):
+    # given
+    mock_clean_delivery.return_value = True
+
+    checkout = checkout_with_item_for_cc
+    checkout.shipping_address = Address.objects.create(country="US")
+    checkout.save()
+    manager = get_plugins_manager(allow_replica=False)
+    lines, _ = fetch_checkout_lines(checkout)
+    checkout_info = fetch_checkout_info(checkout, lines, manager)
+    shipping_method_data = convert_to_shipping_method_data(
+        delivery_method,
+        delivery_method.channel_listings.get(),
+    )
+    query = MUTATION_UPDATE_DELIVERY_METHOD
+    mock_clean_delivery.return_value = True
+
+    method_id = graphene.Node.to_global_id(node_name, delivery_method.id)
+
+    # when
+    response = api_client.post_graphql(
+        query, {"id": to_global_id_or_none(checkout), "deliveryMethodId": method_id}
+    )
+
+    # then
+    data = get_graphql_content(response)["data"]["checkoutDeliveryMethodUpdate"]
+    checkout.refresh_from_db()
+
+    mock_clean_delivery.assert_called_once_with(
+        checkout_info=checkout_info, lines=lines, method=shipping_method_data
+    )
+    errors = data["errors"]
+
+    assert not errors
+    assert getattr(checkout, attribute_name) == delivery_method
+
+
+@pytest.mark.parametrize(
+    "delivery_method, node_name, attribute_name",  # noqa: PT006
+    [
+        ("warehouse", "Warehouse", "collection_point"),
+    ],
+    indirect=("delivery_method",),
+)
+@patch(
+    "saleor.graphql.checkout.mutations.checkout_delivery_method_update."
+    "clean_delivery_method"
+)
+def test_checkout_delivery_method_update_valid_method_not_all_shipping_data_for_cc(
+    mock_clean_delivery,
+    api_client,
+    delivery_method,
+    node_name,
+    attribute_name,
+    checkout_with_item_for_cc,
+    warehouse_for_cc,
+):
+    # given
+    mock_clean_delivery.return_value = True
+    checkout_address = Address.objects.create(country="US")
+    checkout = checkout_with_item_for_cc
+    checkout.shipping_address = checkout_address
+    checkout.save()
+    manager = get_plugins_manager(allow_replica=False)
+    lines, _ = fetch_checkout_lines(checkout)
+    checkout_info = fetch_checkout_info(checkout, lines, manager)
+
+    shipping_method_data = delivery_method
+    checkout_info.shipping_address = shipping_method_data.address
+    query = MUTATION_UPDATE_DELIVERY_METHOD
+    mock_clean_delivery.return_value = True
+
+    method_id = graphene.Node.to_global_id(node_name, delivery_method.id)
+
+    # when
+    response = api_client.post_graphql(
+        query, {"id": to_global_id_or_none(checkout), "deliveryMethodId": method_id}
+    )
+
+    # then
+    data = get_graphql_content(response)["data"]["checkoutDeliveryMethodUpdate"]
+    checkout.refresh_from_db()
+
+    mock_clean_delivery.assert_called_once_with(
+        checkout_info=checkout_info, lines=lines, method=shipping_method_data
+    )
+    errors = data["errors"]
+    assert checkout.shipping_address == delivery_method.address
+    assert not errors
+    assert getattr(checkout, attribute_name) == delivery_method
+
+
 @pytest.mark.parametrize(
     ("delivery_method", "node_name", "attribute_name"),
     [
         ("warehouse", "Warehouse", "collection_point"),
         ("shipping_method", "ShippingMethod", "shipping_method"),
@@ -458,21 +567,20 @@ def test_checkout_delivery_method_update_shipping_zone_with_channel(
 )
 @patch(
     "saleor.graphql.checkout.mutations.checkout_delivery_method_update."
     "clean_delivery_method"
 )
-def test_checkout_delivery_method_update_with_not_all_required_shipping_address_data(
+def test_checkout_delivery_method_update_invalid_method_not_all_shipping_data(
     mock_clean_delivery,
     api_client,
     delivery_method,
     node_name,
     attribute_name,
     checkout_with_item_for_cc,
-    is_valid_delivery_method,
 ):
     # given
-    mock_clean_delivery.return_value = is_valid_delivery_method
+    mock_clean_delivery.return_value = False
 
     checkout = checkout_with_item_for_cc
     checkout.shipping_address = Address.objects.create(country="US")
     checkout.save()
     manager = get_plugins_manager(allow_replica=False)
@@ -484,11 +592,11 @@ def test_checkout_delivery_method_update_with_not_all_required_shipping_address_
         shipping_method_data = convert_to_shipping_method_data(
             delivery_method,
             delivery_method.channel_listings.get(),
         )
     query = MUTATION_UPDATE_DELIVERY_METHOD
-    mock_clean_delivery.return_value = is_valid_delivery_method
+    mock_clean_delivery.return_value = False
 
     method_id = graphene.Node.to_global_id(node_name, delivery_method.id)
 
     # when
     response = api_client.post_graphql(
@@ -501,47 +609,40 @@ def test_checkout_delivery_method_update_with_not_all_required_shipping_address_
 
     mock_clean_delivery.assert_called_once_with(
         checkout_info=checkout_info, lines=lines, method=shipping_method_data
     )
     errors = data["errors"]
-    if is_valid_delivery_method:
-        assert not errors
-        assert getattr(checkout, attribute_name) == delivery_method
-    else:
-        assert len(errors) == 1
-        assert errors[0]["field"] == "deliveryMethodId"
-        assert (
-            errors[0]["code"] == CheckoutErrorCode.DELIVERY_METHOD_NOT_APPLICABLE.name
-        )
-        assert checkout.shipping_method is None
-        assert checkout.collection_point is None
+
+    assert len(errors) == 1
+    assert errors[0]["field"] == "deliveryMethodId"
+    assert errors[0]["code"] == CheckoutErrorCode.DELIVERY_METHOD_NOT_APPLICABLE.name
+    assert checkout.shipping_method is None
+    assert checkout.collection_point is None
 
 
-@pytest.mark.parametrize("is_valid_delivery_method", [True, False])
 @pytest.mark.parametrize(
-    ("delivery_method", "node_name", "attribute_name"),
+    "delivery_method, node_name, attribute_name",  # noqa: PT006
     [
         ("warehouse", "Warehouse", "collection_point"),
         ("shipping_method", "ShippingMethod", "shipping_method"),
     ],
     indirect=("delivery_method",),
 )
 @patch(
     "saleor.graphql.checkout.mutations.checkout_delivery_method_update."
     "clean_delivery_method"
 )
-def test_checkout_delivery_method_update_with_not_valid_address_data(
+def test_checkout_delivery_method_update_invalid_with_not_valid_address_data(
     mock_clean_delivery,
     api_client,
     delivery_method,
     node_name,
     attribute_name,
     checkout_with_item_for_cc,
-    is_valid_delivery_method,
 ):
     # given
-    mock_clean_delivery.return_value = is_valid_delivery_method
+    mock_clean_delivery.return_value = False
 
     checkout = checkout_with_item_for_cc
     checkout.shipping_address = Address.objects.create(
         country="US",
         city="New York",
@@ -559,11 +660,11 @@ def test_checkout_delivery_method_update_with_not_valid_address_data(
         shipping_method_data = convert_to_shipping_method_data(
             delivery_method,
             delivery_method.channel_listings.get(),
         )
     query = MUTATION_UPDATE_DELIVERY_METHOD
-    mock_clean_delivery.return_value = is_valid_delivery_method
+    mock_clean_delivery.return_value = False
 
     method_id = graphene.Node.to_global_id(node_name, delivery_method.id)
 
     # when
     response = api_client.post_graphql(
@@ -576,21 +677,138 @@ def test_checkout_delivery_method_update_with_not_valid_address_data(
 
     mock_clean_delivery.assert_called_once_with(
         checkout_info=checkout_info, lines=lines, method=shipping_method_data
     )
     errors = data["errors"]
-    if is_valid_delivery_method:
-        assert not errors
-        assert getattr(checkout, attribute_name) == delivery_method
-    else:
-        assert len(errors) == 1
-        assert errors[0]["field"] == "deliveryMethodId"
-        assert (
-            errors[0]["code"] == CheckoutErrorCode.DELIVERY_METHOD_NOT_APPLICABLE.name
-        )
-        assert checkout.shipping_method is None
-        assert checkout.collection_point is None
+
+    assert len(errors) == 1
+    assert errors[0]["field"] == "deliveryMethodId"
+    assert errors[0]["code"] == CheckoutErrorCode.DELIVERY_METHOD_NOT_APPLICABLE.name
+    assert checkout.shipping_method is None
+    assert checkout.collection_point is None
+
+
+@pytest.mark.parametrize(
+    "delivery_method, node_name, attribute_name",  # noqa: PT006
+    [
+        ("shipping_method", "ShippingMethod", "shipping_method"),
+    ],
+    indirect=("delivery_method",),
+)
+@patch(
+    "saleor.graphql.checkout.mutations.checkout_delivery_method_update."
+    "clean_delivery_method"
+)
+def test_checkout_delivery_method_update_valid_with_not_valid_address_data(
+    mock_clean_delivery,
+    api_client,
+    delivery_method,
+    node_name,
+    attribute_name,
+    checkout_with_item_for_cc,
+):
+    # given
+    mock_clean_delivery.return_value = True
+
+    checkout = checkout_with_item_for_cc
+    checkout.shipping_address = Address.objects.create(
+        country="US",
+        city="New York",
+        city_area="ABC",
+        street_address_1="New street",
+        postal_code="53-601",
+    )
+    checkout.save()
+    manager = get_plugins_manager(allow_replica=False)
+    lines, _ = fetch_checkout_lines(checkout)
+    checkout_info = fetch_checkout_info(checkout, lines, manager)
+
+    shipping_method_data = convert_to_shipping_method_data(
+        delivery_method,
+        delivery_method.channel_listings.get(),
+    )
+    query = MUTATION_UPDATE_DELIVERY_METHOD
+    mock_clean_delivery.return_value = True
+
+    method_id = graphene.Node.to_global_id(node_name, delivery_method.id)
+
+    # when
+    response = api_client.post_graphql(
+        query, {"id": to_global_id_or_none(checkout), "deliveryMethodId": method_id}
+    )
+
+    # then
+    data = get_graphql_content(response)["data"]["checkoutDeliveryMethodUpdate"]
+    checkout.refresh_from_db()
+
+    mock_clean_delivery.assert_called_once_with(
+        checkout_info=checkout_info, lines=lines, method=shipping_method_data
+    )
+    errors = data["errors"]
+
+    assert not errors
+    assert getattr(checkout, attribute_name) == delivery_method
+
+
+@pytest.mark.parametrize(
+    "delivery_method, node_name, attribute_name",  # noqa: PT006
+    [
+        ("warehouse", "Warehouse", "collection_point"),
+    ],
+    indirect=("delivery_method",),
+)
+@patch(
+    "saleor.graphql.checkout.mutations.checkout_delivery_method_update."
+    "clean_delivery_method"
+)
+def test_checkout_delivery_method_update_valid_with_not_valid_address_data_for_cc(
+    mock_clean_delivery,
+    api_client,
+    delivery_method,
+    node_name,
+    attribute_name,
+    checkout_with_item_for_cc,
+):
+    # given
+    mock_clean_delivery.return_value = True
+
+    checkout = checkout_with_item_for_cc
+    checkout.shipping_address = Address.objects.create(
+        country="US",
+        city="New York",
+        city_area="ABC",
+        street_address_1="New street",
+        postal_code="53-601",
+    )
+    checkout.save()
+    manager = get_plugins_manager(allow_replica=False)
+    lines, _ = fetch_checkout_lines(checkout)
+    checkout_info = fetch_checkout_info(checkout, lines, manager)
+
+    shipping_method_data = delivery_method
+    checkout_info.shipping_address = shipping_method_data.address
+    query = MUTATION_UPDATE_DELIVERY_METHOD
+    mock_clean_delivery.return_value = True
+
+    method_id = graphene.Node.to_global_id(node_name, delivery_method.id)
+
+    # when
+    response = api_client.post_graphql(
+        query, {"id": to_global_id_or_none(checkout), "deliveryMethodId": method_id}
+    )
+
+    # then
+    data = get_graphql_content(response)["data"]["checkoutDeliveryMethodUpdate"]
+    checkout.refresh_from_db()
+
+    mock_clean_delivery.assert_called_once_with(
+        checkout_info=checkout_info, lines=lines, method=shipping_method_data
+    )
+    errors = data["errors"]
+
+    assert not errors
+    assert getattr(checkout, attribute_name) == delivery_method
 
 
 def test_with_active_problems_flow(
     api_client,
     checkout_with_problems,
diff --git a/saleor/graphql/checkout/tests/mutations/test_checkout_shipping_address_update.py b/saleor/graphql/checkout/tests/mutations/test_checkout_shipping_address_update.py
index dace332377..ee3eadd1ca 100644
--- a/saleor/graphql/checkout/tests/mutations/test_checkout_shipping_address_update.py
+++ b/saleor/graphql/checkout/tests/mutations/test_checkout_shipping_address_update.py
@@ -987,5 +987,31 @@ def test_with_active_problems_flow(
     )
     content = get_graphql_content(response)
 
     # then
     assert not content["data"]["checkoutShippingAddressUpdate"]["errors"]
+
+
+def test_checkout_shipping_address_update_with_collection_point_already_set(
+    user_api_client,
+    checkout_with_item,
+    graphql_address_data,
+    warehouse_for_cc,
+):
+    checkout = checkout_with_item
+    checkout.collection_point_id = warehouse_for_cc.id
+    checkout.save(update_fields=["collection_point_id"])
+
+    shipping_address = graphql_address_data
+    variables = {
+        "id": to_global_id_or_none(checkout),
+        "shippingAddress": shipping_address,
+    }
+
+    response = user_api_client.post_graphql(
+        MUTATION_CHECKOUT_SHIPPING_ADDRESS_UPDATE, variables
+    )
+    content = get_graphql_content(response)
+    data = content["data"]["checkoutShippingAddressUpdate"]
+    errors = data["errors"]
+    assert errors[0]["code"] == CheckoutErrorCode.SHIPPING_CHANGE_FORBIDDEN.name
+    assert errors[0]["field"] == "shippingAddress"
diff --git a/saleor/graphql/schema.graphql b/saleor/graphql/schema.graphql
index e466409b03..1be8c0e71a 100644
--- a/saleor/graphql/schema.graphql
+++ b/saleor/graphql/schema.graphql
@@ -19054,11 +19054,11 @@ type Mutation {
     """
     token: UUID
   ): CheckoutShippingMethodUpdate @doc(category: "Checkout") @webhookEventsInfo(asyncEvents: [CHECKOUT_UPDATED], syncEvents: [SHIPPING_LIST_METHODS_FOR_CHECKOUT]) @deprecated(reason: "This field will be removed in Saleor 4.0. Use `checkoutDeliveryMethodUpdate` instead.")
 
   """
-  Updates the delivery method (shipping method or pick up point) of the checkout.
+  Updates the delivery method (shipping method or pick up point) of the checkout. Updates the checkout shipping_address for click and collect delivery for a warehouse address. 
   
   Added in Saleor 3.1.
   
   Triggers the following webhook events:
   - SHIPPING_LIST_METHODS_FOR_CHECKOUT (sync): Triggered when updating the checkout delivery method with the external one.
@@ -28895,10 +28895,11 @@ enum CheckoutErrorCode @doc(category: "Checkout") {
   EMAIL_NOT_SET
   NO_LINES
   INACTIVE_PAYMENT
   NON_EDITABLE_GIFT_LINE
   NON_REMOVABLE_GIFT_LINE
+  SHIPPING_CHANGE_FORBIDDEN
 }
 
 """
 Update billing address in the existing checkout.
 
@@ -29331,11 +29332,11 @@ type CheckoutShippingMethodUpdate @doc(category: "Checkout") @webhookEventsInfo(
   checkoutErrors: [CheckoutError!]! @deprecated(reason: "This field will be removed in Saleor 4.0. Use `errors` field instead.")
   errors: [CheckoutError!]!
 }
 
 """
-Updates the delivery method (shipping method or pick up point) of the checkout.
+Updates the delivery method (shipping method or pick up point) of the checkout. Updates the checkout shipping_address for click and collect delivery for a warehouse address. 
 
 Added in Saleor 3.1.
 
 Triggers the following webhook events:
 - SHIPPING_LIST_METHODS_FOR_CHECKOUT (sync): Triggered when updating the checkout delivery method with the external one.

Vulnerable code:             "of the checkout." + ADDED_IN_31
            update_fields=[
                "shipping_method",
                "collection_point",
            ]
            + invalidate_prices_updated_fields

@pytest.mark.parametrize("is_valid_delivery_method", [True, False])
def test_checkout_delivery_method_update_with_not_all_required_shipping_address_data(
    is_valid_delivery_method,
    mock_clean_delivery.return_value = is_valid_delivery_method
    mock_clean_delivery.return_value = is_valid_delivery_method
    if is_valid_delivery_method:
        assert not errors
        assert getattr(checkout, attribute_name) == delivery_method
    else:
        assert len(errors) == 1
        assert errors[0]["field"] == "deliveryMethodId"
        assert (
            errors[0]["code"] == CheckoutErrorCode.DELIVERY_METHOD_NOT_APPLICABLE.name
        )
        assert checkout.shipping_method is None
        assert checkout.collection_point is None
@pytest.mark.parametrize("is_valid_delivery_method", [True, False])
    ("delivery_method", "node_name", "attribute_name"),
def test_checkout_delivery_method_update_with_not_valid_address_data(
    is_valid_delivery_method,
    mock_clean_delivery.return_value = is_valid_delivery_method
    mock_clean_delivery.return_value = is_valid_delivery_method
    if is_valid_delivery_method:
        assert not errors
        assert getattr(checkout, attribute_name) == delivery_method
    else:
        assert len(errors) == 1
        assert errors[0]["field"] == "deliveryMethodId"
        assert (
            errors[0]["code"] == CheckoutErrorCode.DELIVERY_METHOD_NOT_APPLICABLE.name
        )
        assert checkout.shipping_method is None
        assert checkout.collection_point is None
  Updates the delivery method (shipping method or pick up point) of the checkout.
Updates the delivery method (shipping method or pick up point) of the checkout.
Fixed code:     SHIPPING_CHANGE_FORBIDDEN = "shipping_change_forbidden"
from ....warehouse import WarehouseClickAndCollectOption
            "of the checkout. "
            "Updates the checkout shipping_address for click and collect delivery "
            "for a warehouse address. " + ADDED_IN_31
        checkout_fields_to_update = ["shipping_method", "collection_point"]
        if (
            collection_point is not None
            and collection_point.click_and_collect_option
            == WarehouseClickAndCollectOption.LOCAL_STOCK
        ):
            checkout.shipping_address = collection_point.address
            checkout_info.shipping_address = collection_point.address
            checkout_fields_to_update += ["shipping_address"]
            update_fields=checkout_fields_to_update + invalidate_prices_updated_fields
    ERROR_CC_ADDRESS_CHANGE_FORBIDDEN,
        # prevent from changing the shipping address when click and collect is used.
        if checkout.collection_point_id:
            raise ValidationError(
                {
                    "shipping_address": ValidationError(
                        ERROR_CC_ADDRESS_CHANGE_FORBIDDEN,
                        code=CheckoutErrorCode.SHIPPING_CHANGE_FORBIDDEN.value,
                    )
                }
            )
ERROR_CC_ADDRESS_CHANGE_FORBIDDEN = (
    "Can't change shipping address manually. "
    "For click and collect delivery, address is set to a warehouse address."
)
@pytest.mark.parametrize(
    ("delivery_method", "node_name", "attribute_name"),
    [
        ("shipping_method", "ShippingMethod", "shipping_method"),
    ],
    indirect=("delivery_method",),
)
@patch(
    "saleor.graphql.checkout.mutations.checkout_delivery_method_update."
    "clean_delivery_method"
)
def test_checkout_delivery_method_update_valid_method_not_all_shipping_data(
    mock_clean_delivery,
    api_client,
    delivery_method,
    node_name,
    attribute_name,
    checkout_with_item_for_cc,
):
    # given
    mock_clean_delivery.return_value = True

    checkout = checkout_with_item_for_cc
    checkout.shipping_address = Address.objects.create(country="US")
    checkout.save()
    manager = get_plugins_manager(allow_replica=False)
    lines, _ = fetch_checkout_lines(checkout)
    checkout_info = fetch_checkout_info(checkout, lines, manager)
    shipping_method_data = convert_to_shipping_method_data(
        delivery_method,
        delivery_method.channel_listings.get(),
    )
    query = MUTATION_UPDATE_DELIVERY_METHOD
    mock_clean_delivery.return_value = True

    method_id = graphene.Node.to_global_id(node_name, delivery_method.id)

    # when
    response = api_client.post_graphql(
        query, {"id": to_global_id_or_none(checkout), "deliveryMethodId": method_id}
    )

    # then
    data = get_graphql_content(response)["data"]["checkoutDeliveryMethodUpdate"]
    checkout.refresh_from_db()

    mock_clean_delivery.assert_called_once_with(
        checkout_info=checkout_info, lines=lines, method=shipping_method_data
    )
    errors = data["errors"]

    assert not errors
    assert getattr(checkout, attribute_name) == delivery_method


@pytest.mark.parametrize(
    "delivery_method, node_name, attribute_name",  # noqa: PT006
    [
        ("warehouse", "Warehouse", "collection_point"),
    ],
    indirect=("delivery_method",),
)
@patch(
    "saleor.graphql.checkout.mutations.checkout_delivery_method_update."
    "clean_delivery_method"
)
def test_checkout_delivery_method_update_valid_method_not_all_shipping_data_for_cc(
    mock_clean_delivery,
    api_client,
    delivery_method,
    node_name,
    attribute_name,
    checkout_with_item_for_cc,
    warehouse_for_cc,
):
    # given
    mock_clean_delivery.return_value = True
    checkout_address = Address.objects.create(country="US")
    checkout = checkout_with_item_for_cc
    checkout.shipping_address = checkout_address
    checkout.save()
    manager = get_plugins_manager(allow_replica=False)
    lines, _ = fetch_checkout_lines(checkout)
    checkout_info = fetch_checkout_info(checkout, lines, manager)

    shipping_method_data = delivery_method
    checkout_info.shipping_address = shipping_method_data.address
    query = MUTATION_UPDATE_DELIVERY_METHOD
    mock_clean_delivery.return_value = True

    method_id = graphene.Node.to_global_id(node_name, delivery_method.id)

    # when
    response = api_client.post_graphql(
        query, {"id": to_global_id_or_none(checkout), "deliveryMethodId": method_id}
    )

    # then
    data = get_graphql_content(response)["data"]["checkoutDeliveryMethodUpdate"]
    checkout.refresh_from_db()

    mock_clean_delivery.assert_called_once_with(
        checkout_info=checkout_info, lines=lines, method=shipping_method_data
    )
    errors = data["errors"]
    assert checkout.shipping_address == delivery_method.address
    assert not errors
    assert getattr(checkout, attribute_name) == delivery_method


def test_checkout_delivery_method_update_invalid_method_not_all_shipping_data(
    mock_clean_delivery.return_value = False
    mock_clean_delivery.return_value = False

    assert len(errors) == 1
    assert errors[0]["field"] == "deliveryMethodId"
    assert errors[0]["code"] == CheckoutErrorCode.DELIVERY_METHOD_NOT_APPLICABLE.name
    assert checkout.shipping_method is None
    assert checkout.collection_point is None
    "delivery_method, node_name, attribute_name",  # noqa: PT006
def test_checkout_delivery_method_update_invalid_with_not_valid_address_data(
    mock_clean_delivery.return_value = False
    mock_clean_delivery.return_value = False

    assert len(errors) == 1
    assert errors[0]["field"] == "deliveryMethodId"
    assert errors[0]["code"] == CheckoutErrorCode.DELIVERY_METHOD_NOT_APPLICABLE.name
    assert checkout.shipping_method is None
    assert checkout.collection_point is None


@pytest.mark.parametrize(
    "delivery_method, node_name, attribute_name",  # noqa: PT006
    [
        ("shipping_method", "ShippingMethod", "shipping_method"),
    ],
    indirect=("delivery_method",),
)
@patch(
    "saleor.graphql.checkout.mutations.checkout_delivery_method_update."
    "clean_delivery_method"
)
def test_checkout_delivery_method_update_valid_with_not_valid_address_data(
    mock_clean_delivery,
    api_client,
    delivery_method,
    node_name,
    attribute_name,
    checkout_with_item_for_cc,
):
    # given
    mock_clean_delivery.return_value = True

    checkout = checkout_with_item_for_cc
    checkout.shipping_address = Address.objects.create(
        country="US",
        city="New York",
        city_area="ABC",
        street_address_1="New street",
        postal_code="53-601",
    )
    checkout.save()
    manager = get_plugins_manager(allow_replica=False)
    lines, _ = fetch_checkout_lines(checkout)
    checkout_info = fetch_checkout_info(checkout, lines, manager)

    shipping_method_data = convert_to_shipping_method_data(
        delivery_method,
        delivery_method.channel_listings.get(),
    )
    query = MUTATION_UPDATE_DELIVERY_METHOD
    mock_clean_delivery.return_value = True

    method_id = graphene.Node.to_global_id(node_name, delivery_method.id)

    # when
    response = api_client.post_graphql(
        query, {"id": to_global_id_or_none(checkout), "deliveryMethodId": method_id}
    )

    # then
    data = get_graphql_content(response)["data"]["checkoutDeliveryMethodUpdate"]
    checkout.refresh_from_db()

    mock_clean_delivery.assert_called_once_with(
        checkout_info=checkout_info, lines=lines, method=shipping_method_data
    )
    errors = data["errors"]

    assert not errors
    assert getattr(checkout, attribute_name) == delivery_method


@pytest.mark.parametrize(
    "delivery_method, node_name, attribute_name",  # noqa: PT006
    [
        ("warehouse", "Warehouse", "collection_point"),
    ],
    indirect=("delivery_method",),
)
@patch(
    "saleor.graphql.checkout.mutations.checkout_delivery_method_update."
    "clean_delivery_method"
)
def test_checkout_delivery_method_update_valid_with_not_valid_address_data_for_cc(
    mock_clean_delivery,
    api_client,
    delivery_method,
    node_name,
    attribute_name,
    checkout_with_item_for_cc,
):
    # given
    mock_clean_delivery.return_value = True

    checkout = checkout_with_item_for_cc
    checkout.shipping_address = Address.objects.create(
        country="US",
        city="New York",
        city_area="ABC",
        street_address_1="New street",
        postal_code="53-601",
    )
    checkout.save()
    manager = get_plugins_manager(allow_replica=False)
    lines, _ = fetch_checkout_lines(checkout)
    checkout_info = fetch_checkout_info(checkout, lines, manager)

    shipping_method_data = delivery_method
    checkout_info.shipping_address = shipping_method_data.address
    query = MUTATION_UPDATE_DELIVERY_METHOD
    mock_clean_delivery.return_value = True

    method_id = graphene.Node.to_global_id(node_name, delivery_method.id)

    # when
    response = api_client.post_graphql(
        query, {"id": to_global_id_or_none(checkout), "deliveryMethodId": method_id}
    )

    # then
    data = get_graphql_content(response)["data"]["checkoutDeliveryMethodUpdate"]
    checkout.refresh_from_db()

    mock_clean_delivery.assert_called_once_with(
        checkout_info=checkout_info, lines=lines, method=shipping_method_data
    )
    errors = data["errors"]

    assert not errors
    assert getattr(checkout, attribute_name) == delivery_method


def test_checkout_shipping_address_update_with_collection_point_already_set(
    user_api_client,
    checkout_with_item,
    graphql_address_data,
    warehouse_for_cc,
):
    checkout = checkout_with_item
    checkout.collection_point_id = warehouse_for_cc.id
    checkout.save(update_fields=["collection_point_id"])

    shipping_address = graphql_address_data
    variables = {
        "id": to_global_id_or_none(checkout),
        "shippingAddress": shipping_address,
    }

    response = user_api_client.post_graphql(
        MUTATION_CHECKOUT_SHIPPING_ADDRESS_UPDATE, variables
    )
    content = get_graphql_content(response)
    data = content["data"]["checkoutShippingAddressUpdate"]
    errors = data["errors"]
    assert errors[0]["code"] == CheckoutErrorCode.SHIPPING_CHANGE_FORBIDDEN.name
    assert errors[0]["field"] == "shippingAddress"
  Updates the delivery method (shipping method or pick up point) of the checkout. Updates the checkout shipping_address for click and collect delivery for a warehouse address. 
  SHIPPING_CHANGE_FORBIDDEN
Updates the delivery method (shipping method or pick up point) of the checkout. Updates the checkout shipping_address for click and collect delivery for a warehouse address.