From 4c67f5844bde1a5e5ed0b61830ce4efc95daa499 Mon Sep 17 00:00:00 2001 From: Savely Krendelhoff Date: Sat, 6 Sep 2025 16:09:22 +0700 Subject: [PATCH] Add app layer tests --- app/convert_currency_test.go | 146 +++++++++++++++++++++++++++++++++++ app/mocks/RateProvider.go | 57 ++++++++++++++ 2 files changed, 203 insertions(+) create mode 100644 app/convert_currency_test.go create mode 100644 app/mocks/RateProvider.go diff --git a/app/convert_currency_test.go b/app/convert_currency_test.go new file mode 100644 index 0000000..fae27f1 --- /dev/null +++ b/app/convert_currency_test.go @@ -0,0 +1,146 @@ +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") + }) + } +} diff --git a/app/mocks/RateProvider.go b/app/mocks/RateProvider.go new file mode 100644 index 0000000..ccf4d37 --- /dev/null +++ b/app/mocks/RateProvider.go @@ -0,0 +1,57 @@ +// Code generated by mockery v2.53.5. DO NOT EDIT. + +package mocks + +import ( + context "context" + coinmarketcap "converter/infrastructure/coinmarketcap" + + mock "github.com/stretchr/testify/mock" +) + +// RateProvider is an autogenerated mock type for the RateProvider type +type RateProvider struct { + mock.Mock +} + +// GetRate provides a mock function with given fields: ctx, fromCode, toCode +func (_m *RateProvider) GetRate(ctx context.Context, fromCode string, toCode string) (coinmarketcap.RateDTO, error) { + ret := _m.Called(ctx, fromCode, toCode) + + if len(ret) == 0 { + panic("no return value specified for GetRate") + } + + var r0 coinmarketcap.RateDTO + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) (coinmarketcap.RateDTO, error)); ok { + return rf(ctx, fromCode, toCode) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) coinmarketcap.RateDTO); ok { + r0 = rf(ctx, fromCode, toCode) + } else { + r0 = ret.Get(0).(coinmarketcap.RateDTO) + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, fromCode, toCode) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewRateProvider creates a new instance of RateProvider. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewRateProvider(t interface { + mock.TestingT + Cleanup(func()) +}) *RateProvider { + mock := &RateProvider{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +}