Repository: slackapi/python-slackclient (python)

Issue:
SlackClient missing Web API methods
### Description

Describe your issue here.

### What type of issue is this? (place an `x` in one of the `[ ]`)
- [x] bug
- [ ] enhancement (feature request)
- [ ] question
- [ ] documentation related
- [ ] testing related
- [ ] discussion

### Requirements (place an `x` in each of the `[ ]`)
* [x ] I've read and understood the [Contributing guidelines](https://github.com/slackapi/python-slackclient/blob/master/.github/contributing.md) and have done my best effort to follow them.
* [ ] I've read and agree to the [Code of Conduct](https://slackhq.github.io/code-of-conduct).
* [ ] I've searched for any related issues and avoided creating a duplicate issue.

---

### Bug Report

Filling out the following details about bugs will help us solve your issue sooner.
The following methods are not available in the python API client. 

https://api.slack.com/methods/admin.apps.approved.list 
https://api.slack.com/methods/admin.apps.restricted.list
https://api.slack.com/methods/admin.conversations.setTeams 
https://api.slack.com/methods/admin.emoji.add 
https://api.slack.com/methods/admin.emoji.addAlias 
https://api.slack.com/methods/admin.emoji.list 
https://api.slack.com/methods/admin.emoji.remove 
https://api.slack.com/methods/admin.emoji.rename 
#### Reproducible in:

slackclient version: 2.5.0

python version: 3.7


Pull request description:
Fix #634 by adding missing method supports
###  Summary

This pull request fixes #634 by adding support for 9 admin methods. Also, I've added a test `test_web_client_coverage.py` to verify if there are no missing API methods.

### Requirements (place an `x` in each `[ ]`)

* [x] I've read and understood the [Contributing Guidelines](https://github.com/slackapi/python-slackclient/blob/master/.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).

Patch:
diff --git a/setup.py b/setup.py
index db31c57..3e01d1a 100644
--- a/setup.py
+++ b/setup.py
@@ -126,7 +126,7 @@ setup(
     packages=find_packages(
         exclude=["docs", "docs-src", "tests", "tests.*", "tutorial"]
     ),
-    install_requires=["aiohttp>3.5.2"],
+    install_requires=["aiohttp>3.5.2,<4.0.0"],
     extras_require={"optional": ["aiodns>1.0"]},
     setup_requires=["pytest-runner"],
     test_suite="tests",
diff --git a/slack/web/base_client.py b/slack/web/base_client.py
index f489a3f..6b78192 100644
--- a/slack/web/base_client.py
+++ b/slack/web/base_client.py
@@ -35,7 +35,7 @@ class BaseClient:
         session=None,
         headers: Optional[dict] = None,
     ):
-        self.token = token.strip()
+        self.token = None if token is None else token.strip()
         self.base_url = base_url
         self.timeout = timeout
         self.ssl = ssl
diff --git a/slack/web/client.py b/slack/web/client.py
index 22bfdc3..5438670 100644
--- a/slack/web/client.py
+++ b/slack/web/client.py
@@ -106,7 +106,33 @@ class WebClient(BaseClient):
 
     def admin_apps_restricted_list(self, **kwargs) -> Union[Future, SlackResponse]:
         """List restricted apps for an org or workspace."""
-        return self.api_call("admin.apps.restricted.list", http_verb="GET", params=kwargs)
+        return self.api_call(
+            "admin.apps.restricted.list", http_verb="GET", params=kwargs
+        )
+
+    def admin_conversations_setTeams(self, **kwargs) -> Union[Future, SlackResponse]:
+        """Set the workspaces in an Enterprise grid org that connect to a channel."""
+        return self.api_call("admin.conversations.setTeams", json=kwargs)
+
+    def admin_emoji_add(self, **kwargs) -> Union[Future, SlackResponse]:
+        """Add an emoji."""
+        return self.api_call("admin.emoji.add", http_verb="GET", params=kwargs)
+
+    def admin_emoji_addAlias(self, **kwargs) -> Union[Future, SlackResponse]:
+        """Add an emoji alias."""
+        return self.api_call("admin.emoji.addAlias", http_verb="GET", params=kwargs)
+
+    def admin_emoji_list(self, **kwargs) -> Union[Future, SlackResponse]:
+        """List emoji for an Enterprise Grid organization."""
+        return self.api_call("admin.emoji.list", http_verb="GET", params=kwargs)
+
+    def admin_emoji_remove(self, **kwargs) -> Union[Future, SlackResponse]:
+        """Remove an emoji across an Enterprise Grid organization."""
+        return self.api_call("admin.emoji.remove", http_verb="GET", params=kwargs)
+
+    def admin_emoji_rename(self, **kwargs) -> Union[Future, SlackResponse]:
+        """Rename an emoji."""
+        return self.api_call("admin.emoji.rename", http_verb="GET", params=kwargs)
 
     def admin_users_session_reset(
         self, *, user_id: str, **kwargs
@@ -197,6 +223,32 @@ class WebClient(BaseClient):
         kwargs.update({"team_id": team_id})
         return self.api_call("admin.teams.owners.list", http_verb="GET", params=kwargs)
 
+    def admin_teams_settings_info(
+        self, team_id: str, **kwargs
+    ) -> Union[Future, SlackResponse]:
+        """Fetch information about settings in a workspace
+
+        Args:
+            team_id (str): ID of the team.
+        """
+        kwargs.update({"team_id": team_id})
+        return self.api_call("admin.teams.settings.info", json=kwargs)
+
+    def admin_teams_settings_setDefaultChannels(
+        self, *, team_id: str, channel_ids: List[str], **kwargs
+    ) -> Union[Future, SlackResponse]:
+        """Set the default channels of a workspace.
+
+        Args:
+            team_id (str): ID of the team.
+            channel_ids (list): A list of channel_ids.
+                At least one channel is required. e.g. ['C1A2B3C4D', 'C26Z25Y24']
+        """
+        kwargs.update({"team_id": team_id, "channel_ids": ",".join(channel_ids)})
+        return self.api_call(
+            "admin.teams.settings.setDefaultChannels", http_verb="GET", params=kwargs
+        )
+
     def admin_teams_settings_setDescription(
         self, *, team_id: str, description: str, **kwargs
     ) -> Union[Future, SlackResponse]:
@@ -209,6 +261,19 @@ class WebClient(BaseClient):
         kwargs.update({"team_id": team_id, "description": description})
         return self.api_call("admin.teams.settings.setDescription", json=kwargs)
 
+    def admin_teams_settings_setDiscoverability(
+        self, *, team_id: str, discoverability: str, **kwargs
+    ) -> Union[Future, SlackResponse]:
+        """Sets the icon of a workspace.
+
+        Args:
+            team_id (str): ID of the team.
+            discoverability (str): This workspace's discovery setting.
+                It must be set to one of open, invite_only, closed, or unlisted.
+        """
+        kwargs.update({"team_id": team_id, "discoverability": discoverability})
+        return self.api_call("admin.teams.settings.setDiscoverability", json=kwargs)
+
     def admin_teams_settings_setIcon(
         self, *, team_id: str, image_url: str, **kwargs
     ) -> Union[Future, SlackResponse]:
@@ -306,7 +371,9 @@ class WebClient(BaseClient):
             team_id (str): ID of the team. e.g. 'T1234'
             user_id (str): The ID of the user to set an expiration for. e.g. 'W12345678'
         """
-        kwargs.update({"expiration_ts": expiration_ts, "team_id": team_id, "user_id": user_id})
+        kwargs.update(
+            {"expiration_ts": expiration_ts, "team_id": team_id, "user_id": user_id}
+        )
         return self.api_call("admin.users.setExpiration", json=kwargs)
 
     def admin_users_setOwner(


Test patch:
diff --git a/tests/web/test_web_client_coverage.py b/tests/web/test_web_client_coverage.py
new file mode 100644
index 0000000..eddf35e
--- /dev/null
+++ b/tests/web/test_web_client_coverage.py
@@ -0,0 +1,348 @@
+# Standard Imports
+import unittest
+
+# ThirdParty Imports
+import asyncio
+from aiohttp import web
+
+# Internal Imports
+import slack
+
+
+class TestWebClientCoverage(unittest.TestCase):
+    # as of March 16, 2020
+    # Can be fetched by running `var methodNames = [].slice.call(document.getElementsByClassName('bold')).map(e => e.text);console.log(methodNames.toString());console.log(methodNames.length);` on https://api.slack.com/methods
+    all_api_methods = "admin.apps.approve,admin.apps.restrict,admin.apps.approved.list,admin.apps.requests.list,admin.apps.restricted.list,admin.conversations.setTeams,admin.emoji.add,admin.emoji.addAlias,admin.emoji.list,admin.emoji.remove,admin.emoji.rename,admin.inviteRequests.approve,admin.inviteRequests.deny,admin.inviteRequests.list,admin.inviteRequests.approved.list,admin.inviteRequests.denied.list,admin.teams.admins.list,admin.teams.create,admin.teams.list,admin.teams.owners.list,admin.teams.settings.info,admin.teams.settings.setDefaultChannels,admin.teams.settings.setDescription,admin.teams.settings.setDiscoverability,admin.teams.settings.setIcon,admin.teams.settings.setName,admin.users.assign,admin.users.invite,admin.users.list,admin.users.remove,admin.users.setAdmin,admin.users.setExpiration,admin.users.setOwner,admin.users.setRegular,admin.users.session.reset,api.test,apps.permissions.info,apps.permissions.request,apps.permissions.resources.list,apps.permissions.scopes.list,apps.permissions.users.list,apps.permissions.users.request,apps.uninstall,auth.revoke,auth.test,bots.info,chat.delete,chat.deleteScheduledMessage,chat.getPermalink,chat.meMessage,chat.postEphemeral,chat.postMessage,chat.scheduleMessage,chat.unfurl,chat.update,chat.scheduledMessages.list,conversations.archive,conversations.close,conversations.create,conversations.history,conversations.info,conversations.invite,conversations.join,conversations.kick,conversations.leave,conversations.list,conversations.members,conversations.open,conversations.rename,conversations.replies,conversations.setPurpose,conversations.setTopic,conversations.unarchive,dialog.open,dnd.endDnd,dnd.endSnooze,dnd.info,dnd.setSnooze,dnd.teamInfo,emoji.list,files.comments.delete,files.delete,files.info,files.list,files.revokePublicURL,files.sharedPublicURL,files.upload,files.remote.add,files.remote.info,files.remote.list,files.remote.remove,files.remote.share,files.remote.update,migration.exchange,oauth.access,oauth.token,oauth.v2.access,pins.add,pins.list,pins.remove,reactions.add,reactions.get,reactions.list,reactions.remove,reminders.add,reminders.complete,reminders.delete,reminders.info,reminders.list,rtm.connect,rtm.start,search.all,search.files,search.messages,stars.add,stars.list,stars.remove,team.accessLogs,team.billableInfo,team.info,team.integrationLogs,team.profile.get,usergroups.create,usergroups.disable,usergroups.enable,usergroups.list,usergroups.update,usergroups.users.list,usergroups.users.update,users.conversations,users.deletePhoto,users.getPresence,users.identity,users.info,users.list,users.lookupByEmail,users.setActive,users.setPhoto,users.setPresence,users.profile.get,users.profile.set,views.open,views.publish,views.push,views.update,channels.archive,channels.create,channels.history,channels.info,channels.invite,channels.join,channels.kick,channels.leave,channels.list,channels.mark,channels.rename,channels.replies,channels.setPurpose,channels.setTopic,channels.unarchive,groups.archive,groups.create,groups.createChild,groups.history,groups.info,groups.invite,groups.kick,groups.leave,groups.list,groups.mark,groups.open,groups.rename,groups.replies,groups.setPurpose,groups.setTopic,groups.unarchive,im.close,im.history,im.list,im.mark,im.open,im.replies,mpim.close,mpim.history,mpim.list,mpim.mark,mpim.open,mpim.replies".split(
+        ","
+    )
+
+    api_methods_to_call = []
+
+    def setUp(self):
+        self.loop = asyncio.new_event_loop()
+        asyncio.set_event_loop(self.loop)
+        task = asyncio.ensure_future(self.mock_server(), loop=self.loop)
+        self.loop.run_until_complete(asyncio.wait_for(task, 0.3))
+        self.client = slack.WebClient(
+            token="xoxb-abc-123", base_url="http://localhost:8765", loop=self.loop
+        )
+        self.no_token_client = slack.WebClient(
+            base_url="http://localhost:8765", loop=self.loop
+        )
+        for api_method in self.all_api_methods:
+            if api_method.startswith("apps.") or api_method in [
+                "oauth.token",
+                "users.setActive",
+            ]:
+                continue
+            self.api_methods_to_call.append(api_method)
+
+    def tearDown(self):
+        self.loop.run_until_complete(self.site.stop())
+
+    async def mock_server(self):
+        app = web.Application()
+        for method_name in self.all_api_methods:
+            app.router.add_get(f"/{method_name}", self.handler)
+            app.router.add_post(f"/{method_name}", self.handler)
+        runner = web.AppRunner(app)
+        await runner.setup()
+        self.site = web.TCPSite(runner, "localhost", 8765)
+        await self.site.start()
+
+    async def handler(self, request):
+        content_type = request.content_type
+        assert content_type in [
+            "application/json",
+            "application/x-www-form-urlencoded",
+            "multipart/form-data",
+        ]
+        # This `api_method` is done
+        self.api_methods_to_call.remove(request.path.replace("/", ""))
+        return web.json_response({"ok": True})
+
+    def test_coverage(self):
+        for api_method in self.all_api_methods:
+            if self.api_methods_to_call.count(api_method) == 0:
+                continue
+            method_name = api_method.replace(".", "_")
+            method = getattr(self.client, method_name, None)
+
+            # Run the api calls with required arguments
+            if callable(method):
+                if method_name == "admin_apps_approve":
+                    method(app_id="AID123", request_id="RID123")
+                elif method_name == "admin_inviteRequests_approve":
+                    method(invite_request_id="ID123")
+                elif method_name == "admin_inviteRequests_deny":
+                    method(invite_request_id="ID123")
+                elif method_name == "admin_teams_admins_list":
+                    method(team_id="T123")
+                elif method_name == "admin_teams_create":
+                    method(team_domain="awesome-team", team_name="Awesome Team")
+                elif method_name == "admin_teams_owners_list":
+                    method(team_id="T123")
+                elif method_name == "admin_teams_settings_info":
+                    method(team_id="T123")
+                elif method_name == "admin_teams_settings_setDefaultChannels":
+                    method(team_id="T123", channel_ids=["C123", "C234"])
+                elif method_name == "admin_teams_settings_setDescription":
+                    method(team_id="T123", description="Workspace for an awesome team")
+                elif method_name == "admin_teams_settings_setDiscoverability":
+                    method(team_id="T123", discoverability="invite_only")
+                elif method_name == "admin_teams_settings_setIcon":
+                    method(
+                        team_id="T123",
+                        image_url="https://www.example.com/images/dummy.png",
+                    )
+                elif method_name == "admin_teams_settings_setName":
+                    method(team_id="T123", name="Awesome Engineering Team")
+                elif method_name == "admin_users_assign":
+                    method(team_id="T123", user_id="W123")
+                elif method_name == "admin_users_invite":
+                    method(
+                        team_id="T123",
+                        email="test@example.com",
+                        channel_ids=["C1A2B3C4D", "C26Z25Y24"],
+                    )
+                elif method_name == "admin_users_list":
+                    method(team_id="T123")
+                elif method_name == "admin_users_remove":
+                    method(team_id="T123", user_id="W123")
+                elif method_name == "admin_users_setAdmin":
+                    method(team_id="T123", user_id="W123")
+                elif method_name == "admin_users_setExpiration":
+                    method(team_id="T123", user_id="W123", expiration_ts=123)
+                elif method_name == "admin_users_setOwner":
+                    method(team_id="T123", user_id="W123")
+                elif method_name == "admin_users_setRegular":
+                    method(team_id="T123", user_id="W123")
+                elif method_name == "admin_users_session_reset":
+                    method(user_id="W123")
+                elif method_name == "chat_delete":
+                    method(channel="C123", ts="123.123")
+                elif method_name == "chat_deleteScheduledMessage":
+                    method(channel="C123", scheduled_message_id="123")
+                elif method_name == "chat_getPermalink":
+                    method(channel="C123", message_ts="123.123")
+                elif method_name == "chat_meMessage":
+                    method(channel="C123", text=":wave: Hi there!")
+                elif method_name == "chat_postEphemeral":
+                    method(channel="C123", user="U123")
+                elif method_name == "chat_postMessage":
+                    method(channel="C123")
+                elif method_name == "chat_scheduleMessage":
+                    method(channel="C123", post_at=123, text="Hi")
+                elif method_name == "chat_unfurl":
+                    method(
+                        channel="C123",
+                        ts="123.123",
+                        unfurls={
+                            "https://example.com/": {"text": "Every day is the test."}
+                        },
+                    )
+                elif method_name == "chat_update":
+                    method(channel="C123", ts="123.123")
+                elif method_name == "conversations_archive":
+                    method(channel="C123")
+                elif method_name == "conversations_close":
+                    method(channel="C123")
+                elif method_name == "conversations_create":
+                    method(name="announcements")
+                elif method_name == "conversations_history":
+                    method(channel="C123")
+                elif method_name == "conversations_info":
+                    method(channel="C123")
+                elif method_name == "conversations_invite":
+                    method(channel="C123", users=["U2345678901", "U3456789012"])
+                elif method_name == "conversations_join":
+                    method(channel="C123")
+                elif method_name == "conversations_kick":
+                    method(channel="C123", user="U123")
+                elif method_name == "conversations_leave":
+                    method(channel="C123")
+                elif method_name == "conversations_members":
+                    method(channel="C123")
+                elif method_name == "conversations_rename":
+                    method(channel="C123", name="new-name")
+                elif method_name == "conversations_replies":
+                    method(channel="C123", ts="123.123")
+                elif method_name == "conversations_setPurpose":
+                    method(channel="C123", purpose="The purpose")
+                elif method_name == "conversations_setTopic":
+                    method(channel="C123", topic="The topic")
+                elif method_name == "conversations_unarchive":
+                    method(channel="C123")
+                elif method_name == "dialog_open":
+                    method(dialog={}, trigger_id="123")
+                elif method_name == "dnd_setSnooze":
+                    method(num_minutes=120)
+                elif method_name == "files_comments_delete":
+                    method(file="F123", id="FC123")
+                elif method_name == "files_delete":
+                    method(file="F123")
+                elif method_name == "files_info":
+                    method(file="F123")
+                elif method_name == "files_revokePublicURL":
+                    method(file="F123")
+                elif method_name == "files_sharedPublicURL":
+                    method(file="F123")
+                elif method_name == "files_upload":
+                    method(content="This is the content")
+                elif method_name == "files_remote_add":
+                    method(
+                        external_id="123",
+                        external_url="https://www.example.com/remote-files/123",
+                        title="File title",
+                    )
+                elif method_name == "files_remote_share":
+                    method(channels="C123,G123")
+                elif method_name == "migration_exchange":
+                    method(users="U123,U234")
+                elif method_name == "oauth_access":
+                    method = getattr(self.no_token_client, method_name, None)
+                    method(client_id="123.123", client_secret="secret", code="123456")
+                elif method_name == "oauth_v2_access":
+                    method = getattr(self.no_token_client, method_name, None)
+                    method(client_id="123.123", client_secret="secret", code="123456")
+                elif method_name == "pins_add":
+                    method(channel="C123")
+                elif method_name == "pins_list":
+                    method(channel="C123")
+                elif method_name == "pins_remove":
+                    method(channel="C123")
+                elif method_name == "reactions_add":
+                    method(name="eyes")
+                elif method_name == "reactions_remove":
+                    method(name="eyes")
+                elif method_name == "reminders_add":
+                    method(text="The task", time=123)
+                elif method_name == "reminders_complete":
+                    method(reminder="R123")
+                elif method_name == "reminders_delete":
+                    method(reminder="R123")
+                elif method_name == "reminders_info":
+                    method(reminder="R123")
+                elif method_name == "search_all":
+                    method(query="Slack")
+                elif method_name == "search_files":
+                    method(query="Slack")
+                elif method_name == "search_messages":
+                    method(query="Slack")
+                elif method_name == "usergroups_create":
+                    method(name="Engineering Team")
+                elif method_name == "usergroups_disable":
+                    method(usergroup="UG123")
+                elif method_name == "usergroups_enable":
+                    method(usergroup="UG123")
+                elif method_name == "usergroups_update":
+                    method(usergroup="UG123")
+                elif method_name == "usergroups_users_list":
+                    method(usergroup="UG123")
+                elif method_name == "usergroups_users_update":
+                    method(usergroup="UG123", users=["U123", "U234"])
+                elif method_name == "users_getPresence":
+                    method(user="U123")
+                elif method_name == "users_info":
+                    method(user="U123")
+                elif method_name == "users_lookupByEmail":
+                    method(email="test@example.com")
+                elif method_name == "users_setPhoto":
+                    method(image="README.md")
+                elif method_name == "users_setPresence":
+                    method(presence="away")
+                elif method_name == "views_open":
+                    method(trigger_id="123123", view={})
+                elif method_name == "views_publish":
+                    method(user_id="U123", view={})
+                elif method_name == "views_push":
+                    method(trigger_id="123123", view={})
+                elif method_name == "views_update":
+                    method(view_id="V123", view={})
+                elif method_name == "channels_archive":
+                    method(channel="C123")
+                elif method_name == "channels_create":
+                    method(name="channel-name")
+                elif method_name == "channels_history":
+                    method(channel="C123")
+                elif method_name == "channels_info":
+                    method(channel="C123")
+                elif method_name == "channels_invite":
+                    method(channel="C123", user="U123")
+                elif method_name == "channels_join":
+                    method(name="channel-name")
+                elif method_name == "channels_kick":
+                    method(channel="C123", user="U123")
+                elif method_name == "channels_leave":
+                    method(channel="C123")
+                elif method_name == "channels_mark":
+                    method(channel="C123", ts="123.123")
+                elif method_name == "channels_rename":
+                    method(channel="C123", name="new-name")
+                elif method_name == "channels_replies":
+                    method(channel="C123", thread_ts="123.123")
+                elif method_name == "channels_setPurpose":
+                    method(channel="C123", purpose="The purpose")
+                elif method_name == "channels_setTopic":
+                    method(channel="C123", topic="The topic")
+                elif method_name == "channels_unarchive":
+                    method(channel="C123")
+                elif method_name == "groups_archive":
+                    method(channel="G123")
+                elif method_name == "groups_create":
+                    method(name="private-channel-name")
+                elif method_name == "groups_createChild":
+                    method(channel="G123")
+                elif method_name == "groups_history":
+                    method(channel="G123")
+                elif method_name == "groups_info":
+                    method(channel="G123")
+                elif method_name == "groups_invite":
+                    method(channel="G123", user="U123")
+                elif method_name == "groups_kick":
+                    method(channel="G123", user="U123")
+                elif method_name == "groups_leave":
+                    method(channel="G123")
+                elif method_name == "groups_mark":
+                    method(channel="C123", ts="123.123")
+                elif method_name == "groups_open":
+                    method(channel="G123")
+                elif method_name == "groups_rename":
+                    method(channel="G123", name="new-name")
+                elif method_name == "groups_replies":
+                    method(channel="G123", thread_ts="123.123")
+                elif method_name == "groups_setPurpose":
+                    method(channel="G123", purpose="The purpose")
+                elif method_name == "groups_setTopic":
+                    method(channel="G123", topic="The topic")
+                elif method_name == "groups_unarchive":
+                    method(channel="G123")
+                elif method_name == "im_close":
+                    method(channel="D123")
+                elif method_name == "im_history":
+                    method(channel="D123")
+                elif method_name == "im_mark":
+                    method(channel="D123", ts="123.123")
+                elif method_name == "im_open":
+                    method(user="U123")
+                elif method_name == "im_replies":
+                    method(channel="D123", thread_ts="123.123")
+                elif method_name == "mpim_close":
+                    method(channel="D123")
+                elif method_name == "mpim_history":
+                    method(channel="D123")
+                elif method_name == "mpim_mark":
+                    method(channel="D123", ts="123.123")
+                elif method_name == "mpim_open":
+                    method(users=["U123", "U234"])
+                elif method_name == "mpim_replies":
+                    method(channel="D123", thread_ts="123.123")
+                else:
+                    method(*{})
+            else:
+                # Verify if the expected method is supported
+                self.assertTrue(callable(method), f"{method_name} is not supported yet")
+
+        self.assertEqual(
+            self.api_methods_to_call, [], "All methods should be supported"
+        )