Merge pull request #8101 from yyforyongyu/fix-web-fee-unit-test

chainfee: Fix test flake in `TestWebAPIFeeEstimator`
This commit is contained in:
Olaoluwa Osuntokun authored and GitHub committed 2023-10-30 10:50:16 -07:00
commit 19880ffe2c
3 files changed
+89 -84

No files matched your search

+58 -51
View File
@@ -35,6 +35,14 @@ const (
// maxFeeUpdateTimeout represents the maximum interval in which a // maxFeeUpdateTimeout represents the maximum interval in which a
// WebAPIEstimator will request fresh fees from its API. // WebAPIEstimator will request fresh fees from its API.
maxFeeUpdateTimeout = 20 * time.Minute maxFeeUpdateTimeout = 20 * time.Minute
// WebAPIConnectionTimeout specifies the timeout value for connecting
// to the api source.
WebAPIConnectionTimeout = 5 * time.Second
// WebAPIResponseTimeout specifies the timeout value for receiving a
// fee response from the api source.
WebAPIResponseTimeout = 10 * time.Second
) )
var ( var (
@@ -493,15 +501,9 @@ var _ Estimator = (*BitcoindEstimator)(nil)
// implementation of this interface in order to allow the WebAPIEstimator to // implementation of this interface in order to allow the WebAPIEstimator to
// be fully generic in its logic. // be fully generic in its logic.
type WebAPIFeeSource interface { type WebAPIFeeSource interface {
// GenQueryURL generates the full query URL. The value returned by this // GetFeeMap will query the web API, parse the response and return a
// method should be able to be used directly as a path for an HTTP GET // map of confirmation targets to sat/kw fees.
// request. GetFeeMap() (map[uint32]uint32, error)
GenQueryURL() string
// ParseResponse attempts to parse the body of the response generated
// by the above query URL. Typically this will be JSON, but the
// specifics are left to the WebAPIFeeSource implementation.
ParseResponse(r io.Reader) (map[uint32]uint32, error)
} }
// SparseConfFeeSource is an implementation of the WebAPIFeeSource that utilizes // SparseConfFeeSource is an implementation of the WebAPIFeeSource that utilizes
@@ -513,21 +515,12 @@ type SparseConfFeeSource struct {
URL string URL string
} }
// GenQueryURL generates the full query URL. The value returned by this // parseResponse attempts to parse the body of the response generated by the
// method should be able to be used directly as a path for an HTTP GET
// request.
//
// NOTE: Part of the WebAPIFeeSource interface.
func (s SparseConfFeeSource) GenQueryURL() string {
return s.URL
}
// ParseResponse attempts to parse the body of the response generated by the
// above query URL. Typically this will be JSON, but the specifics are left to // above query URL. Typically this will be JSON, but the specifics are left to
// the WebAPIFeeSource implementation. // the WebAPIFeeSource implementation.
// func (s SparseConfFeeSource) parseResponse(r io.Reader) (
// NOTE: Part of the WebAPIFeeSource interface. map[uint32]uint32, error) {
func (s SparseConfFeeSource) ParseResponse(r io.Reader) (map[uint32]uint32, error) {
type jsonResp struct { type jsonResp struct {
FeeByBlockTarget map[uint32]uint32 `json:"fee_by_block_target"` FeeByBlockTarget map[uint32]uint32 `json:"fee_by_block_target"`
} }
@@ -543,6 +536,47 @@ func (s SparseConfFeeSource) ParseResponse(r io.Reader) (map[uint32]uint32, erro
return resp.FeeByBlockTarget, nil return resp.FeeByBlockTarget, nil
} }
// GetFeeMap will query the web API, parse the response and return a map of
// confirmation targets to sat/kw fees.
func (s SparseConfFeeSource) GetFeeMap() (map[uint32]uint32, error) {
// Rather than use the default http.Client, we'll make a custom one
// which will allow us to control how long we'll wait to read the
// response from the service. This way, if the service is down or
// overloaded, we can exit early and use our default fee.
netTransport := &http.Transport{
Dial: (&net.Dialer{
Timeout: WebAPIConnectionTimeout,
}).Dial,
TLSHandshakeTimeout: WebAPIConnectionTimeout,
}
netClient := &http.Client{
Timeout: WebAPIResponseTimeout,
Transport: netTransport,
}
// With the client created, we'll query the API source to fetch the URL
// that we should use to query for the fee estimation.
targetURL := s.URL
resp, err := netClient.Get(targetURL)
if err != nil {
log.Errorf("unable to query web api for fee response: %v",
err)
return nil, err
}
defer resp.Body.Close()
// Once we've obtained the response, we'll instruct the WebAPIFeeSource
// to parse out the body to obtain our final result.
feesByBlockTarget, err := s.parseResponse(resp.Body)
if err != nil {
log.Errorf("unable to parse fee api response: %v", err)
return nil, err
}
return feesByBlockTarget, nil
}
// A compile-time assertion to ensure that SparseConfFeeSource implements the // A compile-time assertion to ensure that SparseConfFeeSource implements the
// WebAPIFeeSource interface. // WebAPIFeeSource interface.
var _ WebAPIFeeSource = (*SparseConfFeeSource)(nil) var _ WebAPIFeeSource = (*SparseConfFeeSource)(nil)
@@ -762,38 +796,11 @@ func (w *WebAPIEstimator) getCachedFee(numBlocks uint32) (uint32, error) {
// updateFeeEstimates re-queries the API for fresh fees and caches them. // updateFeeEstimates re-queries the API for fresh fees and caches them.
func (w *WebAPIEstimator) updateFeeEstimates() { func (w *WebAPIEstimator) updateFeeEstimates() {
// Rather than use the default http.Client, we'll make a custom one
// which will allow us to control how long we'll wait to read the
// response from the service. This way, if the service is down or
// overloaded, we can exit early and use our default fee.
netTransport := &http.Transport{
Dial: (&net.Dialer{
Timeout: 5 * time.Second,
}).Dial,
TLSHandshakeTimeout: 5 * time.Second,
}
netClient := &http.Client{
Timeout: time.Second * 10,
Transport: netTransport,
}
// With the client created, we'll query the API source to fetch the URL
// that we should use to query for the fee estimation.
targetURL := w.apiSource.GenQueryURL()
resp, err := netClient.Get(targetURL)
if err != nil {
log.Errorf("unable to query web api for fee response: %v",
err)
return
}
defer resp.Body.Close()
// Once we've obtained the response, we'll instruct the WebAPIFeeSource // Once we've obtained the response, we'll instruct the WebAPIFeeSource
// to parse out the body to obtain our final result. // to parse out the body to obtain our final result.
feesByBlockTarget, err := w.apiSource.ParseResponse(resp.Body) feesByBlockTarget, err := w.apiSource.GetFeeMap()
if err != nil { if err != nil {
log.Errorf("unable to query web api for fee response: %v", log.Errorf("unable to get fee response: %v", err)
err)
return return
} }
+14 -33
View File
@@ -3,27 +3,12 @@ package chainfee
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"io"
"reflect"
"testing" "testing"
"github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
type mockSparseConfFeeSource struct {
url string
fees map[uint32]uint32
}
func (e mockSparseConfFeeSource) GenQueryURL() string {
return e.url
}
func (e mockSparseConfFeeSource) ParseResponse(r io.Reader) (map[uint32]uint32, error) {
return e.fees, nil
}
// TestFeeRateTypes checks that converting fee rates between the // TestFeeRateTypes checks that converting fee rates between the
// different types that represent fee rates and calculating fees // different types that represent fee rates and calculating fees
// work as expected. // work as expected.
@@ -113,10 +98,6 @@ func TestSparseConfFeeSource(t *testing.T) {
// Test that GenQueryURL returns the URL as is. // Test that GenQueryURL returns the URL as is.
url := "test" url := "test"
feeSource := SparseConfFeeSource{URL: url} feeSource := SparseConfFeeSource{URL: url}
queryURL := feeSource.GenQueryURL()
if queryURL != url {
t.Fatalf("expected query URL of %v, got %v", url, queryURL)
}
// Test parsing a properly formatted JSON API response. // Test parsing a properly formatted JSON API response.
// First, create the response as a bytes.Reader. // First, create the response as a bytes.Reader.
@@ -125,17 +106,17 @@ func TestSparseConfFeeSource(t *testing.T) {
2: 42, 2: 42,
3: 54321, 3: 54321,
} }
testJSON := map[string]map[uint32]uint32{"fee_by_block_target": testFees} testJSON := map[string]map[uint32]uint32{
"fee_by_block_target": testFees,
}
jsonResp, err := json.Marshal(testJSON) jsonResp, err := json.Marshal(testJSON)
require.NoError(t, err, "unable to marshal JSON API response") require.NoError(t, err, "unable to marshal JSON API response")
reader := bytes.NewReader(jsonResp) reader := bytes.NewReader(jsonResp)
// Finally, ensure the expected map is returned without error. // Finally, ensure the expected map is returned without error.
fees, err := feeSource.ParseResponse(reader) fees, err := feeSource.parseResponse(reader)
require.NoError(t, err, "unable to parse API response") require.NoError(t, err, "unable to parse API response")
if !reflect.DeepEqual(fees, testFees) { require.Equal(t, testFees, fees, "unexpected fee map returned")
t.Fatalf("expected %v, got %v", testFees, fees)
}
// Test parsing an improperly formatted JSON API response. // Test parsing an improperly formatted JSON API response.
badFees := map[string]uint32{"hi": 12345, "hello": 42, "satoshi": 54321} badFees := map[string]uint32{"hi": 12345, "hello": 42, "satoshi": 54321}
@@ -145,10 +126,8 @@ func TestSparseConfFeeSource(t *testing.T) {
reader = bytes.NewReader(jsonResp) reader = bytes.NewReader(jsonResp)
// Finally, ensure the improperly formatted fees error. // Finally, ensure the improperly formatted fees error.
_, err = feeSource.ParseResponse(reader) _, err = feeSource.parseResponse(reader)
if err == nil { require.Error(t, err, "expected error when parsing bad JSON")
t.Fatalf("expected ParseResponse to fail")
}
} }
// TestWebAPIFeeEstimator checks that the WebAPIFeeEstimator returns fee rates // TestWebAPIFeeEstimator checks that the WebAPIFeeEstimator returns fee rates
@@ -216,10 +195,9 @@ func TestWebAPIFeeEstimator(t *testing.T) {
maxTarget: minFeeRate, maxTarget: minFeeRate,
} }
feeSource := mockSparseConfFeeSource{ // Create a mock fee source and mock its returned map.
url: "https://www.github.com", feeSource := &mockFeeSource{}
fees: feeRateResp, feeSource.On("GetFeeMap").Return(feeRateResp, nil)
}
estimator := NewWebAPIEstimator(feeSource, false) estimator := NewWebAPIEstimator(feeSource, false)
@@ -250,12 +228,15 @@ func TestWebAPIFeeEstimator(t *testing.T) {
exp := SatPerKVByte(tc.expectedFeeRate).FeePerKWeight() exp := SatPerKVByte(tc.expectedFeeRate).FeePerKWeight()
require.Equalf(t, exp, est, "target %v failed, fee "+ require.Equalf(t, exp, est, "target %v failed, fee "+
"map is %v", tc.target, feeSource.fees) "map is %v", tc.target, feeRateResp)
}) })
} }
// Stop the estimator when test ends. // Stop the estimator when test ends.
require.NoError(t, estimator.Stop(), "unable to stop fee estimator") require.NoError(t, estimator.Stop(), "unable to stop fee estimator")
// Assert the mocked fee source is called as expected.
feeSource.AssertExpectations(t)
} }
// TestGetCachedFee checks that the fee caching logic works as expected. // TestGetCachedFee checks that the fee caching logic works as expected.
+17
View File
@@ -0,0 +1,17 @@
package chainfee
import "github.com/stretchr/testify/mock"
type mockFeeSource struct {
mock.Mock
}
// A compile-time assertion to ensure that mockFeeSource implements the
// WebAPIFeeSource interface.
var _ WebAPIFeeSource = (*mockFeeSource)(nil)
func (m *mockFeeSource) GetFeeMap() (map[uint32]uint32, error) {
args := m.Called()
return args.Get(0).(map[uint32]uint32), args.Error(1)
}