-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathhttptestserver_test.go
434 lines (385 loc) · 15.7 KB
/
httptestserver_test.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
package gosette
import (
"bytes"
"fmt"
"io"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
/*************************************************************************************************/
/* TEST SUITE SETUP */
/*************************************************************************************************/
// Unit test suite for HTTP Test Server
type HTTPTestServerUnitTestSuite struct {
// Test suite
suite.Suite
// HTTPTestServer used by the test suite
hts *HTTPTestServer
}
// Run the unit test suite.
func TestMockHTTPServerUnitTestSuite(t *testing.T) {
suite.Run(t, &HTTPTestServerUnitTestSuite{})
}
// Build and start HTTPTestServer when test suite starts
func (suite *HTTPTestServerUnitTestSuite) SetupSuite() {
suite.hts = NewHTTPTestServer(nil)
suite.hts.Start()
}
// Clear test server predefined responses and records after each test
func (suite *HTTPTestServerUnitTestSuite) TearDownTest() {
suite.hts.Clear()
}
// Close HTTPTestServer before finishing tests
func (suite *HTTPTestServerUnitTestSuite) TearDownSuite() {
suite.hts.Close()
}
/*************************************************************************************************/
/* TESTS */
/*************************************************************************************************/
// Test HTTPTestServer with a predefined JSON response.
//
// This first test shows a simple usage of the test server and how to use recorded request, body
// and response.
func (suite *HTTPTestServerUnitTestSuite) TestWithSingleJsonResponse() {
// Content of a predefined response & expectations
expectedContentType := "application/json"
predefinedJsonResponse := `
{
"id": 1,
"test": "success"
}`
expectedStatusCode := http.StatusOK
// Content of request
payload := `{
"hello": "world"
}`
// Push a predefined response to test server
suite.hts.PushPredefinedServerResponse(&PredefinedServerResponse{
Status: expectedStatusCode,
Headers: map[string][]string{
"Content-Type": {expectedContentType},
},
Body: []byte(predefinedJsonResponse),
})
// Get a http.Client which trusts the server certificate if TLS is enabled
client := suite.hts.Client()
require.NotNil(suite.T(), client)
// Send a HTTP request to server
// Server is expected to reply with the predefined response
resp, err := client.Post(suite.hts.server.URL, "application/json", strings.NewReader(payload))
require.NoError(suite.T(), err)
require.NotNil(suite.T(), resp)
// Check first response status code
require.Equal(suite.T(), expectedStatusCode, resp.StatusCode)
// Check response content type
require.Equal(suite.T(), expectedContentType, resp.Header.Get("Content-Type"))
// Read response body and compare with expected first response
respBody, err := io.ReadAll(resp.Body)
require.NoError(suite.T(), err)
require.NotEmpty(suite.T(), respBody)
require.Equal(suite.T(), []byte(predefinedJsonResponse), respBody)
// Pop server record
record := suite.hts.PopServerRecord()
require.NoError(suite.T(), record.ServerError)
require.NotNil(suite.T(), record.Request)
require.NotNil(suite.T(), record.Response)
// Check recorded request body against original payload
recReqBody, err := io.ReadAll(record.RequestBody)
require.NoError(suite.T(), err)
require.Equal(suite.T(), payload, string(recReqBody))
// Extract recorded response body and compare
recordedRespBody, err := io.ReadAll(record.Response.Result().Body)
require.NoError(suite.T(), err)
require.Equal(suite.T(), respBody, recordedRespBody)
// Compare request method & URL
require.Contains(suite.T(), suite.hts.server.URL, record.Request.Host)
require.Equal(suite.T(), http.MethodPost, record.Request.Method)
}
// Test HTTPTestServer when form encoded data are provided in the incoming request. Test will
// ensure test server handles and record well the request form data.
func (suite *HTTPTestServerUnitTestSuite) TestWithFormEncodedData() {
// Predefined form data & expectations
expectedStatusCode := http.StatusCreated
expectedUrlPath := "/form"
predefinedFormData := url.Values{
"id": []string{"1"},
"messages": []string{"hello", "world"},
}
// Push a predefined response to test server
suite.hts.PushPredefinedServerResponse(&PredefinedServerResponse{
Status: expectedStatusCode,
Headers: map[string][]string{},
Body: nil,
})
// Get a http.Client which trusts the server certificate if TLS is enabled
client := suite.hts.Client()
require.NotNil(suite.T(), client)
// Send a POST request wiith form data
resp, err := client.PostForm(suite.hts.GetBaseURL()+expectedUrlPath, predefinedFormData)
require.NoError(suite.T(), err)
require.NotNil(suite.T(), resp)
// Check response
require.Equal(suite.T(), expectedStatusCode, resp.StatusCode)
respBody, err := io.ReadAll(resp.Body)
require.NoError(suite.T(), err)
require.Empty(suite.T(), respBody)
// Extract recorded request and response
srvrec := suite.hts.PopServerRecord()
require.NotNil(suite.T(), srvrec)
// Check request
require.Equal(suite.T(), http.MethodPost, srvrec.Request.Method)
require.Equal(suite.T(), expectedUrlPath, srvrec.Request.URL.String())
require.Equal(suite.T(), predefinedFormData, srvrec.Request.Form)
// Check recorded request body
recReqBody, err := io.ReadAll(srvrec.RequestBody)
require.NoError(suite.T(), err)
require.Equal(suite.T(), []byte(predefinedFormData.Encode()), recReqBody)
// Check recorded response
require.Equal(suite.T(), resp.StatusCode, srvrec.Response.Result().StatusCode)
recRespBody, err := io.ReadAll(srvrec.Response.Result().Body)
require.NoError(suite.T(), err)
require.Empty(suite.T(), recRespBody)
}
// Test HTTPTestServer when multiple predefined responses are defined. Test will ensure:
// - An empty 404 response is served when no predefined responses are available
// - PopServerRecord pops records and returns nil when no records are available
// - Server records and serves well multiple predefined repsonses in FIFO order.
// - When only one response is left, it is served indefinitly
// - ClearServerRecords clears the record queue
// - ClearPredefinedResponse clears the response queue
func (suite *HTTPTestServerUnitTestSuite) TestWithMultipleResponses() {
// Get a HTTP client
client := suite.hts.Client()
// First, test a request when no predefined repsonse are set
resp, err := client.Get(suite.hts.GetBaseURL())
require.NoError(suite.T(), err)
require.NotNil(suite.T(), resp)
require.Equal(suite.T(), http.StatusNotFound, resp.StatusCode)
respBody, err := io.ReadAll(resp.Body)
require.NoError(suite.T(), err)
require.Empty(suite.T(), respBody)
// Pop server record twice. First record must contain the exchanged request-response
// and the second call must return a nil value
srvrec := suite.hts.PopServerRecord()
require.NotNil(suite.T(), srvrec)
require.Equal(suite.T(), http.StatusNotFound, srvrec.Response.Result().StatusCode)
srvrec = suite.hts.PopServerRecord()
require.Nil(suite.T(), srvrec)
// Push multiple predefined response
expectedStatusCode1 := http.StatusOK
expectedStatusCode2 := http.StatusNoContent
suite.hts.PushPredefinedServerResponse(&PredefinedServerResponse{
Status: expectedStatusCode1,
Headers: map[string][]string{},
Body: nil,
})
suite.hts.PushPredefinedServerResponse(&PredefinedServerResponse{
Status: expectedStatusCode2,
Headers: map[string][]string{},
Body: nil,
})
// Send a first request and ensure first response is served
resp, err = client.Get(suite.hts.GetBaseURL())
require.NoError(suite.T(), err)
require.NotNil(suite.T(), resp)
require.Equal(suite.T(), expectedStatusCode1, resp.StatusCode)
// Send a second request and ensure second response is served
resp, err = client.Get(suite.hts.GetBaseURL())
require.NoError(suite.T(), err)
require.NotNil(suite.T(), resp)
require.Equal(suite.T(), expectedStatusCode2, resp.StatusCode)
// Send a third request and ensure second served is still served
resp, err = client.Get(suite.hts.GetBaseURL())
require.NoError(suite.T(), err)
require.NotNil(suite.T(), resp)
require.Equal(suite.T(), expectedStatusCode2, resp.StatusCode)
// Ensure 3 server records are available then clear them and check
require.NotEmpty(suite.T(), suite.hts.records)
require.Len(suite.T(), suite.hts.records, 3)
suite.hts.ClearServerRecords()
require.Empty(suite.T(), suite.hts.records)
// Clear server responses and ensure an empty response is now served
require.NotEmpty(suite.T(), suite.hts.responses)
suite.hts.ClearPredefinedServerResponses()
require.Empty(suite.T(), suite.hts.responses)
resp, err = client.Get(suite.hts.GetBaseURL())
require.NoError(suite.T(), err)
require.NotNil(suite.T(), resp)
require.Equal(suite.T(), http.StatusNotFound, resp.StatusCode)
}
// Test HTTPServer with TLS enabled
func (suite *HTTPTestServerUnitTestSuite) TestWithTLSEnabled() {
// Create a base httptest server
basetlssrv := httptest.NewUnstartedServer(nil)
require.NotNil(suite.T(), basetlssrv)
// Create a separate HTTPTestServer and plug the httptest.Server
srv := NewHTTPTestServer(basetlssrv)
require.NotNil(suite.T(), srv)
// Start the HTTPTestServer with TLS
srv.StartTLS()
defer srv.Close()
// Check BaseURL has https as scheme
require.Contains(suite.T(), srv.GetBaseURL(), "https")
// Test GetUnderlyingHTTPTestServer
tmp := srv.GetUnderlyingHTTPTestServer()
require.NotNil(suite.T(), tmp)
require.Equal(suite.T(), basetlssrv.Config.Addr, tmp.Config.Addr)
// Get a HTTP client with TLS settings
tlsclient := srv.Client()
// Send a request with TLS client and expect an empty 404 response from test server
resp, err := tlsclient.Get(srv.GetBaseURL())
require.NoError(suite.T(), err)
require.NotNil(suite.T(), resp)
require.Equal(suite.T(), http.StatusNotFound, resp.StatusCode)
// Create a default HTTP client
notlsclient := http.DefaultClient
// Send a request to base url and expect it to fail (remote errorr: tls: bad certificate)
resp, err = notlsclient.Get(srv.GetBaseURL())
require.Error(suite.T(), err)
require.Nil(suite.T(), resp)
}
// Test handleInternalError
func (suite *HTTPTestServerUnitTestSuite) TestHandleInternalError() {
// Create a recorder to record response written by handler
rec := httptest.NewRecorder()
// Create a server record which will receive the provided error
srvrec := &ServerRecord{
Request: nil,
Response: nil,
RequestBody: &bytes.Buffer{},
ServerError: nil,
}
// Expected error
eerr := fmt.Errorf("PWNED")
// Use the error handler
suite.hts.handleInternalError(rec, srvrec, eerr)
// Check the server record and compare errors
require.Equal(suite.T(), eerr, srvrec.ServerError)
// Check response and expect 500 as status code + error string as text body
require.Equal(suite.T(), http.StatusInternalServerError, rec.Result().StatusCode)
require.Equal(suite.T(), "text/plain", rec.Result().Header.Get("Content-Type"))
recRespBody, err := io.ReadAll(rec.Result().Body)
require.NoError(suite.T(), err)
require.NotEmpty(suite.T(), recRespBody)
require.Equal(suite.T(), eerr.Error(), string(recRespBody))
}
// Test test server handler error paths
func (suite *HTTPTestServerUnitTestSuite) TestServeHTTPErrPaths() {
// Create a mockReadCloser which fails when body is read
expectedErr := fmt.Errorf("PWNED")
mockedReadCloser := mockReadCloser{mock.Mock{}}
mockedReadCloser.
On("Read", mock.Anything).Return(0, expectedErr).
On("Close").Return(nil)
// Create a mockResponseWriter which fails when response is written
mockedResponseWriter := mockResponseWriter{mock.Mock{}}
mockedResponseWriter.
On("Header").Return(http.Header{}).
On("WriteHeader", mock.Anything).Return().
On("Write", mock.Anything).Return(0, expectedErr)
// Test server handler - fails when reading body
respRec := httptest.NewRecorder()
require.NotNil(suite.T(), respRec)
req := httptest.NewRequest(http.MethodPost, "/", &mockedReadCloser)
require.NotNil(suite.T(), req)
suite.hts.ServeHTTP(respRec, req)
require.Equal(suite.T(), http.StatusInternalServerError, respRec.Result().StatusCode)
// Test server handler - fails when parsing form data
respRec = httptest.NewRecorder()
require.NotNil(suite.T(), respRec)
req = httptest.NewRequest(http.MethodPost, "/", &mockedReadCloser)
// Set content type to form encoded so body is read by parseForm
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
require.NotNil(suite.T(), req)
suite.hts.ServeHTTP(respRec, req)
require.Equal(suite.T(), http.StatusInternalServerError, respRec.Result().StatusCode)
// Create a multi target writer with a recorder and the mock which will fail write
respRec = httptest.NewRecorder()
require.NotNil(suite.T(), respRec)
mw := newMultiTargetHTTPResponseWriter(respRec, &mockedResponseWriter)
require.NotNil(suite.T(), mw)
// Create a GET request with no body to read
req = httptest.NewRequest(http.MethodGet, "/", nil)
require.NotNil(suite.T(), req)
// Set a predefined response with a body to write
suite.hts.PushPredefinedServerResponse(&PredefinedServerResponse{
Status: http.StatusOK,
Headers: map[string][]string{
"Content-Type": {"text/plain"},
},
Body: []byte("hello world!"),
})
// Clear server records
suite.hts.ClearServerRecords()
// Call ServeHTTP with the multi target writer
suite.hts.ServeHTTP(mw, req)
// Pop record and check error is set and has wrapped the expected error
record := suite.hts.PopServerRecord()
require.ErrorAs(suite.T(), record.ServerError, &expectedErr)
}
// Test MultiTargetResponseWriter Header method when no targets are set.
//
// This is to complete test coverage.
func (suite *HTTPTestServerUnitTestSuite) TestMultiTargetResponseWriterHeaderWhenNoTargets() {
mw := newMultiTargetHTTPResponseWriter()
require.NotNil(suite.T(), mw)
headers := mw.Header()
require.NotNil(suite.T(), headers)
require.Equal(suite.T(), http.Header{}, headers)
}
/*************************************************************************************************/
/* READCLOSER MOCK */
/*************************************************************************************************/
// Mock for ReadCloser interface
type mockReadCloser struct {
mock.Mock
}
// Mocked Read method
func (m *mockReadCloser) Read(p []byte) (n int, err error) {
args := m.Called(p)
return args.Int(0), args.Error(1)
}
// Mocked Close method
func (m *mockReadCloser) Close() error {
args := m.Called()
return args.Error(0)
}
// Test mockReadCloser complies to io.ReadCloser interface
func TestMockReadCloserInterfaceCompliance(t *testing.T) {
var instance interface{} = &mockReadCloser{}
_, ok := instance.(io.ReadCloser)
require.True(t, ok)
}
/*************************************************************************************************/
/* RESPONSE WRITER MOCK */
/*************************************************************************************************/
// Mock for http.ResponseWriter interface
type mockResponseWriter struct {
mock.Mock
}
// Mocked Header method
func (m *mockResponseWriter) Header() http.Header {
args := m.Called()
return args.Get(0).(http.Header)
}
// Mocked Write method
func (m *mockResponseWriter) Write(data []byte) (int, error) {
args := m.Called(data)
return args.Int(0), args.Error(1)
}
// Mocked WriteHeader method
func (m *mockResponseWriter) WriteHeader(statusCode int) {
m.Called(statusCode)
}
// Test mockResponseWriter complies to http.ResponseWriter interface
func TestMockResponseWriterInterfaceCompliance(t *testing.T) {
var instance interface{} = &mockResponseWriter{}
_, ok := instance.(http.ResponseWriter)
require.True(t, ok)
}