Files
sdb2-backend/tests/api/v1/test_sound_endpoints.py
JSC cba1653565
Some checks failed
Backend CI / lint (push) Failing after 5m0s
Backend CI / test (push) Successful in 3m46s
feat: Update player state tests to include previous volume and adjust volume assertions
2025-08-12 22:58:47 +02:00

413 lines
14 KiB
Python

"""Tests for sound API endpoints (non-admin endpoints only)."""
from typing import TYPE_CHECKING
from unittest.mock import patch
import pytest
from httpx import AsyncClient
from app.models.user import User
if TYPE_CHECKING:
from app.services.extraction import ExtractionInfo
class TestSoundEndpoints:
"""Test sound API endpoints (non-admin only)."""
@pytest.mark.asyncio
async def test_create_extraction_success(
self,
authenticated_client: AsyncClient,
authenticated_user: User,
) -> None:
"""Test successful extraction creation."""
mock_extraction_info: ExtractionInfo = {
"id": 1,
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
"title": None,
"service": None,
"service_id": None,
"status": "pending",
"error": None,
"sound_id": None,
"user_id": authenticated_user.id,
"created_at": "2025-08-03T12:00:00Z",
"updated_at": "2025-08-03T12:00:00Z",
}
with (
patch(
"app.services.extraction.ExtractionService.create_extraction",
) as mock_create,
patch(
"app.services.extraction_processor.extraction_processor.queue_extraction",
) as mock_queue,
):
mock_create.return_value = mock_extraction_info
mock_queue.return_value = None
response = await authenticated_client.post(
"/api/v1/extractions/",
params={"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ"},
)
assert response.status_code == 200
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"
)
@pytest.mark.asyncio
async def test_create_extraction_unauthenticated(self, client: AsyncClient) -> None:
"""Test extraction creation without authentication."""
response = await client.post(
"/api/v1/extractions/",
params={"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ"},
)
assert response.status_code == 401
data = response.json()
assert "Could not validate credentials" in data["detail"]
@pytest.mark.asyncio
async def test_create_extraction_invalid_url(
self,
authenticated_client: AsyncClient,
authenticated_user: User,
) -> None:
"""Test extraction creation with invalid URL."""
with patch(
"app.services.extraction.ExtractionService.create_extraction",
) as mock_create:
mock_create.side_effect = ValueError("Invalid URL")
response = await authenticated_client.post(
"/api/v1/extractions/",
params={"url": "invalid-url"},
)
assert response.status_code == 400
data = response.json()
assert "Invalid URL" in data["detail"]
@pytest.mark.asyncio
async def test_get_extraction_by_id_success(
self,
authenticated_client: AsyncClient,
authenticated_user: User,
) -> None:
"""Test getting extraction by ID."""
mock_extraction_info: ExtractionInfo = {
"id": 1,
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
"title": "Never Gonna Give You Up",
"service": "youtube",
"service_id": "dQw4w9WgXcQ",
"status": "completed",
"error": None,
"sound_id": 42,
"user_id": authenticated_user.id,
"created_at": "2025-08-03T12:00:00Z",
"updated_at": "2025-08-03T12:00:00Z",
}
with patch(
"app.services.extraction.ExtractionService.get_extraction_by_id",
) as mock_get:
mock_get.return_value = mock_extraction_info
response = await authenticated_client.get("/api/v1/extractions/1")
assert response.status_code == 200
data = response.json()
assert data["id"] == 1
assert data["title"] == "Never Gonna Give You Up"
assert data["status"] == "completed"
assert data["sound_id"] == 42
@pytest.mark.asyncio
async def test_get_extraction_by_id_not_found(
self,
authenticated_client: AsyncClient,
authenticated_user: User,
) -> None:
"""Test getting non-existent extraction."""
with patch(
"app.services.extraction.ExtractionService.get_extraction_by_id",
) as mock_get:
mock_get.return_value = None
response = await authenticated_client.get("/api/v1/extractions/999")
assert response.status_code == 404
data = response.json()
assert "Extraction 999 not found" in data["detail"]
@pytest.mark.asyncio
async def test_get_user_extractions_success(
self,
authenticated_client: AsyncClient,
authenticated_user: User,
) -> None:
"""Test getting user extractions."""
mock_extractions: list[ExtractionInfo] = [
{
"id": 1,
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
"title": "Never Gonna Give You Up",
"service": "youtube",
"service_id": "dQw4w9WgXcQ",
"status": "completed",
"error": None,
"sound_id": 42,
"user_id": authenticated_user.id,
"created_at": "2025-08-03T12:00:00Z",
"updated_at": "2025-08-03T12:00:00Z",
},
{
"id": 2,
"url": "https://soundcloud.com/example/track",
"title": "Example Track",
"service": "soundcloud",
"service_id": "example-track",
"status": "pending",
"error": None,
"sound_id": None,
"user_id": authenticated_user.id,
"created_at": "2025-08-03T12:00:00Z",
"updated_at": "2025-08-03T12:00:00Z",
},
]
with patch(
"app.services.extraction.ExtractionService.get_user_extractions",
) as mock_get:
mock_get.return_value = mock_extractions
response = await authenticated_client.get("/api/v1/extractions/")
assert response.status_code == 200
data = response.json()
assert len(data["extractions"]) == 2
assert data["extractions"][0]["title"] == "Never Gonna Give You Up"
assert data["extractions"][1]["status"] == "pending"
@pytest.mark.asyncio
async def test_play_sound_with_vlc_success(
self,
authenticated_client: AsyncClient,
authenticated_user: User,
) -> None:
"""Test playing sound with VLC successfully."""
# Mock the sound
mock_sound = type(
"Sound",
(),
{
"id": 1,
"name": "Test Sound",
"filename": "test.mp3",
"type": "SDB",
},
)()
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.vlc_player.VLCPlayerService.play_sound") as mock_play,
patch("app.services.credit.CreditService.deduct_credits") as mock_deduct,
):
mock_get_sound.return_value = mock_sound
mock_validate.return_value = None # No exception means validation passed
mock_play.return_value = True # Success
mock_deduct.return_value = None
response = await authenticated_client.post("/api/v1/sounds/play/1")
assert response.status_code == 200
data = response.json()
assert "Test Sound" in data["message"]
assert data["sound_id"] == 1
assert data["success"] is True
assert data["credits_deducted"] == 1
@pytest.mark.asyncio
async def test_play_sound_with_vlc_sound_not_found(
self,
authenticated_client: AsyncClient,
authenticated_user: User,
) -> None:
"""Test playing non-existent sound with VLC."""
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")
assert response.status_code == 404
data = response.json()
assert "Sound with ID 999 not found" in data["detail"]
@pytest.mark.asyncio
async def test_play_sound_with_vlc_insufficient_credits(
self,
authenticated_client: AsyncClient,
authenticated_user: User,
) -> None:
"""Test playing sound with VLC when user has insufficient credits."""
from app.services.credit import InsufficientCreditsError
# Mock the sound
mock_sound = type(
"Sound",
(),
{
"id": 1,
"name": "Test Sound",
"filename": "test.mp3",
"type": "SDB",
},
)()
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,
):
mock_get_sound.return_value = mock_sound
mock_validate.side_effect = InsufficientCreditsError(
required=1,
available=0,
)
response = await authenticated_client.post("/api/v1/sounds/play/1")
assert response.status_code == 402
data = response.json()
assert "Insufficient credits" in data["detail"]
assert "1 required, 0 available" in data["detail"]
@pytest.mark.asyncio
async def test_stop_all_vlc_instances_success(
self,
authenticated_client: AsyncClient,
authenticated_user: User,
) -> None:
"""Test stopping all VLC instances."""
mock_result = {
"message": "All VLC instances stopped",
"stopped_count": 3,
}
with patch(
"app.services.vlc_player.VLCPlayerService.stop_all_vlc_instances",
) as mock_stop:
mock_stop.return_value = mock_result
response = await authenticated_client.post("/api/v1/sounds/stop")
assert response.status_code == 200
data = response.json()
assert data["message"] == "All VLC instances stopped"
assert data["stopped_count"] == 3
@pytest.mark.asyncio
async def test_get_sounds_unauthenticated(self, client: AsyncClient) -> None:
"""Test getting sounds without authentication."""
response = await client.get("/api/v1/sounds/")
assert response.status_code == 401
data = response.json()
assert "Could not validate credentials" in data["detail"]
@pytest.mark.asyncio
async def test_get_sounds_authenticated(
self,
authenticated_client: AsyncClient,
authenticated_user: User,
) -> None:
"""Test getting sounds with authentication."""
from app.models.sound import Sound
with patch("app.repositories.sound.SoundRepository.search_and_sort") as mock_get:
# Create mock sounds with all required fields
mock_sound_1 = Sound(
id=1,
name="Test Sound 1",
type="SDB",
filename="test1.mp3",
duration=5000,
size=1024,
hash="test_hash_1",
play_count=0,
is_normalized=False,
is_music=False,
is_deletable=True,
)
mock_sound_2 = Sound(
id=2,
name="Test Sound 2",
type="EXT",
filename="test2.mp3",
duration=7000,
size=2048,
hash="test_hash_2",
play_count=5,
is_normalized=False,
is_music=False,
is_deletable=True,
)
mock_get.return_value = [mock_sound_1, mock_sound_2]
response = await authenticated_client.get("/api/v1/sounds/")
assert response.status_code == 200
data = response.json()
assert "sounds" in data
assert len(data["sounds"]) == 2
@pytest.mark.asyncio
async def test_get_sounds_with_type_filter_authenticated(
self,
authenticated_client: AsyncClient,
authenticated_user: User,
) -> None:
"""Test getting sounds with type filtering."""
from app.models.sound import Sound
with patch("app.repositories.sound.SoundRepository.search_and_sort") as mock_get:
# Create mock sound with all required fields
mock_sound = Sound(
id=1,
name="Test Sound 1",
type="SDB",
filename="test1.mp3",
duration=5000,
size=1024,
hash="test_hash_1",
play_count=0,
is_normalized=False,
is_music=False,
is_deletable=True,
)
mock_get.return_value = [mock_sound]
response = await authenticated_client.get("/api/v1/sounds/?types=SDB")
assert response.status_code == 200
data = response.json()
assert "sounds" in data
assert len(data["sounds"]) == 1
# Verify the repository was called with the correct types
mock_get.assert_called_once()
assert mock_get.call_args.kwargs["sound_types"] == ["SDB"]