Repository: slackapi/python-slack-sdk (python)

Issue:
WebClient's paginated response iterator does not work for admin.conversations.search API
Usually, `next_cursor` in an API response exists under `response_metadata` property. However, only admin.conversations.search API method's response has it at the top level: https://github.com/slackapi/java-slack-sdk/blob/main/slack-api-client/src/main/java/com/slack/api/methods/response/admin/conversations/AdminConversationsSearchResponse.java#L19

Due to this inconsistency, this part in this Python SDK does not work for the API. https://github.com/slackapi/python-slack-sdk/blob/v3.7.0/slack_sdk/web/slack_response.py#L141-L145 We can improve the internals of the class to support the API too.

* TODOs
  * [ ] Add unit tests to reproduce the pattern
  * [ ] Change the logic

### Category (place an `x` in each of the `[ ]`)

- [x] **slack_sdk.web.WebClient (sync/async)** (Web API client)
- [ ] **slack_sdk.webhook.WebhookClient (sync/async)** (Incoming Webhook, response_url sender)
- [ ] **slack_sdk.models** (UI component builders)
- [ ] **slack_sdk.oauth** (OAuth Flow Utilities)
- [ ] **slack_sdk.socket_mode** (Socket Mode client)
- [ ] **slack_sdk.audit_logs** (Audit Logs API client)
- [ ] **slack_sdk.scim** (SCIM API client)
- [ ] **slack_sdk.rtm** (RTM client)
- [ ] **slack_sdk.signature** (Request Signature Verifier)

### Requirements

Please read the [Contributing guidelines](https://github.com/slackapi/python-slack-sdk/blob/main/.github/contributing.md) and [Code of Conduct](https://slackhq.github.io/code-of-conduct) before creating this issue or pull request. By submitting, you are agreeing to those rules.


Pull request:
Fix #1049 by correcting request body format and next_cursor extraction
## Summary

This pull request resolves #1049. While checking this issue, I found that the `__next__` iteration does not work if an API method use the JSON request body format. Thus, I've updated all other API methods that support pagination to use www-url-form-encoded request body.

### Category (place an `x` in each of the `[ ]`)

- [x] **slack_sdk.web.WebClient (sync/async)** (Web API client)
- [ ] **slack_sdk.webhook.WebhookClient (sync/async)** (Incoming Webhook, response_url sender)
- [ ] **slack_sdk.socket_mode** (Socket Mode client)
- [ ] **slack_sdk.signature** (Request Signature Verifier)
- [ ] **slack_sdk.oauth** (OAuth Flow Utilities)
- [ ] **slack_sdk.models** (UI component builders)
- [ ] **slack_sdk.scim** (SCIM API client)
- [ ] **slack_sdk.audit_logs** (Audit Logs API client)
- [ ] **slack_sdk.rtm_v2** (RTM client)
- [ ] `/docs-src` (Documents, have you run `./docs.sh`?)
- [ ] `/docs-src-v2` (Documents, have you run `./docs-v2.sh`?)
- [ ] `/tutorial` (PythOnBoardingBot tutorial)
- [ ] `tests`/`integration_tests` (Automated tests for this library)

## Requirements (place an `x` in each `[ ]`)

- [x] I've read and understood the [Contributing Guidelines](https://github.com/slackapi/python-slack-sdk/blob/main/.github/contributing.md) and have done my best effort to follow them.
- [x] I've read and agree to the [Code of Conduct](https://slackhq.github.io/code-of-conduct).
- [x] I've run `python3 -m venv .venv && source .venv/bin/activate && ./scripts/run_validation.sh` after making the changes.


Patch:
diff --git a/.gitignore b/.gitignore
index ea79bd8..68dd7ab 100644
--- a/.gitignore
+++ b/.gitignore
@@ -40,3 +40,5 @@ tmp.txt
 logs/
 
 .pytype/
+*.db
+.env*
diff --git a/slack_sdk/web/async_client.py b/slack_sdk/web/async_client.py
index 5bf0a18..c6d94e9 100644
--- a/slack_sdk/web/async_client.py
+++ b/slack_sdk/web/async_client.py
@@ -393,7 +393,7 @@ class AsyncWebClient(AsyncBaseClient):
 
     async def admin_conversations_search(self, **kwargs) -> AsyncSlackResponse:
         """Search for public or private channels in an Enterprise organization."""
-        return await self.api_call("admin.conversations.search", json=kwargs)
+        return await self.api_call("admin.conversations.search", params=kwargs)
 
     async def admin_conversations_convertToPrivate(
         self, *, channel_id: str, **kwargs
@@ -538,7 +538,7 @@ class AsyncWebClient(AsyncBaseClient):
 
         """
         kwargs.update({"channel_id": channel_id})
-        return await self.api_call("admin.conversations.getTeams", json=kwargs)
+        return await self.api_call("admin.conversations.getTeams", params=kwargs)
 
     async def admin_emoji_add(self, **kwargs) -> AsyncSlackResponse:
         """Add an emoji."""
@@ -668,11 +668,11 @@ class AsyncWebClient(AsyncBaseClient):
 
     async def admin_inviteRequests_approved_list(self, **kwargs) -> AsyncSlackResponse:
         """List all approved workspace invite requests."""
-        return await self.api_call("admin.inviteRequests.approved.list", json=kwargs)
+        return await self.api_call("admin.inviteRequests.approved.list", params=kwargs)
 
     async def admin_inviteRequests_denied_list(self, **kwargs) -> AsyncSlackResponse:
         """List all denied workspace invite requests."""
-        return await self.api_call("admin.inviteRequests.denied.list", json=kwargs)
+        return await self.api_call("admin.inviteRequests.denied.list", params=kwargs)
 
     async def admin_inviteRequests_deny(
         self, *, invite_request_id: str, **kwargs
@@ -687,7 +687,7 @@ class AsyncWebClient(AsyncBaseClient):
 
     async def admin_inviteRequests_list(self, **kwargs) -> AsyncSlackResponse:
         """List all pending workspace invite requests."""
-        return await self.api_call("admin.inviteRequests.list", json=kwargs)
+        return await self.api_call("admin.inviteRequests.list", params=kwargs)
 
     async def admin_teams_admins_list(
         self, *, team_id: str, **kwargs
@@ -716,7 +716,7 @@ class AsyncWebClient(AsyncBaseClient):
 
     async def admin_teams_list(self, **kwargs) -> AsyncSlackResponse:
         """List all teams on an Enterprise organization."""
-        return await self.api_call("admin.teams.list", json=kwargs)
+        return await self.api_call("admin.teams.list", params=kwargs)
 
     async def admin_teams_owners_list(
         self, *, team_id: str, **kwargs
@@ -904,7 +904,7 @@ class AsyncWebClient(AsyncBaseClient):
             team_id (str): ID of the team. e.g. 'T1234'
         """
         kwargs.update({"team_id": team_id})
-        return await self.api_call("admin.users.list", json=kwargs)
+        return await self.api_call("admin.users.list", params=kwargs)
 
     async def admin_users_remove(
         self, *, team_id: str, user_id: str, **kwargs
@@ -1392,7 +1392,7 @@ class AsyncWebClient(AsyncBaseClient):
 
     async def chat_scheduledMessages_list(self, **kwargs) -> AsyncSlackResponse:
         """Lists all scheduled messages."""
-        return await self.api_call("chat.scheduledMessages.list", json=kwargs)
+        return await self.api_call("chat.scheduledMessages.list", params=kwargs)
 
     async def conversations_archive(
         self, *, channel: str, **kwargs
diff --git a/slack_sdk/web/client.py b/slack_sdk/web/client.py
index 8087927..6900df3 100644
--- a/slack_sdk/web/client.py
+++ b/slack_sdk/web/client.py
@@ -364,7 +364,7 @@ class WebClient(BaseClient):
 
     def admin_conversations_search(self, **kwargs) -> SlackResponse:
         """Search for public or private channels in an Enterprise organization."""
-        return self.api_call("admin.conversations.search", json=kwargs)
+        return self.api_call("admin.conversations.search", params=kwargs)
 
     def admin_conversations_convertToPrivate(
         self, *, channel_id: str, **kwargs
@@ -505,7 +505,7 @@ class WebClient(BaseClient):
 
         """
         kwargs.update({"channel_id": channel_id})
-        return self.api_call("admin.conversations.getTeams", json=kwargs)
+        return self.api_call("admin.conversations.getTeams", params=kwargs)
 
     def admin_emoji_add(self, **kwargs) -> SlackResponse:
         """Add an emoji."""
@@ -631,11 +631,11 @@ class WebClient(BaseClient):
 
     def admin_inviteRequests_approved_list(self, **kwargs) -> SlackResponse:
         """List all approved workspace invite requests."""
-        return self.api_call("admin.inviteRequests.approved.list", json=kwargs)
+        return self.api_call("admin.inviteRequests.approved.list", params=kwargs)
 
     def admin_inviteRequests_denied_list(self, **kwargs) -> SlackResponse:
         """List all denied workspace invite requests."""
-        return self.api_call("admin.inviteRequests.denied.list", json=kwargs)
+        return self.api_call("admin.inviteRequests.denied.list", params=kwargs)
 
     def admin_inviteRequests_deny(
         self, *, invite_request_id: str, **kwargs
@@ -650,7 +650,7 @@ class WebClient(BaseClient):
 
     def admin_inviteRequests_list(self, **kwargs) -> SlackResponse:
         """List all pending workspace invite requests."""
-        return self.api_call("admin.inviteRequests.list", json=kwargs)
+        return self.api_call("admin.inviteRequests.list", params=kwargs)
 
     def admin_teams_admins_list(self, *, team_id: str, **kwargs) -> SlackResponse:
         """List all of the admins on a given workspace.
@@ -675,7 +675,7 @@ class WebClient(BaseClient):
 
     def admin_teams_list(self, **kwargs) -> SlackResponse:
         """List all teams on an Enterprise organization."""
-        return self.api_call("admin.teams.list", json=kwargs)
+        return self.api_call("admin.teams.list", params=kwargs)
 
     def admin_teams_owners_list(self, *, team_id: str, **kwargs) -> SlackResponse:
         """List all of the admins on a given workspace.
@@ -855,7 +855,7 @@ class WebClient(BaseClient):
             team_id (str): ID of the team. e.g. 'T1234'
         """
         kwargs.update({"team_id": team_id})
-        return self.api_call("admin.users.list", json=kwargs)
+        return self.api_call("admin.users.list", params=kwargs)
 
     def admin_users_remove(
         self, *, team_id: str, user_id: str, **kwargs
@@ -1313,7 +1313,7 @@ class WebClient(BaseClient):
 
     def chat_scheduledMessages_list(self, **kwargs) -> SlackResponse:
         """Lists all scheduled messages."""
-        return self.api_call("chat.scheduledMessages.list", json=kwargs)
+        return self.api_call("chat.scheduledMessages.list", params=kwargs)
 
     def conversations_archive(self, *, channel: str, **kwargs) -> SlackResponse:
         """Archives a conversation.
diff --git a/slack_sdk/web/internal_utils.py b/slack_sdk/web/internal_utils.py
index 39023fe..0f49988 100644
--- a/slack_sdk/web/internal_utils.py
+++ b/slack_sdk/web/internal_utils.py
@@ -219,7 +219,8 @@ def _next_cursor_is_present(data) -> bool:
     Returns:
         A boolean value.
     """
-    present = (
+    # Only admin.conversations.search returns next_cursor at the top level
+    present = ("next_cursor" in data and data["next_cursor"] != "") or (
         "response_metadata" in data
         and "next_cursor" in data["response_metadata"]
         and data["response_metadata"]["next_cursor"] != ""
diff --git a/slack_sdk/web/legacy_client.py b/slack_sdk/web/legacy_client.py
index d2d2998..91bab73 100644
--- a/slack_sdk/web/legacy_client.py
+++ b/slack_sdk/web/legacy_client.py
@@ -381,7 +381,7 @@ class LegacyWebClient(LegacyBaseClient):
 
     def admin_conversations_search(self, **kwargs) -> Union[Future, SlackResponse]:
         """Search for public or private channels in an Enterprise organization."""
-        return self.api_call("admin.conversations.search", json=kwargs)
+        return self.api_call("admin.conversations.search", params=kwargs)
 
     def admin_conversations_convertToPrivate(
         self, *, channel_id: str, **kwargs
@@ -522,7 +522,7 @@ class LegacyWebClient(LegacyBaseClient):
 
         """
         kwargs.update({"channel_id": channel_id})
-        return self.api_call("admin.conversations.getTeams", json=kwargs)
+        return self.api_call("admin.conversations.getTeams", params=kwargs)
 
     def admin_emoji_add(self, **kwargs) -> Union[Future, SlackResponse]:
         """Add an emoji."""
@@ -652,13 +652,13 @@ class LegacyWebClient(LegacyBaseClient):
         self, **kwargs
     ) -> Union[Future, SlackResponse]:
         """List all approved workspace invite requests."""
-        return self.api_call("admin.inviteRequests.approved.list", json=kwargs)
+        return self.api_call("admin.inviteRequests.approved.list", params=kwargs)
 
     def admin_inviteRequests_denied_list(
         self, **kwargs
     ) -> Union[Future, SlackResponse]:
         """List all denied workspace invite requests."""
-        return self.api_call("admin.inviteRequests.denied.list", json=kwargs)
+        return self.api_call("admin.inviteRequests.denied.list", params=kwargs)
 
     def admin_inviteRequests_deny(
         self, *, invite_request_id: str, **kwargs
@@ -673,7 +673,7 @@ class LegacyWebClient(LegacyBaseClient):
 
     def admin_inviteRequests_list(self, **kwargs) -> Union[Future, SlackResponse]:
         """List all pending workspace invite requests."""
-        return self.api_call("admin.inviteRequests.list", json=kwargs)
+        return self.api_call("admin.inviteRequests.list", params=kwargs)
 
     def admin_teams_admins_list(
         self, *, team_id: str, **kwargs
@@ -700,7 +700,7 @@ class LegacyWebClient(LegacyBaseClient):
 
     def admin_teams_list(self, **kwargs) -> Union[Future, SlackResponse]:
         """List all teams on an Enterprise organization."""
-        return self.api_call("admin.teams.list", json=kwargs)
+        return self.api_call("admin.teams.list", params=kwargs)
 
     def admin_teams_owners_list(
         self, *, team_id: str, **kwargs
@@ -886,7 +886,7 @@ class LegacyWebClient(LegacyBaseClient):
             team_id (str): ID of the team. e.g. 'T1234'
         """
         kwargs.update({"team_id": team_id})
-        return self.api_call("admin.users.list", json=kwargs)
+        return self.api_call("admin.users.list", params=kwargs)
 
     def admin_users_remove(
         self, *, team_id: str, user_id: str, **kwargs
@@ -1378,7 +1378,7 @@ class LegacyWebClient(LegacyBaseClient):
 
     def chat_scheduledMessages_list(self, **kwargs) -> Union[Future, SlackResponse]:
         """Lists all scheduled messages."""
-        return self.api_call("chat.scheduledMessages.list", json=kwargs)
+        return self.api_call("chat.scheduledMessages.list", params=kwargs)
 
     def conversations_archive(
         self, *, channel: str, **kwargs
diff --git a/slack_sdk/web/slack_response.py b/slack_sdk/web/slack_response.py
index 99a1ed2..7d47f14 100644
--- a/slack_sdk/web/slack_response.py
+++ b/slack_sdk/web/slack_response.py
@@ -142,7 +142,10 @@ class SlackResponse:
             params = self.req_args.get("params", {})
             if params is None:
                 params = {}
-            params.update({"cursor": self.data["response_metadata"]["next_cursor"]})
+            next_cursor = self.data.get("response_metadata", {}).get(
+                "next_cursor"
+            ) or self.data.get("next_cursor")
+            params.update({"cursor": next_cursor})
             self.req_args.update({"params": params})
 
             # This method sends a request in a synchronous way


Test patch:
diff --git a/tests/slack_sdk/web/test_web_client_issue_1049.py b/tests/slack_sdk/web/test_web_client_issue_1049.py
new file mode 100644
index 0000000..ebe3642
--- /dev/null
+++ b/tests/slack_sdk/web/test_web_client_issue_1049.py
@@ -0,0 +1,26 @@
+import json
+import unittest
+
+from slack_sdk.web import WebClient
+from tests.slack_sdk.web.mock_web_api_server import (
+    setup_mock_web_api_server,
+    cleanup_mock_web_api_server,
+)
+
+
+class TestWebClient_Issue_1049(unittest.TestCase):
+    def setUp(self):
+        setup_mock_web_api_server(self)
+
+    def tearDown(self):
+        cleanup_mock_web_api_server(self)
+
+    def test_the_pattern(self):
+        client = WebClient(
+            base_url="http://localhost:8888",
+            token="xoxb-admin_convo_pagination",
+        )
+        pages = []
+        for page in client.admin_conversations_search(query="announcement"):
+            pages.append(page)
+        self.assertEqual(len(pages), 2)
diff --git a/tests/slack_sdk_fixture/web_response_admin_convo_pagination.json b/tests/slack_sdk_fixture/web_response_admin_convo_pagination.json
new file mode 100644
index 0000000..6b7bd7e
--- /dev/null
+++ b/tests/slack_sdk_fixture/web_response_admin_convo_pagination.json
@@ -0,0 +1,28 @@
+{
+  "ok": true,
+  "conversations": [
+    {
+      "id": "C013T0FTKU3",
+      "name": "random",
+      "purpose": "test",
+      "member_count": 11,
+      "created": 1589700571,
+      "creator_id": "W111",
+      "is_private": false,
+      "is_archived": false,
+      "is_general": false,
+      "last_activity_ts": 1599130434000900,
+      "is_ext_shared": false,
+      "is_global_shared": false,
+      "is_org_default": false,
+      "is_org_mandatory": false,
+      "is_org_shared": false,
+      "is_frozen": false,
+      "internal_team_ids_count": 1,
+      "internal_team_ids_sample_team": "T111",
+      "pending_connected_team_ids": [],
+      "is_pending_ext_shared": false
+    }
+  ],
+  "next_cursor": "1"
+}
diff --git a/tests/slack_sdk_fixture/web_response_admin_convo_pagination_1.json b/tests/slack_sdk_fixture/web_response_admin_convo_pagination_1.json
new file mode 100644
index 0000000..3a6a2ff
--- /dev/null
+++ b/tests/slack_sdk_fixture/web_response_admin_convo_pagination_1.json
@@ -0,0 +1,5 @@
+{
+  "ok": true,
+  "conversations": [],
+  "next_cursor": ""
+}