Add tests for sound repository, user OAuth repository, credit service, and credit decorators
- Implement comprehensive tests for SoundRepository covering CRUD operations and search functionalities. - Create tests for UserOauthRepository to validate OAuth record management. - Develop tests for CreditService to ensure proper credit management, including validation, deduction, and addition of credits. - Add tests for credit-related decorators to verify correct behavior in credit management scenarios.
This commit is contained in:
358
tests/services/test_credit.py
Normal file
358
tests/services/test_credit.py
Normal file
@@ -0,0 +1,358 @@
|
||||
"""Tests for credit service."""
|
||||
|
||||
import json
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
|
||||
from app.models.credit_action import CreditActionType
|
||||
from app.models.credit_transaction import CreditTransaction
|
||||
from app.models.user import User
|
||||
from app.services.credit import CreditService, InsufficientCreditsError
|
||||
|
||||
|
||||
class TestCreditService:
|
||||
"""Test credit service functionality."""
|
||||
|
||||
@pytest.fixture
|
||||
def mock_db_session_factory(self):
|
||||
"""Create a mock database session factory."""
|
||||
session = AsyncMock(spec=AsyncSession)
|
||||
return lambda: session
|
||||
|
||||
@pytest.fixture
|
||||
def credit_service(self, mock_db_session_factory):
|
||||
"""Create a credit service instance for testing."""
|
||||
return CreditService(mock_db_session_factory)
|
||||
|
||||
@pytest.fixture
|
||||
def sample_user(self):
|
||||
"""Create a sample user for testing."""
|
||||
return User(
|
||||
id=1,
|
||||
name="Test User",
|
||||
email="test@example.com",
|
||||
role="user",
|
||||
credits=10,
|
||||
plan_id=1,
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_check_credits_sufficient(self, credit_service, sample_user):
|
||||
"""Test checking credits when user has sufficient credits."""
|
||||
mock_session = credit_service.db_session_factory()
|
||||
|
||||
with patch("app.services.credit.UserRepository") as mock_repo_class:
|
||||
mock_repo = AsyncMock()
|
||||
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)
|
||||
|
||||
assert result is True
|
||||
mock_repo.get_by_id.assert_called_once_with(1)
|
||||
mock_session.close.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_check_credits_insufficient(self, credit_service):
|
||||
"""Test checking credits when user has insufficient credits."""
|
||||
mock_session = credit_service.db_session_factory()
|
||||
poor_user = User(
|
||||
id=1,
|
||||
name="Poor User",
|
||||
email="poor@example.com",
|
||||
role="user",
|
||||
credits=0, # No credits
|
||||
plan_id=1,
|
||||
)
|
||||
|
||||
with patch("app.services.credit.UserRepository") as mock_repo_class:
|
||||
mock_repo = AsyncMock()
|
||||
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)
|
||||
|
||||
assert result is False
|
||||
mock_session.close.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_check_credits_user_not_found(self, credit_service):
|
||||
"""Test checking credits when user is not found."""
|
||||
mock_session = credit_service.db_session_factory()
|
||||
|
||||
with patch("app.services.credit.UserRepository") as mock_repo_class:
|
||||
mock_repo = AsyncMock()
|
||||
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)
|
||||
|
||||
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):
|
||||
"""Test successful credit validation and reservation."""
|
||||
mock_session = credit_service.db_session_factory()
|
||||
|
||||
with patch("app.services.credit.UserRepository") as mock_repo_class:
|
||||
mock_repo = AsyncMock()
|
||||
mock_repo_class.return_value = mock_repo
|
||||
mock_repo.get_by_id.return_value = sample_user
|
||||
|
||||
user, action = await credit_service.validate_and_reserve_credits(
|
||||
1, CreditActionType.VLC_PLAY_SOUND
|
||||
)
|
||||
|
||||
assert user == sample_user
|
||||
assert action.action_type == CreditActionType.VLC_PLAY_SOUND
|
||||
assert action.cost == 1
|
||||
mock_session.close.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_validate_and_reserve_credits_insufficient(self, credit_service):
|
||||
"""Test credit validation with insufficient credits."""
|
||||
mock_session = credit_service.db_session_factory()
|
||||
poor_user = User(
|
||||
id=1,
|
||||
name="Poor User",
|
||||
email="poor@example.com",
|
||||
role="user",
|
||||
credits=0,
|
||||
plan_id=1,
|
||||
)
|
||||
|
||||
with patch("app.services.credit.UserRepository") as mock_repo_class:
|
||||
mock_repo = AsyncMock()
|
||||
mock_repo_class.return_value = mock_repo
|
||||
mock_repo.get_by_id.return_value = poor_user
|
||||
|
||||
with pytest.raises(InsufficientCreditsError) as exc_info:
|
||||
await credit_service.validate_and_reserve_credits(
|
||||
1, CreditActionType.VLC_PLAY_SOUND
|
||||
)
|
||||
|
||||
assert exc_info.value.required == 1
|
||||
assert exc_info.value.available == 0
|
||||
mock_session.close.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_validate_and_reserve_credits_user_not_found(self, credit_service):
|
||||
"""Test credit validation when user is not found."""
|
||||
mock_session = credit_service.db_session_factory()
|
||||
|
||||
with patch("app.services.credit.UserRepository") as mock_repo_class:
|
||||
mock_repo = AsyncMock()
|
||||
mock_repo_class.return_value = mock_repo
|
||||
mock_repo.get_by_id.return_value = None
|
||||
|
||||
with pytest.raises(ValueError, match="User 999 not found"):
|
||||
await credit_service.validate_and_reserve_credits(
|
||||
999, CreditActionType.VLC_PLAY_SOUND
|
||||
)
|
||||
|
||||
mock_session.close.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_deduct_credits_success(self, credit_service, sample_user):
|
||||
"""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:
|
||||
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()
|
||||
|
||||
transaction = await credit_service.deduct_credits(
|
||||
1, CreditActionType.VLC_PLAY_SOUND, True, {"test": "data"}
|
||||
)
|
||||
|
||||
# Verify user credits were updated
|
||||
mock_repo.update.assert_called_once_with(sample_user, {"credits": 9})
|
||||
|
||||
# Verify transaction was created
|
||||
mock_session.add.assert_called_once()
|
||||
mock_session.commit.assert_called_once()
|
||||
|
||||
# Verify socket event was emitted
|
||||
mock_socket_manager.send_to_user.assert_called_once_with(
|
||||
"1", "user_credits_changed", {
|
||||
"user_id": "1",
|
||||
"credits_before": 10,
|
||||
"credits_after": 9,
|
||||
"credits_deducted": 1,
|
||||
"action_type": "vlc_play_sound",
|
||||
"success": True,
|
||||
}
|
||||
)
|
||||
|
||||
# Check transaction details
|
||||
added_transaction = mock_session.add.call_args[0][0]
|
||||
assert isinstance(added_transaction, CreditTransaction)
|
||||
assert added_transaction.user_id == 1
|
||||
assert added_transaction.action_type == "vlc_play_sound"
|
||||
assert added_transaction.amount == -1
|
||||
assert added_transaction.balance_before == 10
|
||||
assert added_transaction.balance_after == 9
|
||||
assert added_transaction.success is True
|
||||
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):
|
||||
"""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:
|
||||
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()
|
||||
|
||||
transaction = await credit_service.deduct_credits(
|
||||
1, CreditActionType.VLC_PLAY_SOUND, False # Action failed
|
||||
)
|
||||
|
||||
# Verify user credits were NOT updated (action requires success)
|
||||
mock_repo.update.assert_not_called()
|
||||
|
||||
# Verify transaction was still created for auditing
|
||||
mock_session.add.assert_called_once()
|
||||
mock_session.commit.assert_called_once()
|
||||
|
||||
# Verify no socket event was emitted since no credits were actually deducted
|
||||
mock_socket_manager.send_to_user.assert_not_called()
|
||||
|
||||
# Check transaction details
|
||||
added_transaction = mock_session.add.call_args[0][0]
|
||||
assert added_transaction.amount == 0 # No deduction for failed action
|
||||
assert added_transaction.balance_before == 10
|
||||
assert added_transaction.balance_after == 10 # No change
|
||||
assert added_transaction.success is False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_deduct_credits_insufficient(self, credit_service):
|
||||
"""Test credit deduction with insufficient credits."""
|
||||
mock_session = credit_service.db_session_factory()
|
||||
poor_user = User(
|
||||
id=1,
|
||||
name="Poor User",
|
||||
email="poor@example.com",
|
||||
role="user",
|
||||
credits=0,
|
||||
plan_id=1,
|
||||
)
|
||||
|
||||
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
|
||||
mock_socket_manager.send_to_user = AsyncMock()
|
||||
|
||||
with pytest.raises(InsufficientCreditsError):
|
||||
await credit_service.deduct_credits(
|
||||
1, CreditActionType.VLC_PLAY_SOUND, True
|
||||
)
|
||||
|
||||
# Verify no socket event was emitted since credits could not be deducted
|
||||
mock_socket_manager.send_to_user.assert_not_called()
|
||||
|
||||
mock_session.rollback.assert_called_once()
|
||||
mock_session.close.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_credits(self, credit_service, sample_user):
|
||||
"""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:
|
||||
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()
|
||||
|
||||
transaction = await credit_service.add_credits(
|
||||
1, 5, "Bonus credits", {"reason": "signup"}
|
||||
)
|
||||
|
||||
# Verify user credits were updated
|
||||
mock_repo.update.assert_called_once_with(sample_user, {"credits": 15})
|
||||
|
||||
# Verify transaction was created
|
||||
mock_session.add.assert_called_once()
|
||||
mock_session.commit.assert_called_once()
|
||||
|
||||
# Verify socket event was emitted
|
||||
mock_socket_manager.send_to_user.assert_called_once_with(
|
||||
"1", "user_credits_changed", {
|
||||
"user_id": "1",
|
||||
"credits_before": 10,
|
||||
"credits_after": 15,
|
||||
"credits_added": 5,
|
||||
"description": "Bonus credits",
|
||||
"success": True,
|
||||
}
|
||||
)
|
||||
|
||||
# Check transaction details
|
||||
added_transaction = mock_session.add.call_args[0][0]
|
||||
assert added_transaction.amount == 5
|
||||
assert added_transaction.balance_before == 10
|
||||
assert added_transaction.balance_after == 15
|
||||
assert added_transaction.description == "Bonus credits"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_credits_invalid_amount(self, credit_service):
|
||||
"""Test adding invalid amount of credits."""
|
||||
with pytest.raises(ValueError, match="Amount must be positive"):
|
||||
await credit_service.add_credits(1, 0, "Invalid")
|
||||
|
||||
with pytest.raises(ValueError, match="Amount must be positive"):
|
||||
await credit_service.add_credits(1, -5, "Invalid")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_user_balance(self, credit_service, sample_user):
|
||||
"""Test getting user credit balance."""
|
||||
mock_session = credit_service.db_session_factory()
|
||||
|
||||
with patch("app.services.credit.UserRepository") as mock_repo_class:
|
||||
mock_repo = AsyncMock()
|
||||
mock_repo_class.return_value = mock_repo
|
||||
mock_repo.get_by_id.return_value = sample_user
|
||||
|
||||
balance = await credit_service.get_user_balance(1)
|
||||
|
||||
assert balance == 10
|
||||
mock_session.close.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_user_balance_user_not_found(self, credit_service):
|
||||
"""Test getting balance for non-existent user."""
|
||||
mock_session = credit_service.db_session_factory()
|
||||
|
||||
with patch("app.services.credit.UserRepository") as mock_repo_class:
|
||||
mock_repo = AsyncMock()
|
||||
mock_repo_class.return_value = mock_repo
|
||||
mock_repo.get_by_id.return_value = None
|
||||
|
||||
with pytest.raises(ValueError, match="User 999 not found"):
|
||||
await credit_service.get_user_balance(999)
|
||||
|
||||
mock_session.close.assert_called_once()
|
||||
|
||||
|
||||
class TestInsufficientCreditsError:
|
||||
"""Test InsufficientCreditsError exception."""
|
||||
|
||||
def test_insufficient_credits_error_creation(self):
|
||||
"""Test creating InsufficientCreditsError."""
|
||||
error = InsufficientCreditsError(5, 2)
|
||||
assert error.required == 5
|
||||
assert error.available == 2
|
||||
assert str(error) == "Insufficient credits: 5 required, 2 available"
|
||||
Reference in New Issue
Block a user