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:
JSC
2025-07-30 21:33:55 +02:00
parent dd10ef5d41
commit e43650c26c
14 changed files with 2692 additions and 1 deletions

121
app/models/credit_action.py Normal file
View File

@@ -0,0 +1,121 @@
"""Credit action definitions for the credit system."""
from enum import Enum
from typing import Any
class CreditActionType(str, Enum):
"""Types of actions that consume credits."""
VLC_PLAY_SOUND = "vlc_play_sound"
AUDIO_EXTRACTION = "audio_extraction"
TEXT_TO_SPEECH = "text_to_speech"
SOUND_NORMALIZATION = "sound_normalization"
API_REQUEST = "api_request"
PLAYLIST_CREATION = "playlist_creation"
class CreditAction:
"""Definition of a credit-consuming action."""
def __init__(
self,
action_type: CreditActionType,
cost: int,
description: str,
*,
requires_success: bool = True,
) -> None:
"""Initialize a credit action.
Args:
action_type: The type of action
cost: Number of credits required
description: Human-readable description
requires_success: Whether credits are only deducted on successful completion
"""
self.action_type = action_type
self.cost = cost
self.description = description
self.requires_success = requires_success
def __str__(self) -> str:
"""Return string representation of the action."""
return f"{self.action_type.value} ({self.cost} credits)"
def to_dict(self) -> dict[str, Any]:
"""Convert to dictionary for serialization."""
return {
"action_type": self.action_type.value,
"cost": self.cost,
"description": self.description,
"requires_success": self.requires_success,
}
# Predefined credit actions
CREDIT_ACTIONS = {
CreditActionType.VLC_PLAY_SOUND: CreditAction(
action_type=CreditActionType.VLC_PLAY_SOUND,
cost=1,
description="Play a sound using VLC player",
requires_success=True,
),
CreditActionType.AUDIO_EXTRACTION: CreditAction(
action_type=CreditActionType.AUDIO_EXTRACTION,
cost=5,
description="Extract audio from external URL",
requires_success=True,
),
CreditActionType.TEXT_TO_SPEECH: CreditAction(
action_type=CreditActionType.TEXT_TO_SPEECH,
cost=2,
description="Generate speech from text",
requires_success=True,
),
CreditActionType.SOUND_NORMALIZATION: CreditAction(
action_type=CreditActionType.SOUND_NORMALIZATION,
cost=1,
description="Normalize audio levels",
requires_success=True,
),
CreditActionType.API_REQUEST: CreditAction(
action_type=CreditActionType.API_REQUEST,
cost=1,
description="API request (rate limiting)",
requires_success=False, # Charged even if request fails
),
CreditActionType.PLAYLIST_CREATION: CreditAction(
action_type=CreditActionType.PLAYLIST_CREATION,
cost=3,
description="Create a new playlist",
requires_success=True,
),
}
def get_credit_action(action_type: CreditActionType) -> CreditAction:
"""Get a credit action definition by type.
Args:
action_type: The action type to look up
Returns:
The credit action definition
Raises:
KeyError: If action type is not found
"""
return CREDIT_ACTIONS[action_type]
def get_all_credit_actions() -> dict[CreditActionType, CreditAction]:
"""Get all available credit actions.
Returns:
Dictionary of all credit actions
"""
return CREDIT_ACTIONS.copy()

View File

@@ -0,0 +1,29 @@
"""Credit transaction model for tracking credit usage."""
from typing import TYPE_CHECKING
from sqlmodel import Field, Relationship
from app.models.base import BaseModel
if TYPE_CHECKING:
from app.models.user import User
class CreditTransaction(BaseModel, table=True):
"""Database model for credit transactions."""
__tablename__ = "credit_transaction" # pyright: ignore[reportAssignmentType]
user_id: int = Field(foreign_key="user.id", nullable=False)
action_type: str = Field(nullable=False)
amount: int = Field(nullable=False) # Negative for deductions, positive for additions
balance_before: int = Field(nullable=False)
balance_after: int = Field(nullable=False)
description: str = Field(nullable=False)
success: bool = Field(nullable=False, default=True)
# JSON string for additional data
metadata_json: str | None = Field(default=None)
# relationships
user: "User" = Relationship(back_populates="credit_transactions")

View File

@@ -6,6 +6,7 @@ from sqlmodel import Field, Relationship
from app.models.base import BaseModel
if TYPE_CHECKING:
from app.models.credit_transaction import CreditTransaction
from app.models.extraction import Extraction
from app.models.plan import Plan
from app.models.playlist import Playlist
@@ -35,3 +36,4 @@ class User(BaseModel, table=True):
playlists: list["Playlist"] = Relationship(back_populates="user")
sounds_played: list["SoundPlayed"] = Relationship(back_populates="user")
extractions: list["Extraction"] = Relationship(back_populates="user")
credit_transactions: list["CreditTransaction"] = Relationship(back_populates="user")