github.com/yandex/pandora@v0.5.32/components/guns/http/base_test.go (about)

     1  package phttp
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"io"
     7  	"io/ioutil"
     8  	"net/http"
     9  	"net/http/httptest"
    10  	"net/url"
    11  	"strings"
    12  	"testing"
    13  
    14  	"github.com/spf13/afero"
    15  	"github.com/stretchr/testify/assert"
    16  	"github.com/stretchr/testify/suite"
    17  	ammomock "github.com/yandex/pandora/components/guns/http/mocks"
    18  	"github.com/yandex/pandora/core"
    19  	"github.com/yandex/pandora/core/aggregator/netsample"
    20  	"github.com/yandex/pandora/core/coretest"
    21  	"github.com/yandex/pandora/core/engine"
    22  	"github.com/yandex/pandora/lib/testutil"
    23  	"go.uber.org/zap"
    24  	"go.uber.org/zap/zapcore"
    25  )
    26  
    27  func newLogger() *zap.Logger {
    28  	zapConf := zap.NewDevelopmentConfig()
    29  	zapConf.Level.SetLevel(zapcore.DebugLevel)
    30  	log, err := zapConf.Build(zap.AddCaller())
    31  	if err != nil {
    32  		zap.L().Fatal("Logger build failed", zap.Error(err))
    33  	}
    34  	return log
    35  }
    36  
    37  func TestGunSuite(t *testing.T) {
    38  	suite.Run(t, new(BaseGunSuite))
    39  }
    40  
    41  type BaseGunSuite struct {
    42  	suite.Suite
    43  	fs      afero.Fs
    44  	log     *zap.Logger
    45  	metrics engine.Metrics
    46  	base    BaseGun
    47  	gunDeps core.GunDeps
    48  }
    49  
    50  func (s *BaseGunSuite) SetupSuite() {
    51  	s.log = testutil.NewLogger()
    52  	s.metrics = engine.NewMetrics("http_suite")
    53  }
    54  
    55  func (s *BaseGunSuite) SetupTest() {
    56  	s.base = BaseGun{Config: DefaultHTTPGunConfig()}
    57  }
    58  
    59  func (s *BaseGunSuite) Test_BindResultTo_Panics() {
    60  	s.Run("nil panic", func() {
    61  		s.Panics(func() {
    62  			_ = s.base.Bind(nil, testDeps())
    63  		})
    64  	})
    65  	s.Run("nil panic", func() {
    66  		res := &netsample.TestAggregator{}
    67  		_ = s.base.Bind(res, testDeps())
    68  		s.Require().Equal(res, s.base.Aggregator)
    69  		s.Panics(func() {
    70  			_ = s.base.Bind(&netsample.TestAggregator{}, testDeps())
    71  		})
    72  	})
    73  }
    74  
    75  type ammoMock struct {
    76  	requestCallCnt   int
    77  	idCallCnt        int
    78  	isInvalidCallCnt int
    79  }
    80  
    81  func (a *ammoMock) Request() (*http.Request, *netsample.Sample) {
    82  	a.requestCallCnt++
    83  	return nil, nil
    84  }
    85  
    86  func (a *ammoMock) ID() uint64 {
    87  	a.idCallCnt++
    88  	return 0
    89  }
    90  
    91  func (a *ammoMock) IsInvalid() bool {
    92  	a.isInvalidCallCnt++
    93  	return false
    94  }
    95  
    96  type testDecoratedClient struct {
    97  	client    Client
    98  	t         *testing.T
    99  	before    func(req *http.Request)
   100  	after     func(req *http.Request, res *http.Response, err error)
   101  	returnRes *http.Response
   102  	returnErr error
   103  }
   104  
   105  func (c *testDecoratedClient) Do(req *http.Request) (*http.Response, error) {
   106  	if c.before != nil {
   107  		c.before(req)
   108  	}
   109  	if c.client == nil {
   110  		return c.returnRes, c.returnErr
   111  	}
   112  	res, err := c.client.Do(req)
   113  	if c.after != nil {
   114  		c.after(req, res, err)
   115  	}
   116  	return res, err
   117  }
   118  
   119  func (c *testDecoratedClient) CloseIdleConnections() {
   120  	c.client.CloseIdleConnections()
   121  }
   122  
   123  func (s *BaseGunSuite) Test_Shoot_BeforeBindPanics() {
   124  	s.base.Client = &testDecoratedClient{
   125  		client: s.base.Client,
   126  		before: func(req *http.Request) { panic("should not be called\"") },
   127  		after:  nil,
   128  	}
   129  	am := &ammoMock{}
   130  
   131  	s.Panics(func() {
   132  		s.base.Shoot(am)
   133  	})
   134  }
   135  
   136  func (s *BaseGunSuite) Test_Shoot() {
   137  	var (
   138  		body io.ReadCloser
   139  
   140  		am       *ammomock.Ammo
   141  		req      *http.Request
   142  		tag      string
   143  		res      *http.Response
   144  		sample   *netsample.Sample
   145  		results  *netsample.TestAggregator
   146  		shootErr error
   147  	)
   148  	beforeEach := func() {
   149  		am = ammomock.NewAmmo(s.T())
   150  		am.On("IsInvalid").Return(false).Maybe()
   151  		req = httptest.NewRequest("GET", "/1/2/3/4", nil)
   152  		tag = ""
   153  		results = &netsample.TestAggregator{}
   154  		_ = s.base.Bind(results, testDeps())
   155  	}
   156  
   157  	justBeforeEach := func() {
   158  		sample = netsample.Acquire(tag)
   159  		am.On("Request").Return(req, sample).Maybe()
   160  		res = &http.Response{
   161  			StatusCode: http.StatusNotFound,
   162  			Body:       ioutil.NopCloser(body),
   163  			Request:    req,
   164  		}
   165  		s.base.Shoot(am)
   166  		s.Require().Len(results.Samples, 1)
   167  		shootErr = results.Samples[0].Err()
   168  	}
   169  
   170  	s.Run("Do ok", func() {
   171  		beforeEachDoOk := func() {
   172  			body = ioutil.NopCloser(strings.NewReader("aaaaaaa"))
   173  			s.base.AnswLog = zap.NewNop()
   174  			s.base.Client = &testDecoratedClient{
   175  				before: func(doReq *http.Request) {
   176  					s.Require().Equal(req, doReq)
   177  				},
   178  				returnRes: &http.Response{
   179  					StatusCode: http.StatusNotFound,
   180  					Body:       ioutil.NopCloser(body),
   181  					Request:    req,
   182  				},
   183  			}
   184  		}
   185  		s.Run("ammo sample sent to results", func() {
   186  			s.SetupTest()
   187  			beforeEach()
   188  			beforeEachDoOk()
   189  			justBeforeEach()
   190  
   191  			s.Assert().Len(results.Samples, 1)
   192  			s.Assert().Equal(sample, results.Samples[0])
   193  			s.Assert().Equal("__EMPTY__", sample.Tags())
   194  			s.Assert().Equal(res.StatusCode, sample.ProtoCode())
   195  			_ = shootErr
   196  		})
   197  
   198  		s.Run("body read well", func() {
   199  			s.SetupTest()
   200  			beforeEach()
   201  			beforeEachDoOk()
   202  			justBeforeEach()
   203  
   204  			s.Assert().NoError(shootErr)
   205  			_, err := body.Read([]byte{0})
   206  			s.Assert().ErrorIs(err, io.EOF, "body should be read fully")
   207  		})
   208  
   209  		s.Run("autotag options is set", func() {
   210  			beforeEacAautotag := (func() { s.base.Config.AutoTag.Enabled = true })
   211  
   212  			s.Run("autotagged", func() {
   213  				s.SetupTest()
   214  				beforeEach()
   215  				beforeEachDoOk()
   216  				beforeEacAautotag()
   217  				justBeforeEach()
   218  
   219  				s.Assert().Equal("/1/2", sample.Tags())
   220  			})
   221  
   222  			s.Run("tag is already set", func() {
   223  				const presetTag = "TAG"
   224  				beforeEachTagIsAlreadySet := func() { tag = presetTag }
   225  				s.Run("no tag added", func() {
   226  					s.SetupTest()
   227  					beforeEach()
   228  					beforeEachDoOk()
   229  					beforeEacAautotag()
   230  					beforeEachTagIsAlreadySet()
   231  					justBeforeEach()
   232  
   233  					s.Assert().Equal(presetTag, sample.Tags())
   234  				})
   235  
   236  				s.Run("no-tag-only set to false", func() {
   237  					beforeEachNoTagOnly := func() { s.base.Config.AutoTag.NoTagOnly = false }
   238  					s.Run("autotag added", func() {
   239  						s.SetupTest()
   240  						beforeEach()
   241  						beforeEachDoOk()
   242  						beforeEacAautotag()
   243  						beforeEachTagIsAlreadySet()
   244  						beforeEachNoTagOnly()
   245  						justBeforeEach()
   246  
   247  						s.Assert().Equal(presetTag+"|/1/2", sample.Tags())
   248  					})
   249  				})
   250  			})
   251  		})
   252  
   253  		s.Run("Connect set", func() {
   254  			var connectCalled, doCalled bool
   255  			beforeEachConnectSet := func() {
   256  				s.base.Connect = func(ctx context.Context) error {
   257  					connectCalled = true
   258  					return nil
   259  				}
   260  
   261  				s.base.Client = &testDecoratedClient{
   262  					client: s.base.Client,
   263  					before: func(doReq *http.Request) {
   264  						doCalled = true
   265  					},
   266  				}
   267  			}
   268  			s.Run("Connect called", func() {
   269  				s.SetupTest()
   270  				beforeEach()
   271  				beforeEachDoOk()
   272  				beforeEachConnectSet()
   273  				justBeforeEach()
   274  
   275  				s.Assert().NoError(shootErr)
   276  				s.Assert().True(connectCalled)
   277  				s.Assert().True(doCalled)
   278  			})
   279  		})
   280  		s.Run("Connect failed", func() {
   281  			connectErr := errors.New("connect error")
   282  			beforeEachConnectFailed := func() {
   283  				s.base.Connect = func(ctx context.Context) error {
   284  					// Connect should report fail in sample itself.
   285  					s := netsample.Acquire("")
   286  					s.SetErr(connectErr)
   287  					results.Report(s)
   288  					return connectErr
   289  				}
   290  			}
   291  			s.Run("Shoot failed", func() {
   292  				s.SetupTest()
   293  				beforeEach()
   294  				beforeEachDoOk()
   295  				beforeEachConnectFailed()
   296  				justBeforeEach()
   297  
   298  				s.Assert().Error(shootErr)
   299  				s.Assert().ErrorIs(shootErr, connectErr)
   300  			})
   301  		})
   302  	})
   303  }
   304  
   305  func Test_Autotag(t *testing.T) {
   306  	tests := []struct {
   307  		name  string
   308  		path  string
   309  		depth int
   310  		tag   string
   311  	}{
   312  		{"empty", "", 2, ""},
   313  		{"root", "/", 2, "/"},
   314  		{"exact depth", "/1/2", 2, "/1/2"},
   315  		{"more depth", "/1/2", 3, "/1/2"},
   316  		{"less depth", "/1/2", 1, "/1"},
   317  	}
   318  	for _, tt := range tests {
   319  		t.Run(tt.name, func(t *testing.T) {
   320  			URL := &url.URL{Path: tt.path}
   321  			got := autotag(tt.depth, URL)
   322  			assert.Equal(t, got, tt.tag)
   323  		})
   324  	}
   325  }
   326  
   327  func Test_ConfigDecode(t *testing.T) {
   328  	var conf GunConfig
   329  	coretest.DecodeAndValidateT(t, `
   330  target: localhost:80
   331  auto-tag:
   332    enabled: true
   333    uri-elements: 3
   334    no-tag-only: false
   335  `, &conf)
   336  }
   337  
   338  func testDeps() core.GunDeps {
   339  	return core.GunDeps{Log: testutil.NewLogger(), Ctx: context.Background()}
   340  }