"""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 = 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, ): """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): """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, ): """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, ): """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, "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): """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": 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)