-
-
Notifications
You must be signed in to change notification settings - Fork 377
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
integrations: Add ClickUp integration script.
Add a python script to help integrate Zulip with Clickup. Urlopen is used instead of the usual requests library inorder to make the script standalone. Fixes zulip#26529
- Loading branch information
Showing
4 changed files
with
540 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
# A script that automates setting up a webhook with ClickUp | ||
|
||
Usage : | ||
|
||
1. Make sure you have all of the relevant ClickUp credentials before | ||
executing the script: | ||
- The ClickUp Team ID | ||
- The ClickUp Client ID | ||
- The ClickUp Client Secret | ||
|
||
2. Execute the script : | ||
|
||
$ python zulip_clickup.py --clickup-team-id <clickup_team_id> \ | ||
--clickup-client-id <clickup_client_id> \ | ||
--clickup-client-secret <clickup_client_secret> \ | ||
--zulip-webhook-url "GENERATED_WEBHOOK_URL" | ||
|
||
For more information, please see Zulip's documentation on how to set up | ||
a ClickUp integration [here](https://zulip.com/integrations/doc/clickup). |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,175 @@ | ||
import io | ||
import re | ||
from functools import wraps | ||
from typing import Any, Callable, Dict, List, Optional, Union | ||
from unittest import TestCase | ||
from unittest.mock import DEFAULT, patch | ||
|
||
from integrations.clickup import zulip_clickup | ||
from integrations.clickup.zulip_clickup import ClickUpAPIHandler | ||
|
||
MOCK_WEBHOOK_URL = ( | ||
"https://YourZulipApp.com/api/v1/external/clickup?api_key=TJ9DnIiNqt51bpfyPll5n2uT4iYxMBW9" | ||
) | ||
|
||
MOCK_AUTH_CODE = "332KKA3321NNAK3MADS" | ||
MOCK_AUTH_CODE_URL = f"https://YourZulipApp.com/?code={MOCK_AUTH_CODE}" | ||
MOCK_API_KEY = "X" * 32 | ||
|
||
SCRIPT_PATH = "integrations.clickup.zulip_clickup" | ||
|
||
MOCK_CREATED_WEBHOOK_ID = "13-13-13-13-1313-13" | ||
MOCK_DELETE_WEBHOOK_ID = "12-12-12-12-12" | ||
MOCK_GET_WEBHOOK_IDS = {"endpoint": MOCK_WEBHOOK_URL, "id": MOCK_DELETE_WEBHOOK_ID} | ||
|
||
CLICKUP_TEAM_ID = "teamid123" | ||
CLICKUP_CLIENT_ID = "clientid321" | ||
CLICKUP_CLIENT_SECRET = "clientsecret322" # noqa: S105 | ||
|
||
|
||
def make_clickup_request_side_effect( | ||
path: str, query: Dict[str, Union[str, List[str]]], method: str | ||
) -> Optional[Dict[str, Any]]: | ||
clickup_api = ClickUpAPIHandler(CLICKUP_CLIENT_ID, CLICKUP_CLIENT_SECRET, CLICKUP_TEAM_ID) | ||
api_data_mapper: Dict[str, Dict[str, Dict[str, Any]]] = { # path -> method -> response | ||
clickup_api.ENDPOINTS["oauth"]: { | ||
"POST": {"access_token": MOCK_API_KEY}, | ||
}, | ||
clickup_api.ENDPOINTS["team"]: { | ||
"POST": {"id": MOCK_CREATED_WEBHOOK_ID}, | ||
"GET": {"webhooks": [MOCK_GET_WEBHOOK_IDS]}, | ||
}, | ||
clickup_api.ENDPOINTS["webhook"].format(webhook_id=MOCK_DELETE_WEBHOOK_ID): {"DELETE": {}}, | ||
} | ||
return api_data_mapper.get(path, {}).get(method, DEFAULT) | ||
|
||
|
||
def mock_script_args() -> Callable[[Any], Callable[..., Any]]: | ||
def decorator(func: Callable[..., Any]) -> Callable[..., Any]: | ||
@wraps(func) | ||
def wrapper(*args: Any, **kwargs: Any) -> Any: | ||
mock_user_inputs = [ | ||
MOCK_WEBHOOK_URL, # input for 1st step | ||
MOCK_AUTH_CODE_URL, # input for 3rd step | ||
"1,2,3,4,5", # third input for 4th step | ||
] | ||
with patch( | ||
"sys.argv", | ||
[ | ||
"zulip_clickup.py", | ||
"--clickup-team-id", | ||
CLICKUP_TEAM_ID, | ||
"--clickup-client-id", | ||
CLICKUP_CLIENT_ID, | ||
"--clickup-client-secret", | ||
CLICKUP_CLIENT_SECRET, | ||
"--zulip-webhook-url", | ||
MOCK_WEBHOOK_URL, | ||
], | ||
), patch("os.system"), patch("time.sleep"), patch("sys.exit"), patch( | ||
"builtins.input", side_effect=mock_user_inputs | ||
), patch( | ||
SCRIPT_PATH + ".ClickUpAPIHandler.make_clickup_request", | ||
side_effect=make_clickup_request_side_effect, | ||
): | ||
result = func(*args, **kwargs) | ||
|
||
return result | ||
|
||
return wrapper | ||
|
||
return decorator | ||
|
||
|
||
class ZulipClickUpScriptTest(TestCase): | ||
@mock_script_args() | ||
def test_valid_arguments(self) -> None: | ||
with patch(SCRIPT_PATH + ".run") as mock_run, patch( | ||
"sys.stdout", new=io.StringIO() | ||
) as mock_stdout: | ||
zulip_clickup.main() | ||
self.assertRegex(mock_stdout.getvalue(), r"Running Zulip Clickup Integration...") | ||
mock_run.assert_called_once_with( | ||
CLICKUP_CLIENT_ID, CLICKUP_CLIENT_SECRET, CLICKUP_TEAM_ID, MOCK_WEBHOOK_URL | ||
) | ||
|
||
def test_missing_arguments(self) -> None: | ||
with self.assertRaises(SystemExit) as cm: | ||
with patch("sys.stderr", new=io.StringIO()) as mock_stderr: | ||
zulip_clickup.main() | ||
self.assertEqual(cm.exception.code, 2) | ||
self.assertRegex( | ||
mock_stderr.getvalue(), | ||
r"""the following arguments are required: --clickup-team-id, --clickup-client-id, --clickup-client-secret, --zulip-webhook-url\n""", | ||
) | ||
|
||
@mock_script_args() | ||
def test_redirect_to_auth_page(self) -> None: | ||
with patch("webbrowser.open") as mock_open, patch( | ||
"sys.stdout", new=io.StringIO() | ||
) as mock_stdout: | ||
zulip_clickup.main() | ||
redirect_uri = "https://YourZulipApp.com" | ||
mock_open.assert_called_once_with( | ||
f"https://app.clickup.com/api?client_id={CLICKUP_CLIENT_ID}&redirect_uri={redirect_uri}" | ||
) | ||
expected_output = ( | ||
r"STEP 1[\s\S]*" | ||
r"ClickUp authorization page will open in your browser\.[\s\S]*" | ||
r"Please authorize your workspace\(s\)\.[\s\S]*" | ||
r"Click 'Connect Workspace' on the page to proceed\.\.\." | ||
) | ||
|
||
self.assertRegex( | ||
mock_stdout.getvalue(), | ||
expected_output, | ||
) | ||
|
||
@mock_script_args() | ||
def test_query_for_auth_code(self) -> None: | ||
with patch("webbrowser.open"), patch("sys.stdout", new=io.StringIO()) as mock_stdout: | ||
zulip_clickup.main() | ||
expected_output = ( | ||
"STEP 2\n----\nAfter you've authorized your workspace,\n" | ||
"you should be redirected to your home URL.\n" | ||
"Please copy your home URL and paste it below.\n" | ||
"It should contain a code, and look similar to this:\n\n" | ||
"e.g. " + re.escape(MOCK_AUTH_CODE_URL) | ||
) | ||
self.assertRegex( | ||
mock_stdout.getvalue(), | ||
expected_output, | ||
) | ||
|
||
@mock_script_args() | ||
def test_select_clickup_events(self) -> None: | ||
with patch("webbrowser.open"), patch("sys.stdout", new=io.StringIO()) as mock_stdout: | ||
zulip_clickup.main() | ||
expected_output = r""" | ||
STEP 3 | ||
---- | ||
Please select which ClickUp event notification\(s\) you'd | ||
like to receive in your Zulip app\. | ||
EVENT CODES: | ||
1 = task | ||
2 = list | ||
3 = folder | ||
4 = space | ||
5 = goals | ||
Or, enter \* to subscribe to all events\. | ||
Here's an example input if you intend to only receive notifications | ||
related to task, list and folder: 1,2,3 | ||
""" | ||
self.assertRegex( | ||
mock_stdout.getvalue(), | ||
expected_output, | ||
) | ||
|
||
@mock_script_args() | ||
def test_success_message(self) -> None: | ||
with patch("webbrowser.open"), patch("sys.stdout", new=io.StringIO()) as mock_stdout: | ||
zulip_clickup.main() | ||
expected_output = r"SUCCESS: Completed integrating your Zulip app with ClickUp!\s*webhook_id: \d+-\d+-\d+-\d+-\d+-\d+\s*You may delete this script or run it again to reconfigure\s*your integration\." | ||
self.assertRegex(mock_stdout.getvalue(), expected_output) |
Oops, something went wrong.