Repository: mollie/mollie-api-python

Issue:
Python requests adds params to querystring, where it should be in the body for non-safe HTTP methods (POST/PATCH/DELETE/etc)
## Specifications

  - API Version: 3.6.0

## Describe the issue

See https://github.com/psf/requests/blob/eea3bbf9ac635f465ee6c9903dc57c677952dafd/src/requests/api.py#L19
the params kwargs goes into the query string, whereas the data goes into the body.

I think this is the issue why I get: 
```
Non-existent query parameter "testmode" for this API call. Did you mean: "include"?
```
When setting testmode to True.

For your reference this is where it goes wrong in the code: https://github.com/mollie/mollie-api-python/blob/f5464e4c3ab352ed874ac5a894f85c80c58df0c2/mollie/api/client.py#L180

Discussion:

Patch:
diff --git a/mollie/api/client.py b/mollie/api/client.py
index 82d4d20..56c169a 100644
--- a/mollie/api/client.py
+++ b/mollie/api/client.py
@@ -163,6 +163,7 @@ class Client(object):
         path: str,
         data: Optional[Dict[str, Any]],
         params: Optional[Dict[str, Any]],
+        http_method: str,
     ) -> Tuple[str, str, Optional[Dict[str, Any]]]:
         if path.startswith(f"{self.api_endpoint}/{self.api_version}"):
             url = path
@@ -170,6 +171,9 @@ class Client(object):
             url = f"{self.api_endpoint}/{self.api_version}/{path}"
 
         payload = ""
+
+        data, params = self._get_testmode(data, params, http_method)
+
         if data is not None:
             try:
                 payload = json.dumps(data)
@@ -178,10 +182,6 @@ class Client(object):
 
         if params is None:
             params = {}
-        if self.testmode and "testmode" not in params:
-            if not (self.api_key.startswith("access_") or hasattr(self, "_oauth_client")):
-                raise RequestSetupError("Configuring testmode only works with access_token or OAuth authorization")
-            params["testmode"] = "true"
 
         querystring = generate_querystring(params)
         if querystring:
@@ -190,6 +190,24 @@ class Client(object):
 
         return url, payload, params
 
+    def _get_testmode(self, data, params, http_method):
+        if self.testmode or (params and "testmode" in params):
+            if not (self.api_key.startswith("access_") or hasattr(self, "_oauth_client")):
+                raise RequestSetupError("Configuring testmode only works with access_token or OAuth authorization")
+
+            # Add to params if we're dealing with a GET request, for any other request add to data.
+            # If testmode is passed in the params, we're always overriding self.testmode. If
+            # self.testmode is True, simply pass in "true".
+            if http_method == "GET":
+                params["testmode"] = params.get("testmode") or "true"
+            elif not data or "testmode" not in data:
+                data["testmode"] = params.get("testmode") or "true"
+
+                # Delete from the params since it's not a valid parameter when the request is not GET
+                params.pop("testmode", None)
+
+        return data, params
+
     def _perform_http_call_apikey(
         self,
         http_method: str,
@@ -206,7 +224,7 @@ class Client(object):
             self._client.verify = True
             self._setup_retry()
 
-        url, payload, params = self._format_request_data(path, data, params)
+        url, payload, params = self._format_request_data(path, data, params, http_method)
         try:
             headers = {
                 "Accept": "application/json",
@@ -240,7 +258,7 @@ class Client(object):
         params: Optional[Dict[str, Any]] = None,
         idempotency_key: str = "",
     ) -> requests.Response:
-        url, payload, params = self._format_request_data(path, data, params)
+        url, payload, params = self._format_request_data(path, data, params, http_method)
         try:
             headers = {
                 "Accept": "application/json",


Test patch:
diff --git a/tests/test_api_client.py b/tests/test_api_client.py
index d8808fb..b04ddad 100644
--- a/tests/test_api_client.py
+++ b/tests/test_api_client.py
@@ -530,3 +530,19 @@ def test_update_customer_bad_request(client, response):
         )
 
     assert exc.value.idempotency_key == "test_idempotency_key"
+
+
+@pytest.mark.parametrize(
+    "testmode,params,http_method,expected",
+    [
+        (True, {}, "GET", ({}, {"testmode": "true"})),
+        (True, {}, "POST", ({"testmode": "true"}, {})),
+        (False, {"testmode": "true"}, "GET", ({}, {"testmode": "true"})),
+        (False, {"testmode": "true"}, "POST", ({"testmode": "true"}, {})),
+        (False, {"invalid": "something"}, "POST", ({}, {"invalid": "something"})),
+    ],
+)
+def test__get_testmode_sets_data_or_params_correctly(oauth_client, testmode, params, http_method, expected):
+    oauth_client.testmode = testmode
+    return_value = oauth_client._get_testmode({}, params, http_method)
+    assert return_value == expected