From 6068599a47b77d41b4f528dc3c54eef9e36a3d21 Mon Sep 17 00:00:00 2001 From: JSC Date: Fri, 1 Aug 2025 20:53:30 +0200 Subject: [PATCH] Refactor test cases for improved readability and consistency - Adjusted function signatures in various test files to enhance clarity by aligning parameters. - Updated patching syntax for better readability across test cases. - Improved formatting and spacing in test assertions and mock setups. - Ensured consistent use of async/await patterns in async test functions. - Enhanced comments for better understanding of test intentions. --- .gitignore | 4 +- app/api/v1/admin/sounds.py | 21 ++-- app/api/v1/playlists.py | 4 +- app/api/v1/sounds.py | 10 +- app/core/database.py | 2 + app/models/sound.py | 4 +- app/repositories/base.py | 4 +- app/repositories/credit_transaction.py | 3 +- app/repositories/extraction.py | 8 +- app/repositories/playlist.py | 26 +++-- app/repositories/sound.py | 3 +- app/repositories/user_oauth.py | 2 - app/schemas/player.py | 12 ++- app/services/extraction.py | 14 ++- app/services/extraction_processor.py | 3 +- app/services/playlist.py | 5 +- app/services/sound_normalizer.py | 15 ++- app/utils/credit_decorators.py | 20 +++- tests/api/v1/admin/__init__.py | 2 +- tests/api/v1/admin/test_sound_endpoints.py | 34 ++++-- tests/api/v1/test_api_token_endpoints.py | 28 +++-- tests/api/v1/test_extraction_endpoints.py | 21 ++-- tests/api/v1/test_playlist_endpoints.py | 33 ++++-- tests/api/v1/test_socket_endpoints.py | 12 ++- tests/api/v1/test_sound_endpoints.py | 36 ++++--- tests/core/test_api_token_dependencies.py | 26 +++-- tests/repositories/test_credit_transaction.py | 44 +++++--- tests/repositories/test_playlist.py | 44 +++++--- tests/repositories/test_user_oauth.py | 27 +++-- tests/services/test_credit.py | 87 ++++++++++----- tests/services/test_extraction.py | 28 +++-- tests/services/test_extraction_processor.py | 12 ++- tests/services/test_player.py | 100 ++++++++++++----- tests/services/test_socket_service.py | 27 ++++- tests/services/test_sound_normalizer.py | 35 ++++-- tests/services/test_sound_scanner.py | 18 +++- tests/services/test_vlc_player.py | 102 ++++++++++++------ tests/utils/test_audio.py | 7 +- tests/utils/test_credit_decorators.py | 94 ++++++++++++---- 39 files changed, 691 insertions(+), 286 deletions(-) diff --git a/.gitignore b/.gitignore index 9562d3e..c25058e 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,6 @@ wheels/ # Virtual environments .venv -.env \ No newline at end of file +.env + +.coverage \ No newline at end of file diff --git a/app/api/v1/admin/sounds.py b/app/api/v1/admin/sounds.py index 5f17bee..bbb8e02 100644 --- a/app/api/v1/admin/sounds.py +++ b/app/api/v1/admin/sounds.py @@ -32,7 +32,7 @@ async def get_sound_normalizer_service( # SCAN ENDPOINTS @router.post("/scan") async def scan_sounds( - current_user: Annotated[User, Depends(get_admin_user)], + current_user: Annotated[User, Depends(get_admin_user)], # noqa: ARG001 scanner_service: Annotated[SoundScannerService, Depends(get_sound_scanner_service)], ) -> dict[str, ScanResults | str]: """Sync the soundboard directory (add/update/delete sounds). Admin only.""" @@ -53,11 +53,11 @@ async def scan_sounds( @router.post("/scan/custom") async def scan_custom_directory( directory: str, - current_user: Annotated[User, Depends(get_admin_user)], + current_user: Annotated[User, Depends(get_admin_user)], # noqa: ARG001 scanner_service: Annotated[SoundScannerService, Depends(get_sound_scanner_service)], sound_type: str = "SDB", ) -> dict[str, ScanResults | str]: - """Sync a custom directory with the database (add/update/delete sounds). Admin only.""" + """Sync a custom directory with the database. Admin only.""" try: results = await scanner_service.scan_directory(directory, sound_type) except ValueError as e: @@ -80,14 +80,15 @@ async def scan_custom_directory( # NORMALIZE ENDPOINTS @router.post("/normalize/all") async def normalize_all_sounds( - current_user: Annotated[User, Depends(get_admin_user)], + current_user: Annotated[User, Depends(get_admin_user)], # noqa: ARG001 normalizer_service: Annotated[ SoundNormalizerService, Depends(get_sound_normalizer_service), ], + *, force: Annotated[ bool, - Query( # noqa: FBT002 + Query( description="Force normalization of already normalized sounds", ), ] = False, @@ -119,14 +120,15 @@ async def normalize_all_sounds( @router.post("/normalize/type/{sound_type}") async def normalize_sounds_by_type( sound_type: str, - current_user: Annotated[User, Depends(get_admin_user)], + current_user: Annotated[User, Depends(get_admin_user)], # noqa: ARG001 normalizer_service: Annotated[ SoundNormalizerService, Depends(get_sound_normalizer_service), ], + *, force: Annotated[ bool, - Query( # noqa: FBT002 + Query( description="Force normalization of already normalized sounds", ), ] = False, @@ -167,14 +169,15 @@ async def normalize_sounds_by_type( @router.post("/normalize/{sound_id}") async def normalize_sound_by_id( sound_id: int, - current_user: Annotated[User, Depends(get_admin_user)], + current_user: Annotated[User, Depends(get_admin_user)], # noqa: ARG001 normalizer_service: Annotated[ SoundNormalizerService, Depends(get_sound_normalizer_service), ], + *, force: Annotated[ bool, - Query( # noqa: FBT002 + Query( description="Force normalization of already normalized sound", ), ] = False, diff --git a/app/api/v1/playlists.py b/app/api/v1/playlists.py index 875bb99..1180488 100644 --- a/app/api/v1/playlists.py +++ b/app/api/v1/playlists.py @@ -2,7 +2,7 @@ from typing import Annotated -from fastapi import APIRouter, Depends +from fastapi import APIRouter, Depends, HTTPException, status from sqlmodel.ext.asyncio.session import AsyncSession from app.core.database import get_db @@ -110,7 +110,7 @@ async def update_playlist( status_code=status.HTTP_401_UNAUTHORIZED, detail="User ID not available", ) - + playlist = await playlist_service.update_playlist( playlist_id=playlist_id, user_id=current_user.id, diff --git a/app/api/v1/sounds.py b/app/api/v1/sounds.py index 804d1a6..cf0e1fe 100644 --- a/app/api/v1/sounds.py +++ b/app/api/v1/sounds.py @@ -2,7 +2,7 @@ from typing import Annotated -from fastapi import APIRouter, Depends, HTTPException, Query, status +from fastapi import APIRouter, Depends, HTTPException, status from sqlmodel.ext.asyncio.session import AsyncSession from app.core.database import get_db, get_session_factory @@ -18,7 +18,6 @@ from app.services.vlc_player import VLCPlayerService, get_vlc_player_service router = APIRouter(prefix="/sounds", tags=["sounds"]) - async def get_extraction_service( session: Annotated[AsyncSession, Depends(get_db)], ) -> ExtractionService: @@ -43,7 +42,6 @@ async def get_sound_repository( return SoundRepository(session) - # EXTRACT @router.post("/extract") async def create_extraction( @@ -60,7 +58,8 @@ async def create_extraction( ) extraction_info = await extraction_service.create_extraction( - url, current_user.id, + url, + current_user.id, ) # Queue the extraction for background processing @@ -83,8 +82,6 @@ async def create_extraction( } - - @router.get("/extract/{extraction_id}") async def get_extraction( extraction_id: int, @@ -206,7 +203,6 @@ async def play_sound_with_vlc( } - @router.post("/stop") async def stop_all_vlc_instances( current_user: Annotated[User, Depends(get_current_active_user_flexible)], # noqa: ARG001 diff --git a/app/core/database.py b/app/core/database.py index c75fe1e..93e2d0e 100644 --- a/app/core/database.py +++ b/app/core/database.py @@ -40,8 +40,10 @@ async def get_db() -> AsyncGenerator[AsyncSession, None]: def get_session_factory() -> Callable[[], AsyncSession]: """Get a session factory function for services.""" + def session_factory() -> AsyncSession: return AsyncSession(engine) + return session_factory diff --git a/app/models/sound.py b/app/models/sound.py index ee666ae..1480fa7 100644 --- a/app/models/sound.py +++ b/app/models/sound.py @@ -30,9 +30,7 @@ class Sound(BaseModel, table=True): is_deletable: bool = Field(default=True, nullable=False) # constraints - __table_args__ = ( - UniqueConstraint("hash", name="uq_sound_hash"), - ) + __table_args__ = (UniqueConstraint("hash", name="uq_sound_hash"),) # relationships playlist_sounds: list["PlaylistSound"] = Relationship(back_populates="sound") diff --git a/app/repositories/base.py b/app/repositories/base.py index fb86fe2..2d62b4c 100644 --- a/app/repositories/base.py +++ b/app/repositories/base.py @@ -43,7 +43,9 @@ class BaseRepository[ModelType]: return result.first() except Exception: logger.exception( - "Failed to get %s by ID: %s", self.model.__name__, entity_id, + "Failed to get %s by ID: %s", + self.model.__name__, + entity_id, ) raise diff --git a/app/repositories/credit_transaction.py b/app/repositories/credit_transaction.py index b2f53b4..54c7bc6 100644 --- a/app/repositories/credit_transaction.py +++ b/app/repositories/credit_transaction.py @@ -91,8 +91,7 @@ class CreditTransactionRepository(BaseRepository[CreditTransaction]): """ stmt = ( - select(CreditTransaction) - .where(CreditTransaction.success == True) # noqa: E712 + select(CreditTransaction).where(CreditTransaction.success == True) # noqa: E712 ) if user_id is not None: diff --git a/app/repositories/extraction.py b/app/repositories/extraction.py index b8cb9ba..ff30a9d 100644 --- a/app/repositories/extraction.py +++ b/app/repositories/extraction.py @@ -1,6 +1,5 @@ """Extraction repository for database operations.""" - from sqlalchemy import desc from sqlmodel import select from sqlmodel.ext.asyncio.session import AsyncSession @@ -17,12 +16,15 @@ class ExtractionRepository(BaseRepository[Extraction]): super().__init__(Extraction, session) async def get_by_service_and_id( - self, service: str, service_id: str, + self, + service: str, + service_id: str, ) -> Extraction | None: """Get an extraction by service and service_id.""" result = await self.session.exec( select(Extraction).where( - Extraction.service == service, Extraction.service_id == service_id, + Extraction.service == service, + Extraction.service_id == service_id, ), ) return result.first() diff --git a/app/repositories/playlist.py b/app/repositories/playlist.py index 9126017..1f61e80 100644 --- a/app/repositories/playlist.py +++ b/app/repositories/playlist.py @@ -1,6 +1,5 @@ """Playlist repository for database operations.""" - from sqlalchemy import func from sqlmodel import select from sqlmodel.ext.asyncio.session import AsyncSession @@ -66,7 +65,9 @@ class PlaylistRepository(BaseRepository[Playlist]): raise async def search_by_name( - self, query: str, user_id: int | None = None, + self, + query: str, + user_id: int | None = None, ) -> list[Playlist]: """Search playlists by name (case-insensitive).""" try: @@ -98,7 +99,10 @@ class PlaylistRepository(BaseRepository[Playlist]): raise async def add_sound_to_playlist( - self, playlist_id: int, sound_id: int, position: int | None = None, + self, + playlist_id: int, + sound_id: int, + position: int | None = None, ) -> PlaylistSound: """Add a sound to a playlist.""" try: @@ -121,7 +125,9 @@ class PlaylistRepository(BaseRepository[Playlist]): except Exception: await self.session.rollback() logger.exception( - "Failed to add sound %s to playlist %s", sound_id, playlist_id, + "Failed to add sound %s to playlist %s", + sound_id, + playlist_id, ) raise else: @@ -150,12 +156,16 @@ class PlaylistRepository(BaseRepository[Playlist]): except Exception: await self.session.rollback() logger.exception( - "Failed to remove sound %s from playlist %s", sound_id, playlist_id, + "Failed to remove sound %s from playlist %s", + sound_id, + playlist_id, ) raise async def reorder_playlist_sounds( - self, playlist_id: int, sound_positions: list[tuple[int, int]], + self, + playlist_id: int, + sound_positions: list[tuple[int, int]], ) -> None: """Reorder sounds in a playlist. @@ -220,6 +230,8 @@ class PlaylistRepository(BaseRepository[Playlist]): return result.first() is not None except Exception: logger.exception( - "Failed to check if sound %s is in playlist %s", sound_id, playlist_id, + "Failed to check if sound %s is in playlist %s", + sound_id, + playlist_id, ) raise diff --git a/app/repositories/sound.py b/app/repositories/sound.py index 98ad061..9fd1fa5 100644 --- a/app/repositories/sound.py +++ b/app/repositories/sound.py @@ -91,6 +91,7 @@ class SoundRepository(BaseRepository[Sound]): return list(result.all()) except Exception: logger.exception( - "Failed to get unnormalized sounds by type: %s", sound_type, + "Failed to get unnormalized sounds by type: %s", + sound_type, ) raise diff --git a/app/repositories/user_oauth.py b/app/repositories/user_oauth.py index bcc1d14..1740361 100644 --- a/app/repositories/user_oauth.py +++ b/app/repositories/user_oauth.py @@ -1,6 +1,5 @@ """Repository for user OAuth operations.""" - from sqlmodel import select from sqlmodel.ext.asyncio.session import AsyncSession @@ -60,4 +59,3 @@ class UserOauthRepository(BaseRepository[UserOauth]): raise else: return result.first() - diff --git a/app/schemas/player.py b/app/schemas/player.py index 1faba97..e1bce17 100644 --- a/app/schemas/player.py +++ b/app/schemas/player.py @@ -30,17 +30,21 @@ class PlayerStateResponse(BaseModel): status: str = Field(description="Player status (playing, paused, stopped)") current_sound: dict[str, Any] | None = Field( - None, description="Current sound information", + None, + description="Current sound information", ) playlist: dict[str, Any] | None = Field( - None, description="Current playlist information", + None, + description="Current playlist information", ) position: int = Field(description="Current position in milliseconds") duration: int | None = Field( - None, description="Total duration in milliseconds", + None, + description="Total duration in milliseconds", ) volume: int = Field(description="Current volume (0-100)") mode: str = Field(description="Current playback mode") index: int | None = Field( - None, description="Current track index in playlist", + None, + description="Current track index in playlist", ) diff --git a/app/services/extraction.py b/app/services/extraction.py index c217a5b..2dd6f4d 100644 --- a/app/services/extraction.py +++ b/app/services/extraction.py @@ -156,7 +156,8 @@ class ExtractionService: # Check if extraction already exists for this service existing = await self.extraction_repo.get_by_service_and_id( - service_info["service"], service_info["service_id"], + service_info["service"], + service_info["service_id"], ) if existing and existing.id != extraction_id: error_msg = ( @@ -181,7 +182,8 @@ class ExtractionService: # Extract audio and thumbnail audio_file, thumbnail_file = await self._extract_media( - extraction_id, extraction_url, + extraction_id, + extraction_url, ) # Move files to final locations @@ -227,7 +229,9 @@ class ExtractionService: except Exception as e: error_msg = str(e) logger.exception( - "Failed to process extraction %d: %s", extraction_id, error_msg, + "Failed to process extraction %d: %s", + extraction_id, + error_msg, ) else: return { @@ -262,7 +266,9 @@ class ExtractionService: } async def _extract_media( - self, extraction_id: int, extraction_url: str, + self, + extraction_id: int, + extraction_url: str, ) -> tuple[Path, Path | None]: """Extract audio and thumbnail using yt-dlp.""" temp_dir = Path(settings.EXTRACTION_TEMP_DIR) diff --git a/app/services/extraction_processor.py b/app/services/extraction_processor.py index 4cf2b2f..1835ac6 100644 --- a/app/services/extraction_processor.py +++ b/app/services/extraction_processor.py @@ -65,7 +65,8 @@ class ExtractionProcessor: # The processor will pick it up on the next cycle else: logger.warning( - "Extraction %d is already being processed", extraction_id, + "Extraction %d is already being processed", + extraction_id, ) async def _process_queue(self) -> None: diff --git a/app/services/playlist.py b/app/services/playlist.py index bf2d405..5976bb6 100644 --- a/app/services/playlist.py +++ b/app/services/playlist.py @@ -35,10 +35,11 @@ async def _is_current_playlist(session: AsyncSession, playlist_id: int) -> bool: playlist_repo = PlaylistRepository(session) current_playlist = await playlist_repo.get_current_playlist() - return current_playlist is not None and current_playlist.id == playlist_id except Exception: # noqa: BLE001 logger.warning("Failed to check if playlist is current", exc_info=True) return False + else: + return current_playlist is not None and current_playlist.id == playlist_id class PlaylistService: @@ -199,7 +200,7 @@ class PlaylistService: await self.playlist_repo.delete(playlist) logger.info("Deleted playlist %s for user %s", playlist_id, user_id) - # If the deleted playlist was current, reload player to use main playlist fallback + # If the deleted playlist was current, reload player to use main fallback if was_current: await _reload_player_playlist() diff --git a/app/services/sound_normalizer.py b/app/services/sound_normalizer.py index e71c8d2..39b7ab8 100644 --- a/app/services/sound_normalizer.py +++ b/app/services/sound_normalizer.py @@ -140,7 +140,10 @@ class SoundNormalizerService: stream = ffmpeg.overwrite_output(stream) await asyncio.to_thread( - ffmpeg.run, stream, quiet=True, overwrite_output=True, + ffmpeg.run, + stream, + quiet=True, + overwrite_output=True, ) logger.info("One-pass normalization completed: %s", output_path) @@ -180,7 +183,10 @@ class SoundNormalizerService: # Run first pass and capture output try: result = await asyncio.to_thread( - ffmpeg.run, stream, capture_stderr=True, quiet=True, + ffmpeg.run, + stream, + capture_stderr=True, + quiet=True, ) analysis_output = result[1].decode("utf-8") except ffmpeg.Error as e: @@ -262,7 +268,10 @@ class SoundNormalizerService: try: await asyncio.to_thread( - ffmpeg.run, stream, quiet=True, overwrite_output=True, + ffmpeg.run, + stream, + quiet=True, + overwrite_output=True, ) logger.info("Two-pass normalization completed: %s", output_path) except ffmpeg.Error as e: diff --git a/app/utils/credit_decorators.py b/app/utils/credit_decorators.py index dff04e5..f00969d 100644 --- a/app/utils/credit_decorators.py +++ b/app/utils/credit_decorators.py @@ -40,6 +40,7 @@ def requires_credits( return True """ + def decorator(func: F) -> F: @functools.wraps(func) async def wrapper(*args: Any, **kwargs: Any) -> Any: # noqa: ANN401 @@ -70,7 +71,8 @@ def requires_credits( # Validate credits before execution await credit_service.validate_and_reserve_credits( - user_id, action_type, + user_id, + action_type, ) # Execute the function @@ -86,10 +88,14 @@ def requires_credits( finally: # Deduct credits based on success await credit_service.deduct_credits( - user_id, action_type, success=success, metadata=metadata, + user_id, + action_type, + success=success, + metadata=metadata, ) return wrapper # type: ignore[return-value] + return decorator @@ -111,6 +117,7 @@ def validate_credits_only( Decorated function that validates credits only """ + def decorator(func: F) -> F: @functools.wraps(func) async def wrapper(*args: Any, **kwargs: Any) -> Any: # noqa: ANN401 @@ -141,6 +148,7 @@ def validate_credits_only( return await func(*args, **kwargs) return wrapper # type: ignore[return-value] + return decorator @@ -173,7 +181,8 @@ class CreditManager: async def __aenter__(self) -> "CreditManager": """Enter context manager - validate credits.""" await self.credit_service.validate_and_reserve_credits( - self.user_id, self.action_type, + self.user_id, + self.action_type, ) self.validated = True return self @@ -189,7 +198,10 @@ class CreditManager: # If no exception occurred, consider it successful success = exc_type is None and self.success await self.credit_service.deduct_credits( - self.user_id, self.action_type, success=success, metadata=self.metadata, + self.user_id, + self.action_type, + success=success, + metadata=self.metadata, ) def mark_success(self) -> None: diff --git a/tests/api/v1/admin/__init__.py b/tests/api/v1/admin/__init__.py index 81c86df..9309ee8 100644 --- a/tests/api/v1/admin/__init__.py +++ b/tests/api/v1/admin/__init__.py @@ -1 +1 @@ -"""Tests for admin API endpoints.""" \ No newline at end of file +"""Tests for admin API endpoints.""" diff --git a/tests/api/v1/admin/test_sound_endpoints.py b/tests/api/v1/admin/test_sound_endpoints.py index 3ac74f3..f92b6a9 100644 --- a/tests/api/v1/admin/test_sound_endpoints.py +++ b/tests/api/v1/admin/test_sound_endpoints.py @@ -73,7 +73,9 @@ class TestAdminSoundEndpoints: ) as mock_scan: mock_scan.return_value = mock_results - response = await authenticated_admin_client.post("/api/v1/admin/sounds/scan") + response = await authenticated_admin_client.post( + "/api/v1/admin/sounds/scan", + ) assert response.status_code == 200 data = response.json() @@ -114,6 +116,7 @@ class TestAdminSoundEndpoints: ) -> None: """Test scanning sounds with non-admin user.""" from fastapi import HTTPException + from app.core.dependencies import get_admin_user # Override the admin dependency to raise 403 for non-admin users @@ -150,7 +153,9 @@ class TestAdminSoundEndpoints: ) as mock_scan: mock_scan.side_effect = Exception("Directory not found") - response = await authenticated_admin_client.post("/api/v1/admin/sounds/scan") + response = await authenticated_admin_client.post( + "/api/v1/admin/sounds/scan", + ) assert response.status_code == 500 data = response.json() @@ -300,7 +305,9 @@ class TestAdminSoundEndpoints: assert len(results["files"]) == 3 @pytest.mark.asyncio - async def test_normalize_all_sounds_unauthenticated(self, client: AsyncClient) -> None: + async def test_normalize_all_sounds_unauthenticated( + self, client: AsyncClient, + ) -> None: """Test normalizing sounds without authentication.""" response = await client.post("/api/v1/admin/sounds/normalize/all") @@ -316,6 +323,7 @@ class TestAdminSoundEndpoints: ) -> None: """Test normalizing sounds with non-admin user.""" from fastapi import HTTPException + from app.core.dependencies import get_admin_user # Override the admin dependency to raise 403 for non-admin users @@ -331,7 +339,8 @@ class TestAdminSoundEndpoints: base_url="http://test", ) as client: response = await client.post( - "/api/v1/admin/sounds/normalize/all", headers=headers, + "/api/v1/admin/sounds/normalize/all", + headers=headers, ) assert response.status_code == 403 @@ -405,7 +414,9 @@ class TestAdminSoundEndpoints: # Verify the service was called with correct type mock_normalize.assert_called_once_with( - sound_type="SDB", force=False, one_pass=None, + sound_type="SDB", + force=False, + one_pass=None, ) @pytest.mark.asyncio @@ -491,7 +502,7 @@ class TestAdminSoundEndpoints: ) -> None: """Test getting extraction processor status.""" with patch( - "app.services.extraction_processor.extraction_processor.get_status" + "app.services.extraction_processor.extraction_processor.get_status", ) as mock_get_status: mock_status = { "is_running": True, @@ -502,7 +513,7 @@ class TestAdminSoundEndpoints: mock_get_status.return_value = mock_status response = await authenticated_admin_client.get( - "/api/v1/admin/sounds/extract/status" + "/api/v1/admin/sounds/extract/status", ) assert response.status_code == 200 @@ -511,7 +522,8 @@ class TestAdminSoundEndpoints: @pytest.mark.asyncio async def test_get_extraction_processor_status_unauthenticated( - self, client: AsyncClient + self, + client: AsyncClient, ) -> None: """Test getting extraction processor status without authentication.""" response = await client.get("/api/v1/admin/sounds/extract/status") @@ -528,6 +540,7 @@ class TestAdminSoundEndpoints: ) -> None: """Test getting extraction processor status with non-admin user.""" from fastapi import HTTPException + from app.core.dependencies import get_admin_user # Override the admin dependency to raise 403 for non-admin users @@ -543,7 +556,8 @@ class TestAdminSoundEndpoints: base_url="http://test", ) as client: response = await client.get( - "/api/v1/admin/sounds/extract/status", headers=headers + "/api/v1/admin/sounds/extract/status", + headers=headers, ) assert response.status_code == 403 @@ -551,4 +565,4 @@ class TestAdminSoundEndpoints: assert "Not enough permissions" in data["detail"] # Clean up override - test_app.dependency_overrides.pop(get_admin_user, None) \ No newline at end of file + test_app.dependency_overrides.pop(get_admin_user, None) diff --git a/tests/api/v1/test_api_token_endpoints.py b/tests/api/v1/test_api_token_endpoints.py index 8410407..5bc64e5 100644 --- a/tests/api/v1/test_api_token_endpoints.py +++ b/tests/api/v1/test_api_token_endpoints.py @@ -54,7 +54,11 @@ class TestApiTokenEndpoints: expires_at_str = data["expires_at"] # Handle both ISO format with/without timezone info - if expires_at_str.endswith("Z") or "+" in expires_at_str or expires_at_str.count("-") > 2: + if ( + expires_at_str.endswith("Z") + or "+" in expires_at_str + or expires_at_str.count("-") > 2 + ): expires_at = datetime.fromisoformat(expires_at_str) else: # Naive datetime, assume UTC @@ -84,7 +88,11 @@ class TestApiTokenEndpoints: expires_at_str = data["expires_at"] # Handle both ISO format with/without timezone info - if expires_at_str.endswith("Z") or "+" in expires_at_str or expires_at_str.count("-") > 2: + if ( + expires_at_str.endswith("Z") + or "+" in expires_at_str + or expires_at_str.count("-") > 2 + ): expires_at = datetime.fromisoformat(expires_at_str) else: # Naive datetime, assume UTC @@ -116,7 +124,9 @@ class TestApiTokenEndpoints: assert response.status_code == 422 @pytest.mark.asyncio - async def test_generate_api_token_unauthenticated(self, client: AsyncClient) -> None: + async def test_generate_api_token_unauthenticated( + self, client: AsyncClient, + ) -> None: """Test API token generation without authentication.""" response = await client.post( "/api/v1/auth/api-token", @@ -186,7 +196,9 @@ class TestApiTokenEndpoints: assert data["is_expired"] is True @pytest.mark.asyncio - async def test_get_api_token_status_unauthenticated(self, client: AsyncClient) -> None: + async def test_get_api_token_status_unauthenticated( + self, client: AsyncClient, + ) -> None: """Test getting API token status without authentication.""" response = await client.get("/api/v1/auth/api-token/status") assert response.status_code == 401 @@ -264,7 +276,9 @@ class TestApiTokenEndpoints: assert "email" in data @pytest.mark.asyncio - async def test_api_token_authentication_invalid_token(self, client: AsyncClient) -> None: + async def test_api_token_authentication_invalid_token( + self, client: AsyncClient, + ) -> None: """Test authentication with invalid API token.""" headers = {"API-TOKEN": "invalid_token"} response = await client.get("/api/v1/auth/me", headers=headers) @@ -297,7 +311,9 @@ class TestApiTokenEndpoints: assert "API token has expired" in data["detail"] @pytest.mark.asyncio - async def test_api_token_authentication_empty_token(self, client: AsyncClient) -> None: + async def test_api_token_authentication_empty_token( + self, client: AsyncClient, + ) -> None: """Test authentication with empty API-TOKEN header.""" # Empty token headers = {"API-TOKEN": ""} diff --git a/tests/api/v1/test_extraction_endpoints.py b/tests/api/v1/test_extraction_endpoints.py index 0cc472a..cb57ad7 100644 --- a/tests/api/v1/test_extraction_endpoints.py +++ b/tests/api/v1/test_extraction_endpoints.py @@ -1,6 +1,5 @@ """Tests for extraction API endpoints.""" - import pytest from httpx import AsyncClient @@ -10,7 +9,9 @@ class TestExtractionEndpoints: @pytest.mark.asyncio async def test_create_extraction_success( - self, test_client: AsyncClient, auth_cookies: dict[str, str], + self, + test_client: AsyncClient, + auth_cookies: dict[str, str], ) -> None: """Test successful extraction creation.""" # Set cookies on client instance to avoid deprecation warning @@ -26,7 +27,9 @@ class TestExtractionEndpoints: assert response.status_code in [200, 400, 500] # Allow any non-auth error @pytest.mark.asyncio - async def test_create_extraction_unauthenticated(self, test_client: AsyncClient) -> None: + async def test_create_extraction_unauthenticated( + self, test_client: AsyncClient, + ) -> None: """Test extraction creation without authentication.""" response = await test_client.post( "/api/v1/sounds/extract", @@ -37,7 +40,9 @@ class TestExtractionEndpoints: assert response.status_code == 401 @pytest.mark.asyncio - async def test_get_extraction_unauthenticated(self, test_client: AsyncClient) -> None: + async def test_get_extraction_unauthenticated( + self, test_client: AsyncClient, + ) -> None: """Test extraction retrieval without authentication.""" response = await test_client.get("/api/v1/sounds/extract/1") @@ -46,7 +51,9 @@ class TestExtractionEndpoints: @pytest.mark.asyncio async def test_get_processor_status_moved_to_admin( - self, test_client: AsyncClient, admin_cookies: dict[str, str], + self, + test_client: AsyncClient, + admin_cookies: dict[str, str], ) -> None: """Test that processor status endpoint was moved to admin.""" # Set cookies on client instance to avoid deprecation warning @@ -61,7 +68,9 @@ class TestExtractionEndpoints: @pytest.mark.asyncio async def test_get_user_extractions( - self, test_client: AsyncClient, auth_cookies: dict[str, str], + self, + test_client: AsyncClient, + auth_cookies: dict[str, str], ) -> None: """Test getting user extractions.""" # Set cookies on client instance to avoid deprecation warning diff --git a/tests/api/v1/test_playlist_endpoints.py b/tests/api/v1/test_playlist_endpoints.py index b2d8e9d..db558ea 100644 --- a/tests/api/v1/test_playlist_endpoints.py +++ b/tests/api/v1/test_playlist_endpoints.py @@ -1,6 +1,5 @@ """Tests for playlist API endpoints.""" - import pytest import pytest_asyncio from httpx import AsyncClient @@ -348,7 +347,8 @@ class TestPlaylistEndpoints: } response = await authenticated_client.put( - f"/api/v1/playlists/{playlist_id}", json=payload, + f"/api/v1/playlists/{playlist_id}", + json=payload, ) assert response.status_code == 200 @@ -386,7 +386,8 @@ class TestPlaylistEndpoints: payload = {"name": "Updated Playlist", "description": "Updated description"} response = await authenticated_client.put( - f"/api/v1/playlists/{playlist_id}", json=payload, + f"/api/v1/playlists/{playlist_id}", + json=payload, ) assert response.status_code == 200 @@ -613,7 +614,8 @@ class TestPlaylistEndpoints: payload = {"sound_id": sound_id} response = await authenticated_client.post( - f"/api/v1/playlists/{playlist_id}/sounds", json=payload, + f"/api/v1/playlists/{playlist_id}/sounds", + json=payload, ) assert response.status_code == 200 @@ -670,7 +672,8 @@ class TestPlaylistEndpoints: payload = {"sound_id": sound_id, "position": 5} response = await authenticated_client.post( - f"/api/v1/playlists/{playlist_id}/sounds", json=payload, + f"/api/v1/playlists/{playlist_id}/sounds", + json=payload, ) assert response.status_code == 200 @@ -718,13 +721,15 @@ class TestPlaylistEndpoints: # Add sound first time response = await authenticated_client.post( - f"/api/v1/playlists/{playlist_id}/sounds", json=payload, + f"/api/v1/playlists/{playlist_id}/sounds", + json=payload, ) assert response.status_code == 200 # Try to add same sound again response = await authenticated_client.post( - f"/api/v1/playlists/{playlist_id}/sounds", json=payload, + f"/api/v1/playlists/{playlist_id}/sounds", + json=payload, ) assert response.status_code == 400 assert "already in this playlist" in response.json()["detail"] @@ -758,7 +763,8 @@ class TestPlaylistEndpoints: payload = {"sound_id": 99999} response = await authenticated_client.post( - f"/api/v1/playlists/{playlist_id}/sounds", json=payload, + f"/api/v1/playlists/{playlist_id}/sounds", + json=payload, ) assert response.status_code == 404 @@ -806,7 +812,8 @@ class TestPlaylistEndpoints: # Add sound first payload = {"sound_id": sound_id} await authenticated_client.post( - f"/api/v1/playlists/{playlist_id}/sounds", json=payload, + f"/api/v1/playlists/{playlist_id}/sounds", + json=payload, ) # Remove sound @@ -918,11 +925,15 @@ class TestPlaylistEndpoints: # Reorder sounds - use positions that don't cause constraints # When swapping, we need to be careful about unique constraints payload = { - "sound_positions": [[sound1_id, 10], [sound2_id, 5]], # Use different positions to avoid constraints + "sound_positions": [ + [sound1_id, 10], + [sound2_id, 5], + ], # Use different positions to avoid constraints } response = await authenticated_client.put( - f"/api/v1/playlists/{playlist_id}/sounds/reorder", json=payload, + f"/api/v1/playlists/{playlist_id}/sounds/reorder", + json=payload, ) assert response.status_code == 200 diff --git a/tests/api/v1/test_socket_endpoints.py b/tests/api/v1/test_socket_endpoints.py index e0532bb..7c957a3 100644 --- a/tests/api/v1/test_socket_endpoints.py +++ b/tests/api/v1/test_socket_endpoints.py @@ -158,7 +158,9 @@ class TestSocketEndpoints: @pytest.mark.asyncio async def test_send_message_missing_parameters( - self, authenticated_client: AsyncClient, authenticated_user: User, + self, + authenticated_client: AsyncClient, + authenticated_user: User, ) -> None: """Test sending message with missing parameters.""" # Missing target_user_id @@ -177,7 +179,9 @@ class TestSocketEndpoints: @pytest.mark.asyncio async def test_broadcast_message_missing_parameters( - self, authenticated_client: AsyncClient, authenticated_user: User, + self, + authenticated_client: AsyncClient, + authenticated_user: User, ) -> None: """Test broadcasting message with missing parameters.""" response = await authenticated_client.post("/api/v1/socket/broadcast") @@ -185,7 +189,9 @@ class TestSocketEndpoints: @pytest.mark.asyncio async def test_send_message_invalid_user_id( - self, authenticated_client: AsyncClient, authenticated_user: User, + self, + authenticated_client: AsyncClient, + authenticated_user: User, ) -> None: """Test sending message with invalid user ID.""" response = await authenticated_client.post( diff --git a/tests/api/v1/test_sound_endpoints.py b/tests/api/v1/test_sound_endpoints.py index 1e46f70..f5f8bf1 100644 --- a/tests/api/v1/test_sound_endpoints.py +++ b/tests/api/v1/test_sound_endpoints.py @@ -35,10 +35,10 @@ class TestSoundEndpoints: with ( patch( - "app.services.extraction.ExtractionService.create_extraction" + "app.services.extraction.ExtractionService.create_extraction", ) as mock_create, patch( - "app.services.extraction_processor.extraction_processor.queue_extraction" + "app.services.extraction_processor.extraction_processor.queue_extraction", ) as mock_queue, ): mock_create.return_value = mock_extraction_info @@ -53,7 +53,10 @@ class TestSoundEndpoints: data = response.json() assert data["message"] == "Extraction queued successfully" assert data["extraction"]["id"] == 1 - assert data["extraction"]["url"] == "https://www.youtube.com/watch?v=dQw4w9WgXcQ" + assert ( + data["extraction"]["url"] + == "https://www.youtube.com/watch?v=dQw4w9WgXcQ" + ) @pytest.mark.asyncio async def test_create_extraction_unauthenticated(self, client: AsyncClient) -> None: @@ -75,7 +78,7 @@ class TestSoundEndpoints: ) -> None: """Test extraction creation with invalid URL.""" with patch( - "app.services.extraction.ExtractionService.create_extraction" + "app.services.extraction.ExtractionService.create_extraction", ) as mock_create: mock_create.side_effect = ValueError("Invalid URL") @@ -107,7 +110,7 @@ class TestSoundEndpoints: } with patch( - "app.services.extraction.ExtractionService.get_extraction_by_id" + "app.services.extraction.ExtractionService.get_extraction_by_id", ) as mock_get: mock_get.return_value = mock_extraction_info @@ -128,7 +131,7 @@ class TestSoundEndpoints: ) -> None: """Test getting non-existent extraction.""" with patch( - "app.services.extraction.ExtractionService.get_extraction_by_id" + "app.services.extraction.ExtractionService.get_extraction_by_id", ) as mock_get: mock_get.return_value = None @@ -169,7 +172,7 @@ class TestSoundEndpoints: ] with patch( - "app.services.extraction.ExtractionService.get_user_extractions" + "app.services.extraction.ExtractionService.get_user_extractions", ) as mock_get: mock_get.return_value = mock_extractions @@ -202,7 +205,9 @@ class TestSoundEndpoints: with ( patch("app.repositories.sound.SoundRepository.get_by_id") as mock_get_sound, - patch("app.services.credit.CreditService.validate_and_reserve_credits") as mock_validate, + patch( + "app.services.credit.CreditService.validate_and_reserve_credits", + ) as mock_validate, patch("app.services.vlc_player.VLCPlayerService.play_sound") as mock_play, patch("app.services.credit.CreditService.deduct_credits") as mock_deduct, ): @@ -227,7 +232,9 @@ class TestSoundEndpoints: authenticated_user: User, ) -> None: """Test playing non-existent sound with VLC.""" - with patch("app.repositories.sound.SoundRepository.get_by_id") as mock_get_sound: + with patch( + "app.repositories.sound.SoundRepository.get_by_id", + ) as mock_get_sound: mock_get_sound.return_value = None response = await authenticated_client.post("/api/v1/sounds/play/999") @@ -259,11 +266,14 @@ class TestSoundEndpoints: with ( patch("app.repositories.sound.SoundRepository.get_by_id") as mock_get_sound, - patch("app.services.credit.CreditService.validate_and_reserve_credits") as mock_validate, + patch( + "app.services.credit.CreditService.validate_and_reserve_credits", + ) as mock_validate, ): mock_get_sound.return_value = mock_sound mock_validate.side_effect = InsufficientCreditsError( - required=1, available=0 + required=1, + available=0, ) response = await authenticated_client.post("/api/v1/sounds/play/1") @@ -286,7 +296,7 @@ class TestSoundEndpoints: } with patch( - "app.services.vlc_player.VLCPlayerService.stop_all_vlc_instances" + "app.services.vlc_player.VLCPlayerService.stop_all_vlc_instances", ) as mock_stop: mock_stop.return_value = mock_result @@ -295,4 +305,4 @@ class TestSoundEndpoints: assert response.status_code == 200 data = response.json() assert data["message"] == "All VLC instances stopped" - assert data["stopped_count"] == 3 \ No newline at end of file + assert data["stopped_count"] == 3 diff --git a/tests/core/test_api_token_dependencies.py b/tests/core/test_api_token_dependencies.py index 6a6b154..e3e79d1 100644 --- a/tests/core/test_api_token_dependencies.py +++ b/tests/core/test_api_token_dependencies.py @@ -57,7 +57,8 @@ class TestApiTokenDependencies: @pytest.mark.asyncio async def test_get_current_user_api_token_no_header( - self, mock_auth_service: AsyncMock, + self, + mock_auth_service: AsyncMock, ) -> None: """Test API token authentication without API-TOKEN header.""" with pytest.raises(HTTPException) as exc_info: @@ -68,7 +69,8 @@ class TestApiTokenDependencies: @pytest.mark.asyncio async def test_get_current_user_api_token_empty_token( - self, mock_auth_service: AsyncMock, + self, + mock_auth_service: AsyncMock, ) -> None: """Test API token authentication with empty token.""" api_token_header = " " @@ -81,7 +83,8 @@ class TestApiTokenDependencies: @pytest.mark.asyncio async def test_get_current_user_api_token_whitespace_token( - self, mock_auth_service: AsyncMock, + self, + mock_auth_service: AsyncMock, ) -> None: """Test API token authentication with whitespace-only token.""" api_token_header = " " @@ -94,7 +97,8 @@ class TestApiTokenDependencies: @pytest.mark.asyncio async def test_get_current_user_api_token_invalid_token( - self, mock_auth_service: AsyncMock, + self, + mock_auth_service: AsyncMock, ) -> None: """Test API token authentication with invalid token.""" mock_auth_service.get_user_by_api_token.return_value = None @@ -146,7 +150,8 @@ class TestApiTokenDependencies: @pytest.mark.asyncio async def test_get_current_user_api_token_service_exception( - self, mock_auth_service: AsyncMock, + self, + mock_auth_service: AsyncMock, ) -> None: """Test API token authentication with service exception.""" mock_auth_service.get_user_by_api_token.side_effect = Exception( @@ -186,7 +191,8 @@ class TestApiTokenDependencies: @pytest.mark.asyncio async def test_get_current_user_flexible_falls_back_to_jwt( - self, mock_auth_service: AsyncMock, + self, + mock_auth_service: AsyncMock, ) -> None: """Test flexible authentication falls back to JWT when no API token.""" # Mock the get_current_user function (normally imported) @@ -197,7 +203,9 @@ class TestApiTokenDependencies: @pytest.mark.asyncio async def test_api_token_no_expiry_never_expires( - self, mock_auth_service: AsyncMock, test_user: User, + self, + mock_auth_service: AsyncMock, + test_user: User, ) -> None: """Test API token with no expiry date never expires.""" test_user.api_token_expires_at = None @@ -211,7 +219,9 @@ class TestApiTokenDependencies: @pytest.mark.asyncio async def test_api_token_with_whitespace( - self, mock_auth_service: AsyncMock, test_user: User, + self, + mock_auth_service: AsyncMock, + test_user: User, ) -> None: """Test API token with leading/trailing whitespace is handled correctly.""" mock_auth_service.get_user_by_api_token.return_value = test_user diff --git a/tests/repositories/test_credit_transaction.py b/tests/repositories/test_credit_transaction.py index d79755c..ab5fdf9 100644 --- a/tests/repositories/test_credit_transaction.py +++ b/tests/repositories/test_credit_transaction.py @@ -202,13 +202,17 @@ class TestCreditTransactionRepository: """Test getting transactions by user ID with pagination.""" # Get first 2 transactions first_page = await credit_transaction_repository.get_by_user_id( - test_user_id, limit=2, offset=0, + test_user_id, + limit=2, + offset=0, ) assert len(first_page) == PAGE_SIZE # Get next 2 transactions second_page = await credit_transaction_repository.get_by_user_id( - test_user_id, limit=2, offset=2, + test_user_id, + limit=2, + offset=2, ) assert len(second_page) == PAGE_SIZE @@ -251,14 +255,17 @@ class TestCreditTransactionRepository: """Test getting transactions by action type with pagination.""" # Test with limit transactions = await credit_transaction_repository.get_by_action_type( - "vlc_play_sound", limit=1, + "vlc_play_sound", + limit=1, ) assert len(transactions) == 1 assert transactions[0].action_type == "vlc_play_sound" # Test with offset transactions = await credit_transaction_repository.get_by_action_type( - "vlc_play_sound", limit=1, offset=1, + "vlc_play_sound", + limit=1, + offset=1, ) assert len(transactions) <= 1 # Might be 0 if only 1 VLC transaction in total @@ -269,7 +276,9 @@ class TestCreditTransactionRepository: test_transactions: list[CreditTransaction], ) -> None: """Test getting only successful transactions.""" - successful_transactions = await credit_transaction_repository.get_successful_transactions() + successful_transactions = ( + await credit_transaction_repository.get_successful_transactions() + ) # Should only return successful transactions assert all(t.success is True for t in successful_transactions) @@ -285,8 +294,10 @@ class TestCreditTransactionRepository: test_user_id: int, ) -> None: """Test getting successful transactions filtered by user.""" - successful_transactions = await credit_transaction_repository.get_successful_transactions( - user_id=test_user_id, + successful_transactions = ( + await credit_transaction_repository.get_successful_transactions( + user_id=test_user_id, + ) ) # Should only return successful transactions for test_user @@ -305,14 +316,18 @@ class TestCreditTransactionRepository: """Test getting successful transactions with pagination.""" # Get first 2 successful transactions first_page = await credit_transaction_repository.get_successful_transactions( - user_id=test_user_id, limit=2, offset=0, + user_id=test_user_id, + limit=2, + offset=0, ) assert len(first_page) == PAGE_SIZE assert all(t.success is True for t in first_page) # Get next successful transaction second_page = await credit_transaction_repository.get_successful_transactions( - user_id=test_user_id, limit=2, offset=2, + user_id=test_user_id, + limit=2, + offset=2, ) assert len(second_page) == 1 # Should be 1 remaining assert all(t.success is True for t in second_page) @@ -328,7 +343,9 @@ class TestCreditTransactionRepository: all_transactions = await credit_transaction_repository.get_all() # Should return all transactions - assert len(all_transactions) >= MIN_ALL_TRANSACTIONS # 4 from test_transactions + 1 other_user_transaction + assert ( + len(all_transactions) >= MIN_ALL_TRANSACTIONS + ) # 4 from test_transactions + 1 other_user_transaction @pytest.mark.asyncio async def test_create_transaction( @@ -374,7 +391,8 @@ class TestCreditTransactionRepository: } updated_transaction = await credit_transaction_repository.update( - transaction, update_data, + transaction, + update_data, ) assert updated_transaction.id == transaction.id @@ -412,7 +430,9 @@ class TestCreditTransactionRepository: # Verify transaction is deleted assert transaction_id is not None - deleted_transaction = await credit_transaction_repository.get_by_id(transaction_id) + deleted_transaction = await credit_transaction_repository.get_by_id( + transaction_id, + ) assert deleted_transaction is None @pytest.mark.asyncio diff --git a/tests/repositories/test_playlist.py b/tests/repositories/test_playlist.py index 796746c..e884df8 100644 --- a/tests/repositories/test_playlist.py +++ b/tests/repositories/test_playlist.py @@ -407,7 +407,8 @@ class TestPlaylistRepository: # Test the repository method playlist_sound = await playlist_repository.add_sound_to_playlist( - playlist_id, sound_id, + playlist_id, + sound_id, ) assert playlist_sound.playlist_id == playlist_id @@ -472,7 +473,9 @@ class TestPlaylistRepository: # Test the repository method playlist_sound = await playlist_repository.add_sound_to_playlist( - playlist_id, sound_id, position=5, + playlist_id, + sound_id, + position=5, ) assert playlist_sound.position == TEST_POSITION @@ -535,17 +538,20 @@ class TestPlaylistRepository: # Verify it was added assert await playlist_repository.is_sound_in_playlist( - playlist_id, sound_id, + playlist_id, + sound_id, ) # Remove the sound await playlist_repository.remove_sound_from_playlist( - playlist_id, sound_id, + playlist_id, + sound_id, ) # Verify it was removed assert not await playlist_repository.is_sound_in_playlist( - playlist_id, sound_id, + playlist_id, + sound_id, ) @pytest.mark.asyncio @@ -732,7 +738,8 @@ class TestPlaylistRepository: # Initially not in playlist assert not await playlist_repository.is_sound_in_playlist( - playlist_id, sound_id, + playlist_id, + sound_id, ) # Add sound @@ -740,7 +747,8 @@ class TestPlaylistRepository: # Now in playlist assert await playlist_repository.is_sound_in_playlist( - playlist_id, sound_id, + playlist_id, + sound_id, ) @pytest.mark.asyncio @@ -794,16 +802,21 @@ class TestPlaylistRepository: # Add sounds to playlist await playlist_repository.add_sound_to_playlist( - playlist_id, sound1_id, position=0, + playlist_id, + sound1_id, + position=0, ) await playlist_repository.add_sound_to_playlist( - playlist_id, sound2_id, position=1, + playlist_id, + sound2_id, + position=1, ) # Reorder sounds - use different positions to avoid constraint issues sound_positions = [(sound1_id, 10), (sound2_id, 5)] await playlist_repository.reorder_playlist_sounds( - playlist_id, sound_positions, + playlist_id, + sound_positions, ) # Verify new order @@ -863,10 +876,14 @@ class TestPlaylistRepository: # Add sounds to playlist at positions 0 and 1 await playlist_repository.add_sound_to_playlist( - playlist_id, sound1_id, position=0, + playlist_id, + sound1_id, + position=0, ) await playlist_repository.add_sound_to_playlist( - playlist_id, sound2_id, position=1, + playlist_id, + sound2_id, + position=1, ) # Verify initial order @@ -878,7 +895,8 @@ class TestPlaylistRepository: # Swap positions - this used to cause unique constraint violation sound_positions = [(sound1_id, 1), (sound2_id, 0)] await playlist_repository.reorder_playlist_sounds( - playlist_id, sound_positions, + playlist_id, + sound_positions, ) # Verify swapped order diff --git a/tests/repositories/test_user_oauth.py b/tests/repositories/test_user_oauth.py index 4769b54..0ef8aef 100644 --- a/tests/repositories/test_user_oauth.py +++ b/tests/repositories/test_user_oauth.py @@ -61,7 +61,8 @@ class TestUserOauthRepository: ) -> None: """Test getting OAuth by provider user ID when it exists.""" oauth = await user_oauth_repository.get_by_provider_user_id( - "google", "google_123456", + "google", + "google_123456", ) assert oauth is not None @@ -77,7 +78,8 @@ class TestUserOauthRepository: ) -> None: """Test getting OAuth by provider user ID when it doesn't exist.""" oauth = await user_oauth_repository.get_by_provider_user_id( - "google", "nonexistent_id", + "google", + "nonexistent_id", ) assert oauth is None @@ -91,7 +93,8 @@ class TestUserOauthRepository: ) -> None: """Test getting OAuth by user ID and provider when it exists.""" oauth = await user_oauth_repository.get_by_user_id_and_provider( - test_user_id, "google", + test_user_id, + "google", ) assert oauth is not None @@ -107,7 +110,8 @@ class TestUserOauthRepository: ) -> None: """Test getting OAuth by user ID and provider when it doesn't exist.""" oauth = await user_oauth_repository.get_by_user_id_and_provider( - test_user_id, "github", + test_user_id, + "github", ) assert oauth is None @@ -186,7 +190,8 @@ class TestUserOauthRepository: # Verify it's deleted by trying to find it deleted_oauth = await user_oauth_repository.get_by_provider_user_id( - "twitter", "twitter_456", + "twitter", + "twitter_456", ) assert deleted_oauth is None @@ -243,10 +248,12 @@ class TestUserOauthRepository: # Verify both exist by querying back from database found_google = await user_oauth_repository.get_by_user_id_and_provider( - test_user_id, "google", + test_user_id, + "google", ) found_github = await user_oauth_repository.get_by_user_id_and_provider( - test_user_id, "github", + test_user_id, + "github", ) assert found_google is not None @@ -260,10 +267,12 @@ class TestUserOauthRepository: # Verify we can also find them by provider_user_id found_google_by_provider = await user_oauth_repository.get_by_provider_user_id( - "google", "google_user_1", + "google", + "google_user_1", ) found_github_by_provider = await user_oauth_repository.get_by_provider_user_id( - "github", "github_user_1", + "github", + "github_user_1", ) assert found_google_by_provider is not None diff --git a/tests/services/test_credit.py b/tests/services/test_credit.py index 5bc4ae5..85bfe3a 100644 --- a/tests/services/test_credit.py +++ b/tests/services/test_credit.py @@ -48,7 +48,9 @@ class TestCreditService: mock_repo_class.return_value = mock_repo mock_repo.get_by_id.return_value = sample_user - result = await credit_service.check_credits(1, CreditActionType.VLC_PLAY_SOUND) + result = await credit_service.check_credits( + 1, CreditActionType.VLC_PLAY_SOUND, + ) assert result is True mock_repo.get_by_id.assert_called_once_with(1) @@ -72,7 +74,9 @@ class TestCreditService: mock_repo_class.return_value = mock_repo mock_repo.get_by_id.return_value = poor_user - result = await credit_service.check_credits(1, CreditActionType.VLC_PLAY_SOUND) + result = await credit_service.check_credits( + 1, CreditActionType.VLC_PLAY_SOUND, + ) assert result is False mock_session.close.assert_called_once() @@ -87,13 +91,17 @@ class TestCreditService: mock_repo_class.return_value = mock_repo mock_repo.get_by_id.return_value = None - result = await credit_service.check_credits(999, CreditActionType.VLC_PLAY_SOUND) + result = await credit_service.check_credits( + 999, CreditActionType.VLC_PLAY_SOUND, + ) assert result is False mock_session.close.assert_called_once() @pytest.mark.asyncio - async def test_validate_and_reserve_credits_success(self, credit_service, sample_user) -> None: + async def test_validate_and_reserve_credits_success( + self, credit_service, sample_user, + ) -> None: """Test successful credit validation and reservation.""" mock_session = credit_service.db_session_factory() @@ -103,7 +111,8 @@ class TestCreditService: mock_repo.get_by_id.return_value = sample_user user, action = await credit_service.validate_and_reserve_credits( - 1, CreditActionType.VLC_PLAY_SOUND, + 1, + CreditActionType.VLC_PLAY_SOUND, ) assert user == sample_user @@ -112,7 +121,9 @@ class TestCreditService: mock_session.close.assert_called_once() @pytest.mark.asyncio - async def test_validate_and_reserve_credits_insufficient(self, credit_service) -> None: + async def test_validate_and_reserve_credits_insufficient( + self, credit_service, + ) -> None: """Test credit validation with insufficient credits.""" mock_session = credit_service.db_session_factory() poor_user = User( @@ -131,7 +142,8 @@ class TestCreditService: with pytest.raises(InsufficientCreditsError) as exc_info: await credit_service.validate_and_reserve_credits( - 1, CreditActionType.VLC_PLAY_SOUND, + 1, + CreditActionType.VLC_PLAY_SOUND, ) assert exc_info.value.required == 1 @@ -139,7 +151,9 @@ class TestCreditService: mock_session.close.assert_called_once() @pytest.mark.asyncio - async def test_validate_and_reserve_credits_user_not_found(self, credit_service) -> None: + async def test_validate_and_reserve_credits_user_not_found( + self, credit_service, + ) -> None: """Test credit validation when user is not found.""" mock_session = credit_service.db_session_factory() @@ -150,7 +164,8 @@ class TestCreditService: with pytest.raises(ValueError, match="User 999 not found"): await credit_service.validate_and_reserve_credits( - 999, CreditActionType.VLC_PLAY_SOUND, + 999, + CreditActionType.VLC_PLAY_SOUND, ) mock_session.close.assert_called_once() @@ -160,15 +175,20 @@ class TestCreditService: """Test successful credit deduction.""" mock_session = credit_service.db_session_factory() - with patch("app.services.credit.UserRepository") as mock_repo_class, \ - patch("app.services.credit.socket_manager") as mock_socket_manager: + with ( + patch("app.services.credit.UserRepository") as mock_repo_class, + patch("app.services.credit.socket_manager") as mock_socket_manager, + ): mock_repo = AsyncMock() mock_repo_class.return_value = mock_repo mock_repo.get_by_id.return_value = sample_user mock_socket_manager.send_to_user = AsyncMock() await credit_service.deduct_credits( - 1, CreditActionType.VLC_PLAY_SOUND, success=True, metadata={"test": "data"}, + 1, + CreditActionType.VLC_PLAY_SOUND, + success=True, + metadata={"test": "data"}, ) # Verify user credits were updated @@ -180,7 +200,9 @@ class TestCreditService: # Verify socket event was emitted mock_socket_manager.send_to_user.assert_called_once_with( - "1", "user_credits_changed", { + "1", + "user_credits_changed", + { "user_id": "1", "credits_before": 10, "credits_after": 9, @@ -202,19 +224,25 @@ class TestCreditService: assert json.loads(added_transaction.metadata_json) == {"test": "data"} @pytest.mark.asyncio - async def test_deduct_credits_failed_action_requires_success(self, credit_service, sample_user) -> None: + async def test_deduct_credits_failed_action_requires_success( + self, credit_service, sample_user, + ) -> None: """Test credit deduction when action failed but requires success.""" mock_session = credit_service.db_session_factory() - with patch("app.services.credit.UserRepository") as mock_repo_class, \ - patch("app.services.credit.socket_manager") as mock_socket_manager: + with ( + patch("app.services.credit.UserRepository") as mock_repo_class, + patch("app.services.credit.socket_manager") as mock_socket_manager, + ): mock_repo = AsyncMock() mock_repo_class.return_value = mock_repo mock_repo.get_by_id.return_value = sample_user mock_socket_manager.send_to_user = AsyncMock() await credit_service.deduct_credits( - 1, CreditActionType.VLC_PLAY_SOUND, success=False, # Action failed + 1, + CreditActionType.VLC_PLAY_SOUND, + success=False, # Action failed ) # Verify user credits were NOT updated (action requires success) @@ -247,8 +275,10 @@ class TestCreditService: plan_id=1, ) - with patch("app.services.credit.UserRepository") as mock_repo_class, \ - patch("app.services.credit.socket_manager") as mock_socket_manager: + with ( + patch("app.services.credit.UserRepository") as mock_repo_class, + patch("app.services.credit.socket_manager") as mock_socket_manager, + ): mock_repo = AsyncMock() mock_repo_class.return_value = mock_repo mock_repo.get_by_id.return_value = poor_user @@ -256,7 +286,9 @@ class TestCreditService: with pytest.raises(InsufficientCreditsError): await credit_service.deduct_credits( - 1, CreditActionType.VLC_PLAY_SOUND, success=True, + 1, + CreditActionType.VLC_PLAY_SOUND, + success=True, ) # Verify no socket event was emitted since credits could not be deducted @@ -270,15 +302,20 @@ class TestCreditService: """Test adding credits to user account.""" mock_session = credit_service.db_session_factory() - with patch("app.services.credit.UserRepository") as mock_repo_class, \ - patch("app.services.credit.socket_manager") as mock_socket_manager: + with ( + patch("app.services.credit.UserRepository") as mock_repo_class, + patch("app.services.credit.socket_manager") as mock_socket_manager, + ): mock_repo = AsyncMock() mock_repo_class.return_value = mock_repo mock_repo.get_by_id.return_value = sample_user mock_socket_manager.send_to_user = AsyncMock() await credit_service.add_credits( - 1, 5, "Bonus credits", {"reason": "signup"}, + 1, + 5, + "Bonus credits", + {"reason": "signup"}, ) # Verify user credits were updated @@ -290,7 +327,9 @@ class TestCreditService: # Verify socket event was emitted mock_socket_manager.send_to_user.assert_called_once_with( - "1", "user_credits_changed", { + "1", + "user_credits_changed", + { "user_id": "1", "credits_before": 10, "credits_after": 15, diff --git a/tests/services/test_extraction.py b/tests/services/test_extraction.py index fed5308..69be3c2 100644 --- a/tests/services/test_extraction.py +++ b/tests/services/test_extraction.py @@ -53,7 +53,9 @@ class TestExtractionService: @patch("app.services.extraction.yt_dlp.YoutubeDL") @pytest.mark.asyncio async def test_detect_service_info_youtube( - self, mock_ydl_class, extraction_service, + self, + mock_ydl_class, + extraction_service, ) -> None: """Test service detection for YouTube.""" mock_ydl = Mock() @@ -78,7 +80,9 @@ class TestExtractionService: @patch("app.services.extraction.yt_dlp.YoutubeDL") @pytest.mark.asyncio async def test_detect_service_info_failure( - self, mock_ydl_class, extraction_service, + self, + mock_ydl_class, + extraction_service, ) -> None: """Test service detection failure.""" mock_ydl = Mock() @@ -170,7 +174,9 @@ class TestExtractionService: assert result["status"] == "pending" @pytest.mark.asyncio - async def test_process_extraction_with_service_detection(self, extraction_service) -> None: + async def test_process_extraction_with_service_detection( + self, extraction_service, + ) -> None: """Test extraction processing with service detection.""" extraction_id = 1 @@ -202,14 +208,18 @@ class TestExtractionService: with ( patch.object( - extraction_service, "_detect_service_info", return_value=service_info, + extraction_service, + "_detect_service_info", + return_value=service_info, ), patch.object(extraction_service, "_extract_media") as mock_extract, patch.object( - extraction_service, "_move_files_to_final_location", + extraction_service, + "_move_files_to_final_location", ) as mock_move, patch.object( - extraction_service, "_create_sound_record", + extraction_service, + "_create_sound_record", ) as mock_create_sound, patch.object(extraction_service, "_normalize_sound"), patch.object(extraction_service, "_add_to_main_playlist"), @@ -289,11 +299,13 @@ class TestExtractionService: with ( patch( - "app.services.extraction.get_audio_duration", return_value=240000, + "app.services.extraction.get_audio_duration", + return_value=240000, ), patch("app.services.extraction.get_file_size", return_value=1024), patch( - "app.services.extraction.get_file_hash", return_value="test_hash", + "app.services.extraction.get_file_hash", + return_value="test_hash", ), ): extraction_service.sound_repo.create = AsyncMock( diff --git a/tests/services/test_extraction_processor.py b/tests/services/test_extraction_processor.py index af0cf64..e15f691 100644 --- a/tests/services/test_extraction_processor.py +++ b/tests/services/test_extraction_processor.py @@ -29,7 +29,9 @@ class TestExtractionProcessor: """Test starting and stopping the processor.""" # Mock the _process_queue method to avoid actual processing with patch.object( - processor, "_process_queue", new_callable=AsyncMock, + processor, + "_process_queue", + new_callable=AsyncMock, ): # Start the processor await processor.start() @@ -229,7 +231,9 @@ class TestExtractionProcessor: "app.services.extraction_processor.AsyncSession", ) as mock_session_class, patch.object( - processor, "_process_single_extraction", new_callable=AsyncMock, + processor, + "_process_single_extraction", + new_callable=AsyncMock, ), patch( "app.services.extraction_processor.ExtractionService", @@ -274,7 +278,9 @@ class TestExtractionProcessor: "app.services.extraction_processor.AsyncSession", ) as mock_session_class, patch.object( - processor, "_process_single_extraction", new_callable=AsyncMock, + processor, + "_process_single_extraction", + new_callable=AsyncMock, ), patch( "app.services.extraction_processor.ExtractionService", diff --git a/tests/services/test_player.py b/tests/services/test_player.py index c2bace8..3dff6e5 100644 --- a/tests/services/test_player.py +++ b/tests/services/test_player.py @@ -131,11 +131,15 @@ class TestPlayerService: yield mock @pytest.fixture - def player_service(self, mock_db_session_factory, mock_vlc_instance, mock_socket_manager): + def player_service( + self, mock_db_session_factory, mock_vlc_instance, mock_socket_manager, + ): """Create a player service instance for testing.""" return PlayerService(mock_db_session_factory) - def test_init_creates_player_service(self, mock_db_session_factory, mock_vlc_instance) -> None: + def test_init_creates_player_service( + self, mock_db_session_factory, mock_vlc_instance, + ) -> None: """Test that player service initializes correctly.""" with patch("app.services.player.socket_manager"): service = PlayerService(mock_db_session_factory) @@ -152,7 +156,9 @@ class TestPlayerService: assert service._loop is None @pytest.mark.asyncio - async def test_start_initializes_service(self, player_service, mock_vlc_instance) -> None: + async def test_start_initializes_service( + self, player_service, mock_vlc_instance, + ) -> None: """Test that start method initializes the service.""" with patch.object(player_service, "reload_playlist", new_callable=AsyncMock): await player_service.start() @@ -197,7 +203,9 @@ class TestPlayerService: mock_file_path.exists.return_value = True mock_path.return_value = mock_file_path - with patch.object(player_service, "_broadcast_state", new_callable=AsyncMock): + with patch.object( + player_service, "_broadcast_state", new_callable=AsyncMock, + ): mock_media = Mock() player_service._vlc_instance.media_new.return_value = mock_media player_service._player.play.return_value = 0 # Success @@ -252,7 +260,9 @@ class TestPlayerService: """Test pausing when not playing does nothing.""" player_service.state.status = PlayerStatus.STOPPED - with patch.object(player_service, "_broadcast_state", new_callable=AsyncMock) as mock_broadcast: + with patch.object( + player_service, "_broadcast_state", new_callable=AsyncMock, + ) as mock_broadcast: await player_service.pause() assert player_service.state.status == PlayerStatus.STOPPED @@ -264,8 +274,12 @@ class TestPlayerService: player_service.state.status = PlayerStatus.PLAYING player_service.state.current_sound_position = 5000 - with patch.object(player_service, "_process_play_count", new_callable=AsyncMock): - with patch.object(player_service, "_broadcast_state", new_callable=AsyncMock): + with patch.object( + player_service, "_process_play_count", new_callable=AsyncMock, + ): + with patch.object( + player_service, "_broadcast_state", new_callable=AsyncMock, + ): await player_service.stop_playback() assert player_service.state.status == PlayerStatus.STOPPED @@ -314,7 +328,9 @@ class TestPlayerService: """Test seeking when stopped does nothing.""" player_service.state.status = PlayerStatus.STOPPED - with patch.object(player_service, "_broadcast_state", new_callable=AsyncMock) as mock_broadcast: + with patch.object( + player_service, "_broadcast_state", new_callable=AsyncMock, + ) as mock_broadcast: await player_service.seek(15000) player_service._player.set_position.assert_not_called() @@ -364,7 +380,9 @@ class TestPlayerService: mock_playlist = Mock() mock_playlist.id = 1 mock_playlist.name = "Test Playlist" - mock_repo.get_current_playlist.return_value = mock_playlist # Return current playlist directly + mock_repo.get_current_playlist.return_value = ( + mock_playlist # Return current playlist directly + ) # Mock sounds sound1 = Sound(id=1, name="Song 1", filename="song1.mp3", duration=30000) @@ -372,7 +390,9 @@ class TestPlayerService: mock_sounds = [sound1, sound2] mock_repo.get_playlist_sounds.return_value = mock_sounds - with patch.object(player_service, "_broadcast_state", new_callable=AsyncMock): + with patch.object( + player_service, "_broadcast_state", new_callable=AsyncMock, + ): await player_service.reload_playlist() assert player_service.state.playlist_id == 1 @@ -394,7 +414,9 @@ class TestPlayerService: sound2 = Sound(id=2, name="Song 2", filename="song2.mp3", duration=45000) sounds = [sound1, sound2] - with patch.object(player_service, "_stop_playback", new_callable=AsyncMock) as mock_stop: + with patch.object( + player_service, "_stop_playback", new_callable=AsyncMock, + ) as mock_stop: await player_service._handle_playlist_id_changed(1, 2, sounds) # Should stop playback and set first track as current @@ -404,11 +426,15 @@ class TestPlayerService: assert player_service.state.current_sound_id == 1 @pytest.mark.asyncio - async def test_handle_playlist_id_changed_empty_playlist(self, player_service) -> None: + async def test_handle_playlist_id_changed_empty_playlist( + self, player_service, + ) -> None: """Test handling playlist ID change with empty playlist.""" player_service.state.status = PlayerStatus.PLAYING - with patch.object(player_service, "_stop_playback", new_callable=AsyncMock) as mock_stop: + with patch.object( + player_service, "_stop_playback", new_callable=AsyncMock, + ) as mock_stop: await player_service._handle_playlist_id_changed(1, 2, []) mock_stop.assert_called_once() @@ -417,7 +443,9 @@ class TestPlayerService: assert player_service.state.current_sound_id is None @pytest.mark.asyncio - async def test_handle_same_playlist_track_exists_same_index(self, player_service) -> None: + async def test_handle_same_playlist_track_exists_same_index( + self, player_service, + ) -> None: """Test handling same playlist when track exists at same index.""" sound1 = Sound(id=1, name="Song 1", filename="song1.mp3", duration=30000) sound2 = Sound(id=2, name="Song 2", filename="song2.mp3", duration=45000) @@ -426,11 +454,15 @@ class TestPlayerService: await player_service._handle_same_playlist_track_check(1, 0, sounds) # Should update sound object reference but keep same index - assert player_service.state.current_sound_index == 0 # Should be set to 0 from new_index + assert ( + player_service.state.current_sound_index == 0 + ) # Should be set to 0 from new_index assert player_service.state.current_sound == sound1 @pytest.mark.asyncio - async def test_handle_same_playlist_track_exists_different_index(self, player_service) -> None: + async def test_handle_same_playlist_track_exists_different_index( + self, player_service, + ) -> None: """Test handling same playlist when track exists at different index.""" sound1 = Sound(id=2, name="Song 2", filename="song2.mp3", duration=45000) sound2 = Sound(id=1, name="Song 1", filename="song1.mp3", duration=30000) @@ -450,7 +482,9 @@ class TestPlayerService: sound2 = Sound(id=3, name="Song 3", filename="song3.mp3", duration=60000) sounds = [sound1, sound2] # Track with ID 1 is missing - with patch.object(player_service, "_handle_track_removed", new_callable=AsyncMock) as mock_removed: + with patch.object( + player_service, "_handle_track_removed", new_callable=AsyncMock, + ) as mock_removed: await player_service._handle_same_playlist_track_check(1, 0, sounds) mock_removed.assert_called_once_with(1, sounds) @@ -461,7 +495,9 @@ class TestPlayerService: sound1 = Sound(id=2, name="Song 2", filename="song2.mp3", duration=45000) sounds = [sound1] - with patch.object(player_service, "_stop_playback", new_callable=AsyncMock) as mock_stop: + with patch.object( + player_service, "_stop_playback", new_callable=AsyncMock, + ) as mock_stop: await player_service._handle_track_removed(1, sounds) mock_stop.assert_called_once() @@ -474,7 +510,9 @@ class TestPlayerService: """Test handling when current track is removed with empty playlist.""" player_service.state.status = PlayerStatus.PLAYING - with patch.object(player_service, "_stop_playback", new_callable=AsyncMock) as mock_stop: + with patch.object( + player_service, "_stop_playback", new_callable=AsyncMock, + ) as mock_stop: await player_service._handle_track_removed(1, []) mock_stop.assert_called_once() @@ -562,14 +600,20 @@ class TestPlayerService: mock_playlist = Mock() mock_playlist.id = 2 # Different ID mock_playlist.name = "New Playlist" - mock_repo.get_current_playlist.return_value = mock_playlist # Return current playlist directly + mock_repo.get_current_playlist.return_value = ( + mock_playlist # Return current playlist directly + ) sound1 = Sound(id=1, name="Song 1", filename="song1.mp3", duration=30000) mock_sounds = [sound1] mock_repo.get_playlist_sounds.return_value = mock_sounds - with patch.object(player_service, "_stop_playback", new_callable=AsyncMock) as mock_stop: - with patch.object(player_service, "_broadcast_state", new_callable=AsyncMock): + with patch.object( + player_service, "_stop_playback", new_callable=AsyncMock, + ) as mock_stop: + with patch.object( + player_service, "_broadcast_state", new_callable=AsyncMock, + ): await player_service.reload_playlist() # Should stop and reset to first track @@ -597,7 +641,9 @@ class TestPlayerService: mock_playlist = Mock() mock_playlist.id = 1 mock_playlist.name = "Same Playlist" - mock_repo.get_current_playlist.return_value = mock_playlist # Return current playlist directly + mock_repo.get_current_playlist.return_value = ( + mock_playlist # Return current playlist directly + ) # Track 2 moved to index 0 sound1 = Sound(id=2, name="Song 2", filename="song2.mp3", duration=45000) @@ -605,7 +651,9 @@ class TestPlayerService: mock_sounds = [sound1, sound2] # Track 2 now at index 0 mock_repo.get_playlist_sounds.return_value = mock_sounds - with patch.object(player_service, "_broadcast_state", new_callable=AsyncMock): + with patch.object( + player_service, "_broadcast_state", new_callable=AsyncMock, + ): await player_service.reload_playlist() # Should update index but keep same track @@ -614,7 +662,6 @@ class TestPlayerService: assert player_service.state.current_sound_id == 2 # Same track assert player_service.state.current_sound == sound1 - def test_get_next_index_continuous_mode(self, player_service) -> None: """Test getting next index in continuous mode.""" player_service.state.mode = PlayerMode.CONTINUOUS @@ -734,7 +781,8 @@ class TestPlayerService: # Verify sound play count was updated mock_sound_repo.update.assert_called_once_with( - mock_sound, {"play_count": 6}, + mock_sound, + {"play_count": 6}, ) # Verify SoundPlayed record was created with None user_id for player diff --git a/tests/services/test_socket_service.py b/tests/services/test_socket_service.py index 1d64d34..2c5a620 100644 --- a/tests/services/test_socket_service.py +++ b/tests/services/test_socket_service.py @@ -98,7 +98,11 @@ class TestSocketManager: @patch("app.services.socket.extract_access_token_from_cookies") @patch("app.services.socket.JWTUtils.decode_access_token") async def test_connect_handler_success( - self, mock_decode, mock_extract_token, socket_manager, mock_sio, + self, + mock_decode, + mock_extract_token, + socket_manager, + mock_sio, ) -> None: """Test successful connection with valid token.""" # Setup mocks @@ -132,7 +136,10 @@ class TestSocketManager: @pytest.mark.asyncio @patch("app.services.socket.extract_access_token_from_cookies") async def test_connect_handler_no_token( - self, mock_extract_token, socket_manager, mock_sio, + self, + mock_extract_token, + socket_manager, + mock_sio, ) -> None: """Test connection with no access token.""" # Setup mocks @@ -165,7 +172,11 @@ class TestSocketManager: @patch("app.services.socket.extract_access_token_from_cookies") @patch("app.services.socket.JWTUtils.decode_access_token") async def test_connect_handler_invalid_token( - self, mock_decode, mock_extract_token, socket_manager, mock_sio, + self, + mock_decode, + mock_extract_token, + socket_manager, + mock_sio, ) -> None: """Test connection with invalid token.""" # Setup mocks @@ -199,7 +210,11 @@ class TestSocketManager: @patch("app.services.socket.extract_access_token_from_cookies") @patch("app.services.socket.JWTUtils.decode_access_token") async def test_connect_handler_missing_user_id( - self, mock_decode, mock_extract_token, socket_manager, mock_sio, + self, + mock_decode, + mock_extract_token, + socket_manager, + mock_sio, ) -> None: """Test connection with token missing user ID.""" # Setup mocks @@ -254,7 +269,9 @@ class TestSocketManager: assert "123" not in socket_manager.user_rooms @pytest.mark.asyncio - async def test_disconnect_handler_unknown_socket(self, socket_manager, mock_sio) -> None: + async def test_disconnect_handler_unknown_socket( + self, socket_manager, mock_sio, + ) -> None: """Test disconnect handler with unknown socket.""" # Access the disconnect handler directly handlers = {} diff --git a/tests/services/test_sound_normalizer.py b/tests/services/test_sound_normalizer.py index d1e4f24..eff8766 100644 --- a/tests/services/test_sound_normalizer.py +++ b/tests/services/test_sound_normalizer.py @@ -154,7 +154,9 @@ class TestSoundNormalizerService: assert result["id"] == 1 @pytest.mark.asyncio - async def test_normalize_sound_force_already_normalized(self, normalizer_service) -> None: + async def test_normalize_sound_force_already_normalized( + self, normalizer_service, + ) -> None: """Test force normalizing a sound that's already normalized.""" sound = Sound( id=1, @@ -172,14 +174,17 @@ class TestSoundNormalizerService: patch.object(normalizer_service, "_get_original_path") as mock_orig_path, patch.object(normalizer_service, "_get_normalized_path") as mock_norm_path, patch.object( - normalizer_service, "_normalize_audio_two_pass", + normalizer_service, + "_normalize_audio_two_pass", ), patch( - "app.services.sound_normalizer.get_audio_duration", return_value=6000, + "app.services.sound_normalizer.get_audio_duration", + return_value=6000, ), patch("app.services.sound_normalizer.get_file_size", return_value=2048), patch( - "app.services.sound_normalizer.get_file_hash", return_value="new_hash", + "app.services.sound_normalizer.get_file_hash", + return_value="new_hash", ), ): # Setup path mocks @@ -245,14 +250,17 @@ class TestSoundNormalizerService: patch.object(normalizer_service, "_get_original_path") as mock_orig_path, patch.object(normalizer_service, "_get_normalized_path") as mock_norm_path, patch.object( - normalizer_service, "_normalize_audio_one_pass", + normalizer_service, + "_normalize_audio_one_pass", ) as mock_normalize, patch( - "app.services.sound_normalizer.get_audio_duration", return_value=5500, + "app.services.sound_normalizer.get_audio_duration", + return_value=5500, ), patch("app.services.sound_normalizer.get_file_size", return_value=1500), patch( - "app.services.sound_normalizer.get_file_hash", return_value="norm_hash", + "app.services.sound_normalizer.get_file_hash", + return_value="norm_hash", ), ): # Setup path mocks @@ -275,7 +283,9 @@ class TestSoundNormalizerService: mock_normalize.assert_called_once() @pytest.mark.asyncio - async def test_normalize_sound_normalization_error(self, normalizer_service) -> None: + async def test_normalize_sound_normalization_error( + self, normalizer_service, + ) -> None: """Test handling normalization errors.""" sound = Sound( id=1, @@ -300,7 +310,8 @@ class TestSoundNormalizerService: with ( patch("pathlib.Path.exists", return_value=True), patch.object( - normalizer_service, "_normalize_audio_two_pass", + normalizer_service, + "_normalize_audio_two_pass", ) as mock_normalize, ): mock_normalize.side_effect = Exception("Normalization failed") @@ -529,7 +540,11 @@ class TestSoundNormalizerService: # Verify ffmpeg chain was called correctly mock_ffmpeg.input.assert_called_once_with(str(input_path)) mock_ffmpeg.filter.assert_called_once_with( - mock_stream, "loudnorm", I=-23, TP=-2, LRA=7, + mock_stream, + "loudnorm", + I=-23, + TP=-2, + LRA=7, ) mock_ffmpeg.output.assert_called_once() mock_ffmpeg.run.assert_called_once() diff --git a/tests/services/test_sound_scanner.py b/tests/services/test_sound_scanner.py index b0b861a..b7d8e5a 100644 --- a/tests/services/test_sound_scanner.py +++ b/tests/services/test_sound_scanner.py @@ -153,7 +153,10 @@ class TestSoundScannerService: "files": [], } await scanner_service._sync_audio_file( - temp_path, "SDB", existing_sound, results, + temp_path, + "SDB", + existing_sound, + results, ) assert results["skipped"] == 1 @@ -257,7 +260,10 @@ class TestSoundScannerService: "files": [], } await scanner_service._sync_audio_file( - temp_path, "SDB", existing_sound, results, + temp_path, + "SDB", + existing_sound, + results, ) assert results["updated"] == 1 @@ -296,7 +302,8 @@ class TestSoundScannerService: # Mock file operations with ( patch( - "app.services.sound_scanner.get_file_hash", return_value="custom_hash", + "app.services.sound_scanner.get_file_hash", + return_value="custom_hash", ), patch("app.services.sound_scanner.get_audio_duration", return_value=60000), patch("app.services.sound_scanner.get_file_size", return_value=2048), @@ -316,7 +323,10 @@ class TestSoundScannerService: "files": [], } await scanner_service._sync_audio_file( - temp_path, "CUSTOM", None, results, + temp_path, + "CUSTOM", + None, + results, ) assert results["added"] == 1 diff --git a/tests/services/test_vlc_player.py b/tests/services/test_vlc_player.py index c26eef9..6156f24 100644 --- a/tests/services/test_vlc_player.py +++ b/tests/services/test_vlc_player.py @@ -80,6 +80,7 @@ class TestVLCPlayerService: # Mock Path to return True for the first absolute path with patch("app.services.vlc_player.Path") as mock_path: + def path_side_effect(path_str): mock_instance = Mock() mock_instance.exists.return_value = str(path_str) == "/usr/bin/vlc" @@ -105,11 +106,13 @@ class TestVLCPlayerService: service = VLCPlayerService() assert service.vlc_executable == "vlc" - @pytest.mark.asyncio @patch("app.services.vlc_player.asyncio.create_subprocess_exec") async def test_play_sound_success( - self, mock_subprocess, vlc_service, sample_sound, + self, + mock_subprocess, + vlc_service, + sample_sound, ) -> None: """Test successful sound playback.""" # Mock subprocess @@ -142,7 +145,9 @@ class TestVLCPlayerService: @pytest.mark.asyncio async def test_play_sound_file_not_found( - self, vlc_service, sample_sound, + self, + vlc_service, + sample_sound, ) -> None: """Test sound playback when file doesn't exist.""" # Mock the file path utility to return a non-existent path @@ -158,7 +163,10 @@ class TestVLCPlayerService: @pytest.mark.asyncio @patch("app.services.vlc_player.asyncio.create_subprocess_exec") async def test_play_sound_subprocess_error( - self, mock_subprocess, vlc_service, sample_sound, + self, + mock_subprocess, + vlc_service, + sample_sound, ) -> None: """Test sound playback when subprocess fails.""" # Mock the file path utility to return an existing path @@ -176,7 +184,9 @@ class TestVLCPlayerService: @pytest.mark.asyncio @patch("app.services.vlc_player.asyncio.create_subprocess_exec") - async def test_stop_all_vlc_instances_success(self, mock_subprocess, vlc_service) -> None: + async def test_stop_all_vlc_instances_success( + self, mock_subprocess, vlc_service, + ) -> None: """Test successful stopping of all VLC instances.""" # Mock pgrep process (find VLC processes) mock_find_process = Mock() @@ -212,7 +222,9 @@ class TestVLCPlayerService: @pytest.mark.asyncio @patch("app.services.vlc_player.asyncio.create_subprocess_exec") async def test_stop_all_vlc_instances_no_processes( - self, mock_subprocess, vlc_service, + self, + mock_subprocess, + vlc_service, ) -> None: """Test stopping VLC instances when none are running.""" # Mock pgrep process (no VLC processes found) @@ -232,7 +244,9 @@ class TestVLCPlayerService: @pytest.mark.asyncio @patch("app.services.vlc_player.asyncio.create_subprocess_exec") async def test_stop_all_vlc_instances_partial_kill( - self, mock_subprocess, vlc_service, + self, + mock_subprocess, + vlc_service, ) -> None: """Test stopping VLC instances when some processes remain.""" # Mock pgrep process (find VLC processes) @@ -266,7 +280,9 @@ class TestVLCPlayerService: @pytest.mark.asyncio @patch("app.services.vlc_player.asyncio.create_subprocess_exec") - async def test_stop_all_vlc_instances_error(self, mock_subprocess, vlc_service) -> None: + async def test_stop_all_vlc_instances_error( + self, mock_subprocess, vlc_service, + ) -> None: """Test stopping VLC instances when an error occurs.""" # Mock subprocess exception mock_subprocess.side_effect = Exception("Command failed") @@ -287,6 +303,7 @@ class TestVLCPlayerService: # Clear the global instance import app.services.vlc_player + app.services.vlc_player.vlc_player_service = None # First call should create new instance @@ -304,7 +321,10 @@ class TestVLCPlayerService: @pytest.mark.asyncio @patch("app.services.vlc_player.asyncio.create_subprocess_exec") async def test_play_sound_with_play_count_tracking( - self, mock_subprocess, vlc_service_with_db, sample_sound, + self, + mock_subprocess, + vlc_service_with_db, + sample_sound, ) -> None: """Test sound playback with play count tracking.""" # Mock subprocess @@ -320,11 +340,17 @@ class TestVLCPlayerService: mock_sound_repo = AsyncMock() mock_user_repo = AsyncMock() - with patch("app.services.vlc_player.SoundRepository", return_value=mock_sound_repo): - with patch("app.services.vlc_player.UserRepository", return_value=mock_user_repo): + with patch( + "app.services.vlc_player.SoundRepository", return_value=mock_sound_repo, + ): + with patch( + "app.services.vlc_player.UserRepository", return_value=mock_user_repo, + ): with patch("app.services.vlc_player.socket_manager") as mock_socket: # Mock the file path utility - with patch("app.services.vlc_player.get_sound_file_path") as mock_get_path: + with patch( + "app.services.vlc_player.get_sound_file_path", + ) as mock_get_path: mock_path = Mock() mock_path.exists.return_value = True mock_get_path.return_value = mock_path @@ -397,8 +423,12 @@ class TestVLCPlayerService: role="admin", ) - with patch("app.services.vlc_player.SoundRepository", return_value=mock_sound_repo): - with patch("app.services.vlc_player.UserRepository", return_value=mock_user_repo): + with patch( + "app.services.vlc_player.SoundRepository", return_value=mock_sound_repo, + ): + with patch( + "app.services.vlc_player.UserRepository", return_value=mock_user_repo, + ): with patch("app.services.vlc_player.socket_manager") as mock_socket: # Setup mocks mock_sound_repo.get_by_id.return_value = test_sound @@ -412,7 +442,8 @@ class TestVLCPlayerService: # Verify sound repository calls mock_sound_repo.get_by_id.assert_called_once_with(1) mock_sound_repo.update.assert_called_once_with( - test_sound, {"play_count": 1}, + test_sound, + {"play_count": 1}, ) # Verify user repository calls @@ -442,7 +473,9 @@ class TestVLCPlayerService: # The method should return early without doing anything @pytest.mark.asyncio - async def test_record_play_count_always_creates_record(self, vlc_service_with_db) -> None: + async def test_record_play_count_always_creates_record( + self, vlc_service_with_db, + ) -> None: """Test play count recording always creates a new SoundPlayed record.""" # Mock session and repositories mock_session = AsyncMock() @@ -469,28 +502,33 @@ class TestVLCPlayerService: role="admin", ) - with patch("app.services.vlc_player.SoundRepository", return_value=mock_sound_repo): - with patch("app.services.vlc_player.UserRepository", return_value=mock_user_repo): + with patch( + "app.services.vlc_player.SoundRepository", return_value=mock_sound_repo, + ): + with patch( + "app.services.vlc_player.UserRepository", return_value=mock_user_repo, + ): with patch("app.services.vlc_player.socket_manager") as mock_socket: - # Setup mocks - mock_sound_repo.get_by_id.return_value = test_sound - mock_user_repo.get_by_id.return_value = admin_user + # Setup mocks + mock_sound_repo.get_by_id.return_value = test_sound + mock_user_repo.get_by_id.return_value = admin_user - # Mock socket broadcast - mock_socket.broadcast_to_all = AsyncMock() + # Mock socket broadcast + mock_socket.broadcast_to_all = AsyncMock() - await vlc_service_with_db._record_play_count(1, "Test Sound") + await vlc_service_with_db._record_play_count(1, "Test Sound") - # Verify sound play count was updated - mock_sound_repo.update.assert_called_once_with( - test_sound, {"play_count": 6}, - ) + # Verify sound play count was updated + mock_sound_repo.update.assert_called_once_with( + test_sound, + {"play_count": 6}, + ) - # Verify new SoundPlayed record was always added - mock_session.add.assert_called_once() + # Verify new SoundPlayed record was always added + mock_session.add.assert_called_once() - # Verify commit happened - mock_session.commit.assert_called_once() + # Verify commit happened + mock_session.commit.assert_called_once() def test_uses_shared_sound_path_utility(self, vlc_service, sample_sound) -> None: """Test that VLC service uses the shared sound path utility.""" diff --git a/tests/utils/test_audio.py b/tests/utils/test_audio.py index f31abac..6211081 100644 --- a/tests/utils/test_audio.py +++ b/tests/utils/test_audio.py @@ -19,8 +19,8 @@ from app.utils.audio import ( SHA256_HASH_LENGTH = 64 BINARY_FILE_SIZE = 700 EXPECTED_DURATION_MS_1 = 123456 # 123.456 seconds * 1000 -EXPECTED_DURATION_MS_2 = 60000 # 60 seconds * 1000 -EXPECTED_DURATION_MS_3 = 45123 # 45.123 seconds * 1000 +EXPECTED_DURATION_MS_2 = 60000 # 60 seconds * 1000 +EXPECTED_DURATION_MS_3 = 45123 # 45.123 seconds * 1000 class TestAudioUtils: @@ -220,7 +220,8 @@ class TestAudioUtils: @patch("app.utils.audio.ffmpeg.probe") def test_get_audio_duration_fractional_duration( - self, mock_probe: MagicMock, + self, + mock_probe: MagicMock, ) -> None: """Test audio duration extraction with fractional seconds.""" # Mock ffmpeg.probe to return fractional duration diff --git a/tests/utils/test_credit_decorators.py b/tests/utils/test_credit_decorators.py index 5adb957..46a2132 100644 --- a/tests/utils/test_credit_decorators.py +++ b/tests/utils/test_credit_decorators.py @@ -27,13 +27,17 @@ class TestRequiresCreditsDecorator: return service @pytest.fixture - def credit_service_factory(self, mock_credit_service: AsyncMock) -> Callable[[], AsyncMock]: + def credit_service_factory( + self, mock_credit_service: AsyncMock, + ) -> Callable[[], AsyncMock]: """Create a credit service factory.""" return lambda: mock_credit_service @pytest.mark.asyncio async def test_decorator_success( - self, credit_service_factory: Callable[[], AsyncMock], mock_credit_service: AsyncMock, + self, + credit_service_factory: Callable[[], AsyncMock], + mock_credit_service: AsyncMock, ) -> None: """Test decorator with successful action.""" @@ -49,15 +53,21 @@ class TestRequiresCreditsDecorator: assert result == "Success: test" mock_credit_service.validate_and_reserve_credits.assert_called_once_with( - 123, CreditActionType.VLC_PLAY_SOUND, + 123, + CreditActionType.VLC_PLAY_SOUND, ) mock_credit_service.deduct_credits.assert_called_once_with( - 123, CreditActionType.VLC_PLAY_SOUND, success=True, metadata=None, + 123, + CreditActionType.VLC_PLAY_SOUND, + success=True, + metadata=None, ) @pytest.mark.asyncio async def test_decorator_with_metadata( - self, credit_service_factory: Callable[[], AsyncMock], mock_credit_service: AsyncMock, + self, + credit_service_factory: Callable[[], AsyncMock], + mock_credit_service: AsyncMock, ) -> None: """Test decorator with metadata extraction.""" @@ -76,14 +86,20 @@ class TestRequiresCreditsDecorator: await test_action(user_id=123, sound_name="test.mp3") mock_credit_service.validate_and_reserve_credits.assert_called_once_with( - 123, CreditActionType.VLC_PLAY_SOUND, + 123, + CreditActionType.VLC_PLAY_SOUND, ) mock_credit_service.deduct_credits.assert_called_once_with( - 123, CreditActionType.VLC_PLAY_SOUND, success=True, metadata={"sound_name": "test.mp3"}, + 123, + CreditActionType.VLC_PLAY_SOUND, + success=True, + metadata={"sound_name": "test.mp3"}, ) @pytest.mark.asyncio - async def test_decorator_failed_action(self, credit_service_factory, mock_credit_service) -> None: + async def test_decorator_failed_action( + self, credit_service_factory, mock_credit_service, + ) -> None: """Test decorator with failed action.""" @requires_credits( @@ -98,11 +114,16 @@ class TestRequiresCreditsDecorator: assert result is False mock_credit_service.deduct_credits.assert_called_once_with( - 123, CreditActionType.VLC_PLAY_SOUND, success=False, metadata=None, + 123, + CreditActionType.VLC_PLAY_SOUND, + success=False, + metadata=None, ) @pytest.mark.asyncio - async def test_decorator_exception_in_action(self, credit_service_factory, mock_credit_service) -> None: + async def test_decorator_exception_in_action( + self, credit_service_factory, mock_credit_service, + ) -> None: """Test decorator when action raises exception.""" @requires_credits( @@ -118,13 +139,20 @@ class TestRequiresCreditsDecorator: await test_action(user_id=123) mock_credit_service.deduct_credits.assert_called_once_with( - 123, CreditActionType.VLC_PLAY_SOUND, success=False, metadata=None, + 123, + CreditActionType.VLC_PLAY_SOUND, + success=False, + metadata=None, ) @pytest.mark.asyncio - async def test_decorator_insufficient_credits(self, credit_service_factory, mock_credit_service) -> None: + async def test_decorator_insufficient_credits( + self, credit_service_factory, mock_credit_service, + ) -> None: """Test decorator with insufficient credits.""" - mock_credit_service.validate_and_reserve_credits.side_effect = InsufficientCreditsError(1, 0) + mock_credit_service.validate_and_reserve_credits.side_effect = ( + InsufficientCreditsError(1, 0) + ) @requires_credits( CreditActionType.VLC_PLAY_SOUND, @@ -141,7 +169,9 @@ class TestRequiresCreditsDecorator: mock_credit_service.deduct_credits.assert_not_called() @pytest.mark.asyncio - async def test_decorator_user_id_in_args(self, credit_service_factory, mock_credit_service) -> None: + async def test_decorator_user_id_in_args( + self, credit_service_factory, mock_credit_service, + ) -> None: """Test decorator extracting user_id from positional args.""" @requires_credits( @@ -156,7 +186,8 @@ class TestRequiresCreditsDecorator: assert result == "test" mock_credit_service.validate_and_reserve_credits.assert_called_once_with( - 123, CreditActionType.VLC_PLAY_SOUND, + 123, + CreditActionType.VLC_PLAY_SOUND, ) @pytest.mark.asyncio @@ -186,12 +217,16 @@ class TestValidateCreditsOnlyDecorator: return service @pytest.fixture - def credit_service_factory(self, mock_credit_service: AsyncMock) -> Callable[[], AsyncMock]: + def credit_service_factory( + self, mock_credit_service: AsyncMock, + ) -> Callable[[], AsyncMock]: """Create a credit service factory.""" return lambda: mock_credit_service @pytest.mark.asyncio - async def test_validate_only_decorator(self, credit_service_factory, mock_credit_service) -> None: + async def test_validate_only_decorator( + self, credit_service_factory, mock_credit_service, + ) -> None: """Test validate_credits_only decorator.""" @validate_credits_only( @@ -206,7 +241,8 @@ class TestValidateCreditsOnlyDecorator: assert result == "Validated: test" mock_credit_service.validate_and_reserve_credits.assert_called_once_with( - 123, CreditActionType.VLC_PLAY_SOUND, + 123, + CreditActionType.VLC_PLAY_SOUND, ) # Should not deduct credits, only validate mock_credit_service.deduct_credits.assert_not_called() @@ -235,10 +271,14 @@ class TestCreditManager: manager.mark_success() mock_credit_service.validate_and_reserve_credits.assert_called_once_with( - 123, CreditActionType.VLC_PLAY_SOUND, + 123, + CreditActionType.VLC_PLAY_SOUND, ) mock_credit_service.deduct_credits.assert_called_once_with( - 123, CreditActionType.VLC_PLAY_SOUND, success=True, metadata={"test": "data"}, + 123, + CreditActionType.VLC_PLAY_SOUND, + success=True, + metadata={"test": "data"}, ) @pytest.mark.asyncio @@ -253,7 +293,10 @@ class TestCreditManager: pass mock_credit_service.deduct_credits.assert_called_once_with( - 123, CreditActionType.VLC_PLAY_SOUND, success=False, metadata=None, + 123, + CreditActionType.VLC_PLAY_SOUND, + success=False, + metadata=None, ) @pytest.mark.asyncio @@ -269,13 +312,18 @@ class TestCreditManager: raise ValueError(msg) mock_credit_service.deduct_credits.assert_called_once_with( - 123, CreditActionType.VLC_PLAY_SOUND, success=False, metadata=None, + 123, + CreditActionType.VLC_PLAY_SOUND, + success=False, + metadata=None, ) @pytest.mark.asyncio async def test_credit_manager_validation_failure(self, mock_credit_service) -> None: """Test CreditManager when validation fails.""" - mock_credit_service.validate_and_reserve_credits.side_effect = InsufficientCreditsError(1, 0) + mock_credit_service.validate_and_reserve_credits.side_effect = ( + InsufficientCreditsError(1, 0) + ) with pytest.raises(InsufficientCreditsError): async with CreditManager(