diff --git a/app/services/socket.py b/app/services/socket.py index 6dbb940..83c5c06 100644 --- a/app/services/socket.py +++ b/app/services/socket.py @@ -5,6 +5,8 @@ import logging import socketio from app.core.config import settings +from app.core.database import get_session_factory +from app.services.vlc_player import get_vlc_player_service from app.utils.auth import JWTUtils from app.utils.cookies import extract_access_token_from_cookies @@ -102,44 +104,43 @@ class SocketManager: @self.sio.event async def play_sound(sid: str, data: dict) -> None: """Handle play sound event from client.""" - user_id = self.socket_users.get(sid) + await self._handle_play_sound(sid, data) - if not user_id: - logger.warning("Play sound request from unknown client %s", sid) - return + async def _handle_play_sound(self, sid: str, data: dict) -> None: + """Handle play sound request from WebSocket client.""" + user_id = self.socket_users.get(sid) - sound_id = data.get("sound_id") - if not sound_id: - logger.warning( - "Play sound request missing sound_id from user %s", - user_id, - ) - return + if not user_id: + logger.warning("Play sound request from unknown client %s", sid) + return - try: - # Import here to avoid circular imports - from app.services.vlc_player import get_vlc_player_service - from app.core.database import get_session_factory + sound_id = data.get("sound_id") + if not sound_id: + logger.warning( + "Play sound request missing sound_id from user %s", + user_id, + ) + return - # Get VLC player service with database factory - vlc_player = get_vlc_player_service(get_session_factory()) + try: + # Get VLC player service with database factory + vlc_player = get_vlc_player_service(get_session_factory()) - # Call the service method - await vlc_player.play_sound_with_credits(int(sound_id), int(user_id)) - logger.info("User %s played sound %s via WebSocket", user_id, sound_id) - except Exception as e: - logger.exception( - "Error playing sound %s for user %s: %s", - sound_id, - user_id, - e, - ) - # Emit error back to user - await self.sio.emit( - "sound_play_error", - {"sound_id": sound_id, "error": str(e)}, - room=sid, - ) + # Call the service method + await vlc_player.play_sound_with_credits(int(sound_id), int(user_id)) + logger.info("User %s played sound %s via WebSocket", user_id, sound_id) + except Exception as e: + logger.exception( + "Error playing sound %s for user %s", + sound_id, + user_id, + ) + # Emit error back to user + await self.sio.emit( + "sound_play_error", + {"sound_id": sound_id, "error": str(e)}, + room=sid, + ) async def send_to_user(self, user_id: str, event: str, data: dict) -> bool: """Send a message to a specific user's room.""" diff --git a/app/services/vlc_player.py b/app/services/vlc_player.py index ea3d444..063cab8 100644 --- a/app/services/vlc_player.py +++ b/app/services/vlc_player.py @@ -6,6 +6,7 @@ from collections.abc import Callable from pathlib import Path from typing import Any +from fastapi import HTTPException, status from sqlmodel.ext.asyncio.session import AsyncSession from app.core.logging import get_logger @@ -14,6 +15,7 @@ from app.models.sound import Sound from app.models.sound_played import SoundPlayed from app.repositories.sound import SoundRepository from app.repositories.user import UserRepository +from app.services.credit import CreditService, InsufficientCreditsError from app.services.socket import socket_manager from app.utils.audio import get_sound_file_path @@ -309,7 +311,9 @@ class VLCPlayerService: await session.close() async def play_sound_with_credits( - self, sound_id: int, user_id: int + self, + sound_id: int, + user_id: int, ) -> dict[str, str | int | bool]: """Play sound with VLC with credit validation and deduction. @@ -326,11 +330,8 @@ class VLCPlayerService: Raises: HTTPException: For various error conditions (sound not found, insufficient credits, VLC failure) + """ - from fastapi import HTTPException, status - - from app.services.credit import CreditService, InsufficientCreditsError - if not self.db_session_factory: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, diff --git a/tests/api/v1/test_vlc_endpoints.py b/tests/api/v1/test_vlc_endpoints.py index 9fdfdf5..b16c02d 100644 --- a/tests/api/v1/test_vlc_endpoints.py +++ b/tests/api/v1/test_vlc_endpoints.py @@ -6,8 +6,7 @@ import pytest from fastapi import FastAPI from httpx import AsyncClient -from app.api.v1.sounds import get_credit_service, get_sound_repository, get_vlc_player -from app.models.sound import Sound +from app.api.v1.sounds import get_vlc_player from app.models.user import User @@ -22,32 +21,21 @@ class TestVLCEndpoints: authenticated_user: User, ) -> None: """Test successful sound playback via VLC.""" - # Set up mocks + # Set up mock VLC service mock_vlc_service = AsyncMock() - mock_repo = AsyncMock() - mock_credit_service = AsyncMock() - # Set up test data - mock_sound = Sound( - id=1, - type="SDB", - name="Test Sound", - filename="test.mp3", - duration=5000, - size=1024, - hash="test_hash", - ) - - # Configure mocks - mock_repo.get_by_id.return_value = mock_sound - mock_credit_service.validate_and_reserve_credits.return_value = None - mock_credit_service.deduct_credits.return_value = None - mock_vlc_service.play_sound.return_value = True + # Set up expected response from the service method + expected_response = { + "message": "Sound 'Test Sound' is now playing via VLC", + "sound_id": 1, + "sound_name": "Test Sound", + "success": True, + "credits_deducted": 1, + } + mock_vlc_service.play_sound_with_credits.return_value = expected_response # Override dependencies test_app.dependency_overrides[get_vlc_player] = lambda: mock_vlc_service - test_app.dependency_overrides[get_sound_repository] = lambda: mock_repo - test_app.dependency_overrides[get_credit_service] = lambda: mock_credit_service try: response = await authenticated_client.post("/api/v1/sounds/play/1") @@ -57,15 +45,17 @@ class TestVLCEndpoints: assert data["sound_id"] == 1 assert data["sound_name"] == "Test Sound" assert "Test Sound" in data["message"] + assert data["success"] is True + assert data["credits_deducted"] == 1 - # Verify service calls - mock_repo.get_by_id.assert_called_once_with(1) - mock_vlc_service.play_sound.assert_called_once_with(mock_sound) + # Verify service method was called with correct parameters + mock_vlc_service.play_sound_with_credits.assert_called_once_with( + 1, + authenticated_user.id, + ) finally: - # Clean up dependency overrides (except get_db which is needed for other tests) + # Clean up dependency overrides test_app.dependency_overrides.pop(get_vlc_player, None) - test_app.dependency_overrides.pop(get_sound_repository, None) - test_app.dependency_overrides.pop(get_credit_service, None) @pytest.mark.asyncio async def test_play_sound_with_vlc_sound_not_found( @@ -75,18 +65,19 @@ class TestVLCEndpoints: authenticated_user: User, ) -> None: """Test VLC playback when sound is not found.""" - # Set up mocks - mock_vlc_service = AsyncMock() - mock_repo = AsyncMock() - mock_credit_service = AsyncMock() + from fastapi import HTTPException - # Configure mocks - mock_repo.get_by_id.return_value = None + # Set up mock VLC service + mock_vlc_service = AsyncMock() + + # Configure mock to raise HTTPException for sound not found + mock_vlc_service.play_sound_with_credits.side_effect = HTTPException( + status_code=404, + detail="Sound with ID 999 not found", + ) # Override dependencies test_app.dependency_overrides[get_vlc_player] = lambda: mock_vlc_service - test_app.dependency_overrides[get_sound_repository] = lambda: mock_repo - test_app.dependency_overrides[get_credit_service] = lambda: mock_credit_service try: response = await authenticated_client.post("/api/v1/sounds/play/999") @@ -95,10 +86,8 @@ class TestVLCEndpoints: data = response.json() assert "Sound with ID 999 not found" in data["detail"] finally: - # Clean up dependency overrides (except get_db which is needed for other tests) + # Clean up dependency overrides test_app.dependency_overrides.pop(get_vlc_player, None) - test_app.dependency_overrides.pop(get_sound_repository, None) - test_app.dependency_overrides.pop(get_credit_service, None) @pytest.mark.asyncio async def test_play_sound_with_vlc_launch_failure( @@ -108,32 +97,19 @@ class TestVLCEndpoints: authenticated_user: User, ) -> None: """Test VLC playback when VLC launch fails.""" - # Set up mocks + from fastapi import HTTPException + + # Set up mock VLC service mock_vlc_service = AsyncMock() - mock_repo = AsyncMock() - mock_credit_service = AsyncMock() - # Set up test data - mock_sound = Sound( - id=1, - type="SDB", - name="Test Sound", - filename="test.mp3", - duration=5000, - size=1024, - hash="test_hash", + # Configure mock to raise HTTPException for VLC launch failure + mock_vlc_service.play_sound_with_credits.side_effect = HTTPException( + status_code=500, + detail="Failed to launch VLC for sound playback", ) - # Configure mocks - mock_repo.get_by_id.return_value = mock_sound - mock_credit_service.validate_and_reserve_credits.return_value = None - mock_credit_service.deduct_credits.return_value = None - mock_vlc_service.play_sound.return_value = False - # Override dependencies test_app.dependency_overrides[get_vlc_player] = lambda: mock_vlc_service - test_app.dependency_overrides[get_sound_repository] = lambda: mock_repo - test_app.dependency_overrides[get_credit_service] = lambda: mock_credit_service try: response = await authenticated_client.post("/api/v1/sounds/play/1") @@ -142,10 +118,8 @@ class TestVLCEndpoints: data = response.json() assert "Failed to launch VLC for sound playback" in data["detail"] finally: - # Clean up dependency overrides (except get_db which is needed for other tests) + # Clean up dependency overrides test_app.dependency_overrides.pop(get_vlc_player, None) - test_app.dependency_overrides.pop(get_sound_repository, None) - test_app.dependency_overrides.pop(get_credit_service, None) @pytest.mark.asyncio async def test_play_sound_with_vlc_service_exception( @@ -155,18 +129,16 @@ class TestVLCEndpoints: authenticated_user: User, ) -> None: """Test VLC playback when service raises an exception.""" - # Set up mocks + # Set up mock VLC service mock_vlc_service = AsyncMock() - mock_repo = AsyncMock() - mock_credit_service = AsyncMock() - # Configure mocks - mock_repo.get_by_id.side_effect = Exception("Database error") + # Configure mock to raise a generic exception + mock_vlc_service.play_sound_with_credits.side_effect = Exception( + "Database error", + ) # Override dependencies test_app.dependency_overrides[get_vlc_player] = lambda: mock_vlc_service - test_app.dependency_overrides[get_sound_repository] = lambda: mock_repo - test_app.dependency_overrides[get_credit_service] = lambda: mock_credit_service try: response = await authenticated_client.post("/api/v1/sounds/play/1") @@ -175,10 +147,8 @@ class TestVLCEndpoints: data = response.json() assert "Failed to play sound" in data["detail"] finally: - # Clean up dependency overrides (except get_db which is needed for other tests) + # Clean up dependency overrides test_app.dependency_overrides.pop(get_vlc_player, None) - test_app.dependency_overrides.pop(get_sound_repository, None) - test_app.dependency_overrides.pop(get_credit_service, None) @pytest.mark.asyncio async def test_play_sound_with_vlc_unauthenticated( @@ -373,32 +343,21 @@ class TestVLCEndpoints: admin_user: User, ) -> None: """Test VLC endpoints work with admin user.""" - # Set up mocks + # Set up mock VLC service mock_vlc_service = AsyncMock() - mock_repo = AsyncMock() - mock_credit_service = AsyncMock() - # Set up test data - mock_sound = Sound( - id=1, - type="SDB", - name="Admin Test Sound", - filename="admin_test.mp3", - duration=3000, - size=512, - hash="admin_hash", - ) - - # Configure mocks - mock_repo.get_by_id.return_value = mock_sound - mock_credit_service.validate_and_reserve_credits.return_value = None - mock_credit_service.deduct_credits.return_value = None - mock_vlc_service.play_sound.return_value = True + # Set up expected response from the service method + expected_response = { + "message": "Sound 'Admin Test Sound' is now playing via VLC", + "sound_id": 1, + "sound_name": "Admin Test Sound", + "success": True, + "credits_deducted": 1, + } + mock_vlc_service.play_sound_with_credits.return_value = expected_response # Override dependencies test_app.dependency_overrides[get_vlc_player] = lambda: mock_vlc_service - test_app.dependency_overrides[get_sound_repository] = lambda: mock_repo - test_app.dependency_overrides[get_credit_service] = lambda: mock_credit_service try: response = await authenticated_admin_client.post("/api/v1/sounds/play/1") @@ -406,11 +365,15 @@ class TestVLCEndpoints: assert response.status_code == 200 data = response.json() assert data["sound_name"] == "Admin Test Sound" + + # Verify service method was called with admin user ID + mock_vlc_service.play_sound_with_credits.assert_called_once_with( + 1, + admin_user.id, + ) finally: - # Clean up dependency overrides (except get_db which is needed for other tests) + # Clean up dependency overrides test_app.dependency_overrides.pop(get_vlc_player, None) - test_app.dependency_overrides.pop(get_sound_repository, None) - test_app.dependency_overrides.pop(get_credit_service, None) # Test stop-all endpoint with admin mock_vlc_service_2 = AsyncMock()