"""
測試資料模型 - Transaction, Account, Category 等核心資料結構
這些測試驗證:
1. 資料模型的建立與驗證
2. 業務規則的正確實作
3. 錯誤處理與邊界情況
"""
import pytest
from datetime import datetime, date
from decimal import Decimal
from typing import Dict, Any
# 這些匯入會失敗,因為我們還沒有實作模型
from accounting_mcp.models import Transaction, Account, Category, TransactionType
class TestTransaction:
"""測試交易記錄模型"""
def test_create_valid_expense_transaction(self) -> None:
"""測試建立有效的支出交易"""
transaction = Transaction(
amount=Decimal("-50.00"),
category="food",
description="午餐",
date=date(2025, 1, 15)
)
assert transaction.amount == Decimal("-50.00")
assert transaction.category == "food"
assert transaction.description == "午餐"
assert transaction.date == date(2025, 1, 15)
assert transaction.transaction_type == TransactionType.EXPENSE
assert transaction.id is not None
assert isinstance(transaction.timestamp, datetime)
def test_create_valid_income_transaction(self) -> None:
"""測試建立有效的收入交易"""
transaction = Transaction(
amount=Decimal("1500.00"),
category="income",
description="工資"
)
assert transaction.amount == Decimal("1500.00")
assert transaction.transaction_type == TransactionType.INCOME
assert transaction.category == "income"
def test_transaction_amount_precision(self) -> None:
"""測試金額精度驗證(最多 2 位小數)"""
# 有效精度
transaction = Transaction(
amount=Decimal("123.45"),
category="food"
)
assert transaction.amount == Decimal("123.45")
# 超過精度應該被拒絕
with pytest.raises(ValueError):
Transaction(
amount=Decimal("123.456"),
category="food"
)
def test_invalid_amount_zero(self) -> None:
"""測試零金額(應該被拒絕)"""
with pytest.raises(ValueError, match="金額不能為零"):
Transaction(
amount=Decimal("0.00"),
category="food"
)
def test_invalid_category_empty(self) -> None:
"""測試空分類(應該被拒絕)"""
with pytest.raises(ValueError, match="分類不能為空"):
Transaction(
amount=Decimal("-50.00"),
category=""
)
def test_future_date_validation(self) -> None:
"""測試未來日期驗證"""
from datetime import date, timedelta
future_date = date.today() + timedelta(days=1)
with pytest.raises(ValueError, match="交易日期不能是未來"):
Transaction(
amount=Decimal("-50.00"),
category="food",
date=future_date
)
def test_transaction_id_uniqueness(self) -> None:
"""測試交易 ID 的唯一性"""
transaction1 = Transaction(
amount=Decimal("-50.00"),
category="food"
)
transaction2 = Transaction(
amount=Decimal("-30.00"),
category="transport"
)
assert transaction1.id != transaction2.id
def test_transaction_to_dict(self) -> None:
"""測試轉換為字典格式"""
transaction = Transaction(
amount=Decimal("-50.00"),
category="food",
description="午餐",
date=date(2025, 1, 15)
)
result = transaction.to_dict()
assert isinstance(result, dict)
assert result["amount"] == -50.00
assert result["category"] == "food"
assert result["description"] == "午餐"
assert result["date"] == "2025-01-15"
assert "id" in result
assert "timestamp" in result
def test_transaction_from_dict(self) -> None:
"""測試從字典建立交易"""
data = {
"id": "txn_20250115_001",
"amount": -50.00,
"category": "food",
"description": "午餐",
"date": "2025-01-15",
"timestamp": "2025-01-15T12:30:00Z"
}
transaction = Transaction.from_dict(data)
assert transaction.id == "txn_20250115_001"
assert transaction.amount == Decimal("-50.00")
assert transaction.category == "food"
assert transaction.description == "午餐"
assert transaction.date == date(2025, 1, 15)
class TestAccount:
"""測試帳戶模型"""
def test_create_new_account(self) -> None:
"""測試建立新帳戶"""
account = Account()
assert account.balance == Decimal("0.00")
assert account.total_transactions == 0
assert isinstance(account.created_at, datetime)
assert account.last_updated is not None
def test_account_add_transaction(self) -> None:
"""測試向帳戶新增交易"""
account = Account()
transaction = Transaction(
amount=Decimal("-50.00"),
category="food"
)
account.add_transaction(transaction)
assert account.balance == Decimal("-50.00")
assert account.total_transactions == 1
assert account.last_updated is not None
def test_account_multiple_transactions(self) -> None:
"""測試多筆交易的餘額計算"""
account = Account()
# 新增收入
income = Transaction(
amount=Decimal("1000.00"),
category="income"
)
account.add_transaction(income)
# 新增支出
expense = Transaction(
amount=Decimal("-200.00"),
category="food"
)
account.add_transaction(expense)
assert account.balance == Decimal("800.00")
assert account.total_transactions == 2
def test_account_to_dict(self) -> None:
"""測試帳戶序列化"""
account = Account()
account.add_transaction(Transaction(
amount=Decimal("-50.00"),
category="food"
))
result = account.to_dict()
assert result["balance"] == -50.00
assert result["total_transactions"] == 1
assert "created_at" in result
assert "last_updated" in result
class TestCategory:
"""測試分類模型"""
def test_create_expense_category(self) -> None:
"""測試建立支出分類"""
category = Category(
id="food",
name="餐飲",
description="餐費、外賣等",
category_type=TransactionType.EXPENSE,
color="#FF6B6B",
icon="🍽️"
)
assert category.id == "food"
assert category.name == "餐飲"
assert category.category_type == TransactionType.EXPENSE
assert category.color == "#FF6B6B"
assert category.icon == "🍽️"
def test_create_income_category(self) -> None:
"""測試建立收入分類"""
category = Category(
id="income",
name="收入",
category_type=TransactionType.INCOME
)
assert category.category_type == TransactionType.INCOME
def test_invalid_category_id(self) -> None:
"""測試無效分類 ID"""
with pytest.raises(ValueError, match="分類 ID 不能為空"):
Category(
id="",
name="測試"
)
def test_category_to_dict(self) -> None:
"""測試分類序列化"""
category = Category(
id="food",
name="餐飲",
category_type=TransactionType.EXPENSE
)
result = category.to_dict()
assert result["id"] == "food"
assert result["name"] == "餐飲"
assert result["type"] == "expense"
# 測試列舉型別
class TestTransactionType:
"""測試交易類型列舉"""
def test_transaction_type_values(self) -> None:
"""測試列舉值"""
assert TransactionType.INCOME.value == "income"
assert TransactionType.EXPENSE.value == "expense"
def test_transaction_type_from_amount(self) -> None:
"""測試根據金額判斷交易類型"""
assert TransactionType.from_amount(Decimal("100.00")) == TransactionType.INCOME
assert TransactionType.from_amount(Decimal("-50.00")) == TransactionType.EXPENSE
with pytest.raises(ValueError):
TransactionType.from_amount(Decimal("0.00"))