diff --git a/Makefile b/Makefile index a2d6de6d..7380184c 100644 --- a/Makefile +++ b/Makefile @@ -59,6 +59,7 @@ run-unprocessed-events-replay-local: generate-mock-interface: cd internal/db && mockery --name=DBClient --output=../../tests/mocks --outpkg=mocks --filename=mock_db_client.go cd internal/clients/ordinals && mockery --name=OrdinalsClientInterface --output=../../../tests/mocks --outpkg=mocks --filename=mock_ordinal_client.go + cd internal/clients/coinmarketcap && mockery --name=CoinMarketCapClientInterface --output=../../../tests/mocks --outpkg=mocks --filename=mock_coinmarketcap_client.go test: ./bin/local-startup.sh; diff --git a/internal/services/stats.go b/internal/services/stats.go index 26d21ea9..218627fc 100644 --- a/internal/services/stats.go +++ b/internal/services/stats.go @@ -188,7 +188,7 @@ func (s *Services) GetOverallStats( // Only fetch BTC price if ExternalAPIs are configured var btcPrice *float64 - if s.cfg.ExternalAPIs != nil { + if s.cfg.ExternalAPIs != nil && s.cfg.ExternalAPIs.CoinMarketCap != nil { price, err := s.GetLatestBtcPriceUsd(ctx) if err != nil { log.Ctx(ctx).Error().Err(err).Msg("error while fetching latest btc price") diff --git a/tests/config/config-test.yml b/tests/config/config-test.yml index db0e31c9..14621999 100644 --- a/tests/config/config-test.yml +++ b/tests/config/config-test.yml @@ -36,3 +36,9 @@ assets: timeout: 100 terms_acceptance_logging: enabled: true +external_apis: + coinmarketcap: + api_key: "c382e883-36c9-401b-85c3-ff4607f473a4" + base_url: "https://pro-api.coinmarketcap.com/v1" + timeout: 10s # http client timeout + cache_ttl: 9s # mongodb ttl diff --git a/tests/integration_test/stats_test.go b/tests/integration_test/stats_test.go index 1bc312a6..380fd32c 100644 --- a/tests/integration_test/stats_test.go +++ b/tests/integration_test/stats_test.go @@ -11,12 +11,16 @@ import ( "time" "github.com/babylonlabs-io/staking-api-service/internal/api/handlers" + "github.com/babylonlabs-io/staking-api-service/internal/clients" "github.com/babylonlabs-io/staking-api-service/internal/config" "github.com/babylonlabs-io/staking-api-service/internal/db/model" "github.com/babylonlabs-io/staking-api-service/internal/services" + "github.com/babylonlabs-io/staking-api-service/internal/types" + "github.com/babylonlabs-io/staking-api-service/tests/mocks" "github.com/babylonlabs-io/staking-api-service/tests/testutils" "github.com/babylonlabs-io/staking-queue-client/client" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" ) @@ -466,6 +470,108 @@ func FuzzTestTopStakersWithPaginationResponse(f *testing.F) { }) } +func TestOverallStatsEndpointBTCPriceIntegration(t *testing.T) { + t.Run("should return BTC price when successfully fetched from CoinMarketCap", func(t *testing.T) { + // Setup mock + mockCMC := new(mocks.CoinMarketCapClientInterface) + mockCMC.On("GetLatestBtcPrice", mock.Anything). + Return(42000.50, nil) + + mockedClients := &clients.Clients{ + CoinMarketCap: mockCMC, + } + + testServer := setupTestServer(t, &TestServerDependency{ + MockedClients: mockedClients, + }) + defer testServer.Close() + + // Fetch stats and verify + stats := fetchOverallStatsEndpoint(t, testServer) + assert.NotNil(t, stats.BtcPriceUsd) + assert.Equal(t, 42000.50, *stats.BtcPriceUsd) + mockCMC.AssertExpectations(t) + }) + + t.Run("should return nil BTC price when CoinMarketCap fetch fails", func(t *testing.T) { + mockCMC := new(mocks.CoinMarketCapClientInterface) + mockCMC.On("GetLatestBtcPrice", mock.Anything). + Return(0.0, types.NewErrorWithMsg( + http.StatusInternalServerError, + types.InternalServiceError, + "failed to fetch BTC price", + )) + + mockedClients := &clients.Clients{ + CoinMarketCap: mockCMC, + } + + testServer := setupTestServer(t, &TestServerDependency{ + MockedClients: mockedClients, + }) + defer testServer.Close() + + // Fetch stats and verify + stats := fetchOverallStatsEndpoint(t, testServer) + assert.Nil(t, stats.BtcPriceUsd) + mockCMC.AssertExpectations(t) + }) + + t.Run("should return nil BTC price when external APIs are disabled", func(t *testing.T) { + // Setup mock + mockCMC := new(mocks.CoinMarketCapClientInterface) + mockCMC.On("GetLatestBtcPrice", mock.Anything). + Return(42000.50, nil) + + mockedClients := &clients.Clients{ + CoinMarketCap: mockCMC, + } + + testConfig, err := config.New("../config/config-test.yml") + if err != nil { + t.Fatalf("Failed to load test config: %v", err) + } + testConfig.ExternalAPIs = nil + + testServer := setupTestServer(t, &TestServerDependency{ + ConfigOverrides: testConfig, + MockedClients: mockedClients, + }) + defer testServer.Close() + + // Fetch stats and verify + stats := fetchOverallStatsEndpoint(t, testServer) + assert.Nil(t, stats.BtcPriceUsd) + }) + + t.Run("should return nil BTC price when CoinMarketCap is disabled", func(t *testing.T) { + // Setup mock + mockCMC := new(mocks.CoinMarketCapClientInterface) + mockCMC.On("GetLatestBtcPrice", mock.Anything). + Return(42000.50, nil) + + mockedClients := &clients.Clients{ + CoinMarketCap: mockCMC, + } + + testConfig, err := config.New("../config/config-test.yml") + if err != nil { + t.Fatalf("Failed to load test config: %v", err) + } + testConfig.ExternalAPIs.CoinMarketCap = nil + + testServer := setupTestServer(t, &TestServerDependency{ + ConfigOverrides: testConfig, + MockedClients: mockedClients, + }) + defer testServer.Close() + + // Fetch stats and verify + stats := fetchOverallStatsEndpoint(t, testServer) + assert.Nil(t, stats.BtcPriceUsd) + }) +} + func fetchFinalityEndpoint(t *testing.T, testServer *TestServer) []services.FpDetailsPublic { url := testServer.Server.URL + finalityProvidersPath // Make a GET request to the finality providers endpoint diff --git a/tests/mocks/mock_coinmarketcap_client.go b/tests/mocks/mock_coinmarketcap_client.go new file mode 100644 index 00000000..e4aab81f --- /dev/null +++ b/tests/mocks/mock_coinmarketcap_client.go @@ -0,0 +1,59 @@ +// Code generated by mockery v2.44.1. DO NOT EDIT. + +package mocks + +import ( + context "context" + + types "github.com/babylonlabs-io/staking-api-service/internal/types" + mock "github.com/stretchr/testify/mock" +) + +// CoinMarketCapClientInterface is an autogenerated mock type for the CoinMarketCapClientInterface type +type CoinMarketCapClientInterface struct { + mock.Mock +} + +// GetLatestBtcPrice provides a mock function with given fields: ctx +func (_m *CoinMarketCapClientInterface) GetLatestBtcPrice(ctx context.Context) (float64, *types.Error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for GetLatestBtcPrice") + } + + var r0 float64 + var r1 *types.Error + if rf, ok := ret.Get(0).(func(context.Context) (float64, *types.Error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) float64); ok { + r0 = rf(ctx) + } else { + r0 = ret.Get(0).(float64) + } + + if rf, ok := ret.Get(1).(func(context.Context) *types.Error); ok { + r1 = rf(ctx) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*types.Error) + } + } + + return r0, r1 +} + +// NewCoinMarketCapClientInterface creates a new instance of CoinMarketCapClientInterface. 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 NewCoinMarketCapClientInterface(t interface { + mock.TestingT + Cleanup(func()) +}) *CoinMarketCapClientInterface { + mock := &CoinMarketCapClientInterface{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +}