Skip to main content
Glama

YNAB MCP Server

by calebl
CreateTransactionTool.test.ts15.9 kB
import { describe, it, expect, beforeEach, vi, Mock } from 'vitest'; import * as ynab from 'ynab'; import CreateTransactionTool from '../tools/CreateTransactionTool'; // 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('CreateTransactionTool', () => { let tool: CreateTransactionTool; let mockApi: { transactions: { createTransaction: Mock; }; }; beforeEach(() => { vi.clearAllMocks(); // Create mock API instance mockApi = { transactions: { createTransaction: 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 CreateTransactionTool(); }); describe('execute', () => { const mockCreatedTransaction = { id: 'transaction-123', account_id: 'account-456', payee_name: 'Test Payee', amount: -50000, // -$50.00 in milliunits category_id: 'category-789', memo: 'Test memo', date: '2023-12-01', cleared: ynab.TransactionClearedStatus.Uncleared, approved: false, flag_color: null, }; it('should successfully create a transaction with payee_name', async () => { // Setup mock mockApi.transactions.createTransaction.mockResolvedValue({ data: { transaction: mockCreatedTransaction }, }); const input = { budgetId: 'custom-budget-id', accountId: 'account-456', date: '2023-12-01', amount: -50.00, payeeName: 'Test Payee', categoryId: 'category-789', memo: 'Test memo', cleared: false, approved: false, }; const result = await tool.execute(input); expect(mockApi.transactions.createTransaction).toHaveBeenCalledWith( 'custom-budget-id', { transaction: { account_id: 'account-456', date: '2023-12-01', amount: -50000, // Converted to milliunits payee_id: undefined, payee_name: 'Test Payee', category_id: 'category-789', memo: 'Test memo', cleared: ynab.TransactionClearedStatus.Uncleared, approved: false, flag_color: undefined, }, } ); expect(result).toEqual({ success: true, transactionId: 'transaction-123', message: 'Transaction created successfully', }); }); it('should successfully create a transaction with payee_id', async () => { // Setup mock mockApi.transactions.createTransaction.mockResolvedValue({ data: { transaction: mockCreatedTransaction }, }); const input = { budgetId: 'test-budget-id', accountId: 'account-456', date: '2023-12-01', amount: 25.99, payeeId: 'payee-123', categoryId: 'category-789', }; const result = await tool.execute(input); expect(mockApi.transactions.createTransaction).toHaveBeenCalledWith( 'test-budget-id', { transaction: { account_id: 'account-456', date: '2023-12-01', amount: 25990, // Converted to milliunits (25.99 * 1000) payee_id: 'payee-123', payee_name: undefined, category_id: 'category-789', memo: undefined, cleared: ynab.TransactionClearedStatus.Uncleared, approved: false, flag_color: undefined, }, } ); expect(result).toEqual({ success: true, transactionId: 'transaction-123', message: 'Transaction created successfully', }); }); it('should use budget ID from environment when not provided in input', async () => { // Setup mock mockApi.transactions.createTransaction.mockResolvedValue({ data: { transaction: mockCreatedTransaction }, }); const input = { accountId: 'account-456', date: '2023-12-01', amount: 10.50, payeeName: 'Test Payee', }; const result = await tool.execute(input); expect(mockApi.transactions.createTransaction).toHaveBeenCalledWith( 'test-budget-id', // Should use environment variable expect.any(Object) ); expect(result).toEqual({ success: true, transactionId: 'transaction-123', message: 'Transaction created successfully', }); }); it('should create a transaction with all optional parameters', async () => { // Setup mock mockApi.transactions.createTransaction.mockResolvedValue({ data: { transaction: mockCreatedTransaction }, }); const input = { budgetId: 'test-budget-id', accountId: 'account-456', date: '2023-12-01', amount: 75.25, payeeId: 'payee-123', categoryId: 'category-789', memo: 'Test transaction with all fields', cleared: true, approved: true, flagColor: 'red', }; const result = await tool.execute(input); expect(mockApi.transactions.createTransaction).toHaveBeenCalledWith( 'test-budget-id', { transaction: { account_id: 'account-456', date: '2023-12-01', amount: 75250, // 75.25 * 1000 payee_id: 'payee-123', payee_name: undefined, category_id: 'category-789', memo: 'Test transaction with all fields', cleared: ynab.TransactionClearedStatus.Cleared, approved: true, flag_color: 'red', }, } ); expect(result).toEqual({ success: true, transactionId: 'transaction-123', message: 'Transaction created successfully', }); }); it('should handle decimal amounts correctly (rounding)', async () => { // Setup mock mockApi.transactions.createTransaction.mockResolvedValue({ data: { transaction: mockCreatedTransaction }, }); const input = { budgetId: 'test-budget-id', accountId: 'account-456', date: '2023-12-01', amount: 12.996, // Should round to 12996 milliunits payeeName: 'Test Payee', }; const result = await tool.execute(input); expect(mockApi.transactions.createTransaction).toHaveBeenCalledWith( 'test-budget-id', { transaction: expect.objectContaining({ amount: 12996, // Math.round(12.996 * 1000) = 12996 }), } ); expect(result.success).toBe(true); }); it('should throw error when no budget ID is provided', async () => { // Clear environment budget ID delete process.env.YNAB_BUDGET_ID; tool = new CreateTransactionTool(); const input = { accountId: 'account-456', date: '2023-12-01', amount: 50.00, payeeName: 'Test Payee', }; await expect(tool.execute(input)).rejects.toThrow( 'No budget ID provided. Please provide a budget ID or set the YNAB_BUDGET_ID environment variable.' ); }); it('should throw error when neither payee_id nor payee_name is provided', async () => { const input = { budgetId: 'test-budget-id', accountId: 'account-456', date: '2023-12-01', amount: 50.00, // Neither payeeId nor payeeName provided }; await expect(tool.execute(input)).rejects.toThrow( 'Either payee_id or payee_name must be provided' ); }); it('should return success false when API call fails', async () => { // Setup mock to throw API error const apiError = new Error('API Error: Budget not found'); mockApi.transactions.createTransaction.mockRejectedValue(apiError); const input = { budgetId: 'invalid-budget-id', accountId: 'account-456', date: '2023-12-01', amount: 50.00, payeeName: 'Test Payee', }; const result = await tool.execute(input); expect(result).toEqual({ success: false, error: 'API Error: Budget not found', }); }); it('should return success false when API returns no transaction data', async () => { // Setup mock to return no transaction mockApi.transactions.createTransaction.mockResolvedValue({ data: { transaction: null }, }); const input = { budgetId: 'test-budget-id', accountId: 'account-456', date: '2023-12-01', amount: 50.00, payeeName: 'Test Payee', }; const result = await tool.execute(input); expect(result).toEqual({ success: false, error: 'Failed to create transaction - no transaction data returned', }); }); 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.transactions.createTransaction.mockRejectedValue(nonErrorObject); const input = { budgetId: 'test-budget-id', accountId: 'account-456', date: '2023-12-01', amount: 50.00, payeeName: 'Test Payee', }; const result = await tool.execute(input); expect(result).toEqual({ success: false, error: 'Unknown error occurred', }); }); it('should handle cleared status correctly when false', async () => { // Setup mock mockApi.transactions.createTransaction.mockResolvedValue({ data: { transaction: mockCreatedTransaction }, }); const input = { budgetId: 'test-budget-id', accountId: 'account-456', date: '2023-12-01', amount: 50.00, payeeName: 'Test Payee', cleared: false, }; const result = await tool.execute(input); expect(mockApi.transactions.createTransaction).toHaveBeenCalledWith( 'test-budget-id', { transaction: expect.objectContaining({ cleared: ynab.TransactionClearedStatus.Uncleared, }), } ); expect(result.success).toBe(true); }); it('should handle cleared status correctly when true', async () => { // Setup mock mockApi.transactions.createTransaction.mockResolvedValue({ data: { transaction: mockCreatedTransaction }, }); const input = { budgetId: 'test-budget-id', accountId: 'account-456', date: '2023-12-01', amount: 50.00, payeeName: 'Test Payee', cleared: true, }; const result = await tool.execute(input); expect(mockApi.transactions.createTransaction).toHaveBeenCalledWith( 'test-budget-id', { transaction: expect.objectContaining({ cleared: ynab.TransactionClearedStatus.Cleared, }), } ); expect(result.success).toBe(true); }); it('should handle approved status with nullish coalescing', async () => { // Setup mock mockApi.transactions.createTransaction.mockResolvedValue({ data: { transaction: mockCreatedTransaction }, }); const input = { budgetId: 'test-budget-id', accountId: 'account-456', date: '2023-12-01', amount: 50.00, payeeName: 'Test Payee', // approved not provided, should default to false }; const result = await tool.execute(input); expect(mockApi.transactions.createTransaction).toHaveBeenCalledWith( 'test-budget-id', { transaction: expect.objectContaining({ approved: false, // Should be false when not provided }), } ); expect(result.success).toBe(true); }); it('should handle flag color as enum value', async () => { // Setup mock mockApi.transactions.createTransaction.mockResolvedValue({ data: { transaction: mockCreatedTransaction }, }); const input = { budgetId: 'test-budget-id', accountId: 'account-456', date: '2023-12-01', amount: 50.00, payeeName: 'Test Payee', flagColor: 'blue', }; const result = await tool.execute(input); expect(mockApi.transactions.createTransaction).toHaveBeenCalledWith( 'test-budget-id', { transaction: expect.objectContaining({ flag_color: 'blue', }), } ); expect(result.success).toBe(true); }); }); describe('tool configuration', () => { it('should have correct name and description', () => { expect(tool.name).toBe('create_transaction'); expect(tool.description).toBe('Creates a new transaction in your YNAB budget. Either payee_id or payee_name must be provided in addition to the other required fields.'); }); it('should have correct schema definition', () => { expect(tool.schema).toHaveProperty('budgetId'); expect(tool.schema).toHaveProperty('accountId'); expect(tool.schema).toHaveProperty('date'); expect(tool.schema).toHaveProperty('amount'); expect(tool.schema).toHaveProperty('payeeId'); expect(tool.schema).toHaveProperty('payeeName'); expect(tool.schema).toHaveProperty('categoryId'); expect(tool.schema).toHaveProperty('memo'); expect(tool.schema).toHaveProperty('cleared'); expect(tool.schema).toHaveProperty('approved'); expect(tool.schema).toHaveProperty('flagColor'); // Check descriptions contain expected content expect(tool.schema.budgetId.description).toContain('budget to create the transaction in'); expect(tool.schema.accountId.description).toContain('account to create the transaction in'); expect(tool.schema.date.description).toContain('date of the transaction in ISO format'); expect(tool.schema.amount.description).toContain('amount in dollars'); expect(tool.schema.payeeId.description).toContain('payee_name is provided'); expect(tool.schema.payeeName.description).toContain('payee_id is provided'); expect(tool.schema.categoryId.description).toContain('category id'); expect(tool.schema.memo.description).toContain('memo/note'); expect(tool.schema.cleared.description).toContain('Whether the transaction is cleared'); expect(tool.schema.approved.description).toContain('Whether the transaction is approved'); expect(tool.schema.flagColor.description).toContain('transaction flag color'); }); it('should have correct required vs optional fields', () => { // Required fields (no optional() call) expect(tool.schema.accountId.type._def.typeName).toBe('ZodString'); expect(tool.schema.date.type._def.typeName).toBe('ZodString'); expect(tool.schema.amount.type._def.typeName).toBe('ZodNumber'); // Optional fields (have optional() call) expect(tool.schema.budgetId.type._def.typeName).toBe('ZodOptional'); expect(tool.schema.payeeId.type._def.typeName).toBe('ZodOptional'); expect(tool.schema.payeeName.type._def.typeName).toBe('ZodOptional'); expect(tool.schema.categoryId.type._def.typeName).toBe('ZodOptional'); expect(tool.schema.memo.type._def.typeName).toBe('ZodOptional'); expect(tool.schema.cleared.type._def.typeName).toBe('ZodOptional'); expect(tool.schema.approved.type._def.typeName).toBe('ZodOptional'); expect(tool.schema.flagColor.type._def.typeName).toBe('ZodOptional'); }); }); });

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