Add comprehensive tests for player API endpoints and player service functionality

- Implemented tests for player API endpoints including play, pause, stop, next, previous, seek, set volume, set mode, reload playlist, and get state.
- Added mock player service for testing API interactions.
- Created tests for player service methods including play, pause, stop playback, next, previous, seek, set volume, set mode, and reload playlist.
- Ensured proper handling of success, error, and edge cases in both API and service tests.
- Verified state management and serialization in player state tests.
This commit is contained in:
JSC
2025-07-30 01:22:24 +02:00
parent 5ed19c8f0f
commit 1b0d291ad3
11 changed files with 2291 additions and 98 deletions

View File

@@ -0,0 +1,658 @@
"""Tests for player API endpoints."""
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,
):
"""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):
"""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,
):
"""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,
):
"""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,
):
"""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,
):
"""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,
):
"""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):
"""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,
):
"""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,
):
"""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):
"""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,
):
"""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,
):
"""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):
"""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,
):
"""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,
):
"""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):
"""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,
):
"""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,
):
"""Test seeking to position successfully."""
position_ms = 5000
response = await authenticated_client.post(
"/api/v1/player/seek",
json={"position_ms": position_ms},
)
assert response.status_code == 200
data = response.json()
assert data["message"] == f"Seeked to position {position_ms}ms"
mock_player_service.seek.assert_called_once_with(position_ms)
@pytest.mark.asyncio
async def test_seek_invalid_position(
self,
authenticated_client: AsyncClient,
authenticated_user: User,
mock_player_service,
):
"""Test seeking with invalid position."""
response = await authenticated_client.post(
"/api/v1/player/seek",
json={"position_ms": -1000}, # Negative position
)
assert response.status_code == 422 # Validation error
@pytest.mark.asyncio
async def test_seek_unauthenticated(self, client: AsyncClient):
"""Test seeking without authentication."""
response = await client.post(
"/api/v1/player/seek",
json={"position_ms": 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,
):
"""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_ms": 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,
):
"""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,
):
"""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):
"""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,
):
"""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,
):
"""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,
):
"""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):
"""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,
):
"""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,
):
"""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):
"""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,
):
"""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,
):
"""Test getting player state successfully."""
mock_state = {
"status": PlayerStatus.PLAYING.value,
"mode": PlayerMode.CONTINUOUS.value,
"volume": 50,
"current_sound_id": 1,
"current_sound_index": 0,
"current_sound_position": 5000,
"current_sound_duration": 30000,
"current_sound": {
"id": 1,
"name": "Test Song",
"filename": "test.mp3",
"duration": 30000,
"size": 1024,
"type": "SDB",
"thumbnail": None,
"play_count": 0,
},
"playlist_id": 1,
"playlist_name": "Test Playlist",
"playlist_length": 1,
"playlist_duration": 30000,
"playlist_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):
"""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,
):
"""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,
):
"""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,
):
"""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,
):
"""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,
):
"""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,
):
"""Test seeking to position zero."""
response = await authenticated_client.post(
"/api/v1/player/seek",
json={"position_ms": 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,
):
"""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)