# Directory Structure
```
├── .gitignore
├── LICENSE
├── logger.py
├── main.py
├── nijivoice
│ ├── __init__.py
│ ├── api.py
│ ├── exceptions.py
│ └── models.py
├── pyproject.toml
├── pytest.ini
├── README.md
├── run_mcp.py
├── server.py
├── test_api_client.py
├── tests
│ ├── __init__.py
│ ├── conftest.py
│ ├── mock_client.py
│ ├── README.md
│ ├── test_api.py
│ ├── test_exceptions.py
│ ├── test_models.py
│ └── test_server.py
└── uv.lock
```
# Files
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
# Python-generated files
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# Virtual environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Testing
.coverage
.pytest_cache/
htmlcov/
.tox/
coverage.xml
*.cover
.hypothesis/
# VSCode
.vscode/
*.code-workspace
# Editor specific files
*~
.*.swp
.DS_Store
# Local environment files
.env
.env:Zone.Identifier
.python-version
**/.claude/settings.local.json
```
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
```python
"""Test package for nijivoice-mcp."""
```
--------------------------------------------------------------------------------
/pytest.ini:
--------------------------------------------------------------------------------
```
[pytest]
asyncio_mode = strict
asyncio_default_fixture_loop_scope = function
```
--------------------------------------------------------------------------------
/main.py:
--------------------------------------------------------------------------------
```python
def main():
print("Hello from nijivoice-mcp-beta!")
if __name__ == "__main__":
main()
```
--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
```python
"""Configuration for pytest."""
import pytest
# No need to redefine event_loop fixture
# pytest-asyncio already provides this functionality
```
--------------------------------------------------------------------------------
/nijivoice/__init__.py:
--------------------------------------------------------------------------------
```python
from .models import VoiceActor, VoiceGenerationRequest, Balance, RecommendedParameters
from .api import NijiVoiceClient
from .exceptions import NijiVoiceError, NijiVoiceAPIError
__version__ = "0.1.0"
```
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
```toml
[project]
name = "nijivoice-mcp-beta"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"fastmcp>=2.2.8",
"httpx>=0.28.1",
"logger>=1.4",
"pytest>=8.3.5",
"pytest-asyncio>=0.26.0",
"pytest-cov>=4.1.0",
"python-dotenv>=1.1.0",
]
```
--------------------------------------------------------------------------------
/tests/test_exceptions.py:
--------------------------------------------------------------------------------
```python
"""Test suite for exceptions.py."""
import pytest
from nijivoice.exceptions import NijiVoiceError, NijiVoiceAPIError
class TestNijiVoiceExceptions:
"""Test cases for NijiVoice exceptions."""
def test_nijivoice_error(self):
"""Test NijiVoiceError base exception."""
# Create an instance
error = NijiVoiceError("Test error message")
# Check if it's an instance of Exception
assert isinstance(error, Exception)
# Check the error message
assert str(error) == "Test error message"
def test_nijivoice_api_error_with_status_code(self):
"""Test NijiVoiceAPIError with status code."""
# Create an instance with status code
error = NijiVoiceAPIError("API error message", status_code=404)
# Check inheritance
assert isinstance(error, NijiVoiceError)
# Check properties
assert str(error) == "API error message"
assert error.status_code == 404
def test_nijivoice_api_error_without_status_code(self):
"""Test NijiVoiceAPIError without status code."""
# Create an instance without status code
error = NijiVoiceAPIError("API error message")
# Check properties
assert str(error) == "API error message"
assert error.status_code is None
```
--------------------------------------------------------------------------------
/tests/mock_client.py:
--------------------------------------------------------------------------------
```python
"""Mock implementation of the NijiVoiceClient for testing."""
from typing import List, Optional, Dict, Any
from nijivoice.models import VoiceActor, VoiceGenerationRequest, Balance
from nijivoice.exceptions import NijiVoiceAPIError
class MockNijiVoiceClient:
"""Mock implementation of NijiVoiceClient for testing."""
def __init__(self, api_key: Optional[str] = None, timeout: Optional[float] = 30.0):
"""Mock initialization."""
self.api_key = api_key
self.timeout = timeout
self.should_fail = False
self.headers = {
"x-api-key": api_key or "",
"Accept": "application/json",
}
self.voice_actors = [
VoiceActor(
id="voice-actor-1",
name="Test Actor 1",
description="Test description",
gender="Male",
age=25,
),
VoiceActor(
id="voice-actor-2",
name="Test Actor 2",
description="Another test description",
gender="Female",
age=30,
),
]
def set_should_fail(self, should_fail: bool):
"""Set whether API calls should fail."""
self.should_fail = should_fail
async def get_voice_actors(self) -> List[VoiceActor]:
"""Mock implementation of get_voice_actors."""
if self.should_fail:
raise NijiVoiceAPIError("Failed to get voice actors", status_code=500)
return self.voice_actors
async def generate_voice(self, request: VoiceGenerationRequest) -> Dict[str, Any]:
"""Mock implementation of generate_voice."""
if self.should_fail:
raise NijiVoiceAPIError("Failed to generate voice", status_code=500)
return {
"encoded_voice": "base64_encoded_mock_audio_data",
"remaining_credits": 100,
"format": request.format,
"actor_id": request.id
}
async def get_balance(self) -> Balance:
"""Mock implementation of get_balance."""
if self.should_fail:
raise NijiVoiceAPIError("Failed to get balance", status_code=500)
return Balance(balance=100)
```
--------------------------------------------------------------------------------
/tests/test_server.py:
--------------------------------------------------------------------------------
```python
"""Test suite for server.py."""
import os
import pytest
from unittest.mock import patch, MagicMock, AsyncMock
import asyncio
from fastmcp import FastMCP
from nijivoice.models import VoiceActor, VoiceGenerationRequest, Balance
from nijivoice.exceptions import NijiVoiceAPIError
from tests.mock_client import MockNijiVoiceClient
# Patch environment to avoid loading actual API keys
@pytest.fixture(autouse=True)
def mock_env():
"""Mock environment variables."""
with patch.dict(os.environ, {"NIJIVOICE_API_KEY": "test-api-key"}):
yield
@pytest.fixture
def mock_client():
"""Create a mock NijiVoiceClient."""
client = MockNijiVoiceClient(api_key="test-api-key")
return client
@pytest.fixture
def server_module():
"""Import the server module with patched client."""
with patch("nijivoice.api.NijiVoiceClient", MockNijiVoiceClient):
import server
yield server
@pytest.mark.asyncio
async def test_get_voice_actors(server_module, mock_client):
"""Test get_voice_actors function returns list of voice actors."""
# Patch the client in server module
server_module.client = mock_client
# Get the tool function
get_voice_actors = server_module.get_voice_actors
# Call the function
result = await get_voice_actors()
# Check if result is a list of VoiceActor objects
assert isinstance(result, list)
assert all(isinstance(actor, VoiceActor) for actor in result)
assert len(result) == 2
assert result[0].id == "voice-actor-1"
assert result[1].id == "voice-actor-2"
@pytest.mark.asyncio
async def test_get_voice_actors_error(server_module, mock_client):
"""Test error handling in get_voice_actors function."""
# Set client to fail
mock_client.set_should_fail(True)
# Patch the client in server module
server_module.client = mock_client
# Get the tool function
get_voice_actors = server_module.get_voice_actors
# Call the function and expect an exception
with pytest.raises(NijiVoiceAPIError) as excinfo:
await get_voice_actors()
# Check the exception details
assert excinfo.value.status_code == 500
assert "Failed to get voice actors" in str(excinfo.value)
@pytest.mark.asyncio
async def test_mcp_tools_registration(server_module):
"""Test that the tool is properly registered with FastMCP."""
mcp = server_module.mcp
# Check if FastMCP instance is created
assert isinstance(mcp, FastMCP)
assert mcp.name == "nijivoice MCP"
# Check if the get_voice_actors function exists and is decorated
assert hasattr(server_module, "get_voice_actors")
get_voice_actors = server_module.get_voice_actors
# Check if function has the expected docstring
assert "Get the list of voice actors." in get_voice_actors.__doc__
@pytest.mark.asyncio
async def test_client_initialization(server_module):
"""Test that the NijiVoiceClient is initialized properly."""
# Check that the client is initialized
client = server_module.client
assert isinstance(client, MockNijiVoiceClient)
assert client.api_key == "test-api-key"
@pytest.mark.asyncio
async def test_dotenv_loading():
"""Test that dotenv is loaded properly."""
# Create a mock for load_dotenv
with patch("dotenv.load_dotenv") as mock_load_dotenv:
# Import the server module to trigger load_dotenv
with patch("nijivoice.api.NijiVoiceClient"):
import importlib
import sys
# Remove the module from sys.modules if it exists
if "server" in sys.modules:
del sys.modules["server"]
# Re-import the module
import server
# Check that load_dotenv was called
mock_load_dotenv.assert_called_once()
@pytest.mark.asyncio
async def test_generate_voice(server_module, mock_client):
"""Test generate_voice function."""
# Setup
server_module.client = mock_client
# Create a detailed mock for client.generate_voice
mock_response = {"encoded_voice": "mock_data", "credits": 100}
mock_client.generate_voice = AsyncMock(return_value=mock_response)
# Replace get_voice_actors with a mock
mock_actors = [
VoiceActor(id="test-actor-1", name="Test Actor 1"),
VoiceActor(id="test-actor-2", name="Test Actor 2")
]
server_module.get_voice_actors = AsyncMock(return_value=mock_actors)
# Call the function
result = await server_module.generate_voice(script="こんにちは")
# Verify
server_module.get_voice_actors.assert_called_once()
# Check that the client's generate_voice was called with the correct request
called_request = mock_client.generate_voice.call_args[1]["request"]
assert isinstance(called_request, VoiceGenerationRequest)
assert called_request.script == "こんにちは"
# Assert the result matches the mock response
assert result == mock_response
@pytest.mark.asyncio
async def test_generate_voice_no_actors(server_module, mock_client):
"""Test generate_voice function when no actors are available."""
# Setup
server_module.client = mock_client
# Replace get_voice_actors with a mock that returns empty list
server_module.get_voice_actors = AsyncMock(return_value=[])
# Call the function
result = await server_module.generate_voice(script="こんにちは")
# Verify
server_module.get_voice_actors.assert_called_once()
# The result should indicate an error
assert result["status"] == "error"
assert "利用可能なVoice Actorが見つかりません" in result["message"]
@pytest.mark.asyncio
async def test_get_credit_balance(server_module, mock_client):
"""Test get_credit_balance function."""
# Setup
server_module.client = mock_client
mock_balance = Balance(balance=200)
mock_client.get_balance = AsyncMock(return_value=mock_balance)
# Call the function
result = await server_module.get_credit_balance()
# Verify
mock_client.get_balance.assert_called_once()
assert result == 200
@pytest.mark.asyncio
async def test_voice_actors_resource(server_module, mock_client):
"""Test voice_actors_resource function."""
# Setup
server_module.client = mock_client
# Replace get_voice_actors with a mock
mock_actors = [
VoiceActor(id="test-actor-1", name="Test Actor 1"),
VoiceActor(id="test-actor-2", name="Test Actor 2")
]
server_module.get_voice_actors = AsyncMock(return_value=mock_actors)
# Call the function
result = await server_module.voice_actors_resource()
# Verify
server_module.get_voice_actors.assert_called_once()
assert result == mock_actors
@pytest.mark.asyncio
async def test_voice_actor_resource(server_module, mock_client):
"""Test voice_actor_resource function."""
# Setup
server_module.client = mock_client
# Replace get_voice_actors with a mock
mock_actors = [
VoiceActor(id="test-actor-1", name="Test Actor 1"),
VoiceActor(id="test-actor-2", name="Test Actor 2")
]
server_module.get_voice_actors = AsyncMock(return_value=mock_actors)
# Call the function with existing actor ID
result = await server_module.voice_actor_resource(actor_id="test-actor-1")
# Verify
server_module.get_voice_actors.assert_called_once()
assert result == mock_actors[0]
# Reset the mock
server_module.get_voice_actors.reset_mock()
# Call the function with non-existing actor ID
result = await server_module.voice_actor_resource(actor_id="non-existing")
# Verify
server_module.get_voice_actors.assert_called_once()
assert result is None
@pytest.mark.asyncio
async def test_credit_balance_resource(server_module, mock_client):
"""Test credit_balance_resource function."""
# Setup
server_module.client = mock_client
mock_balance = Balance(balance=200)
mock_client.get_balance = AsyncMock(return_value=mock_balance)
# Call the function
result = await server_module.credit_balance_resource()
# Verify
mock_client.get_balance.assert_called_once()
assert result == mock_balance
@pytest.mark.asyncio
async def test_voice_generation_prompt(server_module):
"""Test voice_generation_prompt function."""
# Call the function
result = server_module.voice_generation_prompt()
# Verify
assert isinstance(result, str)
assert "にじボイス音声生成" in result
assert "利用可能なVoice Actor" in result
```
--------------------------------------------------------------------------------
/tests/test_models.py:
--------------------------------------------------------------------------------
```python
"""Test suite for models.py."""
import pytest
from pydantic import ValidationError
from nijivoice.models import (
RecommendedParameters,
VoiceStyle,
VoiceActor,
VoiceGenerationRequest,
Balance
)
class TestRecommendedParameters:
"""Tests for RecommendedParameters model."""
def test_create_recommended_parameters(self):
"""Test creating RecommendedParameters with default values."""
params = RecommendedParameters()
assert params.emotional_level == 1.0
assert params.sound_duration == 1.0
def test_create_with_values(self):
"""Test creating RecommendedParameters with values."""
params = RecommendedParameters(
emotional_level=0.8,
sound_duration=1.2
)
assert params.emotional_level == 0.8
assert params.sound_duration == 1.2
def test_create_with_aliases(self):
"""Test creating RecommendedParameters using alias names."""
params = RecommendedParameters(
emotionalLevel=0.8,
soundDuration=1.2
)
assert params.emotional_level == 0.8
assert params.sound_duration == 1.2
def test_ignore_extra_fields(self):
"""Test that extra fields are ignored."""
params = RecommendedParameters(
emotional_level=0.8,
sound_duration=1.2,
extra_field="should be ignored"
)
assert not hasattr(params, "extra_field")
class TestVoiceStyle:
"""Tests for VoiceStyle model."""
def test_create_voice_style(self):
"""Test creating VoiceStyle."""
style = VoiceStyle(id=1, style="normal")
assert style.id == 1
assert style.style == "normal"
def test_voice_style_validation(self):
"""Test VoiceStyle validation."""
# Missing required field should raise ValidationError
with pytest.raises(ValidationError):
VoiceStyle(style="normal") # Missing id
with pytest.raises(ValidationError):
VoiceStyle(id=1) # Missing style
class TestVoiceActor:
"""Tests for VoiceActor model."""
def test_create_voice_actor_minimal(self):
"""Test creating VoiceActor with minimal required fields."""
actor = VoiceActor(id="actor-1", name="Test Actor")
assert actor.id == "actor-1"
assert actor.name == "Test Actor"
assert actor.description == "" # Default value
assert actor.gender is None
assert actor.age is None
def test_create_voice_actor_full(self):
"""Test creating VoiceActor with all fields."""
actor = VoiceActor(
id="actor-1",
name="Test Actor",
name_reading="テストアクター",
age=25,
gender="Female",
birth_month=7,
birth_day=15,
description="Test description",
small_image_url="https://example.com/small.jpg",
medium_image_url="https://example.com/medium.jpg",
large_image_url="https://example.com/large.jpg",
sample_voice_url="https://example.com/sample.mp3",
sample_script="こんにちは",
recommended_voice_speed=1.2,
recommended_emotional_level=0.8,
recommended_sound_duration=1.0,
recommended_parameters=RecommendedParameters(
emotional_level=0.8,
sound_duration=1.0
),
voice_styles=[
VoiceStyle(id=1, style="normal"),
VoiceStyle(id=2, style="happy")
]
)
assert actor.id == "actor-1"
assert actor.name == "Test Actor"
assert actor.name_reading == "テストアクター"
assert actor.age == 25
assert actor.gender == "Female"
assert actor.birth_month == 7
assert actor.birth_day == 15
assert actor.description == "Test description"
assert actor.small_image_url == "https://example.com/small.jpg"
assert actor.medium_image_url == "https://example.com/medium.jpg"
assert actor.large_image_url == "https://example.com/large.jpg"
assert actor.sample_voice_url == "https://example.com/sample.mp3"
assert actor.sample_script == "こんにちは"
assert actor.recommended_voice_speed == 1.2
assert actor.recommended_emotional_level == 0.8
assert actor.recommended_sound_duration == 1.0
assert isinstance(actor.recommended_parameters, RecommendedParameters)
assert len(actor.voice_styles) == 2
assert actor.voice_styles[0].id == 1
assert actor.voice_styles[0].style == "normal"
def test_create_with_aliases(self):
"""Test creating VoiceActor using alias names."""
actor = VoiceActor(
id="actor-1",
name="Test Actor",
nameReading="テストアクター",
birthMonth=7,
birthDay=15,
smallImageUrl="https://example.com/small.jpg",
mediumImageUrl="https://example.com/medium.jpg",
largeImageUrl="https://example.com/large.jpg",
sampleVoiceUrl="https://example.com/sample.mp3",
sampleScript="こんにちは",
recommendedVoiceSpeed=1.2,
recommendedEmotionalLevel=0.8,
recommendedSoundDuration=1.0,
recommendedParameters={
"emotionalLevel": 0.8,
"soundDuration": 1.0
},
voiceStyles=[
{"id": 1, "style": "normal"},
{"id": 2, "style": "happy"}
]
)
assert actor.name_reading == "テストアクター"
assert actor.birth_month == 7
assert actor.birth_day == 15
assert actor.small_image_url == "https://example.com/small.jpg"
assert isinstance(actor.recommended_parameters, RecommendedParameters)
assert len(actor.voice_styles) == 2
class TestVoiceGenerationRequest:
"""Tests for VoiceGenerationRequest model."""
def test_create_minimal_request(self):
"""Test creating VoiceGenerationRequest with minimal fields."""
request = VoiceGenerationRequest(
id="actor-1",
script="こんにちは"
)
assert request.id == "actor-1"
assert request.script == "こんにちは"
assert request.speed == 1.0 # Default value
assert request.emotional_level is None
assert request.sound_duration is None
assert request.format == "mp3" # Default value
def test_create_full_request(self):
"""Test creating VoiceGenerationRequest with all fields."""
request = VoiceGenerationRequest(
id="actor-1",
script="こんにちは",
speed=1.5,
emotional_level=0.8,
sound_duration=1.2,
format="wav"
)
assert request.id == "actor-1"
assert request.script == "こんにちは"
assert request.speed == 1.5
assert request.emotional_level == 0.8
assert request.sound_duration == 1.2
assert request.format == "wav"
def test_create_request_with_aliases(self):
"""Test creating VoiceGenerationRequest with aliases."""
request = VoiceGenerationRequest(
id="actor-1",
script="こんにちは",
speed=1.5,
emotionalLevel=0.8,
soundDuration=1.2
)
assert request.emotional_level == 0.8
assert request.sound_duration == 1.2
def test_speed_validation(self):
"""Test speed validation."""
# Valid values
request = VoiceGenerationRequest(id="actor-1", script="こんにちは", speed=0.4)
assert request.speed == 0.4
request = VoiceGenerationRequest(id="actor-1", script="こんにちは", speed=3.0)
assert request.speed == 3.0
# Invalid values
with pytest.raises(ValidationError):
VoiceGenerationRequest(id="actor-1", script="こんにちは", speed=0.3) # Too low
with pytest.raises(ValidationError):
VoiceGenerationRequest(id="actor-1", script="こんにちは", speed=3.1) # Too high
def test_emotional_level_validation(self):
"""Test emotional_level validation."""
# Valid values
request = VoiceGenerationRequest(id="actor-1", script="こんにちは", emotional_level=0.0)
assert request.emotional_level == 0.0
request = VoiceGenerationRequest(id="actor-1", script="こんにちは", emotional_level=1.5)
assert request.emotional_level == 1.5
# Invalid values
with pytest.raises(ValidationError):
VoiceGenerationRequest(id="actor-1", script="こんにちは", emotional_level=-0.1) # Too low
with pytest.raises(ValidationError):
VoiceGenerationRequest(id="actor-1", script="こんにちは", emotional_level=1.6) # Too high
def test_format_validation(self):
"""Test format validation."""
# Valid values
request = VoiceGenerationRequest(id="actor-1", script="こんにちは", format="mp3")
assert request.format == "mp3"
request = VoiceGenerationRequest(id="actor-1", script="こんにちは", format="wav")
assert request.format == "wav"
# Invalid values
with pytest.raises(ValidationError):
VoiceGenerationRequest(id="actor-1", script="こんにちは", format="flac")
def test_serializers(self):
"""Test field serializers."""
request = VoiceGenerationRequest(
id="actor-1",
script="こんにちは",
speed=1.5,
emotional_level=0.8,
sound_duration=1.2,
format="wav" # Already lowercase since validation happens before serialization
)
data = request.model_dump(by_alias=True)
# Check serialized values
assert data["speed"] == "1.5" # Serialized as string
assert data["emotionalLevel"] == "0.8" # Serialized as string
assert data["soundDuration"] == "1.2" # Serialized as string
assert data["format"] == "wav" # Lowercase conversion
class TestBalance:
"""Tests for Balance model."""
def test_create_simple_balance(self):
"""Test creating Balance with simple structure."""
balance = Balance(balance=500)
assert balance.balance == 500
assert balance.balances is None
assert balance.get_credit() == 500
def test_create_complex_balance(self):
"""Test creating Balance with complex structure."""
balance = Balance(balances={
"remainingBalance": 800,
"credits": [
{"balance": 500, "type": "regular"},
{"balance": 300, "type": "bonus"}
]
})
assert balance.balance is None
assert isinstance(balance.balances, dict)
assert balance.get_credit() == 800 # Should get remainingBalance
def test_get_credit_with_balance_field(self):
"""Test get_credit with balance field."""
balance = Balance(balance=500)
assert balance.get_credit() == 500
def test_get_credit_with_remaining_balance(self):
"""Test get_credit with remainingBalance in balances."""
balance = Balance(balances={"remainingBalance": 800})
assert balance.get_credit() == 800
def test_get_credit_with_balance_in_balances(self):
"""Test get_credit with balance in balances."""
balance = Balance(balances={"balance": 700})
assert balance.get_credit() == 700
def test_get_credit_with_credits_list(self):
"""Test get_credit with credits list in balances."""
balance = Balance(balances={
"credits": [
{"balance": 500, "type": "regular"},
{"balance": 300, "type": "bonus"}
]
})
assert balance.get_credit() == 500 # Should get first credit balance
def test_get_credit_with_no_balance_info(self):
"""Test get_credit with no balance information."""
balance = Balance(balances={"other": "value"})
assert balance.get_credit() == 0 # Should return 0 if no balance info
def test_get_credit_with_empty_balance(self):
"""Test get_credit with empty Balance."""
balance = Balance()
assert balance.get_credit() == 0
```
--------------------------------------------------------------------------------
/tests/test_api.py:
--------------------------------------------------------------------------------
```python
"""Test suite for NijiVoiceClient in api.py."""
import os
import pytest
import json
from unittest.mock import patch, AsyncMock, MagicMock
import httpx
import pytest_asyncio
from nijivoice.api import NijiVoiceClient
from nijivoice.models import VoiceActor, VoiceGenerationRequest, Balance
from nijivoice.exceptions import NijiVoiceAPIError
@pytest.fixture
def mock_env():
"""Mock environment variables."""
with patch.dict(os.environ, {"NIJIVOICE_API_KEY": "test-api-key"}):
yield
@pytest.fixture
def mock_response():
"""Create a mock response."""
mock = MagicMock()
mock.status_code = 200
mock.text = "Success"
return mock
@pytest.fixture
def client():
"""Create a NijiVoiceClient instance."""
return NijiVoiceClient(api_key="test-api-key")
class TestNijiVoiceClient:
"""Test cases for NijiVoiceClient."""
def test_init_with_api_key(self):
"""Test initialization with api_key parameter."""
client = NijiVoiceClient(api_key="test-api-key")
assert client.api_key == "test-api-key"
assert client.timeout == 30.0
assert client.headers == {
"x-api-key": "test-api-key",
"Accept": "application/json",
}
def test_init_without_api_key(self, mock_env):
"""Test initialization without api_key parameter."""
client = NijiVoiceClient()
assert client.api_key == "test-api-key"
assert client.timeout == 30.0
def test_init_with_custom_timeout(self):
"""Test initialization with custom timeout."""
client = NijiVoiceClient(api_key="test-api-key", timeout=60.0)
assert client.timeout == 60.0
def test_init_without_api_key_and_env(self):
"""Test initialization without api_key parameter and environment variable."""
with patch.dict(os.environ, {}, clear=True):
with pytest.raises(ValueError) as excinfo:
NijiVoiceClient()
assert "APIキーが指定されていません" in str(excinfo.value)
@pytest.mark.asyncio
async def test_get_voice_actors_success(self, client, mock_response):
"""Test get_voice_actors successful response."""
# Create mock data
voice_actors_data = [
{
"id": "actor-id-1",
"name": "Actor 1",
"gender": "Female",
"age": 25
},
{
"id": "actor-id-2",
"name": "Actor 2",
"gender": "Male",
"age": 30
}
]
mock_response.json.return_value = voice_actors_data
# Mock the AsyncClient.get method
with patch("httpx.AsyncClient") as mock_client:
mock_client.return_value.__aenter__.return_value.get.return_value = mock_response
# Call the method
result = await client.get_voice_actors()
# Verify the result
assert len(result) == 2
assert isinstance(result[0], VoiceActor)
assert result[0].id == "actor-id-1"
assert result[0].name == "Actor 1"
assert result[1].id == "actor-id-2"
assert result[1].name == "Actor 2"
# Verify the request was made correctly
mock_client.return_value.__aenter__.return_value.get.assert_called_once_with(
f"{client.BASE_URL}/voice-actors",
headers=client.headers
)
@pytest.mark.asyncio
async def test_get_voice_actors_voiceActors_key(self, client, mock_response):
"""Test get_voice_actors with voiceActors key in response."""
# Create mock data with voiceActors key
voice_actors_data = {
"voiceActors": [
{
"id": "actor-id-1",
"name": "Actor 1",
"gender": "Female",
"age": 25
},
{
"id": "actor-id-2",
"name": "Actor 2",
"gender": "Male",
"age": 30
}
]
}
mock_response.json.return_value = voice_actors_data
# Mock the AsyncClient.get method
with patch("httpx.AsyncClient") as mock_client:
mock_client.return_value.__aenter__.return_value.get.return_value = mock_response
# Call the method
result = await client.get_voice_actors()
# Verify the result
assert len(result) == 2
assert isinstance(result[0], VoiceActor)
assert result[0].id == "actor-id-1"
assert result[0].name == "Actor 1"
@pytest.mark.asyncio
async def test_get_voice_actors_unexpected_format(self, client, mock_response):
"""Test get_voice_actors with unexpected data format."""
# Create unexpected data format
mock_response.json.return_value = {"unexpectedKey": "unexpected value"}
# Mock the AsyncClient.get method
with patch("httpx.AsyncClient") as mock_client:
mock_client.return_value.__aenter__.return_value.get.return_value = mock_response
# Call the method
result = await client.get_voice_actors()
# Verify the result is an empty list
assert isinstance(result, list)
assert len(result) == 0
@pytest.mark.asyncio
async def test_get_voice_actors_error_response(self, client):
"""Test get_voice_actors with error response."""
# Create error response
error_response = MagicMock()
error_response.status_code = 404
error_response.text = "Not Found"
# Mock the AsyncClient.get method
with patch("httpx.AsyncClient") as mock_client:
mock_client.return_value.__aenter__.return_value.get.return_value = error_response
# Call the method and expect exception
with pytest.raises(NijiVoiceAPIError) as excinfo:
await client.get_voice_actors()
# Verify the exception
assert excinfo.value.status_code == 404
assert "Voice Actor一覧の取得に失敗しました" in str(excinfo.value)
@pytest.mark.asyncio
async def test_get_voice_actors_timeout(self, client):
"""Test get_voice_actors with timeout."""
# Mock the AsyncClient.get method to raise TimeoutError
with patch("httpx.AsyncClient") as mock_client:
mock_client.return_value.__aenter__.return_value.get.side_effect = TimeoutError("Timeout")
# Call the method and expect exception
with pytest.raises(NijiVoiceAPIError) as excinfo:
await client.get_voice_actors()
# Verify the exception
assert excinfo.value.status_code == 408
assert "APIリクエストがタイムアウトしました" in str(excinfo.value)
@pytest.mark.asyncio
async def test_get_voice_actors_general_exception(self, client):
"""Test get_voice_actors with general exception."""
# Mock the AsyncClient.get method to raise Exception
with patch("httpx.AsyncClient") as mock_client:
mock_client.return_value.__aenter__.return_value.get.side_effect = Exception("Some error")
# Call the method and expect exception
with pytest.raises(NijiVoiceAPIError) as excinfo:
await client.get_voice_actors()
# Verify the exception
assert excinfo.value.status_code == 500
assert "API呼び出し中にエラーが発生しました" in str(excinfo.value)
@pytest.mark.asyncio
async def test_generate_voice_success(self, client, mock_response):
"""Test generate_voice successful response."""
# Create mock response data
response_data = {
"encoded_voice": "base64_encoded_data",
"remaining_credits": 100
}
mock_response.json.return_value = response_data
# Create request
request = VoiceGenerationRequest(
id="actor-id-1",
script="これはテストです",
speed=1.0
)
# Mock the AsyncClient.post method
with patch("httpx.AsyncClient") as mock_client:
mock_client.return_value.__aenter__.return_value.post.return_value = mock_response
# Call the method
result = await client.generate_voice(request=request)
# Verify the result
assert result == response_data
# Verify the request was made correctly
mock_client.return_value.__aenter__.return_value.post.assert_called_once_with(
f"{client.BASE_URL}/voice-actors/{request.id}/generate-voice",
headers=client.headers,
json=request.model_dump(by_alias=True)
)
@pytest.mark.asyncio
async def test_generate_voice_error_response(self, client):
"""Test generate_voice with error response."""
# Create error response
error_response = MagicMock()
error_response.status_code = 500
error_response.text = "Server Error"
# Create request
request = VoiceGenerationRequest(
id="actor-id-1",
script="これはテストです",
speed=1.0
)
# Mock the AsyncClient.post method
with patch("httpx.AsyncClient") as mock_client:
mock_client.return_value.__aenter__.return_value.post.return_value = error_response
# Call the method and expect exception
with pytest.raises(NijiVoiceAPIError) as excinfo:
await client.generate_voice(request=request)
# Verify the exception
assert excinfo.value.status_code == 500
assert "音声生成に失敗しました" in str(excinfo.value)
@pytest.mark.asyncio
async def test_get_balance_success(self, client, mock_response):
"""Test get_balance successful response."""
# Create mock response data
response_data = {"balance": 500}
mock_response.json.return_value = response_data
# Mock the AsyncClient.get method
with patch("httpx.AsyncClient") as mock_client:
mock_client.return_value.__aenter__.return_value.get.return_value = mock_response
# Call the method
result = await client.get_balance()
# Verify the result
assert isinstance(result, Balance)
assert result.balance == 500
assert result.get_credit() == 500
# Verify the request was made correctly
mock_client.return_value.__aenter__.return_value.get.assert_called_once_with(
f"{client.BASE_URL}/balances",
headers=client.headers
)
@pytest.mark.asyncio
async def test_get_balance_complex_response(self, client, mock_response):
"""Test get_balance with complex response structure."""
# Create mock response data with complex structure
response_data = {
"balances": {
"remainingBalance": 500,
"credits": [
{"balance": 300, "type": "regular"},
{"balance": 200, "type": "bonus"}
]
}
}
mock_response.json.return_value = response_data
# Mock the AsyncClient.get method
with patch("httpx.AsyncClient") as mock_client:
mock_client.return_value.__aenter__.return_value.get.return_value = mock_response
# Call the method
result = await client.get_balance()
# Verify the result
assert isinstance(result, Balance)
assert result.balances == response_data["balances"]
assert result.get_credit() == 500
@pytest.mark.asyncio
async def test_get_balance_error_response(self, client):
"""Test get_balance with error response."""
# Create error response
error_response = MagicMock()
error_response.status_code = 403
error_response.text = "Forbidden"
# Mock the AsyncClient.get method
with patch("httpx.AsyncClient") as mock_client:
mock_client.return_value.__aenter__.return_value.get.return_value = error_response
# Call the method and expect exception
with pytest.raises(NijiVoiceAPIError) as excinfo:
await client.get_balance()
# Verify the exception
assert excinfo.value.status_code == 403
assert "クレジット残高の取得に失敗しました" in str(excinfo.value)
```