test_updates.py•12.6 kB
"""
Tests for update functionality in YNAB MCP Server.
Tests the update_category_budget and update_transaction tools.
"""
from datetime import date
from typing import Any
from unittest.mock import MagicMock
import ynab
from assertions import extract_response_data
from fastmcp.client import Client, FastMCPTransport
def create_ynab_category(
*,
id: str = "cat-1",
category_group_id: str = "group-1",
budgeted: int = 100_000, # $100.00
activity: int = -50_000, # -$50.00
balance: int = 50_000, # $50.00
**kwargs: Any,
) -> ynab.Category:
"""Create a YNAB Category for testing with sensible defaults."""
return ynab.Category(
id=id,
category_group_id=category_group_id,
category_group_name=kwargs.get("category_group_name", "Test Group"),
name=kwargs.get("name", "Test Category"),
hidden=kwargs.get("hidden", False),
original_category_group_id=kwargs.get("original_category_group_id"),
note=kwargs.get("note"),
budgeted=budgeted,
activity=activity,
balance=balance,
goal_type=kwargs.get("goal_type"),
goal_needs_whole_amount=kwargs.get("goal_needs_whole_amount"),
goal_day=kwargs.get("goal_day"),
goal_cadence=kwargs.get("goal_cadence"),
goal_cadence_frequency=kwargs.get("goal_cadence_frequency"),
goal_creation_month=kwargs.get("goal_creation_month"),
goal_target=kwargs.get("goal_target"),
goal_target_month=kwargs.get("goal_target_month"),
goal_percentage_complete=kwargs.get("goal_percentage_complete"),
goal_months_to_budget=kwargs.get("goal_months_to_budget"),
goal_under_funded=kwargs.get("goal_under_funded"),
goal_overall_funded=kwargs.get("goal_overall_funded"),
goal_overall_left=kwargs.get("goal_overall_left"),
deleted=kwargs.get("deleted", False),
)
def create_ynab_transaction_detail(
*,
id: str = "txn-1",
date: date = date(2024, 1, 15),
amount: int = -50_000, # -$50.00
account_id: str = "acc-1",
**kwargs: Any,
) -> ynab.TransactionDetail:
"""Create a YNAB TransactionDetail for testing with sensible defaults."""
return ynab.TransactionDetail(
id=id,
date=date,
amount=amount,
memo=kwargs.get("memo"),
cleared=kwargs.get("cleared", ynab.TransactionClearedStatus.CLEARED),
approved=kwargs.get("approved", True),
flag_color=kwargs.get("flag_color"),
account_id=account_id,
account_name=kwargs.get("account_name", "Test Account"),
payee_id=kwargs.get("payee_id"),
payee_name=kwargs.get("payee_name"),
category_id=kwargs.get("category_id"),
category_name=kwargs.get("category_name"),
transfer_account_id=kwargs.get("transfer_account_id"),
transfer_transaction_id=kwargs.get("transfer_transaction_id"),
matched_transaction_id=kwargs.get("matched_transaction_id"),
import_id=kwargs.get("import_id"),
import_payee_name=kwargs.get("import_payee_name"),
import_payee_name_original=kwargs.get("import_payee_name_original"),
debt_transaction_type=kwargs.get("debt_transaction_type"),
deleted=kwargs.get("deleted", False),
subtransactions=kwargs.get("subtransactions", []),
)
async def test_update_category_budget_success(
mock_environment_variables: None,
categories_api: MagicMock,
mock_repository: MagicMock,
mcp_client: Client[FastMCPTransport],
) -> None:
"""Test successful category budget update."""
# Create the updated category that will be returned
updated_category = create_ynab_category(
id="cat-groceries",
category_group_id="group-everyday",
name="Groceries",
budgeted=200_000, # $200.00 (new budgeted amount)
activity=-150_000, # -$150.00
balance=50_000, # $50.00
)
# Mock repository methods
mock_repository.update_month_category.return_value = updated_category
# Mock the categories response for group names
category_group = ynab.CategoryGroupWithCategories(
id="group-everyday",
name="Everyday Expenses",
hidden=False,
deleted=False,
categories=[updated_category],
)
# Mock repository to return category groups
mock_repository.get_category_groups.return_value = [category_group]
# Execute the tool
result = await mcp_client.call_tool(
"update_category_budget",
{
"category_id": "cat-groceries",
"budgeted": "200.00",
"month": "current",
},
)
# Verify the response
category_data = extract_response_data(result)
assert category_data["id"] == "cat-groceries"
assert category_data["name"] == "Groceries"
assert category_data["category_group_name"] == "Everyday Expenses"
assert category_data["budgeted"] == "200" # $200.00
assert category_data["activity"] == "-150" # -$150.00
assert category_data["balance"] == "50" # $50.00
# Verify the repository was called correctly
mock_repository.update_month_category.assert_called_once()
call_args = mock_repository.update_month_category.call_args
assert call_args[0][0] == "cat-groceries" # category_id
assert call_args[0][1].year == 2025 # current month (from date.today())
assert call_args[0][2] == 200_000 # budgeted_milliunits
async def test_update_transaction_success(
mock_environment_variables: None,
mock_repository: MagicMock,
mcp_client: Client[FastMCPTransport],
) -> None:
"""Test successful transaction update."""
# Create the updated transaction that will be returned
updated_transaction = create_ynab_transaction_detail(
id="txn-123",
date=date(2024, 1, 15),
amount=-75_000, # -$75.00
account_id="acc-checking",
account_name="Checking",
payee_id="payee-amazon",
payee_name="Amazon",
category_id="cat-household", # Updated category
category_name="Household Items", # Updated category name
memo="Amazon purchase - household items", # Updated memo
cleared=ynab.TransactionClearedStatus.CLEARED,
approved=True,
)
# Mock the existing transaction response (what we fetch before updating)
original_transaction = create_ynab_transaction_detail(
id="txn-123",
date=date(2024, 1, 15),
amount=-75_000, # -$75.00
account_id="acc-checking",
account_name="Checking",
payee_id="payee-amazon",
payee_name="Amazon",
category_id="cat-food", # Original category
category_name="Food", # Original category name
memo="Amazon purchase", # Original memo
cleared=ynab.TransactionClearedStatus.CLEARED,
approved=True,
)
# Mock the API to return the transaction directly via the repository
mock_repository.get_transaction_by_id.return_value = original_transaction
mock_repository.update_transaction.return_value = updated_transaction
# Execute the tool
result = await mcp_client.call_tool(
"update_transaction",
{
"transaction_id": "txn-123",
"category_id": "cat-household",
"memo": "Amazon purchase - household items",
},
)
# Verify the response
transaction_data = extract_response_data(result)
assert transaction_data["id"] == "txn-123"
assert transaction_data["amount"] == "-75" # -$75.00
assert transaction_data["category_id"] == "cat-household"
assert transaction_data["category_name"] == "Household Items"
assert transaction_data["memo"] == "Amazon purchase - household items"
assert transaction_data["cleared"] == "cleared"
# Verify the repository was called correctly
mock_repository.get_transaction_by_id.assert_called_once_with("txn-123")
mock_repository.update_transaction.assert_called_once()
async def test_update_category_budget_with_specific_month(
mock_environment_variables: None,
categories_api: MagicMock,
mcp_client: Client[FastMCPTransport],
) -> None:
"""Test category budget update for a specific month."""
updated_category = create_ynab_category(
id="cat-dining",
name="Dining Out",
budgeted=150_000, # $150.00
)
save_response = ynab.SaveCategoryResponse(
data=ynab.SaveCategoryResponseData(
category=updated_category, server_knowledge=0
)
)
categories_api.update_month_category.return_value = save_response
# Mock categories response for group names
category_group = ynab.CategoryGroupWithCategories(
id="group-1",
name="Fun Money",
hidden=False,
deleted=False,
categories=[updated_category],
)
categories_response = ynab.CategoriesResponse(
data=ynab.CategoriesResponseData(
category_groups=[category_group], server_knowledge=0
)
)
categories_api.get_categories.return_value = categories_response
# Execute with specific date
result = await mcp_client.call_tool(
"update_category_budget",
{
"category_id": "cat-dining",
"budgeted": "150.00",
"month": "2024-03-01", # Specific month
},
)
# Verify the response
category_data = extract_response_data(result)
assert category_data["budgeted"] == "150"
# Verify correct month was passed to API
call_args = categories_api.update_month_category.call_args[0]
month_arg = call_args[1]
assert month_arg == date(2024, 3, 1)
async def test_update_transaction_minimal_fields(
mock_environment_variables: None,
mock_repository: MagicMock,
mcp_client: Client[FastMCPTransport],
) -> None:
"""Test transaction update with only category change."""
# Mock the existing transaction response (what we fetch before updating)
original_transaction = create_ynab_transaction_detail(
id="txn-456",
category_id="cat-food",
category_name="Food",
)
updated_transaction = create_ynab_transaction_detail(
id="txn-456",
category_id="cat-gas",
category_name="Gas & Fuel",
)
# Mock the repository methods
mock_repository.get_transaction_by_id.return_value = original_transaction
mock_repository.update_transaction.return_value = updated_transaction
# Execute with only category_id change
result = await mcp_client.call_tool(
"update_transaction",
{
"transaction_id": "txn-456",
"category_id": "cat-gas",
},
)
# Verify the response
transaction_data = extract_response_data(result)
assert transaction_data["category_id"] == "cat-gas"
# Verify the repository was called correctly
mock_repository.get_transaction_by_id.assert_called_once_with("txn-456")
mock_repository.update_transaction.assert_called_once()
async def test_update_transaction_with_payee(
mock_environment_variables: None,
mock_repository: MagicMock,
mcp_client: Client[FastMCPTransport],
) -> None:
"""Test transaction update with payee_id to cover all branches."""
# Mock the existing transaction response (what we fetch before updating)
original_transaction = create_ynab_transaction_detail(
id="txn-789",
amount=-25_500, # -$25.50
payee_id="payee-generic",
payee_name="Generic Store",
memo="Store purchase",
)
updated_transaction = create_ynab_transaction_detail(
id="txn-789",
amount=-25_500, # -$25.50
payee_id="payee-starbucks",
payee_name="Starbucks",
memo="Coffee run",
)
# Mock the repository methods
mock_repository.get_transaction_by_id.return_value = original_transaction
mock_repository.update_transaction.return_value = updated_transaction
# Execute with payee_id
result = await mcp_client.call_tool(
"update_transaction",
{
"transaction_id": "txn-789",
"payee_id": "payee-starbucks",
"memo": "Coffee run",
},
)
# Verify the response
transaction_data = extract_response_data(result)
assert transaction_data["payee_id"] == "payee-starbucks"
assert transaction_data["memo"] == "Coffee run"
assert transaction_data["amount"] == "-25.5"
# Verify the repository was called correctly
mock_repository.get_transaction_by_id.assert_called_once_with("txn-789")
mock_repository.update_transaction.assert_called_once()