hyperpocket 0.0.1__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (131) hide show
  1. hyperpocket/__init__.py +7 -0
  2. hyperpocket/auth/README.KR.md +309 -0
  3. hyperpocket/auth/README.md +323 -0
  4. hyperpocket/auth/__init__.py +24 -0
  5. hyperpocket/auth/calendly/__init__.py +0 -0
  6. hyperpocket/auth/calendly/context.py +13 -0
  7. hyperpocket/auth/calendly/oauth2_context.py +25 -0
  8. hyperpocket/auth/calendly/oauth2_handler.py +146 -0
  9. hyperpocket/auth/calendly/oauth2_schema.py +16 -0
  10. hyperpocket/auth/context.py +38 -0
  11. hyperpocket/auth/github/__init__.py +0 -0
  12. hyperpocket/auth/github/context.py +13 -0
  13. hyperpocket/auth/github/oauth2_context.py +25 -0
  14. hyperpocket/auth/github/oauth2_handler.py +143 -0
  15. hyperpocket/auth/github/oauth2_schema.py +16 -0
  16. hyperpocket/auth/github/token_context.py +12 -0
  17. hyperpocket/auth/github/token_handler.py +79 -0
  18. hyperpocket/auth/github/token_schema.py +9 -0
  19. hyperpocket/auth/google/__init__.py +0 -0
  20. hyperpocket/auth/google/context.py +15 -0
  21. hyperpocket/auth/google/oauth2_context.py +31 -0
  22. hyperpocket/auth/google/oauth2_handler.py +137 -0
  23. hyperpocket/auth/google/oauth2_schema.py +18 -0
  24. hyperpocket/auth/handler.py +171 -0
  25. hyperpocket/auth/linear/__init__.py +0 -0
  26. hyperpocket/auth/linear/context.py +15 -0
  27. hyperpocket/auth/linear/token_context.py +15 -0
  28. hyperpocket/auth/linear/token_handler.py +68 -0
  29. hyperpocket/auth/linear/token_schema.py +9 -0
  30. hyperpocket/auth/provider.py +16 -0
  31. hyperpocket/auth/schema.py +19 -0
  32. hyperpocket/auth/slack/__init__.py +0 -0
  33. hyperpocket/auth/slack/context.py +15 -0
  34. hyperpocket/auth/slack/oauth2_context.py +40 -0
  35. hyperpocket/auth/slack/oauth2_handler.py +151 -0
  36. hyperpocket/auth/slack/oauth2_schema.py +40 -0
  37. hyperpocket/auth/slack/tests/__init__.py +0 -0
  38. hyperpocket/auth/slack/tests/test_oauth2_handler.py +32 -0
  39. hyperpocket/auth/slack/tests/test_token_handler.py +23 -0
  40. hyperpocket/auth/slack/token_context.py +14 -0
  41. hyperpocket/auth/slack/token_handler.py +64 -0
  42. hyperpocket/auth/slack/token_schema.py +9 -0
  43. hyperpocket/auth/tests/__init__.py +0 -0
  44. hyperpocket/auth/tests/test_google_oauth2_handler.py +147 -0
  45. hyperpocket/auth/tests/test_slack_oauth2_handler.py +147 -0
  46. hyperpocket/auth/tests/test_slack_token_handler.py +66 -0
  47. hyperpocket/cli/__init__.py +0 -0
  48. hyperpocket/cli/__main__.py +12 -0
  49. hyperpocket/cli/pull.py +18 -0
  50. hyperpocket/cli/sync.py +17 -0
  51. hyperpocket/config/__init__.py +9 -0
  52. hyperpocket/config/auth.py +36 -0
  53. hyperpocket/config/git.py +17 -0
  54. hyperpocket/config/logger.py +81 -0
  55. hyperpocket/config/session.py +35 -0
  56. hyperpocket/config/settings.py +62 -0
  57. hyperpocket/constants.py +0 -0
  58. hyperpocket/curated_tools.py +10 -0
  59. hyperpocket/external/__init__.py +7 -0
  60. hyperpocket/external/github_client.py +19 -0
  61. hyperpocket/futures/__init__.py +7 -0
  62. hyperpocket/futures/futurestore.py +48 -0
  63. hyperpocket/pocket_auth.py +344 -0
  64. hyperpocket/pocket_main.py +351 -0
  65. hyperpocket/prompts.py +15 -0
  66. hyperpocket/repository/__init__.py +5 -0
  67. hyperpocket/repository/lock.py +156 -0
  68. hyperpocket/repository/lockfile.py +56 -0
  69. hyperpocket/repository/repository.py +18 -0
  70. hyperpocket/server/__init__.py +3 -0
  71. hyperpocket/server/auth/__init__.py +15 -0
  72. hyperpocket/server/auth/calendly.py +16 -0
  73. hyperpocket/server/auth/github.py +25 -0
  74. hyperpocket/server/auth/google.py +16 -0
  75. hyperpocket/server/auth/linear.py +18 -0
  76. hyperpocket/server/auth/slack.py +28 -0
  77. hyperpocket/server/auth/token.py +51 -0
  78. hyperpocket/server/proxy.py +63 -0
  79. hyperpocket/server/server.py +178 -0
  80. hyperpocket/server/tool/__init__.py +10 -0
  81. hyperpocket/server/tool/dto/__init__.py +0 -0
  82. hyperpocket/server/tool/dto/script.py +15 -0
  83. hyperpocket/server/tool/wasm.py +31 -0
  84. hyperpocket/session/README.KR.md +62 -0
  85. hyperpocket/session/README.md +61 -0
  86. hyperpocket/session/__init__.py +4 -0
  87. hyperpocket/session/in_memory.py +76 -0
  88. hyperpocket/session/interface.py +118 -0
  89. hyperpocket/session/redis.py +126 -0
  90. hyperpocket/session/tests/__init__.py +0 -0
  91. hyperpocket/session/tests/test_in_memory.py +145 -0
  92. hyperpocket/session/tests/test_redis.py +151 -0
  93. hyperpocket/tests/__init__.py +0 -0
  94. hyperpocket/tests/test_pocket.py +118 -0
  95. hyperpocket/tests/test_pocket_auth.py +982 -0
  96. hyperpocket/tool/README.KR.md +68 -0
  97. hyperpocket/tool/README.md +75 -0
  98. hyperpocket/tool/__init__.py +13 -0
  99. hyperpocket/tool/builtins/__init__.py +0 -0
  100. hyperpocket/tool/builtins/example/__init__.py +0 -0
  101. hyperpocket/tool/builtins/example/add_tool.py +18 -0
  102. hyperpocket/tool/function/README.KR.md +159 -0
  103. hyperpocket/tool/function/README.md +169 -0
  104. hyperpocket/tool/function/__init__.py +9 -0
  105. hyperpocket/tool/function/annotation.py +30 -0
  106. hyperpocket/tool/function/tool.py +87 -0
  107. hyperpocket/tool/tests/__init__.py +0 -0
  108. hyperpocket/tool/tests/test_function_tool.py +266 -0
  109. hyperpocket/tool/tool.py +106 -0
  110. hyperpocket/tool/wasm/README.KR.md +144 -0
  111. hyperpocket/tool/wasm/README.md +144 -0
  112. hyperpocket/tool/wasm/__init__.py +3 -0
  113. hyperpocket/tool/wasm/browser.py +63 -0
  114. hyperpocket/tool/wasm/invoker.py +41 -0
  115. hyperpocket/tool/wasm/script.py +82 -0
  116. hyperpocket/tool/wasm/templates/__init__.py +28 -0
  117. hyperpocket/tool/wasm/templates/node.py +87 -0
  118. hyperpocket/tool/wasm/templates/python.py +75 -0
  119. hyperpocket/tool/wasm/tool.py +147 -0
  120. hyperpocket/util/__init__.py +1 -0
  121. hyperpocket/util/extract_func_param_desc_from_docstring.py +97 -0
  122. hyperpocket/util/find_all_leaf_class_in_package.py +17 -0
  123. hyperpocket/util/find_all_subclass_in_package.py +29 -0
  124. hyperpocket/util/flatten_json_schema.py +45 -0
  125. hyperpocket/util/function_to_model.py +46 -0
  126. hyperpocket/util/get_objects_from_subpackage.py +28 -0
  127. hyperpocket/util/json_schema_to_model.py +69 -0
  128. hyperpocket-0.0.1.dist-info/METADATA +304 -0
  129. hyperpocket-0.0.1.dist-info/RECORD +131 -0
  130. hyperpocket-0.0.1.dist-info/WHEEL +4 -0
  131. hyperpocket-0.0.1.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,151 @@
1
+ from typing import Optional
2
+ from urllib.parse import urljoin, urlencode
3
+
4
+ import httpx
5
+
6
+ from hyperpocket.auth import AuthProvider
7
+ from hyperpocket.auth.context import AuthContext
8
+ from hyperpocket.auth.handler import AuthHandlerInterface
9
+ from hyperpocket.auth.slack.oauth2_context import SlackOAuth2AuthContext
10
+ from hyperpocket.auth.slack.oauth2_schema import SlackOAuth2Response, SlackOAuth2Request
11
+ from hyperpocket.config import config as config
12
+ from hyperpocket.futures import FutureStore
13
+
14
+
15
+ class SlackOAuth2AuthHandler(AuthHandlerInterface):
16
+ _SLACK_OAUTH_URL: str = "https://slack.com/oauth/v2/authorize"
17
+ _SLACK_TOKEN_URL: str = "https://slack.com/api/oauth.v2.access"
18
+
19
+ name: str = "slack-oauth2"
20
+ description: str = "This handler is used to authenticate users using the Slack OAuth2 authentication method."
21
+ scoped: bool = True
22
+
23
+ @staticmethod
24
+ def provider() -> AuthProvider:
25
+ return AuthProvider.SLACK
26
+
27
+ @staticmethod
28
+ def provider_default() -> bool:
29
+ return True
30
+
31
+ @staticmethod
32
+ def recommended_scopes() -> set[str]:
33
+ if config.auth.slack.use_recommended_scope:
34
+ recommended_scopes = {
35
+ "channels:history",
36
+ "channels:read",
37
+ "chat:write",
38
+ "groups:history",
39
+ "groups:read",
40
+ "im:history",
41
+ "mpim:history",
42
+ "reactions:read",
43
+ "reactions:write",
44
+ }
45
+ else:
46
+ recommended_scopes = {}
47
+ return recommended_scopes
48
+
49
+ def prepare(self, auth_req: SlackOAuth2Request, thread_id: str, profile: str,
50
+ future_uid: str, *args, **kwargs) -> str:
51
+ redirect_uri = urljoin(
52
+ config.public_base_url + "/",
53
+ f"{config.callback_url_rewrite_prefix}/auth/slack/oauth2/callback",
54
+ )
55
+ print(f"redirect_uri: {redirect_uri}")
56
+ auth_url = self._make_auth_url(req=auth_req, redirect_uri=redirect_uri, state=future_uid)
57
+
58
+ FutureStore.create_future(future_uid, data={
59
+ "redirect_uri": redirect_uri,
60
+ "thread_id": thread_id,
61
+ "profile": profile,
62
+ })
63
+
64
+ return f'User needs to authenticate using the following URL: {auth_url}'
65
+
66
+ async def authenticate(self, auth_req: SlackOAuth2Request, future_uid: str, *args, **kwargs) -> AuthContext:
67
+ future_data = FutureStore.get_future(future_uid)
68
+ auth_code = await future_data.future
69
+
70
+ async with httpx.AsyncClient() as client:
71
+ resp = await client.post(
72
+ url=self._SLACK_TOKEN_URL,
73
+ data={
74
+ 'client_id': auth_req.client_id,
75
+ 'client_secret': auth_req.client_secret,
76
+ 'code': auth_code,
77
+ 'redirect_uri': future_data.data["redirect_uri"],
78
+ }
79
+ )
80
+ if resp.status_code != 200:
81
+ raise Exception(f"failed to authenticate. status_code : {resp.status_code}")
82
+
83
+ resp_json = resp.json()
84
+ if resp_json["ok"] is False:
85
+ raise Exception(f"failed to authenticate. error : {resp_json['error']}")
86
+
87
+ resp_typed = SlackOAuth2Response(**resp_json)
88
+ return SlackOAuth2AuthContext.from_slack_oauth2_response(resp_typed)
89
+
90
+ async def refresh(self, auth_req: SlackOAuth2Request, context: AuthContext, *args, **kwargs) -> AuthContext:
91
+ slack_context: SlackOAuth2AuthContext = context
92
+ last_oauth2_resp: SlackOAuth2Response = slack_context.detail
93
+ refresh_token = slack_context.refresh_token
94
+
95
+ async with httpx.AsyncClient() as client:
96
+ resp = await client.post(
97
+ url=self._SLACK_TOKEN_URL,
98
+ data={
99
+ 'client_id': config.auth.slack.client_id,
100
+ 'client_secret': config.auth.slack.client_secret,
101
+ 'grant_type': 'refresh_token',
102
+ 'refresh_token': refresh_token,
103
+ },
104
+ )
105
+
106
+ if resp.status_code != 200:
107
+ raise Exception(f"failed to refresh. status_code : {resp.status_code}")
108
+
109
+ resp_json = resp.json()
110
+ if resp_json["ok"] is False:
111
+ raise Exception(f"failed to refresh. status_code : {resp.status_code}")
112
+
113
+ if last_oauth2_resp.authed_user:
114
+ new_resp = last_oauth2_resp.model_copy(
115
+ update={
116
+ "authed_user": SlackOAuth2Response.AuthedUser(**{
117
+ **last_oauth2_resp.authed_user.model_dump(),
118
+ "access_token": resp_json["access_token"],
119
+ "refresh_token": resp_json["refresh_token"],
120
+ "expires_in": resp_json["expires_in"],
121
+ })
122
+ }
123
+ )
124
+ else:
125
+ new_resp = last_oauth2_resp.model_copy(
126
+ update={
127
+ **last_oauth2_resp.model_dump(),
128
+ "access_token": resp_json["access_token"],
129
+ "refresh_token": resp_json["refresh_token"],
130
+ "expires_in": resp_json["expires_in"],
131
+ }
132
+ )
133
+
134
+ return SlackOAuth2AuthContext.from_slack_oauth2_response(new_resp)
135
+
136
+ def _make_auth_url(self, req: SlackOAuth2Request, redirect_uri: str, state: str):
137
+ params = {
138
+ "user_scope": ','.join(req.auth_scopes),
139
+ "client_id": req.client_id,
140
+ "redirect_uri": redirect_uri,
141
+ "state": state,
142
+ }
143
+ auth_url = f"{self._SLACK_OAUTH_URL}?{urlencode(params)}"
144
+ return auth_url
145
+
146
+ def make_request(self, auth_scopes: Optional[list[str]] = None, **kwargs) -> SlackOAuth2Request:
147
+ return SlackOAuth2Request(
148
+ auth_scopes=auth_scopes,
149
+ client_id=config.auth.slack.client_id,
150
+ client_secret=config.auth.slack.client_secret,
151
+ )
@@ -0,0 +1,40 @@
1
+ from typing import Optional
2
+
3
+ from pydantic import BaseModel
4
+
5
+ from hyperpocket.auth.schema import AuthenticateRequest, AuthenticateResponse
6
+
7
+
8
+ class SlackOAuth2Request(AuthenticateRequest):
9
+ client_id: str
10
+ client_secret: str
11
+
12
+
13
+ class SlackOAuth2Response(AuthenticateResponse):
14
+ class Team(BaseModel):
15
+ name: str
16
+ id: str
17
+
18
+ class Enterprise(BaseModel):
19
+ name: str
20
+ id: str
21
+
22
+ class AuthedUser(BaseModel):
23
+ id: str
24
+ access_token: Optional[str] = None
25
+ refresh_token: Optional[str] = None
26
+ expires_in: Optional[int] = None
27
+ scope: Optional[str] = None
28
+ token_type: Optional[str] = None
29
+
30
+ ok: bool
31
+ access_token: Optional[str] = None
32
+ refresh_token: Optional[str] = None
33
+ expires_in: Optional[int] = None
34
+ token_type: Optional[str] = None
35
+ scope: Optional[str] = None
36
+ bot_user_id: Optional[str] = None
37
+ app_id: Optional[str] = None
38
+ team: Optional[Team] = None
39
+ enterprise: Optional[Enterprise] = None
40
+ authed_user: Optional[AuthedUser] = None
File without changes
@@ -0,0 +1,32 @@
1
+ # import asyncio
2
+ # from unittest import TestCase
3
+ #
4
+ # from pocket.auth.slack.oauth2_handler import SlackOAuth2AuthHandler
5
+ # from pocket.auth.slack.oauth2_schema import SlackOAuth2Request
6
+ # from pocket.config import config
7
+ # from pocket.server.server import get_proxy_server, get_server
8
+ #
9
+ #
10
+ # class TestSlackOAuth2AuthHandler(TestCase):
11
+ #
12
+ # def test_authenticate(self):
13
+ # loop = asyncio.new_event_loop()
14
+ # asyncio.set_event_loop(loop)
15
+ # server = get_server()
16
+ # asyncio.ensure_future(server.serve(), loop=loop)
17
+ # proxy_server = get_proxy_server()
18
+ # if proxy_server:
19
+ # asyncio.ensure_future(proxy_server.serve(), loop=loop)
20
+ #
21
+ # slack_auth = SlackOAuth2AuthHandler()
22
+ # auth_req = SlackOAuth2Request(
23
+ # auth_scopes=["channels:history", "im:history", "mpim:history", "groups:history", "reactions:read"],
24
+ # client_id=config.auth.slack.client_id,
25
+ # client_secret=config.auth.slack.client_secret,
26
+ # )
27
+ #
28
+ # # when
29
+ # context = loop.run_until_complete(slack_auth.authenticate(auth_req))
30
+ #
31
+ # # then
32
+ # print("access_token : ", context.access_token)
@@ -0,0 +1,23 @@
1
+ # import asyncio
2
+ # from unittest import TestCase
3
+ # from unittest.mock import patch
4
+ #
5
+ # from pocket.auth.context import AuthContext
6
+ # from pocket.auth.slack.token_handler import SlackTokenAuthHandler
7
+ # from pocket.auth.slack.token_schema import SlackTokenRequest
8
+ #
9
+ #
10
+ # class TestSlackTokenAuthHandler(TestCase):
11
+ # def test_authenticate(self):
12
+ # loop = asyncio.new_event_loop()
13
+ # asyncio.set_event_loop(loop)
14
+ #
15
+ # slack_auth = SlackTokenAuthHandler()
16
+ # auth_req = SlackTokenRequest()
17
+ #
18
+ # # when
19
+ # with patch("builtins.input", return_value="test-slack-token"):
20
+ # context: AuthContext = loop.run_until_complete(slack_auth.authenticate(auth_req))
21
+ #
22
+ # # then
23
+ # assert context.access_token == "test-slack-token"
@@ -0,0 +1,14 @@
1
+ from hyperpocket.auth.slack.context import SlackAuthContext
2
+ from hyperpocket.auth.slack.token_schema import SlackTokenResponse
3
+
4
+
5
+ class SlackTokenAuthContext(SlackAuthContext):
6
+ @classmethod
7
+ def from_slack_token_response(cls, response: SlackTokenResponse):
8
+ description = f'Slack Token Context logged in'
9
+
10
+ return cls(
11
+ access_token=response.access_token,
12
+ description=description,
13
+ expires_at=None
14
+ )
@@ -0,0 +1,64 @@
1
+ from typing import Optional
2
+ from urllib.parse import urljoin, urlencode
3
+
4
+ from hyperpocket.auth import AuthProvider
5
+ from hyperpocket.auth.context import AuthContext
6
+ from hyperpocket.auth.handler import AuthHandlerInterface
7
+ from hyperpocket.auth.slack.token_context import SlackTokenAuthContext
8
+ from hyperpocket.auth.slack.token_schema import SlackTokenResponse, SlackTokenRequest
9
+ from hyperpocket.config import config
10
+ from hyperpocket.futures import FutureStore
11
+
12
+
13
+ class SlackTokenAuthHandler(AuthHandlerInterface):
14
+ name: str = "slack-token"
15
+ description: str = "This handler is used to authenticate users using the Slack token."
16
+ scoped: bool = False
17
+
18
+ _TOKEN_URL: str = urljoin(config.public_base_url + "/", f"{config.callback_url_rewrite_prefix}/auth/token")
19
+
20
+ @staticmethod
21
+ def provider() -> AuthProvider:
22
+ return AuthProvider.SLACK
23
+
24
+ @staticmethod
25
+ def recommended_scopes() -> set[str]:
26
+ return set()
27
+
28
+ def prepare(self, auth_req: SlackTokenRequest, thread_id: str, profile: str,
29
+ future_uid: str, *args, **kwargs) -> str:
30
+ redirect_uri = urljoin(
31
+ config.public_base_url + "/",
32
+ f"{config.callback_url_rewrite_prefix}/auth/slack/token/callback",
33
+ )
34
+ url = self._make_auth_url(req=auth_req, redirect_uri=redirect_uri, state=future_uid)
35
+ FutureStore.create_future(future_uid, data={
36
+ "redirect_uri": redirect_uri,
37
+ "thread_id": thread_id,
38
+ "profile": profile,
39
+ })
40
+
41
+ return f'User needs to authenticate using the following URL: {url}'
42
+
43
+ async def authenticate(self, auth_req: SlackTokenRequest, future_uid: str, *args, **kwargs) -> AuthContext:
44
+ future_data = FutureStore.get_future( future_uid)
45
+ access_token = await future_data.future
46
+
47
+ response = SlackTokenResponse(access_token=access_token)
48
+ context = SlackTokenAuthContext.from_slack_token_response(response)
49
+
50
+ return context
51
+
52
+ async def refresh(self, auth_req: SlackTokenRequest, context: AuthContext, *args, **kwargs) -> AuthContext:
53
+ raise Exception("Slack token doesn't support refresh")
54
+
55
+ def _make_auth_url(self, req: SlackTokenRequest, redirect_uri: str, state: str):
56
+ params = {
57
+ "redirect_uri": redirect_uri,
58
+ "state": state,
59
+ }
60
+ auth_url = f"{self._TOKEN_URL}?{urlencode(params)}"
61
+ return auth_url
62
+
63
+ def make_request(self, auth_scopes: Optional[list[str]] = None, **kwargs) -> SlackTokenRequest:
64
+ return SlackTokenRequest()
@@ -0,0 +1,9 @@
1
+ from hyperpocket.auth.schema import AuthenticateRequest, AuthenticateResponse
2
+
3
+
4
+ class SlackTokenRequest(AuthenticateRequest):
5
+ pass
6
+
7
+
8
+ class SlackTokenResponse(AuthenticateResponse):
9
+ access_token: str
File without changes
@@ -0,0 +1,147 @@
1
+ import uuid
2
+ from datetime import timezone, datetime
3
+ from unittest import IsolatedAsyncioTestCase
4
+ from unittest.mock import patch
5
+ from urllib.parse import urlparse, parse_qs
6
+
7
+ import httpx
8
+
9
+ from hyperpocket.auth import GoogleOAuth2AuthContext
10
+ from hyperpocket.auth.google.oauth2_handler import GoogleOAuth2AuthHandler
11
+ from hyperpocket.auth.google.oauth2_schema import GoogleOAuth2Request, GoogleOAuth2Response
12
+ from hyperpocket.config import config
13
+ from hyperpocket.config.auth import GoogleAuthConfig
14
+ from hyperpocket.futures import FutureStore
15
+
16
+
17
+ class TestGoogleOAuth2AuthHandler(IsolatedAsyncioTestCase):
18
+
19
+ async def asyncSetUp(self):
20
+ config.auth.google = GoogleAuthConfig(
21
+ client_id="test-client-id",
22
+ client_secret="test-client-secret",
23
+ )
24
+
25
+ self.handler = GoogleOAuth2AuthHandler()
26
+ self.auth_req = GoogleOAuth2Request(
27
+ auth_scopes=["https://www.googleapis.com/auth/calendar"],
28
+ client_id="test-client-id",
29
+ client_secret="test-client-secret",
30
+ )
31
+
32
+ async def test_make_auth_url(self):
33
+ future_uid = str(uuid.uuid4())
34
+
35
+ auth_url = self.handler._make_auth_url(
36
+ auth_req=self.auth_req,
37
+ redirect_uri="http://test-redirect-uri.com",
38
+ state=future_uid
39
+ )
40
+ parsed = urlparse(auth_url)
41
+ query_params = parse_qs(parsed.query)
42
+ base_url = f"{parsed.scheme}://{parsed.netloc}{parsed.path}"
43
+
44
+ # then
45
+ self.assertEqual(base_url, self.handler._GOOGLE_AUTH_URL)
46
+ self.assertEqual(query_params["state"][0], future_uid)
47
+ self.assertEqual(query_params["redirect_uri"][0], "http://test-redirect-uri.com")
48
+ self.assertEqual(query_params["client_id"][0], "test-client-id")
49
+ self.assertEqual(query_params["scope"][0], "https://www.googleapis.com/auth/calendar")
50
+
51
+ async def test_prepare(self):
52
+ future_uid = str(uuid.uuid4())
53
+
54
+ # when
55
+ prepare: str = self.handler.prepare(
56
+ auth_req=self.auth_req,
57
+ thread_id="test-prepare-thread-id",
58
+ profile="test-prepare-profile",
59
+ future_uid=future_uid,
60
+ )
61
+ auth_url = prepare.removeprefix("User needs to authenticate using the following URL:").strip()
62
+ future_data = FutureStore.get_future( uid=future_uid)
63
+
64
+ # then
65
+ self.assertTrue(auth_url.startswith(self.handler._GOOGLE_AUTH_URL))
66
+ self.assertIsNotNone(future_data)
67
+ self.assertEqual(future_data.data["thread_id"], "test-prepare-thread-id")
68
+ self.assertEqual(future_data.data["profile"], "test-prepare-profile")
69
+ self.assertFalse(future_data.future.done())
70
+
71
+ async def test_authenticate(self):
72
+ mock_response = httpx.Response(
73
+ status_code=200,
74
+ json={
75
+ "access_token": "test-token",
76
+ "refresh_token": "test-refresh-token",
77
+ "expires_in": 3600,
78
+ "scope": "https://www.googleapis.com/auth/calendar",
79
+ "token_type": "Bearer",
80
+
81
+ }
82
+ )
83
+ future_uid = str(uuid.uuid4())
84
+
85
+ self.handler.prepare(
86
+ auth_req=self.auth_req,
87
+ thread_id="test-thread-id",
88
+ profile="test-profile",
89
+ future_uid=future_uid
90
+ )
91
+ future_data = FutureStore.get_future( uid=future_uid)
92
+ future_data.future.set_result("test-code")
93
+
94
+ with patch("httpx.AsyncClient.post", return_value=mock_response):
95
+ response: GoogleOAuth2AuthContext = await self.handler.authenticate(
96
+ auth_req=self.auth_req,
97
+ future_uid=future_uid
98
+ )
99
+
100
+ time_diff = (response.expires_at - datetime.now(tz=timezone.utc)).total_seconds()
101
+
102
+ self.assertIsInstance(response, GoogleOAuth2AuthContext)
103
+ self.assertEqual(response.access_token, "test-token")
104
+ self.assertEqual(response.refresh_token, "test-refresh-token")
105
+ self.assertTrue(time_diff > 3500)
106
+
107
+ async def test_refresh(self):
108
+ # given
109
+ mock_response = httpx.Response(
110
+ status_code=200,
111
+ json={
112
+ "access_token": "new-test-token",
113
+ "expires_in": 3600,
114
+ "scope": "https://www.googleapis.com/auth/calendar",
115
+ "token_type": "Bearer",
116
+ }
117
+ )
118
+ response = GoogleOAuth2Response(
119
+ **{
120
+ "access_token": "test-token",
121
+ "expires_in": 100,
122
+ "scope": "https://www.googleapis.com/auth/calendar",
123
+ "refresh_token": "test-refresh-token",
124
+ "token_type": "Bearer",
125
+ }
126
+ )
127
+ context = GoogleOAuth2AuthContext.from_google_oauth2_response(response)
128
+
129
+ # when
130
+ with patch("httpx.AsyncClient.post", return_value=mock_response):
131
+ new_context: GoogleOAuth2AuthContext = await self.handler.refresh(
132
+ auth_req=self.auth_req,
133
+ context=context
134
+ )
135
+
136
+ old_time_diff = (context.expires_at - datetime.now(tz=timezone.utc))
137
+ new_time_diff = (new_context.expires_at - datetime.now(tz=timezone.utc))
138
+
139
+ # then
140
+ self.assertIsInstance(new_context, GoogleOAuth2AuthContext)
141
+ self.assertEqual(context.access_token, "test-token")
142
+ self.assertEqual(context.refresh_token, "test-refresh-token")
143
+ self.assertTrue(old_time_diff.total_seconds() < 100)
144
+
145
+ self.assertEqual(new_context.access_token, "new-test-token")
146
+ self.assertEqual(new_context.refresh_token, "test-refresh-token")
147
+ self.assertTrue(new_time_diff.total_seconds() > 3500)
@@ -0,0 +1,147 @@
1
+ import uuid
2
+ from datetime import timezone, datetime
3
+ from unittest.async_case import IsolatedAsyncioTestCase
4
+ from unittest.mock import patch
5
+ from urllib.parse import urlparse, parse_qs
6
+
7
+ import httpx
8
+
9
+ from hyperpocket.auth import SlackOAuth2AuthContext
10
+ from hyperpocket.auth.slack.oauth2_handler import SlackOAuth2AuthHandler
11
+ from hyperpocket.auth.slack.oauth2_schema import SlackOAuth2Request, SlackOAuth2Response
12
+ from hyperpocket.config import config
13
+ from hyperpocket.config.auth import SlackAuthConfig
14
+ from hyperpocket.futures import FutureStore
15
+
16
+
17
+ class TestSlackOAuth2AuthHandler(IsolatedAsyncioTestCase):
18
+
19
+ async def asyncSetUp(self):
20
+ config.auth.slack = SlackAuthConfig(
21
+ client_id="test-client-id",
22
+ client_secret="test-client-secret",
23
+ )
24
+
25
+ self.handler = SlackOAuth2AuthHandler()
26
+ self.auth_req = SlackOAuth2Request(
27
+ auth_scopes=["channels:history", "im:history", "mpim:history", "groups:history", "reactions:read"],
28
+ client_id="test-client-id",
29
+ client_secret="test-client-secret",
30
+ )
31
+
32
+ async def test_make_auth_url(self):
33
+ future_uid = str(uuid.uuid4())
34
+
35
+ auth_url = self.handler._make_auth_url(
36
+ req=self.auth_req,
37
+ redirect_uri="http://test-redirect-uri.com",
38
+ state=future_uid
39
+ )
40
+ parsed = urlparse(auth_url)
41
+ query_params = parse_qs(parsed.query)
42
+ base_url = f"{parsed.scheme}://{parsed.netloc}{parsed.path}"
43
+
44
+ # then
45
+ self.assertEqual(base_url, SlackOAuth2AuthHandler._SLACK_OAUTH_URL)
46
+ self.assertEqual(query_params["state"][0], future_uid)
47
+ self.assertEqual(query_params["redirect_uri"][0], "http://test-redirect-uri.com")
48
+ self.assertEqual(query_params["client_id"][0], "test-client-id")
49
+ self.assertEqual(query_params["user_scope"][0],
50
+ "channels:history,im:history,mpim:history,groups:history,reactions:read")
51
+
52
+ async def test_prepare(self):
53
+ future_uid = str(uuid.uuid4())
54
+
55
+ # when
56
+ prepare: str = self.handler.prepare(
57
+ auth_req=self.auth_req,
58
+ thread_id="test-prepare-thread-id",
59
+ profile="test-prepare-profile",
60
+ future_uid=future_uid,
61
+ )
62
+ auth_url = prepare.removeprefix("User needs to authenticate using the following URL:").strip()
63
+ future_data = FutureStore.get_future( uid=future_uid)
64
+
65
+ # then
66
+ self.assertTrue(auth_url.startswith(SlackOAuth2AuthHandler._SLACK_OAUTH_URL))
67
+ self.assertIsNotNone(future_data)
68
+ self.assertEqual(future_data.data["thread_id"], "test-prepare-thread-id")
69
+ self.assertEqual(future_data.data["profile"], "test-prepare-profile")
70
+ self.assertFalse(future_data.future.done())
71
+
72
+ async def test_authenticate(self):
73
+ # given
74
+ future_uid = str(uuid.uuid4())
75
+ mock_response = httpx.Response(
76
+ status_code=200,
77
+ json={
78
+ "ok": True,
79
+ "authed_user": {
80
+ "id": "test-user",
81
+ "access_token": "test-token"
82
+ }
83
+
84
+ }
85
+ )
86
+
87
+ # when
88
+ self.handler.prepare(
89
+ auth_req=self.auth_req,
90
+ thread_id="test-thread-id",
91
+ profile="test-profile",
92
+ future_uid=future_uid
93
+ )
94
+ future_data = FutureStore.get_future( uid=future_uid)
95
+ future_data.future.set_result("test-code")
96
+
97
+ with patch("httpx.AsyncClient.post", return_value=mock_response):
98
+ response: SlackOAuth2AuthContext = await self.handler.authenticate(
99
+ auth_req=self.auth_req,
100
+ future_uid=future_uid
101
+ )
102
+
103
+ self.assertIsInstance(response, SlackOAuth2AuthContext)
104
+ self.assertEqual(response.access_token, "test-token")
105
+
106
+ async def test_refresh(self):
107
+ # given
108
+ # https://api.slack.com/authentication/rotation
109
+ mock_response = httpx.Response(
110
+ status_code=200,
111
+ json={
112
+ "ok": True,
113
+ "access_token": "new-access-token",
114
+ "refresh_token": "new-refresh-token",
115
+ "expires_in": 3600,
116
+ }
117
+ )
118
+
119
+ response = SlackOAuth2Response(
120
+ **{
121
+ "ok": True,
122
+ "authed_user": {
123
+ "id": "test",
124
+ "access_token": "access-token",
125
+ "refresh_token": "refresh-token",
126
+ "expires_in": 3600,
127
+ }
128
+ }
129
+ )
130
+ context = SlackOAuth2AuthContext.from_slack_oauth2_response(response)
131
+
132
+ # when
133
+ with patch("httpx.AsyncClient.post", return_value=mock_response):
134
+ new_context: SlackOAuth2AuthContext = await self.handler.refresh(
135
+ auth_req=self.auth_req,
136
+ context=context
137
+ )
138
+
139
+ time_diff = (new_context.expires_at - datetime.now(tz=timezone.utc))
140
+
141
+ # then
142
+ self.assertIsInstance(new_context, SlackOAuth2AuthContext)
143
+ self.assertEqual(context.access_token, "access-token")
144
+ self.assertEqual(context.refresh_token, "refresh-token")
145
+ self.assertEqual(new_context.access_token, "new-access-token")
146
+ self.assertEqual(new_context.refresh_token, "new-refresh-token")
147
+ self.assertTrue(time_diff.total_seconds() > 3500)