Files
sdb2-backend/tests/api/v1/test_player_endpoints.py
2025-08-01 09:30:15 +02:00

661 lines
22 KiB
Python

"""Tests for player API endpoints."""
# ruff: noqa: ARG002, PLR2004, ANN001, ANN201
from unittest.mock import AsyncMock, Mock, patch
import pytest
from httpx import AsyncClient
from app.models.user import User
from app.services.player import PlayerMode, PlayerStatus
@pytest.fixture
def mock_player_service():
"""Mock player service for testing."""
with patch("app.api.v1.player.get_player_service") as mock:
service = Mock()
service.play = AsyncMock()
service.pause = AsyncMock()
service.stop_playback = AsyncMock()
service.next = AsyncMock()
service.previous = AsyncMock()
service.seek = AsyncMock()
service.set_volume = AsyncMock()
service.set_mode = AsyncMock()
service.reload_playlist = AsyncMock()
service.get_state = Mock() # This should return a dict, not a coroutine
mock.return_value = service
yield service
class TestPlayerEndpoints:
"""Test player API endpoints."""
@pytest.mark.asyncio
async def test_play_success(
self,
authenticated_client: AsyncClient,
authenticated_user: User,
mock_player_service,
) -> None:
"""Test starting playback successfully."""
response = await authenticated_client.post("/api/v1/player/play")
assert response.status_code == 200
data = response.json()
assert data["message"] == "Playback started"
mock_player_service.play.assert_called_once_with()
@pytest.mark.asyncio
async def test_play_unauthenticated(self, client: AsyncClient) -> None:
"""Test starting playback without authentication."""
response = await client.post("/api/v1/player/play")
assert response.status_code == 401
@pytest.mark.asyncio
async def test_play_service_error(
self,
authenticated_client: AsyncClient,
authenticated_user: User,
mock_player_service,
) -> None:
"""Test starting playback with service error."""
mock_player_service.play.side_effect = Exception("Service error")
response = await authenticated_client.post("/api/v1/player/play")
assert response.status_code == 500
data = response.json()
assert data["detail"] == "Failed to start playback"
@pytest.mark.asyncio
async def test_play_at_index_success(
self,
authenticated_client: AsyncClient,
authenticated_user: User,
mock_player_service,
) -> None:
"""Test playing sound at specific index successfully."""
index = 2
response = await authenticated_client.post(f"/api/v1/player/play/{index}")
assert response.status_code == 200
data = response.json()
assert data["message"] == f"Playing sound at index {index}"
mock_player_service.play.assert_called_once_with(index)
@pytest.mark.asyncio
async def test_play_at_index_invalid_index(
self,
authenticated_client: AsyncClient,
authenticated_user: User,
mock_player_service,
) -> None:
"""Test playing sound with invalid index."""
mock_player_service.play.side_effect = ValueError("Invalid sound index")
response = await authenticated_client.post("/api/v1/player/play/999")
assert response.status_code == 400
data = response.json()
assert data["detail"] == "Invalid sound index"
@pytest.mark.asyncio
async def test_play_at_index_service_error(
self,
authenticated_client: AsyncClient,
authenticated_user: User,
mock_player_service,
) -> None:
"""Test playing sound at index with service error."""
mock_player_service.play.side_effect = Exception("Service error")
response = await authenticated_client.post("/api/v1/player/play/0")
assert response.status_code == 500
data = response.json()
assert data["detail"] == "Failed to play sound"
@pytest.mark.asyncio
async def test_pause_success(
self,
authenticated_client: AsyncClient,
authenticated_user: User,
mock_player_service,
) -> None:
"""Test pausing playback successfully."""
response = await authenticated_client.post("/api/v1/player/pause")
assert response.status_code == 200
data = response.json()
assert data["message"] == "Playback paused"
mock_player_service.pause.assert_called_once()
@pytest.mark.asyncio
async def test_pause_unauthenticated(self, client: AsyncClient) -> None:
"""Test pausing playback without authentication."""
response = await client.post("/api/v1/player/pause")
assert response.status_code == 401
@pytest.mark.asyncio
async def test_pause_service_error(
self,
authenticated_client: AsyncClient,
authenticated_user: User,
mock_player_service,
) -> None:
"""Test pausing playback with service error."""
mock_player_service.pause.side_effect = Exception("Service error")
response = await authenticated_client.post("/api/v1/player/pause")
assert response.status_code == 500
data = response.json()
assert data["detail"] == "Failed to pause playback"
@pytest.mark.asyncio
async def test_stop_success(
self,
authenticated_client: AsyncClient,
authenticated_user: User,
mock_player_service,
) -> None:
"""Test stopping playback successfully."""
response = await authenticated_client.post("/api/v1/player/stop")
assert response.status_code == 200
data = response.json()
assert data["message"] == "Playback stopped"
mock_player_service.stop_playback.assert_called_once()
@pytest.mark.asyncio
async def test_stop_unauthenticated(self, client: AsyncClient) -> None:
"""Test stopping playback without authentication."""
response = await client.post("/api/v1/player/stop")
assert response.status_code == 401
@pytest.mark.asyncio
async def test_stop_service_error(
self,
authenticated_client: AsyncClient,
authenticated_user: User,
mock_player_service,
) -> None:
"""Test stopping playback with service error."""
mock_player_service.stop_playback.side_effect = Exception("Service error")
response = await authenticated_client.post("/api/v1/player/stop")
assert response.status_code == 500
data = response.json()
assert data["detail"] == "Failed to stop playback"
@pytest.mark.asyncio
async def test_next_track_success(
self,
authenticated_client: AsyncClient,
authenticated_user: User,
mock_player_service,
) -> None:
"""Test skipping to next track successfully."""
response = await authenticated_client.post("/api/v1/player/next")
assert response.status_code == 200
data = response.json()
assert data["message"] == "Skipped to next track"
mock_player_service.next.assert_called_once()
@pytest.mark.asyncio
async def test_next_track_unauthenticated(self, client: AsyncClient) -> None:
"""Test skipping to next track without authentication."""
response = await client.post("/api/v1/player/next")
assert response.status_code == 401
@pytest.mark.asyncio
async def test_next_track_service_error(
self,
authenticated_client: AsyncClient,
authenticated_user: User,
mock_player_service,
) -> None:
"""Test skipping to next track with service error."""
mock_player_service.next.side_effect = Exception("Service error")
response = await authenticated_client.post("/api/v1/player/next")
assert response.status_code == 500
data = response.json()
assert data["detail"] == "Failed to skip to next track"
@pytest.mark.asyncio
async def test_previous_track_success(
self,
authenticated_client: AsyncClient,
authenticated_user: User,
mock_player_service,
) -> None:
"""Test going to previous track successfully."""
response = await authenticated_client.post("/api/v1/player/previous")
assert response.status_code == 200
data = response.json()
assert data["message"] == "Went to previous track"
mock_player_service.previous.assert_called_once()
@pytest.mark.asyncio
async def test_previous_track_unauthenticated(self, client: AsyncClient) -> None:
"""Test going to previous track without authentication."""
response = await client.post("/api/v1/player/previous")
assert response.status_code == 401
@pytest.mark.asyncio
async def test_previous_track_service_error(
self,
authenticated_client: AsyncClient,
authenticated_user: User,
mock_player_service,
) -> None:
"""Test going to previous track with service error."""
mock_player_service.previous.side_effect = Exception("Service error")
response = await authenticated_client.post("/api/v1/player/previous")
assert response.status_code == 500
data = response.json()
assert data["detail"] == "Failed to go to previous track"
@pytest.mark.asyncio
async def test_seek_success(
self,
authenticated_client: AsyncClient,
authenticated_user: User,
mock_player_service,
) -> None:
"""Test seeking to position successfully."""
position = 5000
response = await authenticated_client.post(
"/api/v1/player/seek",
json={"position": position},
)
assert response.status_code == 200
data = response.json()
assert data["message"] == f"Seeked to position {position}ms"
mock_player_service.seek.assert_called_once_with(position)
@pytest.mark.asyncio
async def test_seek_invalid_position(
self,
authenticated_client: AsyncClient,
authenticated_user: User,
mock_player_service,
) -> None:
"""Test seeking with invalid position."""
response = await authenticated_client.post(
"/api/v1/player/seek",
json={"position": -1000}, # Negative position
)
assert response.status_code == 422 # Validation error
@pytest.mark.asyncio
async def test_seek_unauthenticated(self, client: AsyncClient) -> None:
"""Test seeking without authentication."""
response = await client.post(
"/api/v1/player/seek",
json={"position": 5000},
)
assert response.status_code == 401
@pytest.mark.asyncio
async def test_seek_service_error(
self,
authenticated_client: AsyncClient,
authenticated_user: User,
mock_player_service,
) -> None:
"""Test seeking with service error."""
mock_player_service.seek.side_effect = Exception("Service error")
response = await authenticated_client.post(
"/api/v1/player/seek",
json={"position": 5000},
)
assert response.status_code == 500
data = response.json()
assert data["detail"] == "Failed to seek"
@pytest.mark.asyncio
async def test_set_volume_success(
self,
authenticated_client: AsyncClient,
authenticated_user: User,
mock_player_service,
) -> None:
"""Test setting volume successfully."""
volume = 75
response = await authenticated_client.post(
"/api/v1/player/volume",
json={"volume": volume},
)
assert response.status_code == 200
data = response.json()
assert data["message"] == f"Volume set to {volume}"
mock_player_service.set_volume.assert_called_once_with(volume)
@pytest.mark.asyncio
async def test_set_volume_invalid_range(
self,
authenticated_client: AsyncClient,
authenticated_user: User,
mock_player_service,
) -> None:
"""Test setting volume with invalid range."""
# Test volume too high
response = await authenticated_client.post(
"/api/v1/player/volume",
json={"volume": 150},
)
assert response.status_code == 422
# Test volume too low
response = await authenticated_client.post(
"/api/v1/player/volume",
json={"volume": -10},
)
assert response.status_code == 422
@pytest.mark.asyncio
async def test_set_volume_unauthenticated(self, client: AsyncClient) -> None:
"""Test setting volume without authentication."""
response = await client.post(
"/api/v1/player/volume",
json={"volume": 50},
)
assert response.status_code == 401
@pytest.mark.asyncio
async def test_set_volume_service_error(
self,
authenticated_client: AsyncClient,
authenticated_user: User,
mock_player_service,
) -> None:
"""Test setting volume with service error."""
mock_player_service.set_volume.side_effect = Exception("Service error")
response = await authenticated_client.post(
"/api/v1/player/volume",
json={"volume": 75},
)
assert response.status_code == 500
data = response.json()
assert data["detail"] == "Failed to set volume"
@pytest.mark.asyncio
async def test_set_mode_success(
self,
authenticated_client: AsyncClient,
authenticated_user: User,
mock_player_service,
) -> None:
"""Test setting playback mode successfully."""
mode = PlayerMode.LOOP
response = await authenticated_client.post(
"/api/v1/player/mode",
json={"mode": mode.value},
)
assert response.status_code == 200
data = response.json()
assert data["message"] == f"Mode set to {mode.value}"
mock_player_service.set_mode.assert_called_once_with(mode)
@pytest.mark.asyncio
async def test_set_mode_invalid_mode(
self,
authenticated_client: AsyncClient,
authenticated_user: User,
mock_player_service,
) -> None:
"""Test setting invalid playback mode."""
response = await authenticated_client.post(
"/api/v1/player/mode",
json={"mode": "invalid_mode"},
)
assert response.status_code == 422 # Validation error
@pytest.mark.asyncio
async def test_set_mode_unauthenticated(self, client: AsyncClient) -> None:
"""Test setting mode without authentication."""
response = await client.post(
"/api/v1/player/mode",
json={"mode": "loop"},
)
assert response.status_code == 401
@pytest.mark.asyncio
async def test_set_mode_service_error(
self,
authenticated_client: AsyncClient,
authenticated_user: User,
mock_player_service,
) -> None:
"""Test setting mode with service error."""
mock_player_service.set_mode.side_effect = Exception("Service error")
response = await authenticated_client.post(
"/api/v1/player/mode",
json={"mode": "loop"},
)
assert response.status_code == 500
data = response.json()
assert data["detail"] == "Failed to set mode"
@pytest.mark.asyncio
async def test_reload_playlist_success(
self,
authenticated_client: AsyncClient,
authenticated_user: User,
mock_player_service,
) -> None:
"""Test reloading playlist successfully."""
response = await authenticated_client.post("/api/v1/player/reload-playlist")
assert response.status_code == 200
data = response.json()
assert data["message"] == "Playlist reloaded"
mock_player_service.reload_playlist.assert_called_once()
@pytest.mark.asyncio
async def test_reload_playlist_unauthenticated(self, client: AsyncClient) -> None:
"""Test reloading playlist without authentication."""
response = await client.post("/api/v1/player/reload-playlist")
assert response.status_code == 401
@pytest.mark.asyncio
async def test_reload_playlist_service_error(
self,
authenticated_client: AsyncClient,
authenticated_user: User,
mock_player_service,
) -> None:
"""Test reloading playlist with service error."""
mock_player_service.reload_playlist.side_effect = Exception("Service error")
response = await authenticated_client.post("/api/v1/player/reload-playlist")
assert response.status_code == 500
data = response.json()
assert data["detail"] == "Failed to reload playlist"
@pytest.mark.asyncio
async def test_get_state_success(
self,
authenticated_client: AsyncClient,
authenticated_user: User,
mock_player_service,
) -> None:
"""Test getting player state successfully."""
mock_state = {
"status": PlayerStatus.PLAYING.value,
"mode": PlayerMode.CONTINUOUS.value,
"volume": 50,
"position": 5000,
"duration": 30000,
"index": 0,
"current_sound": {
"id": 1,
"name": "Test Song",
"filename": "test.mp3",
"duration": 30000,
"size": 1024,
"type": "SDB",
"thumbnail": None,
"play_count": 0,
},
"playlist": {
"id": 1,
"name": "Test Playlist",
"length": 1,
"duration": 30000,
"sounds": [],
},
}
mock_player_service.get_state.return_value = mock_state
response = await authenticated_client.get("/api/v1/player/state")
assert response.status_code == 200
data = response.json()
assert data == mock_state
mock_player_service.get_state.assert_called_once()
@pytest.mark.asyncio
async def test_get_state_unauthenticated(self, client: AsyncClient) -> None:
"""Test getting player state without authentication."""
response = await client.get("/api/v1/player/state")
assert response.status_code == 401
@pytest.mark.asyncio
async def test_get_state_service_error(
self,
authenticated_client: AsyncClient,
authenticated_user: User,
mock_player_service,
) -> None:
"""Test getting player state with service error."""
mock_player_service.get_state.side_effect = Exception("Service error")
response = await authenticated_client.get("/api/v1/player/state")
assert response.status_code == 500
data = response.json()
assert data["detail"] == "Failed to get player state"
@pytest.mark.asyncio
async def test_seek_missing_body(
self,
authenticated_client: AsyncClient,
authenticated_user: User,
) -> None:
"""Test seeking without request body."""
response = await authenticated_client.post("/api/v1/player/seek")
assert response.status_code == 422
@pytest.mark.asyncio
async def test_volume_missing_body(
self,
authenticated_client: AsyncClient,
authenticated_user: User,
) -> None:
"""Test setting volume without request body."""
response = await authenticated_client.post("/api/v1/player/volume")
assert response.status_code == 422
@pytest.mark.asyncio
async def test_mode_missing_body(
self,
authenticated_client: AsyncClient,
authenticated_user: User,
) -> None:
"""Test setting mode without request body."""
response = await authenticated_client.post("/api/v1/player/mode")
assert response.status_code == 422
@pytest.mark.asyncio
async def test_play_at_index_negative_index(
self,
authenticated_client: AsyncClient,
authenticated_user: User,
mock_player_service,
) -> None:
"""Test playing sound with negative index."""
mock_player_service.play.side_effect = ValueError("Invalid sound index")
response = await authenticated_client.post("/api/v1/player/play/-1")
assert response.status_code == 400
data = response.json()
assert data["detail"] == "Invalid sound index"
@pytest.mark.asyncio
async def test_seek_zero_position(
self,
authenticated_client: AsyncClient,
authenticated_user: User,
mock_player_service,
) -> None:
"""Test seeking to position zero."""
response = await authenticated_client.post(
"/api/v1/player/seek",
json={"position": 0},
)
assert response.status_code == 200
data = response.json()
assert data["message"] == "Seeked to position 0ms"
mock_player_service.seek.assert_called_once_with(0)
@pytest.mark.asyncio
async def test_set_volume_boundary_values(
self,
authenticated_client: AsyncClient,
authenticated_user: User,
mock_player_service,
) -> None:
"""Test setting volume with boundary values."""
# Test minimum volume
response = await authenticated_client.post(
"/api/v1/player/volume",
json={"volume": 0},
)
assert response.status_code == 200
mock_player_service.set_volume.assert_called_with(0)
# Test maximum volume
response = await authenticated_client.post(
"/api/v1/player/volume",
json={"volume": 100},
)
assert response.status_code == 200
mock_player_service.set_volume.assert_called_with(100)