github.com/System-Glitch/goyave/v2@v2.10.3-0.20200819142921-51011e75d504/testsuite.go (about) 1 package goyave 2 3 import ( 4 "bytes" 5 "context" 6 "crypto/tls" 7 "encoding/json" 8 "io" 9 "io/ioutil" 10 "mime/multipart" 11 "net/http" 12 "net/http/httptest" 13 "os" 14 "path" 15 "path/filepath" 16 "runtime" 17 "sync" 18 "testing" 19 "time" 20 21 "github.com/System-Glitch/goyave/v2/database" 22 "github.com/System-Glitch/goyave/v2/helper/filesystem" 23 24 "github.com/System-Glitch/goyave/v2/config" 25 "github.com/System-Glitch/goyave/v2/lang" 26 "github.com/stretchr/testify/assert" 27 testify "github.com/stretchr/testify/suite" 28 ) 29 30 // ITestSuite is an extension of testify's Suite for 31 // Goyave-specific testing. 32 type ITestSuite interface { 33 RunServer(func(*Router), func()) 34 Timeout() time.Duration 35 SetTimeout(time.Duration) 36 Middleware(Middleware, *Request, Handler) *http.Response 37 38 Get(string, map[string]string) (*http.Response, error) 39 Post(string, map[string]string, io.Reader) (*http.Response, error) 40 Put(string, map[string]string, io.Reader) (*http.Response, error) 41 Patch(string, map[string]string, io.Reader) (*http.Response, error) 42 Delete(string, map[string]string, io.Reader) (*http.Response, error) 43 Request(string, string, map[string]string, io.Reader) (*http.Response, error) 44 45 T() *testing.T 46 SetT(*testing.T) 47 48 GetBody(*http.Response) []byte 49 GetJSONBody(*http.Response, interface{}) error 50 CreateTestFiles(paths ...string) []filesystem.File 51 WriteFile(*multipart.Writer, string, string, string) 52 WriteField(*multipart.Writer, string, string) 53 CreateTestRequest(*http.Request) *Request 54 CreateTestResponse(http.ResponseWriter) *Response 55 getHTTPClient() *http.Client 56 } 57 58 // TestSuite is an extension of testify's Suite for 59 // Goyave-specific testing. 60 type TestSuite struct { 61 testify.Suite 62 timeout time.Duration // Timeout for functional tests 63 httpClient *http.Client 64 mu sync.Mutex 65 } 66 67 var _ ITestSuite = (*TestSuite)(nil) // implements ITestSuite 68 69 // Use a mutex to avoid parallel goyave test suites to be run concurrently. 70 var mu sync.Mutex 71 72 // Timeout get the timeout for test failure when using RunServer or requests. 73 func (s *TestSuite) Timeout() time.Duration { 74 s.mu.Lock() 75 defer s.mu.Unlock() 76 return s.timeout 77 } 78 79 // SetTimeout set the timeout for test failure when using RunServer or requests. 80 func (s *TestSuite) SetTimeout(timeout time.Duration) { 81 s.mu.Lock() 82 s.timeout = timeout 83 s.mu.Unlock() 84 } 85 86 // CreateTestRequest create a "goyave.Request" from the given raw request. 87 // This function is aimed at making it easier to unit test Requests. 88 // 89 // If passed request is "nil", a default GET request to "/" is used. 90 // 91 // rawRequest := httptest.NewRequest("GET", "/test-route", nil) 92 // rawRequest.Header.Set("Content-Type", "application/json") 93 // request := goyave.CreateTestRequest(rawRequest) 94 // request.Lang = "en-US" 95 // request.Data = map[string]interface{}{"field": "value"} 96 func (s *TestSuite) CreateTestRequest(rawRequest *http.Request) *Request { 97 if rawRequest == nil { 98 rawRequest = httptest.NewRequest("GET", "/", nil) 99 } 100 return &Request{ 101 httpRequest: rawRequest, 102 route: nil, 103 Data: nil, 104 Rules: nil, 105 Lang: "en-US", 106 Params: map[string]string{}, 107 } 108 } 109 110 // CreateTestResponse create an empty response with the given response writer. 111 // This function is aimed at making it easier to unit test Responses. 112 // 113 // writer := httptest.NewRecorder() 114 // response := suite.CreateTestResponse(writer) 115 // response.Status(http.StatusNoContent) 116 // result := writer.Result() 117 // fmt.Println(result.StatusCode) // 204 118 func (s *TestSuite) CreateTestResponse(recorder http.ResponseWriter) *Response { 119 return newResponse(recorder, nil) 120 } 121 122 // CreateTestResponseWithRequest create an empty response with the given response writer HTTP request. 123 // This function is aimed at making it easier to unit test Responses needing the raw request's 124 // information, such as redirects. 125 // 126 // writer := httptest.NewRecorder() 127 // rawRequest := httptest.NewRequest("POST", "/test-route", strings.NewReader("body")) 128 // response := suite.CreateTestResponseWithRequest(writer, rawRequest) 129 // response.Status(http.StatusNoContent) 130 // result := writer.Result() 131 // fmt.Println(result.StatusCode) // 204 132 func (s *TestSuite) CreateTestResponseWithRequest(recorder http.ResponseWriter, rawRequest *http.Request) *Response { 133 return newResponse(recorder, rawRequest) 134 } 135 136 // RunServer start the application and run the given functional test procedure. 137 // 138 // This function is the equivalent of "goyave.Start()". 139 // The test fails if the suite's timeout is exceeded. 140 // The server automatically shuts down when the function ends. 141 // This function is synchronized, that means that the server is properly stopped 142 // when the function returns. 143 func (s *TestSuite) RunServer(routeRegistrer func(*Router), procedure func()) { 144 c := make(chan bool, 1) 145 c2 := make(chan bool, 1) 146 ctx, cancel := context.WithTimeout(context.Background(), s.Timeout()) 147 defer cancel() 148 149 RegisterStartupHook(func() { 150 procedure() 151 if ctx.Err() == nil { 152 Stop() 153 c <- true 154 } 155 }) 156 157 go func() { 158 if err := Start(routeRegistrer); err != nil { 159 s.Fail(err.Error()) 160 c <- true 161 } 162 c2 <- true 163 }() 164 165 select { 166 case <-ctx.Done(): 167 s.Fail("Timeout exceeded in goyave.TestSuite.RunServer") 168 Stop() 169 case sig := <-c: 170 s.True(sig) 171 } 172 ClearStartupHooks() 173 <-c2 174 } 175 176 // Middleware executes the given middleware and returns the HTTP response. 177 // Core middleware (recovery, parsing and language) is not executed. 178 func (s *TestSuite) Middleware(middleware Middleware, request *Request, procedure Handler) *http.Response { 179 recorder := httptest.NewRecorder() 180 response := s.CreateTestResponse(recorder) 181 router := newRouter() 182 router.Middleware(middleware) 183 middleware(procedure)(response, request) 184 router.finalize(response, request) 185 186 return recorder.Result() 187 } 188 189 // Get execute a GET request on the given route. 190 // Headers are optional. 191 func (s *TestSuite) Get(route string, headers map[string]string) (*http.Response, error) { 192 return s.Request(http.MethodGet, route, headers, nil) 193 } 194 195 // Post execute a POST request on the given route. 196 // Headers and body are optional. 197 func (s *TestSuite) Post(route string, headers map[string]string, body io.Reader) (*http.Response, error) { 198 return s.Request(http.MethodPost, route, headers, body) 199 } 200 201 // Put execute a PUT request on the given route. 202 // Headers and body are optional. 203 func (s *TestSuite) Put(route string, headers map[string]string, body io.Reader) (*http.Response, error) { 204 return s.Request(http.MethodPut, route, headers, body) 205 } 206 207 // Patch execute a PATCH request on the given route. 208 // Headers and body are optional. 209 func (s *TestSuite) Patch(route string, headers map[string]string, body io.Reader) (*http.Response, error) { 210 return s.Request(http.MethodPatch, route, headers, body) 211 } 212 213 // Delete execute a DELETE request on the given route. 214 // Headers and body are optional. 215 func (s *TestSuite) Delete(route string, headers map[string]string, body io.Reader) (*http.Response, error) { 216 return s.Request(http.MethodDelete, route, headers, body) 217 } 218 219 // Request execute a request on the given route. 220 // Headers and body are optional. 221 func (s *TestSuite) Request(method, route string, headers map[string]string, body io.Reader) (*http.Response, error) { 222 protocol := config.GetString("server.protocol") 223 req, err := http.NewRequest(method, getAddress(protocol)+route, body) 224 if err != nil { 225 return nil, err 226 } 227 req.Close = true 228 for k, v := range headers { 229 req.Header.Set(k, v) 230 } 231 return s.getHTTPClient().Do(req) 232 } 233 234 // GetBody read the whole body of a response. 235 // If read failed, test fails and return empty byte slice. 236 func (s *TestSuite) GetBody(response *http.Response) []byte { 237 body, err := ioutil.ReadAll(response.Body) 238 if err != nil { 239 s.Fail("Couldn't read response body", err) 240 } 241 return body 242 } 243 244 // GetJSONBody read the whole body of a response and decode it as JSON. 245 // If read or decode failed, test fails. 246 func (s *TestSuite) GetJSONBody(response *http.Response, data interface{}) error { 247 err := json.NewDecoder(response.Body).Decode(data) 248 if err != nil { 249 s.Fail("Couldn't read response body as JSON", err) 250 return err 251 } 252 return nil 253 } 254 255 // CreateTestFiles create a slice of "filesystem.File" from the given paths. 256 // Files are passed to a temporary http request and parsed as Multipart form, 257 // to reproduce the way files are obtained in real scenarios. 258 func (s *TestSuite) CreateTestFiles(paths ...string) []filesystem.File { 259 body := &bytes.Buffer{} 260 writer := multipart.NewWriter(body) 261 for _, p := range paths { 262 s.WriteFile(writer, p, "file", filepath.Base(p)) 263 } 264 err := writer.Close() 265 if err != nil { 266 panic(err) 267 } 268 269 req, _ := http.NewRequest("POST", "/test-route", body) 270 req.Header.Set("Content-Type", writer.FormDataContentType()) 271 if err := req.ParseMultipartForm(10 << 20); err != nil { 272 panic(err) 273 } 274 return filesystem.ParseMultipartFiles(req, "file") 275 } 276 277 // WriteFile write a file to the given writer. 278 // This function is handy for file upload testing. 279 // The test fails if an error occurred. 280 func (s *TestSuite) WriteFile(writer *multipart.Writer, path, fieldName, fileName string) { 281 file, err := os.Open(path) 282 if err != nil { 283 s.Fail(err.Error()) 284 return 285 } 286 defer file.Close() 287 part, err := writer.CreateFormFile(fieldName, fileName) 288 if err != nil { 289 s.Fail(err.Error()) 290 return 291 } 292 _, err = io.Copy(part, file) 293 if err != nil { 294 s.Fail(err.Error()) 295 } 296 } 297 298 // WriteField create and write a new multipart form field. 299 // The test fails if the field couldn't be written. 300 func (s *TestSuite) WriteField(writer *multipart.Writer, fieldName, value string) { 301 if err := writer.WriteField(fieldName, value); err != nil { 302 s.Fail(err.Error()) 303 } 304 } 305 306 // getHTTPClient get suite's http client or create it if it doesn't exist yet. 307 // The HTTP client is created with a timeout, disabled redirect and disabled TLS cert checking. 308 func (s *TestSuite) getHTTPClient() *http.Client { 309 config := &tls.Config{ 310 InsecureSkipVerify: true, 311 } 312 313 if s.httpClient == nil { 314 s.httpClient = &http.Client{ 315 Timeout: s.Timeout(), 316 Transport: &http.Transport{TLSClientConfig: config}, 317 CheckRedirect: func(req *http.Request, via []*http.Request) error { 318 return http.ErrUseLastResponse 319 }, 320 } 321 } 322 323 return s.httpClient 324 } 325 326 // ClearDatabase delete all records in all tables. 327 // This function only clears the tables of registered models. 328 func (s *TestSuite) ClearDatabase() { 329 db := database.GetConnection() 330 for _, m := range database.GetRegisteredModels() { 331 db.Unscoped().Delete(m) 332 } 333 } 334 335 // ClearDatabaseTables drop all tables. 336 // This function only clears the tables of registered models. 337 func (s *TestSuite) ClearDatabaseTables() { 338 db := database.GetConnection() 339 for _, m := range database.GetRegisteredModels() { 340 db.DropTableIfExists(m) 341 } 342 } 343 344 // RunTest run a test suite with prior initialization of a test environment. 345 // The GOYAVE_ENV environment variable is automatically set to "test" and restored 346 // to its original value at the end of the test run. 347 // All tests are run using your project's root as working directory. This directory is determined 348 // by the presence of a "go.mod" file. 349 func RunTest(t *testing.T, suite ITestSuite) bool { 350 mu.Lock() 351 defer mu.Unlock() 352 if suite.Timeout() == 0 { 353 suite.SetTimeout(5 * time.Second) 354 } 355 oldEnv := os.Getenv("GOYAVE_ENV") 356 os.Setenv("GOYAVE_ENV", "test") 357 defer os.Setenv("GOYAVE_ENV", oldEnv) 358 setRootWorkingDirectory() 359 if err := config.Load(); err != nil { 360 return assert.Fail(t, "Failed to load config", err) 361 } 362 defer config.Clear() 363 lang.LoadDefault() 364 lang.LoadAllAvailableLanguages() 365 366 testify.Run(t, suite) 367 368 database.Close() 369 return !t.Failed() 370 } 371 372 func setRootWorkingDirectory() { 373 sep := string(os.PathSeparator) 374 _, filename, _, _ := runtime.Caller(2) 375 directory := path.Dir(filename) + sep 376 for !filesystem.FileExists(directory + sep + "go.mod") { 377 directory += ".." + sep 378 if !filesystem.IsDirectory(directory) { 379 panic("Couldn't find project's root directory.") 380 } 381 } 382 if err := os.Chdir(directory); err != nil { 383 panic(err) 384 } 385 }