Skip to content

Commit 070853e

Browse files
committed
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
1 parent 20ccb22 commit 070853e

File tree

4 files changed

+547
-0
lines changed

4 files changed

+547
-0
lines changed

zulip/integrations/clickup/README.md

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# A script that automates setting up a webhook with ClickUp
2+
3+
Usage :
4+
5+
1. Make sure you have all of the relevant ClickUp credentials before
6+
executing the script:
7+
- The ClickUp Team ID
8+
- The ClickUp Client ID
9+
- The ClickUp Client Secret
10+
11+
2. Execute the script :
12+
13+
$ python zulip_clickup.py --clickup-team-id <clickup_team_id> \
14+
--clickup-client-id <clickup_board_name> \
15+
--clickup-client-secret <clickup_board_id> \
16+
17+
For more information, please see Zulip's documentation on how to set up
18+
a ClickUp integration [here](https://zulip.com/integrations/doc/clickup).

zulip/integrations/clickup/__init__.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import io
2+
from functools import wraps
3+
from typing import Any, Callable, Dict, List, Optional, Union
4+
from unittest import TestCase
5+
from unittest.mock import DEFAULT, patch
6+
7+
from integrations.clickup import zulip_clickup
8+
9+
MOCK_WEBHOOK_URL = (
10+
"https://YourZulipApp.com/api/v1/external/clickup?api_key=TJ9DnIiNqt51bpfyPll5n2uT4iYxMBW9"
11+
)
12+
13+
MOCK_AUTH_CODE = "332KKA3321NNAK3MADS"
14+
MOCK_AUTH_CODE_URL = f"https://YourZulipApp.com/?code={MOCK_AUTH_CODE}"
15+
MOCK_API_KEY = "X" * 32
16+
17+
SCRIPT_PATH = "integrations.clickup.zulip_clickup"
18+
19+
MOCK_CREATED_WEBHOOK_ID = "13-13-13-13-1313-13"
20+
MOCK_DELETE_WEBHOOK_ID = "12-12-12-12-12"
21+
MOCK_GET_WEBHOOK_IDS = {"endpoint": MOCK_WEBHOOK_URL, "id": MOCK_DELETE_WEBHOOK_ID}
22+
23+
CLICKUP_TEAM_ID = "teamid123"
24+
CLICKUP_CLIENT_ID = "clientid321"
25+
CLICKUP_CLIENT_SECRET = "clientsecret322" # noqa: S105
26+
27+
28+
def make_clickup_request_side_effect(
29+
path: str, query: Dict[str, Union[str, List[str]]], method: str
30+
) -> Optional[Dict[str, Any]]:
31+
api_data_mapper: Dict[str, Dict[str, Dict[str, Any]]] = { # path -> method -> response
32+
"oauth/token": {
33+
"POST": {"access_token": MOCK_API_KEY},
34+
}, # used for get_access_token()
35+
f"team/{CLICKUP_TEAM_ID}/webhook": {
36+
"POST": {"id": MOCK_CREATED_WEBHOOK_ID},
37+
"GET": {"webhooks": [MOCK_GET_WEBHOOK_IDS]},
38+
}, # used for create_webhook(), get_webhooks()
39+
f"webhook/{MOCK_DELETE_WEBHOOK_ID}": {"DELETE": {}}, # used for delete_webhook()
40+
}
41+
return api_data_mapper.get(path, {}).get(method, DEFAULT)
42+
43+
44+
def mock_script_args() -> Callable[[Any], Callable[..., Any]]:
45+
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
46+
@wraps(func)
47+
def wrapper(*args: Any, **kwargs: Any) -> Any:
48+
mock_user_inputs = [
49+
MOCK_WEBHOOK_URL, # input for 1st step
50+
MOCK_AUTH_CODE_URL, # input for 3rd step
51+
"1,2,3,4,5", # third input for 4th step
52+
]
53+
with patch(
54+
"sys.argv",
55+
[
56+
"zulip_clickup.py",
57+
"--clickup-team-id",
58+
CLICKUP_TEAM_ID,
59+
"--clickup-client-id",
60+
CLICKUP_CLIENT_ID,
61+
"--clickup-client-secret",
62+
CLICKUP_CLIENT_SECRET,
63+
],
64+
), patch("os.system"), patch("time.sleep"), patch("sys.exit"), patch(
65+
"builtins.input", side_effect=mock_user_inputs
66+
), patch(
67+
SCRIPT_PATH + ".ClickUpAPIHandler.make_clickup_request",
68+
side_effect=make_clickup_request_side_effect,
69+
):
70+
result = func(*args, **kwargs)
71+
72+
return result
73+
74+
return wrapper
75+
76+
return decorator
77+
78+
79+
class ZulipClickUpScriptTest(TestCase):
80+
@mock_script_args()
81+
def test_valid_arguments(self) -> None:
82+
with patch(SCRIPT_PATH + ".run") as mock_run, patch(
83+
"sys.stdout", new=io.StringIO()
84+
) as mock_stdout:
85+
zulip_clickup.main()
86+
self.assertRegex(mock_stdout.getvalue(), r"Running Zulip Clickup Integration...")
87+
mock_run.assert_called_once_with("clientid321", "clientsecret322", "teamid123")
88+
89+
def test_missing_arguments(self) -> None:
90+
with self.assertRaises(SystemExit) as cm:
91+
with patch("sys.stderr", new=io.StringIO()) as mock_stderr:
92+
zulip_clickup.main()
93+
self.assertEqual(cm.exception.code, 2)
94+
self.assertRegex(
95+
mock_stderr.getvalue(),
96+
r"""the following arguments are required: --clickup-team-id, --clickup-client-id, --clickup-client-secret\n""",
97+
)
98+
99+
@mock_script_args()
100+
def test_step_one(self) -> None:
101+
with patch("webbrowser.open"), patch("sys.stdout", new=io.StringIO()) as mock_stdout:
102+
zulip_clickup.main()
103+
self.assertRegex(
104+
mock_stdout.getvalue(),
105+
(
106+
r"STEP 1[\s\S]*Please enter the integration URL you've just generated[\s\S]*It should look similar to this:[\s\S]*e.g. http://YourZulipApp\.com/api/v1/external/clickup\?api_key=TJ9DnIiNqt51bpfyPll5n2uT4iYxMBW9"
107+
),
108+
)
109+
110+
@mock_script_args()
111+
def test_step_two(self) -> None:
112+
with patch("webbrowser.open") as mock_open, patch(
113+
"sys.stdout", new=io.StringIO()
114+
) as mock_stdout:
115+
zulip_clickup.main()
116+
redirect_uri = "https://YourZulipApp.com"
117+
mock_open.assert_called_once_with(
118+
f"https://app.clickup.com/api?client_id=clientid321&redirect_uri={redirect_uri}"
119+
)
120+
expected_output = r"STEP 2[\s\S]*ClickUp authorization page will open in your browser\.[\s\S]*Please authorize your workspace\(s\)\.[\s\S]*Click 'Connect Workspace' on the page to proceed\.\.\."
121+
self.assertRegex(
122+
mock_stdout.getvalue(),
123+
expected_output,
124+
)
125+
126+
@mock_script_args()
127+
def test_step_three(self) -> None:
128+
with patch("webbrowser.open"), patch("sys.stdout", new=io.StringIO()) as mock_stdout:
129+
zulip_clickup.main()
130+
self.assertRegex(
131+
mock_stdout.getvalue(),
132+
(
133+
r"STEP 3[\s\S]*After you've authorized your workspace,\s*you should be redirected to your home URL.\s*Please copy your home URL and paste it below.\s*It should contain a code, and look similar to this:\s*e.g. https://YourZulipDomain\.com/\?code=332KKA3321NNAK3MADS"
134+
),
135+
)
136+
137+
@mock_script_args()
138+
def test_step_four(self) -> None:
139+
with patch("webbrowser.open"), patch("sys.stdout", new=io.StringIO()) as mock_stdout:
140+
zulip_clickup.main()
141+
self.assertRegex(
142+
mock_stdout.getvalue(),
143+
(
144+
r"STEP 4[\s\S]*Please select which ClickUp event notification\(s\) you'd[\s\S]*like to receive in your Zulip app\.[\s\S]*EVENT CODES:[\s\S]*1 = task[\s\S]*2 = list[\s\S]*3 = folder[\s\S]*4 = space[\s\S]*5 = goals[\s\S]*Here's an example input if you intend to only receive notifications[\s\S]*related to task, list and folder: 1,2,3"
145+
),
146+
)
147+
148+
@mock_script_args()
149+
def test_final_step(self) -> None:
150+
with patch("webbrowser.open"), patch("sys.stdout", new=io.StringIO()) as mock_stdout:
151+
zulip_clickup.main()
152+
self.assertRegex(
153+
mock_stdout.getvalue(),
154+
(
155+
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\."
156+
),
157+
)

0 commit comments

Comments
 (0)