Repository: getmoto/moto
Pull request #7457 (2024-03-11 18:58:46)

APIGateway: update_usage_plan() now supports some remove-operations
Follow-up to #7361 

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/getmoto/moto/pull/7457?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 95.80%. Comparing base [(`9aef694`)](https://app.codecov.io/gh/getmoto/moto/commit/9aef6944da6a6f1b84be99cefb34f1724e6ca58f?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) to head [(`b80ef85`)](https://app.codecov.io/gh/getmoto/moto/pull/7457?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).
> Report is 98 commits behind head on master.


<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #7457      +/-   ##
==========================================
- Coverage   95.88%   95.80%   -0.08%     
==========================================
  Files         843      854      +11     
  Lines       82578    83973    +1395     
==========================================
+ Hits        79178    80453    +1275     
- Misses       3400     3520     +120     
```

| [Flag](https://app.codecov.io/gh/getmoto/moto/pull/7457/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | Coverage Δ | |
|---|---|---|
| [servertests](https://app.codecov.io/gh/getmoto/moto/pull/7457/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `32.22% <21.05%> (-0.30%)` | :arrow_down: |
| [unittests](https://app.codecov.io/gh/getmoto/moto/pull/7457/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `95.77% <100.00%> (-0.08%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto#carryforward-flags-in-the-pull-request-comment) to find out more.


</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/getmoto/moto/pull/7457?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).


Patch:
diff --git a/moto/apigateway/models.py b/moto/apigateway/models.py
index 14cf41b39..90d6d49c8 100644
--- a/moto/apigateway/models.py
+++ b/moto/apigateway/models.py
@@ -859,32 +859,37 @@ class UsagePlan(BaseModel):
         self.tags = tags
 
     def to_json(self) -> Dict[str, Any]:
-        return {
+        resp = {
             "id": self.id,
             "name": self.name,
             "description": self.description,
             "apiStages": self.api_stages,
-            "throttle": self.throttle,
-            "quota": self.quota,
             "productCode": self.product_code,
             "tags": self.tags,
         }
+        if self.throttle:
+            resp["throttle"] = self.throttle
+        if self.quota:
+            resp["quota"] = self.quota
+        return resp
 
     def apply_patch_operations(self, patch_operations: List[Dict[str, Any]]) -> None:
         for op in patch_operations:
             path = op["path"]
-            value = op["value"]
             if op["op"] == "add":
+                value = op["value"]
                 if path == "/apiStages":
                     self.api_stages.append(
                         {"apiId": value.split(":")[0], "stage": value.split(":")[1]}
                     )
             if op["op"] == "replace":
+                value = op["value"]
                 if "/name" in path:
                     self.name = value
                 if "/description" in path:
                     self.description = value
             if op["op"] in ["add", "replace"]:
+                value = op["value"]
                 if "/productCode" in path:
                     self.product_code = value
                 if "/quota/limit" in path:
@@ -897,6 +902,18 @@ class UsagePlan(BaseModel):
                     self.throttle["rateLimit"] = int(value)
                 if "/throttle/burstLimit" in path:
                     self.throttle["burstLimit"] = int(value)
+            if op["op"] == "remove":
+                if path == "/apiStages":
+                    value = op["value"]
+                    self.api_stages.remove(
+                        {"apiId": value.split(":")[0], "stage": value.split(":")[1]}
+                    )
+                if path == "/productCode":
+                    self.product_code = None
+                if path == "/quota":
+                    self.quota.clear()
+                if path == "/throttle":
+                    self.throttle.clear()
 
 
 class RequestValidator(BaseModel):
@@ -2115,7 +2132,7 @@ class APIGatewayBackend(BaseBackend):
         The following PatchOperations are currently supported:
         add    : Everything except /apiStages/{apidId:stageName}/throttle/ and children
         replace: Everything except /apiStages/{apidId:stageName}/throttle/ and children
-        remove : Nothing yet
+        remove : Everything except /apiStages/{apidId:stageName}/throttle/ and children
         copy   : Nothing yet
         """
         if usage_plan_id not in self.usage_plans:


Test patch:
diff --git a/tests/test_apigateway/test_apigateway.py b/tests/test_apigateway/test_apigateway.py
index 1abc572c8..ea20f4758 100644
--- a/tests/test_apigateway/test_apigateway.py
+++ b/tests/test_apigateway/test_apigateway.py
@@ -1698,11 +1698,7 @@ def test_update_usage_plan():
     region_name = "us-west-2"
     client = boto3.client("apigateway", region_name=region_name)
 
-    payload = {
-        "name": "TEST-PLAN-2",
-        "description": "Description",
-        "tags": {"tag_key": "tag_value"},
-    }
+    payload = {"name": "TEST-PLAN-2", "description": "Description"}
     response = client.create_usage_plan(**payload)
     usage_plan_id = response["id"]
 
@@ -1741,6 +1737,21 @@ def test_update_usage_plan():
     assert response["description"] == "new-description"
     assert response["productCode"] == "new-productionCode"
 
+    remove = client.update_usage_plan(
+        usagePlanId=usage_plan_id,
+        patchOperations=[
+            {"op": "remove", "path": "/apiStages", "value": "foo:bar"},
+            {"op": "remove", "path": "/productCode"},
+            {"op": "remove", "path": "/quota"},
+            {"op": "remove", "path": "/throttle"},
+        ],
+    )
+    assert remove["apiStages"] == []
+    assert "productCode" not in remove
+    assert remove["description"] == "new-description"
+    assert "quota" not in remove
+    assert "throttle" not in remove
+
 
 @mock_aws
 def test_usage_plan_keys():