Skip to main content
Glama

YNAB MCP Server

by calebl
BudgetSummaryTool.test.ts12.3 kB
import { describe, it, expect, beforeEach, vi, Mock } from 'vitest'; import * as ynab from 'ynab'; import BudgetSummaryTool from '../tools/BudgetSummaryTool'; // Mock the entire ynab module vi.mock('ynab'); // Mock the mcp-framework logger vi.mock('mcp-framework', () => ({ MCPTool: class { constructor() {} }, logger: { error: vi.fn(), info: vi.fn(), }, })); describe('BudgetSummaryTool', () => { let tool: BudgetSummaryTool; let mockApi: { accounts: { getAccounts: Mock; }; months: { getBudgetMonth: Mock; }; }; beforeEach(() => { vi.clearAllMocks(); // Create mock API instance mockApi = { accounts: { getAccounts: vi.fn(), }, months: { getBudgetMonth: vi.fn(), }, }; // Mock the ynab.API constructor (ynab.API as any).mockImplementation(() => mockApi); // Set environment variables process.env.YNAB_API_TOKEN = 'test-token'; process.env.YNAB_BUDGET_ID = 'test-budget-id'; tool = new BudgetSummaryTool(); }); describe('execute', () => { const mockAccounts = [ { id: 'account-1', name: 'Checking Account', type: 'checking', balance: 150000, // $150.00 deleted: false, closed: false, }, { id: 'account-2', name: 'Savings Account', type: 'savings', balance: 500000, // $500.00 deleted: false, closed: false, }, { id: 'account-3', name: 'Deleted Account', type: 'checking', balance: 0, deleted: true, closed: false, }, { id: 'account-4', name: 'Closed Account', type: 'checking', balance: 0, deleted: false, closed: true, }, ]; const mockCategories = [ { id: 'category-1', name: 'Groceries', balance: -25000, // -$25.00 (overspent) budgeted: 30000, // $30.00 activity: -55000, // -$55.00 deleted: false, hidden: false, }, { id: 'category-2', name: 'Gas', balance: 5000, // $5.00 (positive balance) budgeted: 20000, // $20.00 activity: -15000, // -$15.00 deleted: false, hidden: false, }, { id: 'category-3', name: 'Deleted Category', balance: 0, budgeted: 0, activity: 0, deleted: true, hidden: false, }, { id: 'category-4', name: 'Hidden Category', balance: 0, budgeted: 0, activity: 0, deleted: false, hidden: true, }, ]; const mockMonthBudget = { month: '2023-12-01', income: 400000, // $400.00 budgeted: 350000, // $350.00 activity: -300000, // -$300.00 to_be_budgeted: 50000, // $50.00 age_of_money: 25, note: 'December budget', categories: mockCategories, }; it('should successfully get budget summary with budget ID from input', async () => { // Setup mocks mockApi.accounts.getAccounts.mockResolvedValue({ data: { accounts: mockAccounts }, }); mockApi.months.getBudgetMonth.mockResolvedValue({ data: { month: mockMonthBudget }, }); const input = { budgetId: 'custom-budget-id', month: '2023-12-01', }; const result = await tool.execute(input); expect(mockApi.accounts.getAccounts).toHaveBeenCalledWith('custom-budget-id'); expect(mockApi.months.getBudgetMonth).toHaveBeenCalledWith('custom-budget-id', '2023-12-01'); expect(result).toHaveProperty('monthBudget'); expect(result).toHaveProperty('accounts'); expect(result).toHaveProperty('note', 'Divide all numbers by 1000 to get the balance in dollars.'); if (typeof result === 'object') { expect(result.monthBudget).toEqual(mockMonthBudget); expect(result.accounts).toHaveLength(2); // Only non-deleted, non-closed accounts expect(result.accounts).toEqual([ mockAccounts[0], // Checking Account mockAccounts[1], // Savings Account ]); } }); it('should successfully get budget summary with budget ID from environment', async () => { // Setup mocks mockApi.accounts.getAccounts.mockResolvedValue({ data: { accounts: mockAccounts }, }); mockApi.months.getBudgetMonth.mockResolvedValue({ data: { month: mockMonthBudget }, }); const input = { month: 'current', }; const result = await tool.execute(input); expect(mockApi.accounts.getAccounts).toHaveBeenCalledWith('test-budget-id'); expect(mockApi.months.getBudgetMonth).toHaveBeenCalledWith('test-budget-id', 'current'); expect(result).toHaveProperty('monthBudget'); expect(result).toHaveProperty('accounts'); expect(result).toHaveProperty('note', 'Divide all numbers by 1000 to get the balance in dollars.'); }); it('should filter out deleted and closed accounts', async () => { // Setup mocks mockApi.accounts.getAccounts.mockResolvedValue({ data: { accounts: mockAccounts }, }); mockApi.months.getBudgetMonth.mockResolvedValue({ data: { month: mockMonthBudget }, }); const input = { budgetId: 'test-budget-id', month: 'current', }; const result = await tool.execute(input); expect(typeof result).toBe('object'); if (typeof result === 'object') { expect(result.accounts).toHaveLength(2); expect(result.accounts.every((account: any) => !account.deleted && !account.closed)).toBe(true); } }); it('should filter out deleted and hidden categories', async () => { // Setup mocks mockApi.accounts.getAccounts.mockResolvedValue({ data: { accounts: mockAccounts }, }); mockApi.months.getBudgetMonth.mockResolvedValue({ data: { month: mockMonthBudget }, }); const input = { budgetId: 'test-budget-id', month: 'current', }; const result = await tool.execute(input); // Categories should be filtered in the monthBudget.categories const visibleCategories = mockMonthBudget.categories.filter( cat => !cat.deleted && !cat.hidden ); expect(visibleCategories).toHaveLength(2); expect(visibleCategories.every(cat => !cat.deleted && !cat.hidden)).toBe(true); }); it('should return error when no budget ID is provided', async () => { // Clear environment budget ID delete process.env.YNAB_BUDGET_ID; tool = new BudgetSummaryTool(); const input = { month: 'current', }; const result = await tool.execute(input); expect(result).toBe( 'No budget ID provided. Please provide a budget ID or set the YNAB_BUDGET_ID environment variable. Use the ListBudgets tool to get a list of available budgets.' ); }); it('should handle API error when getting accounts', async () => { // Setup mock to throw API error const apiError = new Error('API Error: Budget not found'); mockApi.accounts.getAccounts.mockRejectedValue(apiError); const input = { budgetId: 'invalid-budget-id', month: 'current', }; const result = await tool.execute(input); // Error objects get serialized as {} by JSON.stringify expect(result).toMatch(/Error getting budget invalid-budget-id: {}/); }); it('should handle API error when getting budget month', async () => { // Setup mocks - accounts succeeds, month fails mockApi.accounts.getAccounts.mockResolvedValue({ data: { accounts: mockAccounts }, }); const apiError = new Error('API Error: Month not found'); mockApi.months.getBudgetMonth.mockRejectedValue(apiError); const input = { budgetId: 'test-budget-id', month: '2025-01-01', }; const result = await tool.execute(input); // Error objects get serialized as {} by JSON.stringify expect(result).toMatch(/Error getting budget test-budget-id: {}/); }); it('should handle non-Error objects in catch block', async () => { // Setup mock to throw non-Error object const nonErrorObject = { message: 'Custom error object', code: 500 }; mockApi.accounts.getAccounts.mockRejectedValue(nonErrorObject); const input = { budgetId: 'test-budget-id', month: 'current', }; const result = await tool.execute(input); expect(result).toMatch(/Error getting budget test-budget-id: {"message":"Custom error object","code":500}/); }); it('should use current month as default when month not specified', async () => { // Setup mocks mockApi.accounts.getAccounts.mockResolvedValue({ data: { accounts: mockAccounts }, }); mockApi.months.getBudgetMonth.mockResolvedValue({ data: { month: mockMonthBudget }, }); const input = { budgetId: 'test-budget-id', month: 'current', // explicitly set to current since the framework may not apply defaults }; const result = await tool.execute(input); expect(mockApi.months.getBudgetMonth).toHaveBeenCalledWith('test-budget-id', 'current'); expect(result).toHaveProperty('monthBudget'); }); it('should handle empty accounts array', async () => { // Setup mocks with empty accounts mockApi.accounts.getAccounts.mockResolvedValue({ data: { accounts: [] }, }); mockApi.months.getBudgetMonth.mockResolvedValue({ data: { month: mockMonthBudget }, }); const input = { budgetId: 'test-budget-id', month: 'current', }; const result = await tool.execute(input); expect(typeof result).toBe('object'); if (typeof result === 'object') { expect(result.accounts).toHaveLength(0); expect(result).toHaveProperty('monthBudget'); } }); it('should handle empty categories array', async () => { // Setup mocks with empty categories const emptyMonthBudget = { ...mockMonthBudget, categories: [], }; mockApi.accounts.getAccounts.mockResolvedValue({ data: { accounts: mockAccounts }, }); mockApi.months.getBudgetMonth.mockResolvedValue({ data: { month: emptyMonthBudget }, }); const input = { budgetId: 'test-budget-id', month: 'current', }; const result = await tool.execute(input); expect(typeof result).toBe('object'); if (typeof result === 'object') { expect(result.monthBudget.categories).toHaveLength(0); expect(result).toHaveProperty('accounts'); } }); }); describe('tool configuration', () => { it('should have correct name and description', () => { expect(tool.name).toBe('budget_summary'); expect(tool.description).toBe( 'Get a summary of the budget for a specific month highlighting overspent categories that need attention and categories with a positive balance that are doing well.' ); }); it('should have correct schema definition', () => { expect(tool.schema).toHaveProperty('budgetId'); expect(tool.schema).toHaveProperty('month'); expect(tool.schema.budgetId.description).toContain('budget to get a summary for'); expect(tool.schema.month.description).toContain('budget month in ISO format'); expect(tool.schema.month.default).toBe('current'); }); it('should have correct month regex pattern', () => { // Test that the regex accepts valid formats const regex = /^(current|\d{4}-\d{2}-\d{2})$/; expect(regex.test('current')).toBe(true); expect(regex.test('2023-12-01')).toBe(true); expect(regex.test('2024-01-15')).toBe(true); // Test invalid formats expect(regex.test('invalid')).toBe(false); expect(regex.test('2023-12')).toBe(false); expect(regex.test('23-12-01')).toBe(false); expect(regex.test('2023/12/01')).toBe(false); }); }); });

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/calebl/ynab-mcp-server'

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