Files
sdb2-backend/tests/api/v1/test_player_endpoints.py
JSC 12243b1424
Some checks failed
Backend CI / lint (push) Failing after 9s
Backend CI / test (push) Failing after 1m36s
feat: Clear and manage play_next queue on playlist changes
2025-10-04 19:39:44 +02:00

662 lines
22 KiB
Python

"""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,
) -> 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,
"previous_volume": 80,
"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": [],
},
"play_next_queue": [],
}
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)