"""
Tests for the TickTick API wrapper
"""
import pytest
from unittest.mock import patch, MagicMock, Mock
import responses
from src.api.ticktick_api import TickTickAPI, TickTickOAuth
@pytest.mark.unit
class TestTickTickOAuth:
"""Test the TickTickOAuth class"""
def test_oauth_initialization(self, monkeypatch):
"""Test OAuth initialization with env variables"""
monkeypatch.setenv('TICKTICK_CLIENT_ID', 'test_client_id')
monkeypatch.setenv('TICKTICK_CLIENT_SECRET', 'test_secret')
monkeypatch.setenv('TICKTICK_REDIRECT_URI', 'http://localhost')
oauth = TickTickOAuth()
assert oauth.client_id == 'test_client_id'
assert oauth.client_secret == 'test_secret'
assert oauth.redirect_uri == 'http://localhost'
def test_get_authorization_url(self, monkeypatch):
"""Test getting authorization URL"""
monkeypatch.setenv('TICKTICK_CLIENT_ID', 'test_id')
monkeypatch.setenv('TICKTICK_CLIENT_SECRET', 'test_secret')
monkeypatch.setenv('TICKTICK_REDIRECT_URI', 'http://localhost')
oauth = TickTickOAuth()
url = oauth.get_authorization_url()
assert 'https://ticktick.com/oauth/authorize' in url
assert 'test_id' in url
# Check for URL-encoded scope values
assert 'tasks%3Aread' in url or 'tasks:read' in url
assert 'tasks%3Awrite' in url or 'tasks:write' in url
@responses.activate
def test_get_access_token_success(self, monkeypatch):
"""Test successfully getting access token"""
monkeypatch.setenv('TICKTICK_CLIENT_ID', 'test_id')
monkeypatch.setenv('TICKTICK_CLIENT_SECRET', 'test_secret')
monkeypatch.setenv('TICKTICK_REDIRECT_URI', 'http://localhost')
# Mock the token endpoint
responses.add(
responses.POST,
'https://ticktick.com/oauth/token',
json={'access_token': 'test_access_token', 'token_type': 'Bearer'},
status=200
)
oauth = TickTickOAuth()
token_data = oauth.exchange_code_for_token('test_auth_code')
assert token_data['access_token'] == 'test_access_token'
@responses.activate
def test_get_access_token_failure(self, monkeypatch):
"""Test failed token request"""
monkeypatch.setenv('TICKTICK_CLIENT_ID', 'test_id')
monkeypatch.setenv('TICKTICK_CLIENT_SECRET', 'test_secret')
monkeypatch.setenv('TICKTICK_REDIRECT_URI', 'http://localhost')
# Mock failed response
responses.add(
responses.POST,
'https://ticktick.com/oauth/token',
json={'error': 'invalid_grant'},
status=400
)
oauth = TickTickOAuth()
token_data = oauth.exchange_code_for_token('invalid_code')
assert token_data is None
@pytest.mark.unit
class TestTickTickAPI:
"""Test the TickTickAPI class"""
def test_api_initialization(self):
"""Test API initialization"""
api = TickTickAPI('test_token')
assert api.access_token == 'test_token'
assert api.api_base == 'https://api.ticktick.com'
assert 'Authorization' in api.headers
assert api.headers['Authorization'] == 'Bearer test_token'
@responses.activate
def test_get_user_info_success(self):
"""Test getting user info"""
responses.add(
responses.GET,
'https://api.ticktick.com/open/v1/user',
json={'username': 'testuser', 'email': 'test@example.com'},
status=200
)
api = TickTickAPI('test_token')
user = api.get_user_info()
assert user is not None
assert user['username'] == 'testuser'
@responses.activate
def test_get_user_info_failure(self):
"""Test failed user info request"""
responses.add(
responses.GET,
'https://api.ticktick.com/open/v1/user',
json={'error': 'unauthorized'},
status=401
)
api = TickTickAPI('invalid_token')
user = api.get_user_info()
assert user is None
@responses.activate
def test_get_projects_success(self):
"""Test getting projects"""
responses.add(
responses.GET,
'https://api.ticktick.com/open/v1/project',
json=[
{'id': 'proj1', 'name': 'Work'},
{'id': 'proj2', 'name': 'Personal'}
],
status=200
)
api = TickTickAPI('test_token')
projects = api.get_projects()
assert len(projects) == 2
assert projects[0]['name'] == 'Work'
@responses.activate
def test_get_projects_data_success(self):
"""Test getting project data with tasks"""
responses.add(
responses.GET,
'https://api.ticktick.com/open/v1/project/proj1/data',
json={
'id': 'proj1',
'name': 'Work',
'tasks': [{'id': 'task1', 'title': 'Test Task'}]
},
status=200
)
api = TickTickAPI('test_token')
project_data = api.get_projects_data('proj1')
assert project_data is not None
assert project_data['name'] == 'Work'
assert len(project_data['tasks']) == 1
@responses.activate
def test_create_task_minimal(self):
"""Test creating task with minimal parameters"""
responses.add(
responses.POST,
'https://api.ticktick.com/open/v1/task',
json={'id': 'task123', 'title': 'New Task'},
status=200
)
api = TickTickAPI('test_token')
task = api.create_task(title='New Task')
assert task is not None
assert task['id'] == 'task123'
@responses.activate
def test_create_task_with_all_params(self):
"""Test creating task with all parameters"""
responses.add(
responses.POST,
'https://api.ticktick.com/open/v1/task',
json={'id': 'task123', 'title': 'Complete Task'},
status=200
)
api = TickTickAPI('test_token')
task = api.create_task(
title='Complete Task',
content='Task description',
project_id='proj1',
due_date='2026-12-31T23:59:59.000Z',
tags=['work', 'urgent']
)
assert task is not None
assert task['id'] == 'task123'
# Verify request body
request_body = responses.calls[0].request.body
assert b'Complete Task' in request_body
assert b'work' in request_body
assert b'urgent' in request_body
@responses.activate
def test_create_task_with_tags_string(self):
"""Test creating task with tags as string (converted to list)"""
responses.add(
responses.POST,
'https://api.ticktick.com/open/v1/task',
json={'id': 'task123'},
status=200
)
api = TickTickAPI('test_token')
task = api.create_task(title='Task', tags='single-tag')
assert task is not None
# Verify tags were converted to list
request_body = responses.calls[0].request.body
assert b'single-tag' in request_body
@responses.activate
def test_create_task_failure(self):
"""Test failed task creation"""
responses.add(
responses.POST,
'https://api.ticktick.com/open/v1/task',
json={'error': 'Bad Request'},
status=400
)
api = TickTickAPI('test_token')
task = api.create_task(title='Failed Task')
assert task is None
@responses.activate
def test_api_handles_network_error(self):
"""Test API handles network errors gracefully"""
responses.add(
responses.GET,
'https://api.ticktick.com/open/v1/user',
body=Exception('Network error')
)
api = TickTickAPI('test_token')
# Should not crash, should return None
try:
user = api.get_user_info()
# Depending on implementation, might return None or raise
except Exception:
# If it raises, that's also acceptable
pass
@pytest.mark.integration
class TestTickTickAPIIntegration:
"""Integration tests for TickTick API (requires real token)"""
def test_api_integration_user_info(self, mock_env_token):
"""Integration test for getting user info"""
# This test would use a real API token if available
# For now, we skip if no token
import os
token = os.getenv('TICKTICK_ACCESS_TOKEN')
if not token or token == 'test_token_123':
pytest.skip("No real API token available")
api = TickTickAPI(token)
user = api.get_user_info()
# If we have a real token, we should get real user info
assert user is not None
assert 'username' in user or 'email' in user