hyperpocket 0.0.2__py3-none-any.whl → 0.1.8__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (97) hide show
  1. hyperpocket/auth/README.md +3 -3
  2. hyperpocket/auth/__init__.py +0 -8
  3. hyperpocket/auth/gumloop/context.py +13 -0
  4. hyperpocket/auth/gumloop/token_context.py +15 -0
  5. hyperpocket/auth/gumloop/token_handler.py +66 -0
  6. hyperpocket/auth/gumloop/token_schema.py +8 -0
  7. hyperpocket/auth/linear/token_context.py +1 -1
  8. hyperpocket/auth/notion/README.md +28 -0
  9. hyperpocket/auth/notion/context.py +15 -0
  10. hyperpocket/auth/notion/token_context.py +14 -0
  11. hyperpocket/auth/notion/token_handler.py +65 -0
  12. hyperpocket/auth/notion/token_schema.py +10 -0
  13. hyperpocket/auth/provider.py +8 -5
  14. hyperpocket/auth/reddit/context.py +15 -0
  15. hyperpocket/auth/reddit/oauth2_context.py +32 -0
  16. hyperpocket/auth/reddit/oauth2_handler.py +151 -0
  17. hyperpocket/auth/reddit/oauth2_schema.py +18 -0
  18. hyperpocket/auth/slack/token_context.py +1 -1
  19. hyperpocket/builtin.py +63 -0
  20. hyperpocket/cli/__main__.py +12 -0
  21. hyperpocket/cli/auth.py +83 -0
  22. hyperpocket/cli/codegen/auth/__init__.py +13 -0
  23. hyperpocket/cli/codegen/auth/auth_context_template.py +16 -0
  24. hyperpocket/cli/codegen/auth/auth_token_context_template.py +16 -0
  25. hyperpocket/cli/codegen/auth/auth_token_handler_template.py +69 -0
  26. hyperpocket/cli/codegen/auth/auth_token_schema_template.py +12 -0
  27. hyperpocket/cli/codegen/auth/server_auth_template.py +18 -0
  28. hyperpocket/cli/eject.py +19 -0
  29. hyperpocket/cli/sync.py +5 -5
  30. hyperpocket/config/settings.py +2 -4
  31. hyperpocket/futures/futurestore.py +0 -1
  32. hyperpocket/pocket_auth.py +25 -5
  33. hyperpocket/pocket_core.py +262 -0
  34. hyperpocket/pocket_main.py +125 -171
  35. hyperpocket/prompts.py +6 -8
  36. hyperpocket/repository/__init__.py +2 -2
  37. hyperpocket/repository/lock.py +19 -0
  38. hyperpocket/repository/lockfile.py +19 -13
  39. hyperpocket/repository/repository.py +26 -1
  40. hyperpocket/server/auth/__init__.py +0 -6
  41. hyperpocket/server/auth/gumloop.py +16 -0
  42. hyperpocket/server/auth/notion.py +19 -0
  43. hyperpocket/server/auth/reddit.py +16 -0
  44. hyperpocket/server/server.py +52 -16
  45. hyperpocket/server/tool/dto/script.py +15 -2
  46. hyperpocket/server/tool/wasm.py +20 -8
  47. hyperpocket/session/README.md +2 -2
  48. hyperpocket/session/in_memory.py +18 -5
  49. hyperpocket/session/interface.py +14 -0
  50. hyperpocket/session/redis.py +29 -5
  51. hyperpocket/tool/README.md +16 -12
  52. hyperpocket/tool/__init__.py +4 -3
  53. hyperpocket/tool/function/README.md +39 -10
  54. hyperpocket/tool/function/__init__.py +2 -0
  55. hyperpocket/tool/function/annotation.py +2 -1
  56. hyperpocket/tool/function/tool.py +98 -13
  57. hyperpocket/tool/tests/test_function_tool.py +55 -0
  58. hyperpocket/tool/tests/test_wasm_tool.py +73 -0
  59. hyperpocket/tool/tool.py +65 -2
  60. hyperpocket/tool/wasm/README.md +27 -5
  61. hyperpocket/tool/wasm/script.py +40 -1
  62. hyperpocket/tool/wasm/templates/python.py +32 -14
  63. hyperpocket/tool/wasm/tool.py +21 -18
  64. hyperpocket/tool_like.py +5 -0
  65. hyperpocket/util/__init__.py +1 -1
  66. hyperpocket/util/extract_func_param_desc_from_docstring.py +4 -4
  67. hyperpocket/util/function_to_model.py +5 -2
  68. hyperpocket/util/json_schema_to_model.py +45 -26
  69. {hyperpocket-0.0.2.dist-info → hyperpocket-0.1.8.dist-info}/METADATA +101 -72
  70. hyperpocket-0.1.8.dist-info/RECORD +139 -0
  71. {hyperpocket-0.0.2.dist-info → hyperpocket-0.1.8.dist-info}/WHEEL +1 -1
  72. hyperpocket-0.1.8.dist-info/entry_points.txt +2 -0
  73. hyperpocket/auth/README.KR.md +0 -309
  74. hyperpocket/auth/slack/tests/test_oauth2_handler.py +0 -32
  75. hyperpocket/auth/slack/tests/test_token_handler.py +0 -23
  76. hyperpocket/auth/tests/test_google_oauth2_handler.py +0 -147
  77. hyperpocket/auth/tests/test_slack_oauth2_handler.py +0 -147
  78. hyperpocket/auth/tests/test_slack_token_handler.py +0 -66
  79. hyperpocket/external/__init__.py +0 -7
  80. hyperpocket/external/github_client.py +0 -19
  81. hyperpocket/session/README.KR.md +0 -62
  82. hyperpocket/session/tests/test_in_memory.py +0 -145
  83. hyperpocket/session/tests/test_redis.py +0 -151
  84. hyperpocket/tests/test_pocket.py +0 -116
  85. hyperpocket/tests/test_pocket_auth.py +0 -982
  86. hyperpocket/tool/README.KR.md +0 -68
  87. hyperpocket/tool/builtins/__init__.py +0 -0
  88. hyperpocket/tool/builtins/example/__init__.py +0 -0
  89. hyperpocket/tool/builtins/example/add_tool.py +0 -18
  90. hyperpocket/tool/function/README.KR.md +0 -159
  91. hyperpocket/tool/wasm/README.KR.md +0 -144
  92. hyperpocket-0.0.2.dist-info/RECORD +0 -130
  93. hyperpocket-0.0.2.dist-info/entry_points.txt +0 -3
  94. /hyperpocket/auth/{slack/tests → gumloop}/__init__.py +0 -0
  95. /hyperpocket/auth/{tests → notion}/__init__.py +0 -0
  96. /hyperpocket/{session/tests → auth/reddit}/__init__.py +0 -0
  97. /hyperpocket/{tests → cli/codegen}/__init__.py +0 -0
@@ -1,309 +0,0 @@
1
- ## Auth
2
-
3
- 각 provider에서 유저를 식별하기 위한 프로세스
4
-
5
- ### AuthContext
6
-
7
- 실제 유저의 인증 정보(e.g., access token)을 담고 있는 객체
8
-
9
- ```python
10
- class AuthContext(BaseModel, ABC):
11
- """
12
- This class is used to define the interface of the authentication model.
13
- """
14
- access_token: str = Field(description="user's access token")
15
- description: str = Field(description="description of this authentication context")
16
- expires_at: Optional[datetime] = Field(description="expiration datetime")
17
- detail: Optional[Any] = Field(default=None, description="detailed information")
18
- ```
19
-
20
- 일반적으로 해당 객체에서 access token의 key 정보나 response 객체를 AuthContext로 변환해주는 역할을 수행한다.
21
-
22
- ### AuthenticateRequest
23
-
24
- authentication 수행 시에 필요한 정보를 담고 있는 객체
25
-
26
- ```python
27
- class AuthenticateRequest(BaseModel):
28
- auth_scopes: Optional[list[str]] = Field(default_factory=list,
29
- description="authentication scopes. if the authentication handler is non scoped, it isn't needed")
30
- ```
31
-
32
- - 일반적으로 인증 시에 필요한 client id 및 secret id 등과 같은 정보를 request에 같이 전달한다.
33
-
34
- **examples**
35
-
36
- ```python
37
- class GitHubOAuth2Request(AuthenticateRequest):
38
- client_id: str
39
- client_secret: str
40
- ```
41
-
42
- ### AuthenticateResponse(Optional)
43
-
44
- authentication response에서 필수 정보를 담고 있는 객체
45
-
46
- ```python
47
- class AuthenticateResponse(BaseModel):
48
- """
49
- This class is used to define the interface of the authentication response.
50
- """
51
- pass
52
- ```
53
-
54
- - handler에서 response를 dict 형태로 처리한다면, 구현할 필요가 없다.
55
-
56
- ### AuthHandlerInterface
57
-
58
- 실제 authentication을 수행하는 객체
59
-
60
- ```python
61
- class AuthHandlerInterface(ABC):
62
- name: str = Field(description="name of the authentication handler")
63
- description: str = Field(description="description of the authentication handler")
64
- scoped: bool = Field(description="Indicates whether the handler requires an auth_scope for access control")
65
-
66
-
67
- @staticmethod
68
- def provider() -> AuthProvider:
69
- """
70
- Returns the authentication provider enum.
71
-
72
- This method is used to determine the appropriate authentication handler
73
- based on the authentication provider.
74
- """
75
- raise NotImplementedError()
76
-
77
- @staticmethod
78
- def provider_default() -> bool:
79
- """
80
- Indicates whether this authentication handler is the default handler.
81
-
82
- If no specific handler is designated, the default handler will be used.
83
-
84
- Returns:
85
- bool: True if this handler is the default, False otherwise.
86
- """
87
- return False
88
-
89
- @staticmethod
90
- def recommended_scopes() -> set[str]:
91
- """
92
- Returns the recommended authentication scopes.
93
-
94
- If `use_recommended_scope` is set to True in the `AuthConfig`,
95
- this method should return the proper recommended scopes. Otherwise,
96
- it should return an empty set.
97
-
98
- Returns:
99
- set[str]: A set of recommended scopes, or an empty set if not applicable.
100
-
101
- Examples:
102
- Slack OAuth2 recommended_scopes::
103
-
104
- def recommended_scopes() -> set[str]:
105
- if config.auth.slack.use_recommended_scope:
106
- recommended_scopes = {
107
- "channels:history",
108
- "channels:read",
109
- "chat:write",
110
- "groups:history",
111
- "groups:read",
112
- "im:history",
113
- "mpim:history",
114
- "reactions:read",
115
- "reactions:write",
116
- }
117
- else:
118
- recommended_scopes = {}
119
- return recommended_scopes
120
- """
121
- raise NotImplementedError()
122
-
123
- def make_request(self, auth_scopes: Optional[list[str]] = None, **kwargs) -> AuthenticateRequest:
124
- """
125
- Make an AuthenticationRequest.
126
-
127
- Usually, this method only requires `auth_scopes`.
128
- If additional static information is needed (e.g., clientID, secretID),
129
- retrieve it from the configuration.
130
-
131
- Args:
132
- auth_scopes (Optional[list[str]]): list of auth scopes
133
-
134
- Returns:
135
- AuthenticateRequest: A authentication request object with the necessary details.
136
-
137
- Examples:
138
- Create a Slack OAuth2 request::
139
-
140
- def make_request(self, auth_scopes: Optional[list[str]] = None, **kwargs) -> SlackOAuth2Request:
141
- return SlackOAuth2Request(
142
- auth_scopes=auth_scopes,
143
- client_id=config.auth.slack.client_id,
144
- client_secret=config.auth.slack.client_secret
145
- )
146
- """
147
- raise NotImplementedError()
148
-
149
- def prepare(self, auth_req: AuthenticateRequest, thread_id: str, profile: str,
150
- future_uid: str, *args, **kwargs) -> str:
151
- """
152
- Performs preliminary tasks required for authentication.
153
-
154
- This method typically performs the following actions:
155
- - Creates a future to wait for user authentication completion during the authentication process.
156
- - Issues an authentication URI that the user can access.
157
-
158
- Args:
159
- auth_req (AuthenticateRequest): The authentication request object.
160
- thread_id (str): The thread ID.
161
- profile (str): The profile name.
162
- future_uid (str): A unique identifier for each future.
163
-
164
- Returns:
165
- str: The authentication URI that the user can access.
166
- """
167
- raise NotImplementedError()
168
-
169
- @abstractmethod
170
- async def authenticate(self, auth_req: AuthenticateRequest, future_uid: str, *args, **kwargs) -> AuthContext:
171
- """
172
- Performs the actual authentication process.
173
-
174
- This function assumes that the user has completed the authentication during the `prepare` step,
175
- and the associated future has been resolved. At this point, the result contains the required
176
- values for authentication (e.g., an auth code).
177
-
178
- Typically, this process involves:
179
- - Accessing the resolved future to retrieve the necessary values for authentication.
180
- - Performing the actual authentication using these values.
181
- - Converting the returned response into an appropriate `AuthContext` object and returning it.
182
-
183
- Args:
184
- auth_req (AuthenticateRequest): The authentication request object.
185
- future_uid (str): A unique identifier for the future, used to retrieve the correct
186
- result issued during the `prepare` step.
187
-
188
- Returns:
189
- AuthContext: The authentication context object containing the authentication result.
190
- """
191
- raise NotImplementedError()
192
-
193
- @abstractmethod
194
- async def refresh(self, auth_req: AuthenticateRequest, context: AuthContext, *args, **kwargs) -> AuthContext:
195
- """
196
- Performs re-authentication for an expired session.
197
-
198
- This method is optional and does not need to be implemented for handlers that do not require re-authentication.
199
-
200
- Typically, the information needed for re-authentication (e.g., a refresh token) should be stored
201
- within the `AuthContext` during the previous authentication step.
202
- In the `refresh` step, this method accesses the necessary re-authentication details from the provided `context`,
203
- performs the re-authentication, and returns an updated `AuthContext`.
204
-
205
- Args:
206
- auth_req (AuthenticateRequest): The authentication request object.
207
- context (AuthContext): The current authentication context that it should contain data required for re-authentication.
208
-
209
- Returns:
210
- AuthContext: An updated authentication context object.
211
- """
212
- raise NotImplementedError()
213
- ```
214
-
215
- ## How To Implement Auth
216
-
217
- 1. `AuthenticateRequest`을 상속받은 구현체 구현
218
- - 해당 클래스에서는 실제 prepare 및 authenticate를 할 때 필요한 정보를 저장하고 있어야 합니다.
219
-
220
- 2. `AuthContext`을 상속받은 구현체 구현
221
- - Context 내에는 다음과 같은 정보들이 존재해야 합니다.
222
- - 각 tool에서 접근할 수 있는 access_key
223
- - response를 context로 변환할 수 있는 함수
224
-
225
- 3. `Response`(Optional) 객체 구현
226
- - 필요시 authentication을 완료 후 받은 정보를 parsing할 구현체를 구현
227
-
228
- 4. `AuthHandlerInterface`을 상속받은 구현체 구현
229
- - 실제 prepare, authentication, refresh를 수행할 객체를 구현합니다.
230
-
231
- 5. (신규 auth provider인 경우) `AuthProvider`에 새로운 enum value를 추가
232
- - 만약 새로운 `AuthProvider`가 추가되는 경우라면, `pocket/auth/provider.py` 내에 새로운 AuthProvider Enum을 등록해야 합니다.
233
-
234
- 6. 신규 Auth callback server enpoint 추가
235
- - `pocket/server/auth/` 하위에 유저가 authentication을 완료하면 callback될 server endpoint를 추가해야 합니다.
236
- - 해당 패키지 하위에 적절한 `APIRouter`를 선언해놓으면 pocket 초기화 시에 자동으로 end point를 pocket server에 등록해주게 됩니다.
237
-
238
- 7. Auth Test code 추가(Optional)
239
- - `pocket/auth/tests` 하위에 테스트 코드 추가
240
-
241
- ---
242
-
243
- ## Auth Flow(Advanced)
244
-
245
- ### Session State 정의
246
-
247
- - SKIP_AUTH : Auth가 존재하고, 들어온 요청에 대한 권한도 가지고 있는 상태
248
- - DO_AUTH : Auth가 존재하지만, 들어온 요청에 대한 권한이 없기 때문에 auth가 필요한 상태
249
- - DO_REFRESH : Auth가 존재하지만, 만료가 되어 refresh가 필요한 상태
250
- - NO_SESSION : Auth가 존재하지 않는 상태
251
- - PENDING_RESOLVE : Resource Owner에게 authorization URI를 전달하고 인증을 기다리는 상태
252
- - RESOLVED : Resource Owner가 authorization을 완료해 서버에서 code를 받은 상태
253
-
254
- ### 01. check
255
-
256
- 처음 요청이 들어왔을 때 현재 사용자의 session이 어느 상태인지를 check하게 된다.
257
-
258
- 1. 사용자의 현재 session 조회
259
- - 존재 하지 않으면 NO_SESSION 상태 반환
260
- 2. session의 auth resolve uid가 존재하는지 확인
261
- - auth resolve uid가 아직 존재하는 것은 사용자에게 authentication URI는 전달하였지만 서버에서 access token을 아직 얻지는 못 한 상태
262
- - auth resolve uid가 존재하면서 현재 사용자가 인증을 완료한 상태인지 확인
263
- - 사용자가 인증을 완료한 상태라면 RESOLVED 상태 반환
264
- - 사용자가 인증을 완료하지 못 한 상태라면 PENDING_RESOLVE 상태 반환
265
- 3. 현재 인증된 session이 현재 요청에 적용이 가능한지를 확인
266
- - 현재 session의 auth provider와 필요한 session의 auth provider가 동일한지를 확인
267
- - 다르다면 반드시 재인증이 필요
268
- - 현재 session을 인증한 auto provider가 non scoped인지 확인
269
- - scoped가 아닌 경우에는 모든 권한이 있기 때문에 항상 사용 가능
270
- - scoped인 경우 필요한 scope을 이미 가지고 있는지 확인
271
- - 이미 가지고 있는 경우 그대로 사용하면 된다.
272
- - 가지고 있지 않은 경우 새로운 인증 요청이 필요. 이때 기존 갖고 있던 scope과 새로 필요한 scope의 합집합을 다시 요청하게 된다.
273
- - 위 과정을 통해 현재 session에서 적용이 불가능 하다고 판단되면 DO_AUTH 상태 반환
274
- 4. 현재 인증된 session이 만료되었는지 확인
275
- - 만료되었다면 DO_REFRESH 상태 반환
276
- 5. 위 단계를 모두 통과한 경우에만 SKIP_AUTH 상태 반환
277
-
278
- ### 02. prepare
279
-
280
- 1. auth state가 SKIP_AUTH, DO_REFRESH, RESOLVED인 경우에는 그대로 함수를 종료
281
- 2. DO_AUTH, NO_SESSION인 경우에는 새로운 session과 future를 만들고, 사용자에게 authenticate URI를 반환
282
- 3. PENDING_RESOLVED인 경우에는 기존 session의 authenticate URI를 다시 반환
283
-
284
- ```mermaid
285
- flowchart TD
286
- Check["Prepare Auth"] --> A["Is AuthState<br>SKIP_AUTH, DO_REFRESH or RESOLVED?"]
287
- A -->|Yes| B["Return<br>None"]
288
- A -->|No| C["Is AuthState PENDING_RESOLVED?"]
289
- C -->|YES| D["Return<br>Previous Authorization URI"]
290
- C -->|NO| E["Make New Session"]
291
- E --> F["Return<br>Authorization URI"]
292
- ```
293
-
294
- ### 03. Authenticate
295
-
296
- 1. auth state가 SKIP_AUTH인 경우에는 기존 session을 그대로 반환
297
- 2. auth state가 DO_REFRESH인 경우에는 refresh를 수행하고 기존 session을 교체
298
- 3. auth state가 PENDING_RESOLVE 또는 RESOLVED인 경우에는 사용자로부터 전달받은 code로 authenticate를 수행
299
- 4. 그 외 auth state(NO_SESSION, DO_AUTH)는 해당 단계로 진입하지 않는다.
300
-
301
- ```mermaid
302
- flowchart TD
303
- Check["Authenticate"]
304
- Check -->|Is AuthState<br>SKIP_AUTH?| A2["Return<br>Existing Session"]
305
- Check -->|Is AuthState<br>DO_REFRESH?| B2["Refresh<br>Access Token"]
306
- Check -->|Is AuthState PENDING_RESOLVE<br>or<br>RESOLVED?| C2["Wait for Code<br>And<br>Authenticate"]
307
- Check -->|Is AuthState<br>NO_SESION<br>or<br>DO_AUTH?| D2["Can't be reached<br>while in state"]
308
- ```
309
-
@@ -1,32 +0,0 @@
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)
@@ -1,23 +0,0 @@
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"
@@ -1,147 +0,0 @@
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)
@@ -1,147 +0,0 @@
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)