package app import ( "context" "converter/app/mocks" "converter/domain" "converter/infrastructure/coinmarketcap" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) func TestConvertCurrencyUseCase_Execute(t *testing.T) { tests := []struct { name string amount string fromCode string toCode string wantCode string wantErr bool setupMock func(*mocks.RateProvider) }{ { name: "direct conversion - crypto to fiat", amount: "1.0", fromCode: "BTC", toCode: "USD", wantCode: "USD", wantErr: false, setupMock: func(m *mocks.RateProvider) { // Direct rate: BTC->USD rateDTO := coinmarketcap.RateDTO{ FromCode: "BTC", // Crypto symbol ToCode: "USD", // Fiat code FromName: "Bitcoin", // Crypto name from API ToName: "USD", // Fiat code Price: 50000.0, // BTC price in USD Source: "coinmarketcap", } m.On("GetRate", mock.Anything, "BTC", "USD").Return(rateDTO, nil) }, }, { name: "reversed conversion - fiat to crypto", amount: "50000.0", fromCode: "USD", toCode: "BTC", wantCode: "BTC", wantErr: false, setupMock: func(m *mocks.RateProvider) { // Reversed rate: requested USD->BTC but got BTC->USD rateDTO := coinmarketcap.RateDTO{ FromCode: "BTC", // What API actually returned ToCode: "USD", // What API actually returned FromName: "Bitcoin", // Crypto name from API ToName: "USD", // Fiat code Price: 50000.0, // BTC price in USD (will be inverted) Source: "coinmarketcap", } m.On("GetRate", mock.Anything, "USD", "BTC").Return(rateDTO, nil) }, }, { name: "invalid amount", amount: "invalid", fromCode: "USD", toCode: "BTC", wantCode: "", wantErr: true, setupMock: func(m *mocks.RateProvider) { // No mock setup needed - error occurs before calling provider }, }, { name: "negative amount", amount: "-50.00", fromCode: "USD", toCode: "BTC", wantCode: "", wantErr: true, setupMock: func(m *mocks.RateProvider) { // No mock setup needed - error occurs before calling provider }, }, { name: "rate not found", amount: "100.00", fromCode: "XYZ", toCode: "ABC", wantCode: "", wantErr: true, setupMock: func(m *mocks.RateProvider) { m.On("GetRate", mock.Anything, "XYZ", "ABC").Return(coinmarketcap.RateDTO{}, domain.ErrRateNotFound) }, }, { name: "provider error", amount: "100.00", fromCode: "USD", toCode: "INVALID", wantCode: "", wantErr: true, setupMock: func(m *mocks.RateProvider) { m.On("GetRate", mock.Anything, "USD", "INVALID").Return(coinmarketcap.RateDTO{}, domain.ErrUnsupportedCurrency) }, }, { name: "empty currency code", amount: "100.00", fromCode: "", toCode: "BTC", wantCode: "", wantErr: true, setupMock: func(m *mocks.RateProvider) { // No mock setup needed - error occurs before calling provider }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Create fresh mock and use case for each test mockProvider := mocks.NewRateProvider(t) converter := domain.NewCurrencyConverter() useCase := NewConvertCurrencyUseCase(mockProvider, converter) // Setup mock expectations tt.setupMock(mockProvider) // Execute result, err := useCase.Execute(context.Background(), tt.amount, tt.fromCode, tt.toCode) // Assert error expectations if tt.wantErr { assert.Error(t, err, "Execute() should return error") return } // Assert success case assert.NoError(t, err, "Execute() should not return error") assert.Equal(t, tt.wantCode, result.Currency.Code, "Currency code mismatch") assert.True(t, result.Amount.IsPositive(), "Amount should be positive") }) } }