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.
- hyperpocket/auth/README.md +3 -3
- hyperpocket/auth/__init__.py +0 -8
- hyperpocket/auth/gumloop/context.py +13 -0
- hyperpocket/auth/gumloop/token_context.py +15 -0
- hyperpocket/auth/gumloop/token_handler.py +66 -0
- hyperpocket/auth/gumloop/token_schema.py +8 -0
- hyperpocket/auth/linear/token_context.py +1 -1
- hyperpocket/auth/notion/README.md +28 -0
- hyperpocket/auth/notion/context.py +15 -0
- hyperpocket/auth/notion/token_context.py +14 -0
- hyperpocket/auth/notion/token_handler.py +65 -0
- hyperpocket/auth/notion/token_schema.py +10 -0
- hyperpocket/auth/provider.py +8 -5
- hyperpocket/auth/reddit/context.py +15 -0
- hyperpocket/auth/reddit/oauth2_context.py +32 -0
- hyperpocket/auth/reddit/oauth2_handler.py +151 -0
- hyperpocket/auth/reddit/oauth2_schema.py +18 -0
- hyperpocket/auth/slack/token_context.py +1 -1
- hyperpocket/builtin.py +63 -0
- hyperpocket/cli/__main__.py +12 -0
- hyperpocket/cli/auth.py +83 -0
- hyperpocket/cli/codegen/auth/__init__.py +13 -0
- hyperpocket/cli/codegen/auth/auth_context_template.py +16 -0
- hyperpocket/cli/codegen/auth/auth_token_context_template.py +16 -0
- hyperpocket/cli/codegen/auth/auth_token_handler_template.py +69 -0
- hyperpocket/cli/codegen/auth/auth_token_schema_template.py +12 -0
- hyperpocket/cli/codegen/auth/server_auth_template.py +18 -0
- hyperpocket/cli/eject.py +19 -0
- hyperpocket/cli/sync.py +5 -5
- hyperpocket/config/settings.py +2 -4
- hyperpocket/futures/futurestore.py +0 -1
- hyperpocket/pocket_auth.py +25 -5
- hyperpocket/pocket_core.py +262 -0
- hyperpocket/pocket_main.py +125 -171
- hyperpocket/prompts.py +6 -8
- hyperpocket/repository/__init__.py +2 -2
- hyperpocket/repository/lock.py +19 -0
- hyperpocket/repository/lockfile.py +19 -13
- hyperpocket/repository/repository.py +26 -1
- hyperpocket/server/auth/__init__.py +0 -6
- hyperpocket/server/auth/gumloop.py +16 -0
- hyperpocket/server/auth/notion.py +19 -0
- hyperpocket/server/auth/reddit.py +16 -0
- hyperpocket/server/server.py +52 -16
- hyperpocket/server/tool/dto/script.py +15 -2
- hyperpocket/server/tool/wasm.py +20 -8
- hyperpocket/session/README.md +2 -2
- hyperpocket/session/in_memory.py +18 -5
- hyperpocket/session/interface.py +14 -0
- hyperpocket/session/redis.py +29 -5
- hyperpocket/tool/README.md +16 -12
- hyperpocket/tool/__init__.py +4 -3
- hyperpocket/tool/function/README.md +39 -10
- hyperpocket/tool/function/__init__.py +2 -0
- hyperpocket/tool/function/annotation.py +2 -1
- hyperpocket/tool/function/tool.py +98 -13
- hyperpocket/tool/tests/test_function_tool.py +55 -0
- hyperpocket/tool/tests/test_wasm_tool.py +73 -0
- hyperpocket/tool/tool.py +65 -2
- hyperpocket/tool/wasm/README.md +27 -5
- hyperpocket/tool/wasm/script.py +40 -1
- hyperpocket/tool/wasm/templates/python.py +32 -14
- hyperpocket/tool/wasm/tool.py +21 -18
- hyperpocket/tool_like.py +5 -0
- hyperpocket/util/__init__.py +1 -1
- hyperpocket/util/extract_func_param_desc_from_docstring.py +4 -4
- hyperpocket/util/function_to_model.py +5 -2
- hyperpocket/util/json_schema_to_model.py +45 -26
- {hyperpocket-0.0.2.dist-info → hyperpocket-0.1.8.dist-info}/METADATA +101 -72
- hyperpocket-0.1.8.dist-info/RECORD +139 -0
- {hyperpocket-0.0.2.dist-info → hyperpocket-0.1.8.dist-info}/WHEEL +1 -1
- hyperpocket-0.1.8.dist-info/entry_points.txt +2 -0
- hyperpocket/auth/README.KR.md +0 -309
- hyperpocket/auth/slack/tests/test_oauth2_handler.py +0 -32
- hyperpocket/auth/slack/tests/test_token_handler.py +0 -23
- hyperpocket/auth/tests/test_google_oauth2_handler.py +0 -147
- hyperpocket/auth/tests/test_slack_oauth2_handler.py +0 -147
- hyperpocket/auth/tests/test_slack_token_handler.py +0 -66
- hyperpocket/external/__init__.py +0 -7
- hyperpocket/external/github_client.py +0 -19
- hyperpocket/session/README.KR.md +0 -62
- hyperpocket/session/tests/test_in_memory.py +0 -145
- hyperpocket/session/tests/test_redis.py +0 -151
- hyperpocket/tests/test_pocket.py +0 -116
- hyperpocket/tests/test_pocket_auth.py +0 -982
- hyperpocket/tool/README.KR.md +0 -68
- hyperpocket/tool/builtins/__init__.py +0 -0
- hyperpocket/tool/builtins/example/__init__.py +0 -0
- hyperpocket/tool/builtins/example/add_tool.py +0 -18
- hyperpocket/tool/function/README.KR.md +0 -159
- hyperpocket/tool/wasm/README.KR.md +0 -144
- hyperpocket-0.0.2.dist-info/RECORD +0 -130
- hyperpocket-0.0.2.dist-info/entry_points.txt +0 -3
- /hyperpocket/auth/{slack/tests → gumloop}/__init__.py +0 -0
- /hyperpocket/auth/{tests → notion}/__init__.py +0 -0
- /hyperpocket/{session/tests → auth/reddit}/__init__.py +0 -0
- /hyperpocket/{tests → cli/codegen}/__init__.py +0 -0
hyperpocket/auth/README.KR.md
DELETED
@@ -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)
|