How to use EXPECT method of mock_pkg Package

Best Mock code snippet using mock_pkg.EXPECT

Run Mock automation tests on LambdaTest cloud grid

Perform automation testing on 3000+ real desktop and mobile devices online.

api_test.go

Source: api_test.go Github

copy
1package handler
2
3import (
4	"bytes"
5	mock_pkg "for_avito_tech_with_gin/pkg/mocks"
6	"for_avito_tech_with_gin/pkg/service"
7	mock_service "for_avito_tech_with_gin/pkg/service/mocks"
8	"github.com/gin-gonic/gin"
9	"github.com/golang/mock/gomock"
10	"github.com/stretchr/testify/assert"
11	"net/http"
12	"net/http/httptest"
13	"testing"
14)
15
16type mockUserBehavior func(s *mock_service.MockUser)
17type mockCalculatorBehavior func(s *mock_pkg.MockCurrencyCalculator)
18
19type testSkillet struct {
20	name                   string
21	inputBody              string
22	inputQueryParams       string
23	mockUserBehavior       mockUserBehavior
24	mockCalculatorBehavior mockCalculatorBehavior
25	expectedStatusCode     int
26	expectedRequestBody    string
27}
28
29func TestHandler_addFundsHandler(t *testing.T) {
30	testData := []testSkillet{
31		{
32			name:      "OK",
33			inputBody: `{"id":348, "sum": 2700}`,
34			mockUserBehavior: func(s *mock_service.MockUser) {
35				s.EXPECT().AddFunds(348, float32(2700)).Return(nil)
36			},
37			expectedStatusCode:  http.StatusOK,
38			expectedRequestBody: "",
39		},
40		{
41			name:                "Invalid Body",
42			inputBody:           `{"id":348}`,
43			mockUserBehavior:    func(s *mock_service.MockUser) {},
44			expectedStatusCode:  http.StatusBadRequest,
45			expectedRequestBody: `{"message":"invalid body."}`,
46		},
47		{
48			name:      "Negative Sum",
49			inputBody: `{"id":34, "sum": -10}`,
50			mockUserBehavior: func(s *mock_service.MockUser) {
51				s.EXPECT().AddFunds(34, float32(-10)).Return(&service.NegativeSum{})
52			},
53			expectedStatusCode:  http.StatusBadRequest,
54			expectedRequestBody: `{"message":"sum can't be negative or 0."}`,
55		},
56		{
57			name:      "Internal Server Error",
58			inputBody: `{"id":14589, "sum": 10}`,
59			mockUserBehavior: func(s *mock_service.MockUser) {
60				s.EXPECT().AddFunds(14589, float32(10)).Return(&service.InternalServerError{})
61			},
62			expectedStatusCode:  http.StatusInternalServerError,
63			expectedRequestBody: `{"message":"internal server error."}`,
64		},
65	}
66
67	t.Parallel()
68	for _, testCase := range testData {
69		t.Run(testCase.name, func(t *testing.T) {
70			// init deps
71			c := gomock.NewController(t)
72			defer c.Finish()
73
74			servi := mock_service.NewMockUser(c)
75			testCase.mockUserBehavior(servi)
76
77			services := &service.Service{User: servi}
78			handler := NewHandler(services)
79
80			// test server
81			r := gin.New()
82			r.POST("/api/v1/add_funds", handler.addFundsHandler)
83
84			// test request
85			w := httptest.NewRecorder()
86			req := httptest.NewRequest("POST", "/api/v1/add_funds", bytes.NewBufferString(testCase.inputBody))
87
88			// perform request
89			r.ServeHTTP(w, req)
90
91			// assert
92			assert.Equal(t, testCase.expectedStatusCode, w.Code)
93			assert.Equal(t, testCase.expectedRequestBody, w.Body.String())
94		})
95	}
96}
97
98func TestHandler_writeOffFundsHandler(t *testing.T) {
99	testData := []testSkillet{
100		{
101			name:      "OK",
102			inputBody: `{"id":348, "sum": 2700}`,
103			mockUserBehavior: func(s *mock_service.MockUser) {
104				s.EXPECT().WriteOffFunds(348, float32(2700)).Return(nil)
105			},
106			expectedStatusCode:  http.StatusOK,
107			expectedRequestBody: "",
108		},
109		{
110			name:                "Invalid Body",
111			inputBody:           `{"id":348}`,
112			mockUserBehavior:    func(s *mock_service.MockUser) {},
113			expectedStatusCode:  http.StatusBadRequest,
114			expectedRequestBody: `{"message":"invalid body."}`,
115		},
116		{
117			name:      "Negative Sum",
118			inputBody: `{"id":34, "sum": -10}`,
119			mockUserBehavior: func(s *mock_service.MockUser) {
120				s.EXPECT().WriteOffFunds(34, float32(-10)).Return(&service.NegativeSum{})
121			},
122			expectedStatusCode:  http.StatusBadRequest,
123			expectedRequestBody: `{"message":"sum can't be negative or 0."}`,
124		},
125		{
126			name:      "User Not Found",
127			inputBody: `{"id":91, "sum": 10}`,
128			mockUserBehavior: func(s *mock_service.MockUser) {
129				s.EXPECT().WriteOffFunds(91, float32(10)).Return(&service.UserNotFound{Id: 91})
130			},
131			expectedStatusCode:  http.StatusNotFound,
132			expectedRequestBody: `{"message":"user 91 does not exist."}`,
133		},
134		{
135			name:      "Insufficient Funds",
136			inputBody: `{"id":23, "sum": 10}`,
137			mockUserBehavior: func(s *mock_service.MockUser) {
138				s.EXPECT().WriteOffFunds(23, float32(10)).Return(&service.InsufficientFunds{Id: 23})
139			},
140			expectedStatusCode:  http.StatusPreconditionFailed,
141			expectedRequestBody: `{"message":"user 23 has insufficient funds."}`,
142		},
143		{
144			name:      "Internal Server Error",
145			inputBody: `{"id":14589, "sum": 10}`,
146			mockUserBehavior: func(s *mock_service.MockUser) {
147				s.EXPECT().WriteOffFunds(14589, float32(10)).Return(&service.InternalServerError{})
148			},
149			expectedStatusCode:  http.StatusInternalServerError,
150			expectedRequestBody: `{"message":"internal server error."}`,
151		},
152	}
153
154	t.Parallel()
155	for _, testCase := range testData {
156		t.Run(testCase.name, func(t *testing.T) {
157			// init deps
158			c := gomock.NewController(t)
159			defer c.Finish()
160
161			servi := mock_service.NewMockUser(c)
162			testCase.mockUserBehavior(servi)
163
164			services := &service.Service{User: servi}
165			handler := NewHandler(services)
166
167			// test server
168			r := gin.New()
169			r.POST("/api/v1/write_off_funds", handler.writeOffFundsHandler)
170
171			// test request
172			w := httptest.NewRecorder()
173			req := httptest.NewRequest("POST", "/api/v1/write_off_funds", bytes.NewBufferString(testCase.inputBody))
174
175			// perform request
176			r.ServeHTTP(w, req)
177
178			// assert
179			assert.Equal(t, testCase.expectedStatusCode, w.Code)
180			assert.Equal(t, testCase.expectedRequestBody, w.Body.String())
181		})
182	}
183}
184
185func TestHandler_fundsTransferHandler(t *testing.T) {
186	testData := []testSkillet{
187		{
188			name:      "OK",
189			inputBody: `{"sender_id":348, "receiver_id": 4389, "sum": 2700}`,
190			mockUserBehavior: func(s *mock_service.MockUser) {
191				s.EXPECT().FundsTransfer(348, 4389, float32(2700)).Return(nil)
192			},
193			expectedStatusCode:  http.StatusOK,
194			expectedRequestBody: "",
195		},
196		{
197			name:                "Invalid Body",
198			inputBody:           `{"sender_id":348, "receiver_id": 4389}`,
199			mockUserBehavior:    func(s *mock_service.MockUser) {},
200			expectedStatusCode:  http.StatusBadRequest,
201			expectedRequestBody: `{"message":"invalid body."}`,
202		},
203		{
204			name:      "Negative Sum",
205			inputBody: `{"sender_id":34, "receiver_id": 89, "sum": -10}`,
206			mockUserBehavior: func(s *mock_service.MockUser) {
207				s.EXPECT().FundsTransfer(34, 89, float32(-10)).Return(&service.NegativeSum{})
208			},
209			expectedStatusCode:  http.StatusBadRequest,
210			expectedRequestBody: `{"message":"sum can't be negative or 0."}`,
211		},
212		{
213			name:      "Equal Sender And Receiver",
214			inputBody: `{"sender_id":34, "receiver_id": 34, "sum": 1000}`,
215			mockUserBehavior: func(s *mock_service.MockUser) {
216				s.EXPECT().FundsTransfer(34, 34, float32(1000)).Return(&service.SameId{})
217			},
218			expectedStatusCode:  http.StatusBadRequest,
219			expectedRequestBody: `{"message":"user cannot send money to himself."}`,
220		},
221		{
222			name:      "User Not Found",
223			inputBody: `{"sender_id":91, "receiver_id": 12, "sum": 599}`,
224			mockUserBehavior: func(s *mock_service.MockUser) {
225				s.EXPECT().FundsTransfer(91, 12, float32(599)).Return(&service.UserNotFound{Id: 91})
226			},
227			expectedStatusCode:  http.StatusNotFound,
228			expectedRequestBody: `{"message":"user 91 does not exist."}`,
229		},
230		{
231			name:      "Insufficient Funds",
232			inputBody: `{"sender_id":23, "receiver_id": 24, "sum": 1000}`,
233			mockUserBehavior: func(s *mock_service.MockUser) {
234				s.EXPECT().FundsTransfer(23, 24, float32(1000)).Return(&service.InsufficientFunds{Id: 23})
235			},
236			expectedStatusCode:  http.StatusPreconditionFailed,
237			expectedRequestBody: `{"message":"user 23 has insufficient funds."}`,
238		},
239		{
240			name:      "Internal Server Error",
241			inputBody: `{"sender_id":14589, "receiver_id": 4389, "sum": 3500}`,
242			mockUserBehavior: func(s *mock_service.MockUser) {
243				s.EXPECT().FundsTransfer(14589, 4389, float32(3500)).Return(&service.InternalServerError{})
244			},
245			expectedStatusCode:  http.StatusInternalServerError,
246			expectedRequestBody: `{"message":"internal server error."}`,
247		},
248	}
249
250	t.Parallel()
251	for _, testCase := range testData {
252		t.Run(testCase.name, func(t *testing.T) {
253			// init deps
254			c := gomock.NewController(t)
255			defer c.Finish()
256
257			servi := mock_service.NewMockUser(c)
258			testCase.mockUserBehavior(servi)
259
260			services := &service.Service{User: servi}
261			handler := NewHandler(services)
262
263			// test server
264			r := gin.New()
265			r.POST("/api/v1/funds_transfer", handler.fundsTransferHandler)
266
267			// test request
268			w := httptest.NewRecorder()
269			req := httptest.NewRequest("POST", "/api/v1/funds_transfer", bytes.NewBufferString(testCase.inputBody))
270
271			// perform request
272			r.ServeHTTP(w, req)
273
274			// assert
275			assert.Equal(t, testCase.expectedStatusCode, w.Code)
276			assert.Equal(t, testCase.expectedRequestBody, w.Body.String())
277		})
278	}
279}
280
281func TestHandler_getBalance(t *testing.T) {
282	testData := []testSkillet{
283		{
284			name:      "OK",
285			inputBody: `{"id":348}`,
286			mockUserBehavior: func(s *mock_service.MockUser) {
287				s.EXPECT().GetBalance(348).Return(float32(100), nil)
288			},
289			mockCalculatorBehavior: func(s *mock_pkg.MockCurrencyCalculator) {},
290			expectedStatusCode:     http.StatusOK,
291			expectedRequestBody:    `{"balance":100}`,
292		},
293		{
294			name:                   "Invalid Body",
295			inputBody:              `{}`,
296			mockUserBehavior:       func(s *mock_service.MockUser) {},
297			mockCalculatorBehavior: func(s *mock_pkg.MockCurrencyCalculator) {},
298			expectedStatusCode:     http.StatusBadRequest,
299			expectedRequestBody:    `{"message":"invalid body."}`,
300		},
301		{
302			name:             "OK With Query Param",
303			inputBody:        `{"id":34}`,
304			inputQueryParams: "?currency=USD",
305			mockUserBehavior: func(s *mock_service.MockUser) {
306				s.EXPECT().GetBalance(34).Return(float32(100), nil)
307			},
308			mockCalculatorBehavior: func(s *mock_pkg.MockCurrencyCalculator) {
309				s.EXPECT().ConvertRubTo("USD", float32(100)).Return(1.3, nil)
310			},
311			expectedStatusCode:  http.StatusOK,
312			expectedRequestBody: `{"balance":1.3}`,
313		},
314		{
315			name:             "Invalid Query Param",
316			inputBody:        `{"id":34}`,
317			inputQueryParams: "?currency=XRP",
318			mockUserBehavior: func(s *mock_service.MockUser) {
319				s.EXPECT().GetBalance(34).Return(float32(100), nil)
320			},
321			mockCalculatorBehavior: func(s *mock_pkg.MockCurrencyCalculator) {
322				s.EXPECT().ConvertRubTo("XRP", float32(100)).Return(0.0, &service.WrongParam{Param: "currency"})
323			},
324			expectedStatusCode:  http.StatusPreconditionFailed,
325			expectedRequestBody: `{"message":"wrong currency param."}`,
326		},
327		{
328			name:      "Internal Server Error",
329			inputBody: `{"id":14589}`,
330			mockUserBehavior: func(s *mock_service.MockUser) {
331				s.EXPECT().GetBalance(14589).Return(float32(0), &service.InternalServerError{})
332			},
333			mockCalculatorBehavior: func(s *mock_pkg.MockCurrencyCalculator) {},
334			expectedStatusCode:     http.StatusInternalServerError,
335			expectedRequestBody:    `{"message":"internal server error."}`,
336		},
337		{
338			name:      "User Not Found",
339			inputBody: `{"id":91}`,
340			mockUserBehavior: func(s *mock_service.MockUser) {
341				s.EXPECT().GetBalance(91).Return(float32(0), &service.UserNotFound{Id: 91})
342			},
343			mockCalculatorBehavior: func(s *mock_pkg.MockCurrencyCalculator) {},
344			expectedStatusCode:     http.StatusNotFound,
345			expectedRequestBody:    `{"message":"user 91 does not exist."}`,
346		},
347	}
348
349	t.Parallel()
350	for _, testCase := range testData {
351		t.Run(testCase.name, func(t *testing.T) {
352			// init deps
353			c := gomock.NewController(t)
354			defer c.Finish()
355
356			userService := mock_service.NewMockUser(c)
357			testCase.mockUserBehavior(userService)
358
359			services := &service.Service{User: userService}
360			handler := NewHandler(services)
361
362			calculator := mock_pkg.NewMockCurrencyCalculator(c)
363			testCase.mockCalculatorBehavior(calculator)
364
365			// test server
366			r := gin.New()
367			r.GET("/api/v1/get_balance", handler.getBalanceHandler(calculator))
368
369			// test request
370			w := httptest.NewRecorder()
371			req := httptest.NewRequest("GET", "/api/v1/get_balance"+testCase.inputQueryParams,
372				bytes.NewBufferString(testCase.inputBody))
373
374			// perform request
375			r.ServeHTTP(w, req)
376
377			// assert
378			assert.Equal(t, testCase.expectedStatusCode, w.Code)
379			assert.Equal(t, testCase.expectedRequestBody, w.Body.String())
380		})
381	}
382}
383
Full Screen

publish_test.go

Source: publish_test.go Github

copy
1package pkg_test
2
3import (
4	"context"
5	"database/sql"
6	"errors"
7	"testing"
8
9	"github.com/pursuit/event-go/mock/shopify/sarama"
10	"github.com/pursuit/event-go/pkg"
11	"github.com/pursuit/event-go/pkg/mock"
12
13	"github.com/golang/mock/gomock"
14
15	"github.com/DATA-DOG/go-sqlmock"
16)
17
18func TestStoreEvent(t *testing.T) {
19	data := pkg.EventData{
20		Topic:   "this is a topic name",
21		Payload: []byte("this is the payload"),
22	}
23
24	for _, testcase := range []struct {
25		tName     string
26		connErr   error
27		outputErr error
28	}{
29		{
30			tName:     "failed to insert to db",
31			connErr:   errors.New("failed insert"),
32			outputErr: errors.New("event: failed insert"),
33		},
34		{
35			tName: "success",
36		},
37	} {
38		mocker := gomock.NewController(t)
39		defer mocker.Finish()
40		db := mock_pkg.NewMockDB(mocker)
41
42		db.EXPECT().ExecContext(gomock.Any(), "INSERT INTO events (topic,payload) VALUES($1,$2)", data.Topic, data.Payload).Return(nil, testcase.connErr)
43
44		err := pkg.StoreEvent(context.Background(), db, data)
45		if (testcase.outputErr == nil && err != nil) ||
46			(testcase.outputErr != nil && err == nil) ||
47			(err != nil && testcase.outputErr.Error() != err.Error()) {
48			t.Errorf("Test %s, err is %v, should be %v", testcase.tName, err, testcase.outputErr)
49		}
50	}
51}
52
53func TestKafkaPublishFromSQL(t *testing.T) {
54	id := 2
55	topic := "topic1"
56	payload := []byte(`{"foo":"bar"}`)
57
58	for _, testcase := range []struct {
59		tName     string
60		txErr     error
61		queryErr  error
62		deleteErr error
63		kafkaErr  error
64		commitErr error
65		outputErr error
66	}{
67		{
68			tName:     "fail start transaction",
69			txErr:     errors.New("fail"),
70			outputErr: errors.New("fail"),
71		},
72		{
73			tName:     "fail query",
74			queryErr:  errors.New("fail"),
75			outputErr: errors.New("fail"),
76		},
77		{
78			tName:     "fail delete",
79			deleteErr: errors.New("fail"),
80			outputErr: errors.New("fail"),
81		},
82		{
83			tName:     "fail kafka",
84			kafkaErr:  errors.New("fail"),
85			outputErr: errors.New("fail"),
86		},
87		{
88			tName:     "fail commit",
89			commitErr: errors.New("fail"),
90			outputErr: errors.New("fail"),
91		},
92		{
93			tName: "success",
94		},
95		{
96			tName: "batch failed to query",
97		},
98	} {
99		t.Run(testcase.tName, func(t *testing.T) {
100			ctrl := gomock.NewController(t)
101			defer ctrl.Finish()
102
103			kafka := mock_sarama.NewMockSyncProducer(ctrl)
104
105			db, mock, err := sqlmock.New()
106			if err != nil {
107				panic(err)
108			}
109			defer db.Close()
110
111			if testcase.txErr != nil {
112				mock.ExpectBegin().WillReturnError(testcase.txErr)
113			} else {
114				mock.ExpectBegin()
115				if testcase.queryErr != nil {
116					mock.ExpectQuery("SELECT id, topic, payload FROM events LIMIT 1 FOR UPDATE SKIP LOCKED").WillReturnError(testcase.queryErr)
117					mock.ExpectRollback()
118				} else {
119					mock.ExpectQuery("SELECT id, topic, payload FROM events LIMIT 1 FOR UPDATE SKIP LOCKED").WillReturnRows(mock.NewRows([]string{"id", "topic", "payload"}).AddRow(id, topic, payload))
120					if testcase.deleteErr != nil {
121						mock.ExpectExec("DELETE FROM events where id = ?").WillReturnError(testcase.deleteErr)
122						mock.ExpectRollback()
123					} else {
124						mock.ExpectExec("DELETE FROM events where id = ?").WillReturnResult(sqlmock.NewResult(1, 1))
125						if testcase.kafkaErr != nil {
126							kafka.EXPECT().SendMessage(gomock.Any()).Return(int32(1), int64(2), testcase.kafkaErr)
127							mock.ExpectRollback()
128						} else {
129							kafka.EXPECT().SendMessage(gomock.Any()).Return(int32(1), int64(2), nil)
130							if testcase.commitErr != nil {
131								mock.ExpectCommit().WillReturnError(testcase.commitErr)
132							} else {
133								mock.ExpectCommit()
134							}
135						}
136					}
137				}
138			}
139
140			consumer := pkg.KafkaPublishFromSQL{
141				DB:        db,
142				Kafka:     kafka,
143				Batch:     1,
144				WorkerNum: 1,
145			}
146
147			err = consumer.Process()
148			if (testcase.outputErr == nil && err != nil) ||
149				(testcase.outputErr != nil && err == nil) ||
150				(err != nil && testcase.outputErr.Error() != err.Error()) {
151				t.Errorf("Test %s, err is %v, should be %v", testcase.tName, err, testcase.outputErr)
152			}
153
154			if err := mock.ExpectationsWereMet(); err != nil {
155				panic(err)
156			}
157		})
158	}
159}
160
161func TestKafkaPublishFromSQLBatch(t *testing.T) {
162	id := 2
163	topic := "topic1"
164	payload := []byte(`{"foo":"bar"}`)
165
166	for _, testcase := range []struct {
167		tName     string
168		emptyRes  bool
169		txErr     error
170		queryErr  error
171		deleteErr error
172		kafkaErr  error
173		commitErr error
174		outputErr error
175	}{
176		{
177			tName:     "fail start transaction",
178			txErr:     errors.New("fail"),
179			outputErr: errors.New("fail"),
180		},
181		{
182			tName:     "fail query",
183			queryErr:  errors.New("fail"),
184			outputErr: errors.New("fail"),
185		},
186		{
187			tName:     "empty result",
188			emptyRes:  true,
189			outputErr: sql.ErrNoRows,
190		},
191		{
192			tName:     "fail scan",
193			outputErr: errors.New(`sql: Scan error on column index 0, name "id": converting driver.Value type string ("invalid id") to a int: invalid syntax`),
194		},
195		{
196			tName:     "fail delete",
197			deleteErr: errors.New("fail"),
198			outputErr: errors.New("fail"),
199		},
200		{
201			tName:     "fail kafka",
202			kafkaErr:  errors.New("fail"),
203			outputErr: errors.New("fail"),
204		},
205		{
206			tName:     "fail commit",
207			commitErr: errors.New("fail"),
208			outputErr: errors.New("fail"),
209		},
210		{
211			tName: "success",
212		},
213	} {
214		t.Run(testcase.tName, func(t *testing.T) {
215			ctrl := gomock.NewController(t)
216			defer ctrl.Finish()
217
218			kafka := mock_sarama.NewMockSyncProducer(ctrl)
219
220			db, mock, err := sqlmock.New()
221			if err != nil {
222				panic(err)
223			}
224			defer db.Close()
225
226			if testcase.txErr != nil {
227				mock.ExpectBegin().WillReturnError(testcase.txErr)
228			} else {
229				mock.ExpectBegin()
230				if testcase.queryErr != nil {
231					mock.ExpectQuery("SELECT id, topic, payload FROM events LIMIT 2 FOR UPDATE SKIP LOCKED").WillReturnError(testcase.queryErr)
232					mock.ExpectRollback()
233				} else {
234					if testcase.tName == "fail scan" {
235						mock.ExpectQuery("SELECT id, topic, payload FROM events LIMIT 2 FOR UPDATE SKIP LOCKED").WillReturnRows(mock.NewRows([]string{"id", "topic", "payload"}).AddRow("invalid id", topic, payload))
236					} else if testcase.emptyRes {
237						mock.ExpectQuery("SELECT id, topic, payload FROM events LIMIT 2 FOR UPDATE SKIP LOCKED").WillReturnRows(mock.NewRows([]string{"id", "topic", "payload"}))
238						mock.ExpectRollback()
239					} else {
240						mock.ExpectQuery("SELECT id, topic, payload FROM events LIMIT 2 FOR UPDATE SKIP LOCKED").WillReturnRows(mock.NewRows([]string{"id", "topic", "payload"}).AddRow(id, topic, payload))
241						if testcase.deleteErr != nil {
242							mock.ExpectExec("DELETE FROM events where id IN \\('2'\\)").WillReturnError(testcase.deleteErr)
243							mock.ExpectRollback()
244						} else {
245							mock.ExpectExec("DELETE FROM events where id IN \\('2'\\)").WillReturnResult(sqlmock.NewResult(1, 1))
246							if testcase.kafkaErr != nil {
247								kafka.EXPECT().SendMessages(gomock.Any()).Return(testcase.kafkaErr)
248								mock.ExpectRollback()
249							} else {
250								kafka.EXPECT().SendMessages(gomock.Any()).Return(nil)
251								if testcase.commitErr != nil {
252									mock.ExpectCommit().WillReturnError(testcase.commitErr)
253								} else {
254									mock.ExpectCommit()
255								}
256							}
257						}
258					}
259				}
260			}
261
262			consumer := pkg.KafkaPublishFromSQL{
263				DB:        db,
264				Kafka:     kafka,
265				Batch:     2,
266				WorkerNum: 1,
267			}
268
269			err = consumer.Process()
270			if (testcase.outputErr == nil && err != nil) ||
271				(testcase.outputErr != nil && err == nil) ||
272				(err != nil && testcase.outputErr.Error() != err.Error()) {
273				t.Errorf("Test %s, err is %v, should be %v", testcase.tName, err, testcase.outputErr)
274			}
275
276			if err := mock.ExpectationsWereMet(); err != nil {
277				panic(err)
278			}
279		})
280	}
281}
282
Full Screen

request_handler_test.go

Source: request_handler_test.go Github

copy
1package pkg
2
3import (
4	"context"
5	"encoding/base64"
6	"encoding/json"
7	"fmt"
8	"io"
9	"io/ioutil"
10	"net"
11	"net/http"
12	"net/url"
13	"strconv"
14	"strings"
15	"sync"
16	"sync/atomic"
17	"testing"
18
19	"github.com/golang/mock/gomock"
20	"github.com/pkg/errors"
21	"github.com/stretchr/testify/assert"
22	"github.com/stretchr/testify/require"
23	"github.com/wk8/github-api-proxy/pkg/types"
24
25	"github.com/wk8/github-api-proxy/pkg/internal"
26	mock_token_pools "github.com/wk8/github-api-proxy/pkg/internal/mock_pkg"
27)
28
29func TestRequestHandler_ProxyRequest(t *testing.T) {
30	// we need two upstream servers to hit and test our proxy against:
31	// one to act as a Github API, one as another upstream
32	githubUpstream := newUpstreamServer(t)
33	githubUpstream.start()
34	defer githubUpstream.stop()
35	otherUpstream := newUpstreamServer(t)
36	otherUpstream.start()
37	defer otherUpstream.stop()
38
39	t.Run("it adds tokens to Github API requests", func(t *testing.T) {
40		mockController := gomock.NewController(t)
41		defer mockController.Finish()
42		tokenPool := mock_token_pools.NewMockTokenPoolStorageBackend(mockController)
43
44		dummyToken := "ohlebeautoken"
45
46		tokenPool.EXPECT().CheckOutToken().Return(&types.Token{
47			TokenSpec: types.TokenSpec{
48				Token:             dummyToken,
49				ExpectedRateLimit: 1000,
50			},
51			RemainingCalls: 1000,
52		}, nil)
53
54		request, err := githubUpstream.buildRequest(githubUpstream.withResponseHeaders(rateLimitHeader, "1000"))
55		require.NoError(t, err)
56
57		response, body, ok := assertHandleRequestSuccessful(t, NewRequestHandler(githubUpstream.urlStruct(), tokenPool), request, http.StatusOK)
58		require.True(t, ok)
59
60		assert.Equal(t, "pong", body)
61		assertAuthHeaderMatchesToken(t, dummyToken, response.Header.Get(receivedAuthHeader))
62	})
63
64	t.Run("it updates the token pool if the token's rate limit is different than expected", func(t *testing.T) {
65		mockController := gomock.NewController(t)
66		defer mockController.Finish()
67		tokenPool := mock_token_pools.NewMockTokenPoolStorageBackend(mockController)
68
69		dummyToken := "ohlebeautoken"
70
71		tokenPool.EXPECT().CheckOutToken().Return(&types.Token{
72			TokenSpec: types.TokenSpec{
73				Token:             dummyToken,
74				ExpectedRateLimit: 100,
75			},
76			RemainingCalls: 100,
77		}, nil)
78		tokenPool.EXPECT().UpdateTokenRateLimit(dummyToken, 1000)
79
80		request, err := githubUpstream.buildRequest(githubUpstream.withResponseHeaders(rateLimitHeader, "1000"))
81		require.NoError(t, err)
82
83		response, body, ok := assertHandleRequestSuccessful(t, NewRequestHandler(githubUpstream.urlStruct(), tokenPool), request, http.StatusOK)
84		require.True(t, ok)
85
86		assert.Equal(t, "pong", body)
87		assertAuthHeaderMatchesToken(t, dummyToken, response.Header.Get(receivedAuthHeader))
88	})
89
90	t.Run("it periodically updates the token pool about the token's usage, based on the response headers", func(t *testing.T) {
91		mockController := gomock.NewController(t)
92		defer mockController.Finish()
93		tokenPool := mock_token_pools.NewMockTokenPoolStorageBackend(mockController)
94
95		dummyToken := "ohlebeautoken"
96
97		tokenPool.EXPECT().CheckOutToken().Return(&types.Token{
98			TokenSpec: types.TokenSpec{
99				Token:             dummyToken,
100				ExpectedRateLimit: 1000,
101			},
102			RemainingCalls: 1000,
103		}, nil)
104		remainingCallsStart := 1000
105		remainingCallsStop := 790
106		for remainingCallsThreshold := remainingCallsStart - remainingCallsReportingInterval; remainingCallsThreshold > remainingCallsStop; remainingCallsThreshold -= remainingCallsReportingInterval {
107			tokenPool.EXPECT().UpdateTokenUsage(dummyToken, remainingCallsThreshold)
108		}
109
110		handler := NewRequestHandler(githubUpstream.urlStruct(), tokenPool)
111		for remainingCalls := remainingCallsStart; remainingCalls > remainingCallsStop; remainingCalls -= 10 {
112			request, err := githubUpstream.buildRequest(githubUpstream.withResponseHeaders(remainingCallsHeader, strconv.Itoa(remainingCalls)))
113			require.NoError(t, err)
114
115			response, body, ok := assertHandleRequestSuccessful(t, handler, request, http.StatusOK)
116			require.True(t, ok)
117
118			assert.Equal(t, "pong", body)
119			assertAuthHeaderMatchesToken(t, dummyToken, response.Header.Get(receivedAuthHeader))
120		}
121	})
122
123	t.Run("if the response headers do not say how many calls are remaining, it does its best effort to keep track of it", func(t *testing.T) {
124		mockController := gomock.NewController(t)
125		defer mockController.Finish()
126		tokenPool := mock_token_pools.NewMockTokenPoolStorageBackend(mockController)
127
128		dummyToken := "ohlebeautoken"
129
130		tokenPool.EXPECT().CheckOutToken().Return(&types.Token{
131			TokenSpec: types.TokenSpec{
132				Token:             dummyToken,
133				ExpectedRateLimit: 111,
134			},
135			RemainingCalls: 111,
136		}, nil)
137		tokenPool.EXPECT().UpdateTokenUsage(dummyToken, 61)
138		tokenPool.EXPECT().UpdateTokenUsage(dummyToken, 11)
139		tokenPool.EXPECT().UpdateTokenUsage(dummyToken, 0)
140
141		handler := NewRequestHandler(githubUpstream.urlStruct(), tokenPool)
142		for i := 0; i < 111; i++ {
143			request, err := githubUpstream.buildRequest()
144			require.NoError(t, err)
145
146			response, body, ok := assertHandleRequestSuccessful(t, handler, request, http.StatusOK)
147			require.True(t, ok)
148
149			assert.Equal(t, "pong", body)
150			assertAuthHeaderMatchesToken(t, dummyToken, response.Header.Get(receivedAuthHeader))
151		}
152	})
153
154	t.Run("when the token has been exhausted based on the response headers, it requests a new one from the backend", func(t *testing.T) {
155		mockController := gomock.NewController(t)
156		defer mockController.Finish()
157		tokenPool := mock_token_pools.NewMockTokenPoolStorageBackend(mockController)
158
159		dummyToken := "ohlebeautoken"
160		nextToken := "encoreplusbeau"
161
162		tokenPool.EXPECT().CheckOutToken().Return(&types.Token{
163			TokenSpec: types.TokenSpec{
164				Token:             dummyToken,
165				ExpectedRateLimit: 10,
166			},
167			RemainingCalls: 10,
168		}, nil)
169		tokenPool.EXPECT().UpdateTokenUsage(dummyToken, 0)
170		tokenPool.EXPECT().CheckOutToken().Return(&types.Token{
171			TokenSpec: types.TokenSpec{
172				Token:             nextToken,
173				ExpectedRateLimit: 10,
174			},
175			RemainingCalls: 10,
176		}, nil)
177
178		handler := NewRequestHandler(githubUpstream.urlStruct(), tokenPool)
179		for remainingCalls := 10; remainingCalls > -1; remainingCalls -= 1 {
180			request, err := githubUpstream.buildRequest(githubUpstream.withResponseHeaders(remainingCallsHeader, strconv.Itoa(remainingCalls)))
181			require.NoError(t, err)
182
183			response, body, ok := assertHandleRequestSuccessful(t, handler, request, http.StatusOK)
184			require.True(t, ok)
185
186			assert.Equal(t, "pong", body)
187			assertAuthHeaderMatchesToken(t, dummyToken, response.Header.Get(receivedAuthHeader))
188		}
189
190		request, err := githubUpstream.buildRequest(githubUpstream.withResponseHeaders(remainingCallsHeader, "10"))
191		require.NoError(t, err)
192
193		response, body, ok := assertHandleRequestSuccessful(t, handler, request, http.StatusOK)
194		require.True(t, ok)
195
196		assert.Equal(t, "pong", body)
197		assertAuthHeaderMatchesToken(t, nextToken, response.Header.Get(receivedAuthHeader))
198	})
199
200	t.Run("if the response headers do not say how many calls are remaining, it does its best effort to keep track of it and requests a new one when exhausted", func(t *testing.T) {
201		mockController := gomock.NewController(t)
202		defer mockController.Finish()
203		tokenPool := mock_token_pools.NewMockTokenPoolStorageBackend(mockController)
204
205		dummyToken := "ohlebeautoken"
206		nextToken := "encoreplusbeau"
207
208		tokenPool.EXPECT().CheckOutToken().Return(&types.Token{
209			TokenSpec: types.TokenSpec{
210				Token:             dummyToken,
211				ExpectedRateLimit: 10,
212			},
213			RemainingCalls: 10,
214		}, nil)
215		tokenPool.EXPECT().UpdateTokenUsage(dummyToken, 0)
216		tokenPool.EXPECT().CheckOutToken().Return(&types.Token{
217			TokenSpec: types.TokenSpec{
218				Token:             nextToken,
219				ExpectedRateLimit: 10,
220			},
221			RemainingCalls: 10,
222		}, nil)
223
224		handler := NewRequestHandler(githubUpstream.urlStruct(), tokenPool)
225		for remainingCalls := 10; remainingCalls > 0; remainingCalls -= 1 {
226			request, err := githubUpstream.buildRequest()
227			require.NoError(t, err)
228
229			response, body, ok := assertHandleRequestSuccessful(t, handler, request, http.StatusOK)
230			require.True(t, ok)
231
232			assert.Equal(t, "pong", body)
233			assertAuthHeaderMatchesToken(t, dummyToken, response.Header.Get(receivedAuthHeader))
234		}
235
236		request, err := githubUpstream.buildRequest()
237		require.NoError(t, err)
238
239		response, body, ok := assertHandleRequestSuccessful(t, handler, request, http.StatusOK)
240		require.True(t, ok)
241
242		assert.Equal(t, "pong", body)
243		assertAuthHeaderMatchesToken(t, nextToken, response.Header.Get(receivedAuthHeader))
244	})
245
246	t.Run("if a token gets unexpectedly exhausted, it retries with another token", func(t *testing.T) {
247		mockController := gomock.NewController(t)
248		defer mockController.Finish()
249		tokenPool := mock_token_pools.NewMockTokenPoolStorageBackend(mockController)
250
251		dummyToken := "ohlebeautoken"
252		nextToken := "encoreplusbeau"
253
254		tokenPool.EXPECT().CheckOutToken().Return(&types.Token{
255			TokenSpec: types.TokenSpec{
256				Token:             dummyToken,
257				ExpectedRateLimit: 1000,
258			},
259			RemainingCalls: 1000,
260		}, nil)
261		tokenPool.EXPECT().CheckOutToken().Return(&types.Token{
262			TokenSpec: types.TokenSpec{
263				Token:             nextToken,
264				ExpectedRateLimit: 1000,
265			},
266			RemainingCalls: 1000,
267		}, nil)
268
269		githubUpstream.resetRequestsCount()
270		githubUpstream.addResponse(http.StatusForbidden, `{"message": "API rate limit exceeded"}`)
271
272		request, err := githubUpstream.buildRequest()
273		require.NoError(t, err)
274
275		response, body, ok := assertHandleRequestSuccessful(t, NewRequestHandler(githubUpstream.urlStruct(), tokenPool), request, http.StatusOK)
276		require.True(t, ok)
277
278		assert.Equal(t, "pong", body)
279		assertAuthHeaderMatchesToken(t, nextToken, response.Header.Get(receivedAuthHeader))
280
281		assert.Equal(t, uint32(2), githubUpstream.requestsCount)
282	})
283
284	t.Run("it doesn't retry failed requests that did not fail because of throttling, and does not request new tokens", func(t *testing.T) {
285		mockController := gomock.NewController(t)
286		defer mockController.Finish()
287		tokenPool := mock_token_pools.NewMockTokenPoolStorageBackend(mockController)
288
289		dummyToken := "ohlebeautoken"
290
291		tokenPool.EXPECT().CheckOutToken().Return(&types.Token{
292			TokenSpec: types.TokenSpec{
293				Token:             dummyToken,
294				ExpectedRateLimit: 1000,
295			},
296			RemainingCalls: 1000,
297		}, nil)
298
299		githubUpstream.resetRequestsCount()
300
301		requestHandler := NewRequestHandler(githubUpstream.urlStruct(), tokenPool)
302
303		// wrong status code
304		request, err := githubUpstream.buildRequest(githubUpstream.withResponseStatusCode(http.StatusUnauthorized))
305		require.NoError(t, err)
306		_, body, ok := assertHandleRequestSuccessful(t, requestHandler, request, http.StatusUnauthorized)
307		require.True(t, ok)
308		assert.Equal(t, "pong", body)
309		assert.Equal(t, uint32(1), githubUpstream.requestsCount)
310
311		// wrong body: not a JSON
312		responseBody := "dummy error"
313		request, err = githubUpstream.buildRequest(githubUpstream.withResponseStatusCode(http.StatusForbidden),
314			githubUpstream.withResponseBody(responseBody))
315		require.NoError(t, err)
316		_, body, ok = assertHandleRequestSuccessful(t, requestHandler, request, http.StatusForbidden)
317		require.True(t, ok)
318		assert.Equal(t, responseBody, body)
319		assert.Equal(t, uint32(2), githubUpstream.requestsCount)
320
321		// wrong body: not the expected message
322		responseBody = `{"message": "dunno"}`
323		request, err = githubUpstream.buildRequest(githubUpstream.withResponseStatusCode(http.StatusForbidden),
324			githubUpstream.withResponseBody(responseBody))
325		require.NoError(t, err)
326		_, body, ok = assertHandleRequestSuccessful(t, requestHandler, request, http.StatusForbidden)
327		require.True(t, ok)
328		assert.Equal(t, responseBody, body)
329		assert.Equal(t, uint32(3), githubUpstream.requestsCount)
330	})
331
332	t.Run("it does not do anything to requests going to other hosts than Github's API", func(t *testing.T) {
333		mockController := gomock.NewController(t)
334		defer mockController.Finish()
335		tokenPool := mock_token_pools.NewMockTokenPoolStorageBackend(mockController)
336
337		request, err := otherUpstream.buildRequest()
338		require.NoError(t, err)
339
340		otherUpstream.resetRequestsCount()
341
342		response, body, ok := assertHandleRequestSuccessful(t, NewRequestHandler(githubUpstream.urlStruct(), tokenPool), request, http.StatusOK)
343		require.True(t, ok)
344
345		assert.Equal(t, "pong", body)
346		assert.Equal(t, "", response.Header.Get(receivedAuthHeader))
347		assert.Equal(t, uint32(1), otherUpstream.requestsCount)
348	})
349
350	t.Run("for requests not going to Github, it correctly copies the request and response's bodies back and forth", func(t *testing.T) {
351		mockController := gomock.NewController(t)
352		defer mockController.Finish()
353		tokenPool := mock_token_pools.NewMockTokenPoolStorageBackend(mockController)
354
355		reqBody := "t'as de beaux yeux tu sais"
356		request, err := otherUpstream.buildRequest(otherUpstream.withRequestPath("/echo"),
357			otherUpstream.withRequestBody(reqBody))
358		require.NoError(t, err)
359
360		otherUpstream.resetRequestsCount()
361
362		response, respBody, ok := assertHandleRequestSuccessful(t, NewRequestHandler(githubUpstream.urlStruct(), tokenPool), request, http.StatusOK)
363		require.True(t, ok)
364
365		assert.Equal(t, reqBody, respBody)
366		assert.Equal(t, "", response.Header.Get(receivedAuthHeader))
367		assert.Equal(t, uint32(1), otherUpstream.requestsCount)
368	})
369
370	for _, poolErrorTestCase := range []struct {
371		name                        string
372		poolError                   error
373		expectedHandlerErrorMessage string
374	}{
375		{
376			name:                        "if the pool doesn't have any tokens, it bubbles up the error gracefully",
377			expectedHandlerErrorMessage: "no available API token",
378		},
379		{
380			name:                        "if the pool errors out, it bubbles up the error gracefully",
381			poolError:                   errors.New("dummy pool error"),
382			expectedHandlerErrorMessage: "dummy pool error",
383		},
384	} {
385		t.Run(poolErrorTestCase.name, func(t *testing.T) {
386			mockController := gomock.NewController(t)
387			defer mockController.Finish()
388			tokenPool := mock_token_pools.NewMockTokenPoolStorageBackend(mockController)
389
390			tokenPool.EXPECT().CheckOutToken().Return(nil, poolErrorTestCase.poolError)
391
392			request, err := githubUpstream.buildRequest()
393			require.NoError(t, err)
394
395			response, err := NewRequestHandler(githubUpstream.urlStruct(), tokenPool).ProxyRequest(request)
396			if assert.Error(t, err) {
397				assert.Contains(t, err.Error(), poolErrorTestCase.expectedHandlerErrorMessage)
398			}
399			assert.Nil(t, response)
400		})
401	}
402
403	t.Run("it is thread-safe", func(t *testing.T) {
404		mockController := gomock.NewController(t)
405		defer mockController.Finish()
406		tokenPool := mock_token_pools.NewMockTokenPoolStorageBackend(mockController)
407
408		dummyToken := "ohlebeautoken"
409		nextToken := "encoreplusbeau"
410		bothTokens := []string{dummyToken, nextToken}
411
412		tokenPool.EXPECT().CheckOutToken().Return(&types.Token{
413			TokenSpec: types.TokenSpec{
414				Token:             dummyToken,
415				ExpectedRateLimit: 250,
416			},
417			RemainingCalls: 250,
418		}, nil)
419		for remaining := 200; remaining >= 0; remaining -= remainingCallsReportingInterval {
420			tokenPool.EXPECT().UpdateTokenUsage(dummyToken, remaining)
421		}
422		tokenPool.EXPECT().CheckOutToken().Return(&types.Token{
423			TokenSpec: types.TokenSpec{
424				Token:             nextToken,
425				ExpectedRateLimit: 250,
426			},
427			RemainingCalls: 250,
428		}, nil)
429		for remaining := 200; remaining >= remainingCallsReportingInterval; remaining -= remainingCallsReportingInterval {
430			tokenPool.EXPECT().UpdateTokenUsage(nextToken, remaining)
431		}
432
433		nRequests := 490
434		requests := make([]*http.Request, nRequests)
435		for i := 0; i < nRequests; i++ {
436			request, err := githubUpstream.buildRequest(githubUpstream.withResponseHeaders(rateLimitHeader, "250"))
437			require.NoError(t, err)
438			requests[i] = request
439		}
440
441		handler := NewRequestHandler(githubUpstream.urlStruct(), tokenPool)
442		nThreads := 10
443		var wg sync.WaitGroup
444		perThread := nRequests / nThreads
445		countsPerToken := make(map[string]int)
446		var countsMutex sync.Mutex
447
448		for threadID := 0; threadID < nThreads; threadID++ {
449			wg.Add(1)
450			go func(startIndex int) {
451				defer wg.Done()
452				for i := startIndex; i < startIndex+perThread; i++ {
453					response, body, ok := assertHandleRequestSuccessful(t, handler, requests[i], http.StatusOK)
454					require.True(t, ok)
455
456					assert.Equal(t, "pong", body)
457					token, err := extractTokenFromAuthHeader(response.Header.Get(receivedAuthHeader))
458					if assert.NoError(t, err) && assert.Contains(t, bothTokens, token) {
459						countsMutex.Lock()
460						countsPerToken[token]++
461						countsMutex.Unlock()
462					}
463				}
464			}(threadID * perThread)
465		}
466
467		wg.Wait()
468		assert.True(t, countsPerToken[dummyToken] >= 250)
469		assert.True(t, countsPerToken[dummyToken] < 260)
470		assert.Equal(t, nRequests, countsPerToken[dummyToken]+countsPerToken[nextToken])
471	})
472
473	t.Run("it stops using tokens when too close to their reset timestamp, based on the response headers", func(t *testing.T) {
474		mockController := gomock.NewController(t)
475		defer mockController.Finish()
476		tokenPool := mock_token_pools.NewMockTokenPoolStorageBackend(mockController)
477
478		dummyToken := "ohlebeautoken"
479		nextToken := "encoreplusbeau"
480
481		tokenPool.EXPECT().CheckOutToken().Return(&types.Token{
482			TokenSpec: types.TokenSpec{
483				Token:             dummyToken,
484				ExpectedRateLimit: 1000,
485			},
486			RemainingCalls: 1000,
487		}, nil)
488		tokenPool.EXPECT().CheckOutToken().Return(&types.Token{
489			TokenSpec: types.TokenSpec{
490				Token:             nextToken,
491				ExpectedRateLimit: 10,
492			},
493			RemainingCalls: 10,
494		}, nil)
495
496		handler := NewRequestHandler(githubUpstream.urlStruct(), tokenPool)
497
498		// a fist request, that says it resets in 30 secs
499		resetIn30Secs := internal.TimeNowUnix() + 30
500		request, err := githubUpstream.buildRequest(githubUpstream.withResponseHeaders(resetTimestampHeader, strconv.FormatInt(resetIn30Secs, 10)))
501		require.NoError(t, err)
502		response, body, ok := assertHandleRequestSuccessful(t, handler, request, http.StatusOK)
503		require.True(t, ok)
504		assert.Equal(t, "pong", body)
505		assertAuthHeaderMatchesToken(t, dummyToken, response.Header.Get(receivedAuthHeader))
506
507		// let's make another request, that should make the handler request a new token from the pool
508		request, err = githubUpstream.buildRequest()
509		require.NoError(t, err)
510		response, body, ok = assertHandleRequestSuccessful(t, handler, request, http.StatusOK)
511		require.True(t, ok)
512		assert.Equal(t, "pong", body)
513		assertAuthHeaderMatchesToken(t, nextToken, response.Header.Get(receivedAuthHeader))
514	})
515
516	t.Run("if the response headers don't include a reset timestamp, it keeps track of"+
517		" when the token was checked out, and stops using it when too close to the reset", func(t *testing.T) {
518		mockController := gomock.NewController(t)
519		defer mockController.Finish()
520		tokenPool := mock_token_pools.NewMockTokenPoolStorageBackend(mockController)
521
522		dummyToken := "ohlebeautoken"
523		nextToken := "encoreplusbeau"
524
525		tokenPool.EXPECT().CheckOutToken().Return(&types.Token{
526			TokenSpec: types.TokenSpec{
527				Token:             dummyToken,
528				ExpectedRateLimit: 1000,
529			},
530			RemainingCalls: 1000,
531		}, nil)
532		tokenPool.EXPECT().CheckOutToken().Return(&types.Token{
533			TokenSpec: types.TokenSpec{
534				Token:             nextToken,
535				ExpectedRateLimit: 10,
536			},
537			RemainingCalls: 10,
538		}, nil)
539
540		handler := NewRequestHandler(githubUpstream.urlStruct(), tokenPool)
541
542		defer internal.WithTimeMock(t, 1000, 4540, 4540)()
543
544		// first request
545		request, err := githubUpstream.buildRequest()
546		require.NoError(t, err)
547		response, body, ok := assertHandleRequestSuccessful(t, handler, request, http.StatusOK)
548		require.True(t, ok)
549		assert.Equal(t, "pong", body)
550		assertAuthHeaderMatchesToken(t, dummyToken, response.Header.Get(receivedAuthHeader))
551
552		// second request is 59 minutes later, shouldn't use the same token
553		request, err = githubUpstream.buildRequest()
554		require.NoError(t, err)
555		response, body, ok = assertHandleRequestSuccessful(t, handler, request, http.StatusOK)
556		require.True(t, ok)
557		assert.Equal(t, "pong", body)
558		assertAuthHeaderMatchesToken(t, nextToken, response.Header.Get(receivedAuthHeader))
559	})
560}
561
562func TestRequestHandler_HandleGithubAPIRequest(t *testing.T) {
563	githubUpstream := newUpstreamServer(t)
564	githubUpstream.start()
565	defer githubUpstream.stop()
566
567	t.Run("it adds tokens to Github API requests", func(t *testing.T) {
568		mockController := gomock.NewController(t)
569		defer mockController.Finish()
570		tokenPool := mock_token_pools.NewMockTokenPoolStorageBackend(mockController)
571
572		handler := NewRequestHandler(githubUpstream.urlStruct(), tokenPool)
573
574		proxy := &testHTTPServer{
575			t: t,
576			handler: http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
577				handler.HandleGithubAPIRequest(writer, request)
578			}),
579		}
580		proxy.start()
581		defer proxy.stop()
582
583		dummyToken := "ohlebeautoken"
584
585		tokenPool.EXPECT().CheckOutToken().Return(&types.Token{
586			TokenSpec: types.TokenSpec{
587				Token:             dummyToken,
588				ExpectedRateLimit: 1000,
589			},
590			RemainingCalls: 1000,
591		}, nil)
592
593		reqBody := "hey you"
594		request, err := proxy.buildRequest(proxy.withRequestPath("/echo"),
595			proxy.withRequestBody(reqBody))
596		require.NoError(t, err)
597
598		response, err := http.DefaultClient.Do(request)
599		require.NoError(t, err)
600
601		respBody, ok := assertResponse(t, response, http.StatusOK)
602		require.True(t, ok)
603
604		assert.Equal(t, reqBody, respBody)
605		assertAuthHeaderMatchesToken(t, dummyToken, response.Header.Get(receivedAuthHeader))
606	})
607
608	t.Run("if ProxyRequest returns an error, it translates that to a 500 error", func(t *testing.T) {
609		mockController := gomock.NewController(t)
610		defer mockController.Finish()
611		tokenPool := mock_token_pools.NewMockTokenPoolStorageBackend(mockController)
612
613		handler := NewRequestHandler(githubUpstream.urlStruct(), tokenPool)
614		handler.Transport = &failingTransport{}
615
616		proxy := &testHTTPServer{
617			t: t,
618			handler: http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
619				handler.HandleGithubAPIRequest(writer, request)
620			}),
621		}
622		proxy.start()
623		defer proxy.stop()
624
625		dummyToken := "ohlebeautoken"
626
627		tokenPool.EXPECT().CheckOutToken().Return(&types.Token{
628			TokenSpec: types.TokenSpec{
629				Token:             dummyToken,
630				ExpectedRateLimit: 1000,
631			},
632			RemainingCalls: 1000,
633		}, nil)
634
635		request, err := proxy.buildRequest()
636		require.NoError(t, err)
637
638		response, err := http.DefaultClient.Do(request)
639		require.NoError(t, err)
640
641		body, ok := assertResponse(t, response, http.StatusInternalServerError)
642		require.True(t, ok)
643
644		assert.Equal(t, proxyErrorResponseBody, body)
645	})
646}
647
648// Test helpers below
649
650type testHTTPServer struct {
651	t       *testing.T
652	handler http.Handler
653
654	server        *http.Server
655	port          int
656	requestsCount uint32
657}
658
659func (s *testHTTPServer) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
660	s.handler.ServeHTTP(writer, request)
661	atomic.AddUint32(&s.requestsCount, 1)
662}
663
664func (s *testHTTPServer) resetRequestsCount() {
665	s.requestsCount = 0
666}
667
668func (s *testHTTPServer) start() {
669	address := findAvailableAddress(s.t)
670
671	s.server = &http.Server{
672		Addr:    address,
673		Handler: s,
674	}
675
676	listeningChan := make(chan interface{})
677	go func() {
678		require.NoError(s.t, startHTTPServer(s.server, nil, listeningChan, ""))
679	}()
680
681	<-listeningChan
682
683	split := strings.Split(address, ":")
684	port, err := strconv.Atoi(split[len(split)-1])
685	require.NoError(s.t, err)
686	s.port = port
687}
688
689func (s *testHTTPServer) stop() {
690	require.NoError(s.t, s.server.Shutdown(context.Background()))
691}
692
693func findAvailableAddress(t *testing.T) string {
694	listener, err := net.Listen("tcp", ":0")
695	require.NoError(t, err)
696	require.NoError(t, listener.Close())
697	return listener.Addr().String()
698}
699
700type testRequestOption func(*http.Request) error
701
702func (s *testHTTPServer) withRequestBody(body string) testRequestOption {
703	return func(request *http.Request) error {
704		reader := strings.NewReader(body)
705		request.Body = io.NopCloser(reader)
706		request.ContentLength = int64(len(body))
707
708		return nil
709	}
710}
711
712func (s *testHTTPServer) withRequestPath(path string) testRequestOption {
713	urlString := s.urlString() + path
714	u, err := url.Parse(urlString)
715	if err != nil {
716		return func(request *http.Request) error {
717			return errors.Wrapf(err, "unable to parse URL %q", urlString)
718		}
719	}
720
721	return func(request *http.Request) error {
722		request.URL = u
723		return nil
724	}
725}
726
727func (s *testHTTPServer) withQueryParam(key, value string) testRequestOption {
728	return func(request *http.Request) error {
729		query := request.URL.Query()
730		query.Add(key, value)
731		request.URL.RawQuery = query.Encode()
732		return nil
733	}
734}
735
736func (s *testHTTPServer) buildRequest(options ...testRequestOption) (request *http.Request, err error) {
737	request, err = http.NewRequest("GET", s.urlString(), nil)
738	if err != nil {
739		return
740	}
741
742	for _, option := range options {
743		if err = option(request); err != nil {
744			return
745		}
746	}
747
748	return
749}
750
751func (s *testHTTPServer) urlString() string {
752	return fmt.Sprintf("http://localhost:%d", s.port)
753}
754
755func (s *testHTTPServer) urlStruct() *url.URL {
756	u, err := url.Parse(s.urlString())
757	require.NoError(s.t, err)
758	return u
759}
760
761// to be used as an upstream server for tests
762type testUpstreamServer struct {
763	*testHTTPServer
764
765	nextStatusCodes []int
766	nextBodies      []string
767}
768
769const (
770	receivedAuthHeader = "x-received-authorization"
771	extraHeadersKey    = "extra_headers"
772	statusCodeKey      = "status_code"
773	bodyKey            = "body"
774)
775
776func newUpstreamServer(t *testing.T) *testUpstreamServer {
777	u := &testUpstreamServer{}
778	u.testHTTPServer = &testHTTPServer{
779		t:       t,
780		handler: u,
781	}
782	return u
783}
784
785func (u *testUpstreamServer) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
786	responseHeaders := writer.Header()
787
788	if authHeader := request.Header.Get("authorization"); authHeader != "" {
789		responseHeaders.Add(receivedAuthHeader, authHeader)
790	}
791
792	if extraHeadersStr := request.URL.Query().Get(extraHeadersKey); extraHeadersStr != "" {
793		extraHeaders := make(map[string]string)
794		require.NoError(u.t, json.Unmarshal([]byte(extraHeadersStr), &extraHeaders))
795		for key, value := range extraHeaders {
796			responseHeaders.Add(key, value)
797		}
798	}
799
800	statusCode := http.StatusOK
801	if len(u.nextStatusCodes) != 0 {
802		statusCode = u.nextStatusCodes[0]
803		u.nextStatusCodes = u.nextStatusCodes[1:]
804	} else if statusCodeStr := request.URL.Query().Get(statusCodeKey); statusCodeStr != "" {
805		var err error
806		statusCode, err = strconv.Atoi(statusCodeStr)
807		require.NoError(u.t, err)
808	}
809	writer.WriteHeader(statusCode)
810
811	body := request.URL.Query().Get(bodyKey)
812	if len(u.nextBodies) != 0 {
813		body = u.nextBodies[0]
814		u.nextBodies = u.nextBodies[1:]
815	} else if request.URL.Path == "/echo" {
816		reqBody, err := ioutil.ReadAll(request.Body)
817		require.NoError(u.t, err)
818		body = string(reqBody)
819	}
820	if body == "" {
821		body = "pong"
822	}
823
824	_, err := writer.Write([]byte(body))
825	require.NoError(u.t, err)
826}
827
828func (u *testUpstreamServer) addResponse(statusCode int, body string) {
829	u.nextStatusCodes = append(u.nextStatusCodes, statusCode)
830	u.nextBodies = append(u.nextBodies, body)
831}
832
833func (u *testUpstreamServer) withResponseHeaders(headers ...string) testRequestOption {
834	if len(headers)%2 != 0 {
835		return func(request *http.Request) error {
836			return errors.New("extra headers must come in key-value pairs")
837		}
838	}
839
840	headersMap := make(map[string]string)
841	for i := 0; i < len(headers); i += 2 {
842		headersMap[headers[i]] = headers[i+1]
843	}
844
845	headersJson, err := json.Marshal(headersMap)
846	if err != nil {
847		return func(request *http.Request) error {
848			return err
849		}
850	}
851
852	return u.withQueryParam(extraHeadersKey, string(headersJson))
853}
854
855func (u *testUpstreamServer) withResponseStatusCode(statusCode int) testRequestOption {
856	return u.withQueryParam(statusCodeKey, strconv.Itoa(statusCode))
857}
858
859func (u *testUpstreamServer) withResponseBody(body string) testRequestOption {
860	return u.withQueryParam(bodyKey, body)
861}
862
863func extractTokenFromAuthHeader(header string) (string, error) {
864	b64Value := strings.TrimPrefix(header, "Basic ")
865	if b64Value == header {
866		return "", errors.Errorf("Expected header %q to start with Basic", header)
867	}
868	decoded, err := base64.StdEncoding.DecodeString(b64Value)
869	if err != nil {
870		return "", errors.Wrapf(err, "Unable to decode b64 string %q", b64Value)
871	}
872	split := strings.Split(string(decoded), ":")
873	if len(split) != 2 || split[1] != basicAuthPassword {
874		return "", errors.Errorf("Unexpected auth header: %q", string(decoded))
875	}
876	return split[0], nil
877}
878
879func assertAuthHeaderMatchesToken(t *testing.T, expectedToken, header string) bool {
880	token, err := extractTokenFromAuthHeader(header)
881	return assert.NoError(t, err) && assert.Equal(t, expectedToken, token)
882}
883
884func assertHandleRequestSuccessful(t *testing.T, handler *RequestHandler, request *http.Request, expectedStatusCode int) (response *http.Response, body string, success bool) {
885	var err error
886	response, err = handler.ProxyRequest(request)
887	body, success = assertResponse(t, response, expectedStatusCode)
888	success = assert.NoError(t, err) && success
889	return
890}
891
892func assertResponse(t *testing.T, response *http.Response, expectedStatusCode int) (body string, success bool) {
893	if !assert.NotNil(t, response) {
894		return
895	}
896	defer func() {
897		assert.NoError(t, response.Body.Close())
898	}()
899	if !assert.Equal(t, expectedStatusCode, response.StatusCode) {
900		return
901	}
902
903	bodyBytes, err := ioutil.ReadAll(response.Body)
904	if !assert.NoError(t, err) {
905		return
906	}
907
908	return string(bodyBytes), true
909}
910
911var failingTransportError = errors.New("failing transport")
912
913type failingTransport struct{}
914
915func (f failingTransport) RoundTrip(request *http.Request) (*http.Response, error) {
916	return nil, failingTransportError
917}
918
Full Screen

Accelerate Your Automation Test Cycles With LambdaTest

Leverage LambdaTest’s cloud-based platform to execute your automation tests in parallel and trim down your test execution time significantly. Your first 100 automation testing minutes are on us.

Try LambdaTest

Most used method in

Trigger EXPECT code on LambdaTest Cloud Grid

Execute automation tests with EXPECT on a cloud-based Grid of 3000+ real browsers and operating systems for both web and mobile applications.

Test now for Free
LambdaTestX

We use cookies to give you the best experience. Cookies help to provide a more personalized experience and relevant advertising for you, and web analytics for us. Learn More in our Cookies policy, Privacy & Terms of service

Allow Cookie
Sarah

I hope you find the best code examples for your project.

If you want to accelerate automated browser testing, try LambdaTest. Your first 100 automation testing minutes are FREE.

Sarah Elson (Product & Growth Lead)