from __future__ import annotations
import os
import sys
import time
from pathlib import Path
from typing import TYPE_CHECKING
from unittest.mock import AsyncMock, MagicMock, mock_open, patch
import pytest
# Add the project root directory to Python path
sys.path.append(str(Path(__file__).parent.parent.parent))
from client.auth import AuthClient
from client.checkin import CheckinClient
from client.search import SearchClient
from models.auth import TraktAuthToken
if TYPE_CHECKING:
from models.types import (
CheckinResponse,
CommentResponse,
DeviceCodeResponse,
SearchResult,
ShowResponse,
TokenResponse,
TraktRating,
TrendingWrapper,
)
@pytest.mark.asyncio
async def test_auth_flow_integration() -> None:
device_code_response: DeviceCodeResponse = {
"device_code": "device_code_123",
"user_code": "USER123",
"verification_url": "https://trakt.tv/activate",
"expires_in": 600,
"interval": 5,
}
auth_token_response: TokenResponse = {
"access_token": "access_token_123",
"refresh_token": "refresh_token_123",
"expires_in": 7200,
"created_at": int(time.time()),
"scope": "public",
"token_type": "bearer",
}
with (
patch("httpx.AsyncClient") as mock_client,
patch("builtins.open", mock_open()),
patch("json.dumps", return_value="{}"),
patch.dict(
os.environ,
{"TRAKT_CLIENT_ID": "test_id", "TRAKT_CLIENT_SECRET": "test_secret"},
),
patch("server.auth.tools.active_auth_flow", {}),
patch("os.path.exists", return_value=True),
):
device_code_mock = MagicMock()
device_code_mock.json.return_value = device_code_response
device_code_mock.raise_for_status = MagicMock()
auth_token_mock = MagicMock()
auth_token_mock.json.return_value = auth_token_response
auth_token_mock.raise_for_status = MagicMock()
mock_instance = mock_client.return_value.__aenter__.return_value
mock_instance.aclose = AsyncMock()
mock_instance.get = AsyncMock()
mock_instance.post = AsyncMock(side_effect=[device_code_mock, auth_token_mock])
# Also configure the direct return value (for non-context-manager usage)
mock_client.return_value.aclose = AsyncMock()
mock_client.return_value.get = AsyncMock()
mock_client.return_value.post = AsyncMock(
side_effect=[device_code_mock, auth_token_mock]
)
from server.auth.tools import start_device_auth
start_auth_result = await start_device_auth()
assert "USER123" in start_auth_result
assert "https://trakt.tv/activate" in start_auth_result
from server.auth.tools import active_auth_flow
assert "device_code" in active_auth_flow
assert active_auth_flow["device_code"] == "device_code_123"
from server.auth.tools import check_auth_status
check_auth_result = await check_auth_status()
assert "Authentication Successful" in check_auth_result
with patch("json.load", return_value=auth_token_response):
client = AuthClient()
assert client.is_authenticated() is True
assert client.auth_token is not None
assert client.auth_token.access_token == "access_token_123"
@pytest.mark.asyncio
async def test_search_and_checkin_integration() -> None:
# Mock search results
search_results: list[SearchResult] = [
{
"type": "show",
"score": 100.0,
"show": {
"title": "Breaking Bad",
"year": 2008,
"ids": {"trakt": 1, "slug": "breaking-bad"},
"overview": "A high school chemistry teacher diagnosed with inoperable lung cancer.",
},
}
]
# Mock checkin response
checkin_response: CheckinResponse = {
"id": 123456,
"watched_at": "2023-05-10T20:30:00Z",
"sharing": {"facebook": False, "twitter": False, "tumblr": False},
"show": {
"title": "Breaking Bad",
"year": 2008,
"ids": {"trakt": 1, "slug": "breaking-bad"},
},
"episode": {
"season": 1,
"number": 1,
"title": "Pilot",
"ids": {"trakt": 1, "slug": "pilot"},
},
}
# Set up mock responses for the focused clients
with (
patch("httpx.AsyncClient") as mock_client,
patch.dict(
os.environ,
{"TRAKT_CLIENT_ID": "test_id", "TRAKT_CLIENT_SECRET": "test_secret"},
),
):
# Mock for search request
search_mock = MagicMock()
search_mock.json.return_value = search_results
search_mock.raise_for_status = MagicMock()
# Mock for checkin request
checkin_mock = MagicMock()
checkin_mock.json.return_value = checkin_response
checkin_mock.raise_for_status = MagicMock()
# Configure the mock to return different responses for each call
mock_instance = mock_client.return_value.__aenter__.return_value
mock_instance.aclose = AsyncMock()
mock_instance.get = AsyncMock(return_value=search_mock)
mock_instance.post = AsyncMock(return_value=checkin_mock)
# Also configure the direct return value (for non-context-manager usage)
mock_client.return_value.aclose = AsyncMock()
mock_client.return_value.get = AsyncMock(return_value=search_mock)
mock_client.return_value.post = AsyncMock(return_value=checkin_mock)
# Create clients with mock auth tokens
search_client = SearchClient()
checkin_client = CheckinClient()
checkin_client.auth_token = TraktAuthToken(
access_token="test_token",
refresh_token="test_refresh",
expires_in=7200,
created_at=int(time.time()),
scope="public",
token_type="bearer",
)
with (
patch("server.search.tools.SearchClient", return_value=search_client),
patch("server.checkin.tools.CheckinClient", return_value=checkin_client),
):
from server.search.tools import search_shows
search_result = await search_shows(query="Breaking Bad")
# Verify the search results contain the expected show
assert "Breaking Bad (2008)" in search_result
assert "ID: 1" in search_result
from server.checkin.tools import checkin_to_show
checkin_result = await checkin_to_show(season=1, episode=1, show_id="1")
# Verify the checkin result contains the expected information
assert "Successfully Checked In" in checkin_result
assert "Breaking Bad" in checkin_result
assert "S01E01" in checkin_result
@pytest.mark.asyncio
async def test_trending_shows_integration() -> None:
# Mock trending shows response
trending_shows: list[TrendingWrapper] = [
{
"watchers": 100,
"show": {
"title": "Breaking Bad",
"year": 2008,
"ids": {"trakt": 1, "slug": "breaking-bad"},
"overview": "A high school chemistry teacher diagnosed with inoperable lung cancer.",
},
},
{
"watchers": 80,
"show": {
"title": "Stranger Things",
"year": 2016,
"ids": {"trakt": 2, "slug": "stranger-things"},
"overview": "When a young boy disappears, his mother and friends must confront terrifying forces.",
},
},
]
# Set up mock responses for the focused clients
with (
patch("httpx.AsyncClient") as mock_client,
patch.dict(
os.environ,
{"TRAKT_CLIENT_ID": "test_id", "TRAKT_CLIENT_SECRET": "test_secret"},
),
):
# Configure the mock
mock_instance = mock_client.return_value.__aenter__.return_value
# Mock for trending shows request
trending_mock = MagicMock()
trending_mock.json.return_value = trending_shows
trending_mock.raise_for_status = MagicMock()
mock_instance.get.return_value = trending_mock
with patch("server.shows.resources.ShowsClient") as mock_trending_client:
mock_client_instance = mock_trending_client.return_value
mock_client_instance.get_trending_shows = AsyncMock(
return_value=trending_shows
)
from server.shows.resources import get_trending_shows
resource_result = await get_trending_shows()
# Verify the resource result contains the expected shows
assert "# Trending Shows on Trakt" in resource_result
assert "Breaking Bad (2008)" in resource_result
assert "100 watchers" in resource_result
assert "Stranger Things (2016)" in resource_result
assert "80 watchers" in resource_result
with patch("server.shows.tools.TrendingShowsClient") as mock_trending_client:
mock_client_instance = mock_trending_client.return_value
mock_client_instance.get_trending_shows = AsyncMock(
return_value=trending_shows[:1]
)
from server.shows.tools import fetch_trending_shows
tool_result = await fetch_trending_shows(limit=1)
# Verify the tool result contains the expected shows
assert "# Trending Shows on Trakt" in tool_result
assert "Breaking Bad (2008)" in tool_result
# Note: Even though we set limit=1, our mock always returns the same data
# In a real scenario, the limit would be passed to the API
@pytest.mark.asyncio
async def test_show_ratings_integration() -> None:
# Mock show data
show_data: ShowResponse = {
"title": "Breaking Bad",
"year": 2008,
"ids": {"trakt": 1, "slug": "breaking-bad"},
}
# Mock ratings data
ratings_data: TraktRating = {
"rating": 9.0,
"votes": 1000,
"distribution": {
"10": 500,
"9": 300,
"8": 100,
"7": 50,
"6": 20,
"5": 15,
"4": 10,
"3": 3,
"2": 1,
"1": 1,
},
}
# Set up mock responses for the focused clients
with (
patch("httpx.AsyncClient") as mock_client,
patch.dict(
os.environ,
{"TRAKT_CLIENT_ID": "test_id", "TRAKT_CLIENT_SECRET": "test_secret"},
),
):
# Configure the mock
mock_instance = mock_client.return_value.__aenter__.return_value
# Mock for show request
show_mock = MagicMock()
show_mock.json.return_value = show_data
show_mock.raise_for_status = MagicMock()
# Mock for ratings request
ratings_mock = MagicMock()
ratings_mock.json.return_value = ratings_data
ratings_mock.raise_for_status = MagicMock()
# Set up the sequence of responses
mock_instance.get.side_effect = [
show_mock,
ratings_mock,
show_mock,
ratings_mock,
]
with patch("server.shows.resources.ShowsClient") as mock_details_client:
mock_client_instance = mock_details_client.return_value
mock_client_instance.get_show = AsyncMock(return_value=show_data)
mock_client_instance.get_show_ratings = AsyncMock(return_value=ratings_data)
from server.shows.resources import get_show_ratings
resource_result = await get_show_ratings(show_id="1")
# Verify the resource result contains the expected ratings
assert "# Ratings for Breaking Bad" in resource_result
assert "**Average Rating:** 9.00/10" in resource_result
assert "from 1000 votes" in resource_result
with patch("server.shows.tools.ShowDetailsClient") as mock_details_client:
mock_client_instance = mock_details_client.return_value
mock_client_instance.get_show = AsyncMock(return_value=show_data)
mock_client_instance.get_show_ratings = AsyncMock(return_value=ratings_data)
from server.shows.tools import fetch_show_ratings
tool_result = await fetch_show_ratings(show_id="1")
# Verify the tool result contains the expected ratings
assert "# Ratings for Breaking Bad" in tool_result
assert "**Average Rating:** 9.00/10" in tool_result
assert "from 1000 votes" in tool_result
@pytest.mark.asyncio
async def test_error_handling_integration() -> None:
# Set up mock responses for the focused clients that will trigger errors
with (
patch("httpx.AsyncClient") as mock_client,
patch.dict(
os.environ,
{"TRAKT_CLIENT_ID": "test_id", "TRAKT_CLIENT_SECRET": "test_secret"},
),
):
# Configure the mock to raise an exception
mock_instance = mock_client.return_value.__aenter__.return_value
mock_instance.get.side_effect = Exception("API error")
# Test error handling in a resource function directly
with patch("server.shows.resources.ShowsClient") as mock_details_client:
mock_client_instance = mock_details_client.return_value
mock_client_instance.get_show = AsyncMock(
side_effect=Exception("API error")
)
from server.shows.resources import get_show_ratings
from utils.api.errors import InternalError
# Should raise an InternalError for unexpected exceptions
with pytest.raises(InternalError) as exc_info:
await get_show_ratings(show_id="1")
assert "unexpected error occurred" in str(exc_info.value).lower()
# Test error handling in a tool function directly
with patch("server.shows.tools.ShowDetailsClient") as mock_details_client:
mock_client_instance = mock_details_client.return_value
mock_client_instance.get_show = AsyncMock(
side_effect=Exception("API error")
)
from server.shows.tools import fetch_show_ratings
# Should raise an InternalError for unexpected exceptions
with pytest.raises(InternalError) as exc_info:
await fetch_show_ratings(show_id="1")
assert "unexpected error occurred" in str(exc_info.value).lower()
@pytest.mark.asyncio
async def test_token_refresh_integration() -> None:
# Since refresh_token method doesn't exist, we'll test authentication status instead
with (
patch("httpx.AsyncClient") as mock_client,
patch("builtins.open", mock_open()),
patch("json.dumps", return_value="{}"),
patch.dict(
os.environ,
{"TRAKT_CLIENT_ID": "test_id", "TRAKT_CLIENT_SECRET": "test_secret"},
),
):
# Configure the mock for API request
mock_instance = mock_client.return_value.__aenter__.return_value
# Mock for API request
api_mock = MagicMock()
show_response: ShowResponse = {
"title": "Breaking Bad",
"year": 2008,
"ids": {"trakt": 1, "slug": "breaking-bad"},
}
api_mock.json.return_value = show_response
api_mock.raise_for_status = MagicMock()
mock_instance.get.return_value = api_mock
# Create a client with a valid token
client = AuthClient()
client.auth_token = TraktAuthToken(
access_token="valid_access_token",
refresh_token="valid_refresh_token",
expires_in=7200,
created_at=int(time.time()), # Current time
scope="public",
token_type="bearer",
)
with patch("server.auth.resources.AuthClient", return_value=client):
# Check authentication status
from server.auth.resources import get_auth_status
result = await get_auth_status()
# Verify the result indicates authentication
assert "You are authenticated with Trakt" in result
@pytest.mark.asyncio
async def test_comments_integration() -> None:
# Mock comments data
sample_comments: list[CommentResponse] = [
{
"id": 123,
"comment": "This show is amazing!",
"spoiler": False,
"review": False,
"likes": 10,
"replies": 2,
"created_at": "2023-01-15T20:30:00Z",
"user": {
"username": "testuser",
"private": False,
"ids": {"slug": "testuser"},
},
}
]
sample_show: ShowResponse = {
"title": "Breaking Bad",
"year": 2008,
"ids": {"trakt": 1, "slug": "breaking-bad"},
}
# Set up mock responses for the focused clients
with (
patch("httpx.AsyncClient") as mock_client,
patch.dict(
os.environ,
{"TRAKT_CLIENT_ID": "test_id", "TRAKT_CLIENT_SECRET": "test_secret"},
),
):
# Configure the mock
mock_instance = mock_client.return_value.__aenter__.return_value
# Mock for show request
show_mock = MagicMock()
show_mock.json.return_value = sample_show
show_mock.raise_for_status = MagicMock()
# Mock for comments request
comments_mock = MagicMock()
comments_mock.json.return_value = sample_comments
comments_mock.raise_for_status = MagicMock()
# Set up the response for comments request
mock_instance.get.return_value = comments_mock
# Call the function directly
with patch("server.comments.tools.ShowCommentsClient") as mock_comments_client:
mock_client_instance = mock_comments_client.return_value
mock_client_instance.get_show_comments = AsyncMock(
return_value=sample_comments
)
from server.comments.tools import fetch_show_comments
result = await fetch_show_comments(show_id="1")
# Verify the result contains the expected comment
assert "# Comments for Show ID: 1" in result
assert "testuser" in result
assert "This show is amazing!" in result
assert "Likes: 10 | Replies: 2" in result