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.
- hyperpocket/__init__.py +7 -0
- hyperpocket/auth/README.KR.md +309 -0
- hyperpocket/auth/README.md +323 -0
- hyperpocket/auth/__init__.py +24 -0
- hyperpocket/auth/calendly/__init__.py +0 -0
- hyperpocket/auth/calendly/context.py +13 -0
- hyperpocket/auth/calendly/oauth2_context.py +25 -0
- hyperpocket/auth/calendly/oauth2_handler.py +146 -0
- hyperpocket/auth/calendly/oauth2_schema.py +16 -0
- hyperpocket/auth/context.py +38 -0
- hyperpocket/auth/github/__init__.py +0 -0
- hyperpocket/auth/github/context.py +13 -0
- hyperpocket/auth/github/oauth2_context.py +25 -0
- hyperpocket/auth/github/oauth2_handler.py +143 -0
- hyperpocket/auth/github/oauth2_schema.py +16 -0
- hyperpocket/auth/github/token_context.py +12 -0
- hyperpocket/auth/github/token_handler.py +79 -0
- hyperpocket/auth/github/token_schema.py +9 -0
- hyperpocket/auth/google/__init__.py +0 -0
- hyperpocket/auth/google/context.py +15 -0
- hyperpocket/auth/google/oauth2_context.py +31 -0
- hyperpocket/auth/google/oauth2_handler.py +137 -0
- hyperpocket/auth/google/oauth2_schema.py +18 -0
- hyperpocket/auth/handler.py +171 -0
- hyperpocket/auth/linear/__init__.py +0 -0
- hyperpocket/auth/linear/context.py +15 -0
- hyperpocket/auth/linear/token_context.py +15 -0
- hyperpocket/auth/linear/token_handler.py +68 -0
- hyperpocket/auth/linear/token_schema.py +9 -0
- hyperpocket/auth/provider.py +16 -0
- hyperpocket/auth/schema.py +19 -0
- hyperpocket/auth/slack/__init__.py +0 -0
- hyperpocket/auth/slack/context.py +15 -0
- hyperpocket/auth/slack/oauth2_context.py +40 -0
- hyperpocket/auth/slack/oauth2_handler.py +151 -0
- hyperpocket/auth/slack/oauth2_schema.py +40 -0
- hyperpocket/auth/slack/tests/__init__.py +0 -0
- hyperpocket/auth/slack/tests/test_oauth2_handler.py +32 -0
- hyperpocket/auth/slack/tests/test_token_handler.py +23 -0
- hyperpocket/auth/slack/token_context.py +14 -0
- hyperpocket/auth/slack/token_handler.py +64 -0
- hyperpocket/auth/slack/token_schema.py +9 -0
- hyperpocket/auth/tests/__init__.py +0 -0
- hyperpocket/auth/tests/test_google_oauth2_handler.py +147 -0
- hyperpocket/auth/tests/test_slack_oauth2_handler.py +147 -0
- hyperpocket/auth/tests/test_slack_token_handler.py +66 -0
- hyperpocket/cli/__init__.py +0 -0
- hyperpocket/cli/__main__.py +12 -0
- hyperpocket/cli/pull.py +18 -0
- hyperpocket/cli/sync.py +17 -0
- hyperpocket/config/__init__.py +9 -0
- hyperpocket/config/auth.py +36 -0
- hyperpocket/config/git.py +17 -0
- hyperpocket/config/logger.py +81 -0
- hyperpocket/config/session.py +35 -0
- hyperpocket/config/settings.py +62 -0
- hyperpocket/constants.py +0 -0
- hyperpocket/curated_tools.py +10 -0
- hyperpocket/external/__init__.py +7 -0
- hyperpocket/external/github_client.py +19 -0
- hyperpocket/futures/__init__.py +7 -0
- hyperpocket/futures/futurestore.py +48 -0
- hyperpocket/pocket_auth.py +344 -0
- hyperpocket/pocket_main.py +351 -0
- hyperpocket/prompts.py +15 -0
- hyperpocket/repository/__init__.py +5 -0
- hyperpocket/repository/lock.py +156 -0
- hyperpocket/repository/lockfile.py +56 -0
- hyperpocket/repository/repository.py +18 -0
- hyperpocket/server/__init__.py +3 -0
- hyperpocket/server/auth/__init__.py +15 -0
- hyperpocket/server/auth/calendly.py +16 -0
- hyperpocket/server/auth/github.py +25 -0
- hyperpocket/server/auth/google.py +16 -0
- hyperpocket/server/auth/linear.py +18 -0
- hyperpocket/server/auth/slack.py +28 -0
- hyperpocket/server/auth/token.py +51 -0
- hyperpocket/server/proxy.py +63 -0
- hyperpocket/server/server.py +178 -0
- hyperpocket/server/tool/__init__.py +10 -0
- hyperpocket/server/tool/dto/__init__.py +0 -0
- hyperpocket/server/tool/dto/script.py +15 -0
- hyperpocket/server/tool/wasm.py +31 -0
- hyperpocket/session/README.KR.md +62 -0
- hyperpocket/session/README.md +61 -0
- hyperpocket/session/__init__.py +4 -0
- hyperpocket/session/in_memory.py +76 -0
- hyperpocket/session/interface.py +118 -0
- hyperpocket/session/redis.py +126 -0
- hyperpocket/session/tests/__init__.py +0 -0
- hyperpocket/session/tests/test_in_memory.py +145 -0
- hyperpocket/session/tests/test_redis.py +151 -0
- hyperpocket/tests/__init__.py +0 -0
- hyperpocket/tests/test_pocket.py +118 -0
- hyperpocket/tests/test_pocket_auth.py +982 -0
- hyperpocket/tool/README.KR.md +68 -0
- hyperpocket/tool/README.md +75 -0
- hyperpocket/tool/__init__.py +13 -0
- hyperpocket/tool/builtins/__init__.py +0 -0
- hyperpocket/tool/builtins/example/__init__.py +0 -0
- hyperpocket/tool/builtins/example/add_tool.py +18 -0
- hyperpocket/tool/function/README.KR.md +159 -0
- hyperpocket/tool/function/README.md +169 -0
- hyperpocket/tool/function/__init__.py +9 -0
- hyperpocket/tool/function/annotation.py +30 -0
- hyperpocket/tool/function/tool.py +87 -0
- hyperpocket/tool/tests/__init__.py +0 -0
- hyperpocket/tool/tests/test_function_tool.py +266 -0
- hyperpocket/tool/tool.py +106 -0
- hyperpocket/tool/wasm/README.KR.md +144 -0
- hyperpocket/tool/wasm/README.md +144 -0
- hyperpocket/tool/wasm/__init__.py +3 -0
- hyperpocket/tool/wasm/browser.py +63 -0
- hyperpocket/tool/wasm/invoker.py +41 -0
- hyperpocket/tool/wasm/script.py +82 -0
- hyperpocket/tool/wasm/templates/__init__.py +28 -0
- hyperpocket/tool/wasm/templates/node.py +87 -0
- hyperpocket/tool/wasm/templates/python.py +75 -0
- hyperpocket/tool/wasm/tool.py +147 -0
- hyperpocket/util/__init__.py +1 -0
- hyperpocket/util/extract_func_param_desc_from_docstring.py +97 -0
- hyperpocket/util/find_all_leaf_class_in_package.py +17 -0
- hyperpocket/util/find_all_subclass_in_package.py +29 -0
- hyperpocket/util/flatten_json_schema.py +45 -0
- hyperpocket/util/function_to_model.py +46 -0
- hyperpocket/util/get_objects_from_subpackage.py +28 -0
- hyperpocket/util/json_schema_to_model.py +69 -0
- hyperpocket-0.0.1.dist-info/METADATA +304 -0
- hyperpocket-0.0.1.dist-info/RECORD +131 -0
- hyperpocket-0.0.1.dist-info/WHEEL +4 -0
- 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()
|
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)
|