[tasks] Fix Todoist callback to lookup by user
This commit is contained in:
127
tests/tasks_tests/test_todoist.py
Normal file
127
tests/tasks_tests/test_todoist.py
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def user_profile(db):
|
||||||
|
user = User.objects.create_user(username="testuser", password="testpass")
|
||||||
|
return user.profile
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
class TestGenerateTodoistOauthUrl:
|
||||||
|
def test_generates_url_with_state(self, user_profile):
|
||||||
|
from tasks.todoist import generate_todoist_oauth_url
|
||||||
|
|
||||||
|
url = generate_todoist_oauth_url(user_profile.user_id)
|
||||||
|
|
||||||
|
user_profile.refresh_from_db()
|
||||||
|
assert user_profile.todoist_state is not None
|
||||||
|
assert len(user_profile.todoist_state) == 32
|
||||||
|
assert url.startswith("https://todoist.com/oauth/authorize")
|
||||||
|
assert user_profile.todoist_state in url
|
||||||
|
|
||||||
|
def test_updates_existing_state(self, user_profile):
|
||||||
|
from tasks.todoist import generate_todoist_oauth_url
|
||||||
|
|
||||||
|
old_state = "oldstate12345678901234567890123"
|
||||||
|
user_profile.todoist_state = old_state
|
||||||
|
user_profile.save()
|
||||||
|
|
||||||
|
url = generate_todoist_oauth_url(user_profile.user_id)
|
||||||
|
|
||||||
|
user_profile.refresh_from_db()
|
||||||
|
assert user_profile.todoist_state != old_state
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
class TestGetTodoistAccessToken:
|
||||||
|
def test_raises_when_profile_not_found(self):
|
||||||
|
from tasks.todoist import get_todoist_access_token
|
||||||
|
|
||||||
|
with pytest.raises(Exception, match="Could not find profile"):
|
||||||
|
get_todoist_access_token(user_id=999, state="anystate", code="anycode")
|
||||||
|
|
||||||
|
def test_raises_when_state_mismatch(self, user_profile):
|
||||||
|
from tasks.todoist import get_todoist_access_token
|
||||||
|
|
||||||
|
user_profile.todoist_state = "correctstate1234567890123"
|
||||||
|
user_profile.save()
|
||||||
|
|
||||||
|
with pytest.raises(Exception, match="state mismatch"):
|
||||||
|
get_todoist_access_token(
|
||||||
|
user_id=user_profile.user_id, state="wrongstate", code="anycode"
|
||||||
|
)
|
||||||
|
|
||||||
|
@patch("tasks.todoist.requests.post")
|
||||||
|
def test_exchanges_code_for_token(self, mock_post, user_profile):
|
||||||
|
from tasks.todoist import get_todoist_access_token
|
||||||
|
|
||||||
|
user_profile.todoist_state = "correctstate1234567890123"
|
||||||
|
user_profile.save()
|
||||||
|
|
||||||
|
mock_token_response = MagicMock()
|
||||||
|
mock_token_response.status_code = 200
|
||||||
|
mock_token_response.json.return_value = {"access_token": "test_access_token"}
|
||||||
|
mock_post.return_value = mock_token_response
|
||||||
|
|
||||||
|
get_todoist_access_token(
|
||||||
|
user_id=user_profile.user_id,
|
||||||
|
state="correctstate1234567890123",
|
||||||
|
code="testcode",
|
||||||
|
)
|
||||||
|
|
||||||
|
user_profile.refresh_from_db()
|
||||||
|
assert user_profile.todoist_auth_key == "test_access_token"
|
||||||
|
assert user_profile.todoist_state is None
|
||||||
|
|
||||||
|
@patch("tasks.todoist.requests.post")
|
||||||
|
def test_fetches_todoist_user_id(self, mock_post, user_profile):
|
||||||
|
from tasks.todoist import get_todoist_access_token
|
||||||
|
|
||||||
|
user_profile.todoist_state = "correctstate1234567890123"
|
||||||
|
user_profile.save()
|
||||||
|
|
||||||
|
mock_token_response = MagicMock()
|
||||||
|
mock_token_response.status_code = 200
|
||||||
|
mock_token_response.json.return_value = {"access_token": "test_access_token"}
|
||||||
|
|
||||||
|
mock_sync_response = MagicMock()
|
||||||
|
mock_sync_response.status_code = 200
|
||||||
|
mock_sync_response.json.return_value = {"user": {"id": "12345"}}
|
||||||
|
|
||||||
|
mock_post.side_effect = [mock_token_response, mock_sync_response]
|
||||||
|
|
||||||
|
get_todoist_access_token(
|
||||||
|
user_id=user_profile.user_id,
|
||||||
|
state="correctstate1234567890123",
|
||||||
|
code="testcode",
|
||||||
|
)
|
||||||
|
|
||||||
|
user_profile.refresh_from_db()
|
||||||
|
assert user_profile.todoist_user_id == "12345"
|
||||||
|
|
||||||
|
@patch("tasks.todoist.requests.post")
|
||||||
|
def test_handles_token_exchange_failure(self, mock_post, user_profile):
|
||||||
|
from tasks.todoist import get_todoist_access_token
|
||||||
|
|
||||||
|
user_profile.todoist_state = "correctstate1234567890123"
|
||||||
|
user_profile.save()
|
||||||
|
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.status_code = 400
|
||||||
|
mock_post.return_value = mock_response
|
||||||
|
|
||||||
|
get_todoist_access_token(
|
||||||
|
user_id=user_profile.user_id,
|
||||||
|
state="correctstate1234567890123",
|
||||||
|
code="badcode",
|
||||||
|
)
|
||||||
|
|
||||||
|
user_profile.refresh_from_db()
|
||||||
|
assert user_profile.todoist_auth_key is None
|
||||||
|
assert user_profile.todoist_state == "correctstate1234567890123"
|
||||||
@ -1,5 +1,4 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
import secrets
|
import secrets
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
@ -16,6 +15,7 @@ TODOIST_OAUTH_START_URL = "https://todoist.com/oauth/authorize?client_id={id}&sc
|
|||||||
id=TODOIST_CLIENT_ID
|
id=TODOIST_CLIENT_ID
|
||||||
)
|
)
|
||||||
TODOIST_OAUTH_TOKEN_URL = "https://todoist.com/oauth/access_token"
|
TODOIST_OAUTH_TOKEN_URL = "https://todoist.com/oauth/access_token"
|
||||||
|
TODOIST_API_URL = "https://api.todoist.com/api/v1/sync"
|
||||||
|
|
||||||
|
|
||||||
def generate_todoist_oauth_url(user_id: int) -> str:
|
def generate_todoist_oauth_url(user_id: int) -> str:
|
||||||
@ -28,12 +28,29 @@ def generate_todoist_oauth_url(user_id: int) -> str:
|
|||||||
def get_todoist_access_token(user_id: int, state: str, code: str):
|
def get_todoist_access_token(user_id: int, state: str, code: str):
|
||||||
logger.info(
|
logger.info(
|
||||||
"[get_todoist_access_token] called",
|
"[get_todoist_access_token] called",
|
||||||
extra={"state": state, "code": code},
|
extra={
|
||||||
|
"user_id": user_id,
|
||||||
|
"state": state,
|
||||||
|
"state_repr": repr(state),
|
||||||
|
"code": code,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
user_profile = UserProfile.objects.filter(todoist_state=state).first()
|
|
||||||
|
|
||||||
if not user_profile:
|
user_profile = UserProfile.objects.filter(user_id=user_id).first()
|
||||||
raise Exception("Could not find profile")
|
|
||||||
|
logger.info(
|
||||||
|
"[get_todoist_access_token] found profile",
|
||||||
|
extra={
|
||||||
|
"user_id": user_id,
|
||||||
|
"profile_state": user_profile.todoist_state if user_profile else None,
|
||||||
|
"passed_state": state,
|
||||||
|
"match": user_profile.todoist_state == state if user_profile else False,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if not user_profile or user_profile.todoist_state != state:
|
||||||
|
logger.error("[get_todoist_access_token] profile not found or state mismatch")
|
||||||
|
raise Exception("Could not find profile or state mismatch")
|
||||||
|
|
||||||
post_data = {
|
post_data = {
|
||||||
"client_id": settings.TODOIST_CLIENT_ID,
|
"client_id": settings.TODOIST_CLIENT_ID,
|
||||||
@ -44,11 +61,27 @@ def get_todoist_access_token(user_id: int, state: str, code: str):
|
|||||||
response = requests.post(TODOIST_OAUTH_TOKEN_URL, data=post_data)
|
response = requests.post(TODOIST_OAUTH_TOKEN_URL, data=post_data)
|
||||||
|
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
user_profile.todoist_auth_key = response.json().get("access_token")
|
access_token = response.json().get("access_token")
|
||||||
|
user_profile.todoist_auth_key = access_token
|
||||||
user_profile.todoist_state = None
|
user_profile.todoist_state = None
|
||||||
user_profile.save()
|
user_profile.save(update_fields=["todoist_auth_key", "todoist_state"])
|
||||||
|
|
||||||
|
sync_response = requests.post(
|
||||||
|
TODOIST_API_URL,
|
||||||
|
data={"sync_token": "*", "resource_types": '["user"]'},
|
||||||
|
headers={"Authorization": f"Bearer {access_token}"},
|
||||||
|
)
|
||||||
|
if sync_response.status_code == 200:
|
||||||
|
todoist_user_id = sync_response.json().get("user", {}).get("id")
|
||||||
|
if todoist_user_id:
|
||||||
|
user_profile.todoist_user_id = todoist_user_id
|
||||||
|
user_profile.save(update_fields=["todoist_user_id"])
|
||||||
|
logger.info(
|
||||||
|
"[get_todoist_access_token] set todoist_user_id",
|
||||||
|
extra={"todoist_user_id": todoist_user_id},
|
||||||
|
)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"[get_todoist_access_token] finished",
|
"[get_todoist_access_token] finished",
|
||||||
extra={"user_id": user_profile.user.id},
|
extra={"user_id": user_id},
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user