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 }