Skip to main content
Glama

YNAB MCP Server

by ntdef
test_ynab.py13.9 kB
import json import httpx import pytest import respx from src.ynab_mcp.ynab import YNABClient @pytest.fixture def client(): return YNABClient(api_token="test-token") @pytest.mark.asyncio @respx.mock async def test_get_budgets(client): """Test that get_budgets calls the YNAB API with the correct path.""" route = respx.get("https://api.ynab.com/v1/budgets").mock( return_value=httpx.Response( 200, json={ "data": { "budgets": [ { "id": "budget-id", "name": "Test Budget", "last_modified_on": "2023-01-01T00:00:00+00:00", } ] } }, ) ) response = await client.get_budgets() assert route.called assert route.calls[0].request.headers["Authorization"] == "Bearer test-token" assert "budgets" in response @pytest.mark.asyncio @respx.mock async def test_get_accounts(client): """Test that get_accounts calls the YNAB API with the correct path.""" route = respx.get("https://api.ynab.com/v1/budgets/budget-id/accounts").mock( return_value=httpx.Response( 200, json={ "data": { "accounts": [ { "id": "account-id", "name": "Test Account", "type": "checking", "balance": 10000, } ] } }, ) ) response = await client.get_accounts("budget-id") assert route.called assert route.calls[0].request.headers["Authorization"] == "Bearer test-token" assert "accounts" in response @pytest.mark.asyncio @respx.mock async def test_get_categories(client): """Test that get_categories calls the YNAB API with the correct path.""" route = respx.get("https://api.ynab.com/v1/budgets/budget-id/categories").mock( return_value=httpx.Response( 200, json={ "data": { "category_groups": [ { "id": "group-id", "name": "Immediate Obligations", "categories": [ { "id": "category-id", "name": "Rent/Mortgage", "budgeted": 100000, "activity": -100000, "balance": 0, } ], } ] } }, ) ) response = await client.get_categories("budget-id") assert route.called assert route.calls[0].request.headers["Authorization"] == "Bearer test-token" assert "category_groups" in response @pytest.mark.asyncio @respx.mock async def test_get_transactions_without_filters(client): """Test that get_transactions without filters calls the YNAB API correctly.""" route = respx.get("https://api.ynab.com/v1/budgets/budget-id/transactions").mock( return_value=httpx.Response( 200, json={ "data": { "transactions": [ { "id": "transaction-id", "date": "2023-01-01", "amount": -1000, "payee_name": "Test Payee", } ] } }, ) ) response = await client.get_transactions("budget-id", limit=10) assert route.called # Check that limit parameter was passed correctly url = str(route.calls[0].request.url) assert "limit=10" in url assert "transactions" in response @pytest.mark.asyncio @respx.mock async def test_get_transactions_with_account_filter(client): """Test that get_transactions with account filter calls the YNAB API correctly.""" route = respx.get( "https://api.ynab.com/v1/budgets/budget-id/accounts/account-id/transactions" ).mock( return_value=httpx.Response( 200, json={ "data": { "transactions": [ { "id": "transaction-id", "date": "2023-01-01", "amount": -1000, "account_id": "account-id", "payee_name": "Test Payee", } ] } }, ) ) response = await client.get_transactions( budget_id="budget-id", account_id="account-id", since_date="2023-01-01" ) assert route.called url = str(route.calls[0].request.url) assert "since_date=2023-01-01" in url assert "transactions" in response @pytest.mark.asyncio @respx.mock async def test_get_transactions_with_category_filter(client): """Test that get_transactions with category filter calls the YNAB API correctly.""" route = respx.get( "https://api.ynab.com/v1/budgets/budget-id/categories/category-id/transactions" ).mock( return_value=httpx.Response( 200, json={ "data": { "transactions": [ { "id": "transaction-id", "date": "2023-01-01", "amount": -1000, "category_id": "category-id", "payee_name": "Test Payee", } ] } }, ) ) response = await client.get_transactions( budget_id="budget-id", category_id="category-id", since_date="2023-01-01" ) assert route.called url = str(route.calls[0].request.url) assert "since_date=2023-01-01" in url assert "transactions" in response @pytest.mark.asyncio @respx.mock async def test_create_transaction(client): """Test that create_transaction calls the YNAB API with the correct data.""" route = respx.post("https://api.ynab.com/v1/budgets/budget-id/transactions").mock( return_value=httpx.Response( 200, json={ "data": { "transaction": { "id": "transaction-id", "date": "2023-01-01", "amount": -1000, "payee_name": "Test Payee", } } }, ) ) response = await client.create_transaction( budget_id="budget-id", account_id="account-id", date="2023-01-01", amount=-1000, payee_name="Test Payee", category_id="category-id", memo="Test memo", ) assert route.called assert route.calls[0].request.headers["Authorization"] == "Bearer test-token" request_body = route.calls[0].request.content.decode("utf-8") request_json = json.loads(request_body) assert request_json["transaction"]["account_id"] == "account-id" assert request_json["transaction"]["date"] == "2023-01-01" assert request_json["transaction"]["amount"] == -1000 assert request_json["transaction"]["payee_name"] == "Test Payee" assert request_json["transaction"]["category_id"] == "category-id" assert request_json["transaction"]["memo"] == "Test memo" assert "transaction" in response @pytest.mark.asyncio @respx.mock async def test_create_category(client): """Test that create_category calls the YNAB API with the correct data.""" route = respx.post( "https://api.ynab.com/v1/budgets/budget-id/categories/group-id/categories" ).mock( return_value=httpx.Response( 200, json={ "data": { "category": { "id": "new-category-id", "name": "New Category", "group_id": "group-id", "budgeted": 0, "activity": 0, "balance": 0, } } }, ) ) response = await client.create_category( budget_id="budget-id", group_id="group-id", name="New Category" ) assert route.called request_body = route.calls[0].request.content.decode("utf-8") request_json = json.loads(request_body) assert request_json["category"]["name"] == "New Category" assert "category" in response assert response["category"]["name"] == "New Category" @pytest.mark.asyncio @respx.mock async def test_update_category_budgeted(client): """Test that update_category_budgeted calls the YNAB API with the correct data.""" route = respx.patch( "https://api.ynab.com/v1/budgets/budget-id/months/2023-05/categories/category-id" ).mock( return_value=httpx.Response( 200, json={ "data": { "category": { "id": "category-id", "name": "Test Category", "budgeted": 50000, # 50 dollars as milliunits "activity": 0, "balance": 50000, } } }, ) ) response = await client.update_category_budgeted( budget_id="budget-id", category_id="category-id", month="2023-05", amount=50000, ) assert route.called request_body = route.calls[0].request.content.decode("utf-8") request_json = json.loads(request_body) assert request_json["category"]["budgeted"] == 50000 assert "category" in response assert response["category"]["budgeted"] == 50000 @pytest.mark.asyncio @respx.mock async def test_update_transaction(client): """Test that update_transaction calls the YNAB API with the correct data.""" route = respx.put( "https://api.ynab.com/v1/budgets/budget-id/transactions/transaction-id" ).mock( return_value=httpx.Response( 200, json={ "data": { "transaction": { "id": "transaction-id", "date": "2023-01-15", "amount": -2500, "payee_name": "Updated Payee", "category_id": "new-category-id", "memo": "Updated memo", "cleared": "cleared", } } }, ) ) response = await client.update_transaction( budget_id="budget-id", transaction_id="transaction-id", date="2023-01-15", amount=-2500, payee_name="Updated Payee", category_id="new-category-id", memo="Updated memo", cleared="cleared", ) assert route.called assert route.calls[0].request.headers["Authorization"] == "Bearer test-token" request_body = route.calls[0].request.content.decode("utf-8") request_json = json.loads(request_body) assert request_json["transaction"]["date"] == "2023-01-15" assert request_json["transaction"]["amount"] == -2500 assert request_json["transaction"]["payee_name"] == "Updated Payee" assert request_json["transaction"]["category_id"] == "new-category-id" assert request_json["transaction"]["memo"] == "Updated memo" assert request_json["transaction"]["cleared"] == "cleared" assert "transaction" in response assert response["transaction"]["id"] == "transaction-id" @pytest.mark.asyncio @respx.mock async def test_get_budget_summary_without_month(client): """Test that get_budget_summary without month specification calls the YNAB API correctly.""" route = respx.get("https://api.ynab.com/v1/budgets/budget-id").mock( return_value=httpx.Response( 200, json={ "data": { "budget": { "id": "budget-id", "name": "My Budget", "last_modified_on": "2023-05-15T12:34:56+00:00", "currency_format": {"iso_code": "USD"}, "accounts": [], "months": [], } } }, ) ) response = await client.get_budget_summary("budget-id") assert route.called assert "budget" in response assert response["budget"]["name"] == "My Budget" @pytest.mark.asyncio @respx.mock async def test_get_budget_summary_with_month(client): """Test that get_budget_summary with month specification calls the YNAB API correctly.""" route = respx.get("https://api.ynab.com/v1/budgets/budget-id/months/2023-05").mock( return_value=httpx.Response( 200, json={ "data": { "month": { "month": "2023-05-01", "income": 500000, "budgeted": 450000, "activity": -400000, "to_be_budgeted": 50000, "categories": [], } } }, ) ) response = await client.get_budget_summary("budget-id", "2023-05") assert route.called assert "month" in response assert response["month"]["month"] == "2023-05-01"

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ntdef/ynab-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server