Update all files
This commit is contained in:
@@ -61,3 +61,10 @@ ignore_missing_imports = true
|
||||
source = ["src/rehearsalhub"]
|
||||
omit = ["src/rehearsalhub/db/models.py"]
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"httpx>=0.28.1",
|
||||
"pytest>=9.0.2",
|
||||
"pytest-asyncio>=1.3.0",
|
||||
]
|
||||
|
||||
|
||||
171
api/tests/integration/test_versions_streaming.py
Normal file
171
api/tests/integration/test_versions_streaming.py
Normal file
@@ -0,0 +1,171 @@
|
||||
"""Integration tests for version streaming endpoints."""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import AsyncMock, patch, MagicMock
|
||||
import httpx
|
||||
|
||||
from rehearsalhub.routers.versions import stream_version, get_waveform
|
||||
from rehearsalhub.db.models import Member, AudioVersion
|
||||
from rehearsalhub.schemas.audio_version import AudioVersionRead
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_stream_version_connection_error():
|
||||
"""Test stream_version endpoint handles connection errors gracefully."""
|
||||
# Mock dependencies
|
||||
mock_session = MagicMock()
|
||||
mock_member = Member(id=1, name="Test User")
|
||||
|
||||
# Mock version with nc_file_path
|
||||
mock_version = AudioVersion(
|
||||
id="test-version-id",
|
||||
nc_file_path="test/path/file.mp3",
|
||||
waveform_url="test/path/waveform.json"
|
||||
)
|
||||
|
||||
# Mock the storage client to raise connection error
|
||||
with patch("rehearsalhub.routers.versions.NextcloudClient") as mock_client_class:
|
||||
mock_client = MagicMock()
|
||||
mock_client.download = AsyncMock(side_effect=httpx.ConnectError("Connection failed"))
|
||||
mock_client_class.return_value = mock_client
|
||||
|
||||
# Mock the membership check
|
||||
with patch("rehearsalhub.routers.versions._get_version_and_assert_band_membership",
|
||||
return_value=(mock_version, None)):
|
||||
|
||||
from fastapi import HTTPException
|
||||
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
await stream_version(
|
||||
version_id="test-version-id",
|
||||
session=mock_session,
|
||||
current_member=mock_member
|
||||
)
|
||||
|
||||
# Should return 503 Service Unavailable
|
||||
assert exc_info.value.status_code == 503
|
||||
assert "Failed to connect to storage" in str(exc_info.value.detail)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_stream_version_file_not_found():
|
||||
"""Test stream_version endpoint handles 404 errors gracefully."""
|
||||
# Mock dependencies
|
||||
mock_session = MagicMock()
|
||||
mock_member = Member(id=1, name="Test User")
|
||||
|
||||
# Mock version with nc_file_path
|
||||
mock_version = AudioVersion(
|
||||
id="test-version-id",
|
||||
nc_file_path="test/path/file.mp3",
|
||||
waveform_url="test/path/waveform.json"
|
||||
)
|
||||
|
||||
# Mock the storage client to raise 404 error
|
||||
with patch("rehearsalhub.routers.versions.NextcloudClient") as mock_client_class:
|
||||
mock_client = MagicMock()
|
||||
|
||||
# Create mock response with 404 status
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 404
|
||||
mock_response.text = "Not Found"
|
||||
|
||||
mock_client.download = AsyncMock(
|
||||
side_effect=httpx.HTTPStatusError("Not found", request=MagicMock(), response=mock_response)
|
||||
)
|
||||
mock_client_class.return_value = mock_client
|
||||
|
||||
# Mock the membership check
|
||||
with patch("rehearsalhub.routers.versions._get_version_and_assert_band_membership",
|
||||
return_value=(mock_version, None)):
|
||||
|
||||
from fastapi import HTTPException
|
||||
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
await stream_version(
|
||||
version_id="test-version-id",
|
||||
session=mock_session,
|
||||
current_member=mock_member
|
||||
)
|
||||
|
||||
# Should return 404 Not Found
|
||||
assert exc_info.value.status_code == 404
|
||||
assert "File not found in storage" in str(exc_info.value.detail)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_get_waveform_connection_error():
|
||||
"""Test get_waveform endpoint handles connection errors gracefully."""
|
||||
# Mock dependencies
|
||||
mock_session = MagicMock()
|
||||
mock_member = Member(id=1, name="Test User")
|
||||
|
||||
# Mock version with waveform_url
|
||||
mock_version = AudioVersion(
|
||||
id="test-version-id",
|
||||
nc_file_path="test/path/file.mp3",
|
||||
waveform_url="test/path/waveform.json"
|
||||
)
|
||||
|
||||
# Mock the storage client to raise connection error
|
||||
with patch("rehearsalhub.routers.versions.NextcloudClient") as mock_client_class:
|
||||
mock_client = MagicMock()
|
||||
mock_client.download = AsyncMock(side_effect=httpx.ConnectError("Connection failed"))
|
||||
mock_client_class.return_value = mock_client
|
||||
|
||||
# Mock the membership check
|
||||
with patch("rehearsalhub.routers.versions._get_version_and_assert_band_membership",
|
||||
return_value=(mock_version, None)):
|
||||
|
||||
from fastapi import HTTPException
|
||||
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
await get_waveform(
|
||||
version_id="test-version-id",
|
||||
session=mock_session,
|
||||
current_member=mock_member
|
||||
)
|
||||
|
||||
# Should return 503 Service Unavailable
|
||||
assert exc_info.value.status_code == 503
|
||||
assert "Failed to connect to storage" in str(exc_info.value.detail)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_stream_version_success():
|
||||
"""Test successful streaming when connection works."""
|
||||
# Mock dependencies
|
||||
mock_session = MagicMock()
|
||||
mock_member = Member(id=1, name="Test User")
|
||||
|
||||
# Mock version with nc_file_path
|
||||
mock_version = AudioVersion(
|
||||
id="test-version-id",
|
||||
nc_file_path="test/path/file.mp3",
|
||||
waveform_url="test/path/waveform.json"
|
||||
)
|
||||
|
||||
# Mock the storage client to return success
|
||||
with patch("rehearsalhub.routers.versions.NextcloudClient") as mock_client_class:
|
||||
mock_client = MagicMock()
|
||||
mock_client.download = AsyncMock(return_value=b"audio_data")
|
||||
mock_client_class.return_value = mock_client
|
||||
|
||||
# Mock the membership check
|
||||
with patch("rehearsalhub.routers.versions._get_version_and_assert_band_membership",
|
||||
return_value=(mock_version, None)):
|
||||
|
||||
result = await stream_version(
|
||||
version_id="test-version-id",
|
||||
session=mock_session,
|
||||
current_member=mock_member
|
||||
)
|
||||
|
||||
# Should return Response with audio data
|
||||
assert result.status_code == 200
|
||||
assert result.body == b"audio_data"
|
||||
assert result.media_type == "audio/mpeg"
|
||||
85
api/tests/unit/test_versions_error_handling.py
Normal file
85
api/tests/unit/test_versions_error_handling.py
Normal file
@@ -0,0 +1,85 @@
|
||||
"""Test error handling in versions router."""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import AsyncMock, patch, MagicMock
|
||||
import httpx
|
||||
|
||||
from rehearsalhub.routers.versions import _download_with_retry
|
||||
from rehearsalhub.storage.nextcloud import NextcloudClient
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_download_with_retry_success():
|
||||
"""Test successful download on first attempt."""
|
||||
mock_storage = MagicMock()
|
||||
mock_storage.download = AsyncMock(return_value=b"test_data")
|
||||
|
||||
result = await _download_with_retry(mock_storage, "test_path")
|
||||
assert result == b"test_data"
|
||||
mock_storage.download.assert_awaited_once_with("test_path")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_download_with_retry_connection_error():
|
||||
"""Test retry logic on connection errors."""
|
||||
mock_storage = MagicMock()
|
||||
mock_storage.download = AsyncMock(side_effect=httpx.ConnectError("Connection failed"))
|
||||
|
||||
with pytest.raises(httpx.ConnectError):
|
||||
await _download_with_retry(mock_storage, "test_path", max_retries=2)
|
||||
|
||||
# Should retry max_retries times
|
||||
assert mock_storage.download.await_count == 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_download_with_retry_server_error():
|
||||
"""Test retry logic on 500 server errors."""
|
||||
mock_storage = MagicMock()
|
||||
|
||||
# Create a mock response with 500 status
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 500
|
||||
mock_response.text = "Internal Server Error"
|
||||
|
||||
mock_storage.download = AsyncMock(side_effect=httpx.HTTPStatusError("Server error", request=MagicMock(), response=mock_response))
|
||||
|
||||
with pytest.raises(httpx.HTTPStatusError):
|
||||
await _download_with_retry(mock_storage, "test_path", max_retries=2)
|
||||
|
||||
# Should retry max_retries times for 5xx errors
|
||||
assert mock_storage.download.await_count == 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_download_with_retry_client_error():
|
||||
"""Test no retry on 4xx client errors."""
|
||||
mock_storage = MagicMock()
|
||||
|
||||
# Create a mock response with 404 status
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 404
|
||||
mock_response.text = "Not Found"
|
||||
|
||||
mock_storage.download = AsyncMock(side_effect=httpx.HTTPStatusError("Not found", request=MagicMock(), response=mock_response))
|
||||
|
||||
with pytest.raises(httpx.HTTPStatusError):
|
||||
await _download_with_retry(mock_storage, "test_path")
|
||||
|
||||
# Should not retry on 4xx errors
|
||||
assert mock_storage.download.await_count == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_download_with_retry_fallback_error():
|
||||
"""Test fallback HTTPException when no specific error is caught."""
|
||||
mock_storage = MagicMock()
|
||||
mock_storage.download = AsyncMock(side_effect=Exception("Unknown error"))
|
||||
|
||||
from fastapi import HTTPException
|
||||
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
await _download_with_retry(mock_storage, "test_path")
|
||||
|
||||
# Should raise the original exception for unknown errors
|
||||
assert str(exc_info.value) == "Unknown error"
|
||||
1724
api/uv.lock
generated
Normal file
1724
api/uv.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user