goyave.dev/goyave/v4@v4.4.11/testsuite_test.go (about) 1 package goyave 2 3 import ( 4 "bytes" 5 "context" 6 "fmt" 7 "io" 8 "mime/multipart" 9 "net/http" 10 "net/http/httptest" 11 "os" 12 "path/filepath" 13 "strings" 14 "sync" 15 "testing" 16 "time" 17 18 "github.com/stretchr/testify/assert" 19 "gorm.io/gorm" 20 "goyave.dev/goyave/v4/config" 21 "goyave.dev/goyave/v4/database" 22 "goyave.dev/goyave/v4/lang" 23 "goyave.dev/goyave/v4/util/fsutil" 24 ) 25 26 type CustomTestSuite struct { 27 TestSuite 28 } 29 30 type MigratingTestSuite struct { 31 TestSuite 32 } 33 34 type FailingTestSuite struct { 35 TestSuite 36 } 37 38 type ConcurrentTestSuite struct { 39 res *int 40 TestSuite 41 } 42 43 type TestModel struct { 44 Name string `gorm:"type:varchar(100)"` 45 ID uint `gorm:"primaryKey"` 46 } 47 48 type TestViewModel struct { 49 database.View 50 Name string `gorm:"type:varchar(100)"` 51 ID uint `gorm:"primaryKey"` 52 } 53 54 func genericHandler(message string) func(response *Response, request *Request) { 55 return func(response *Response, request *Request) { 56 response.String(http.StatusOK, message) 57 } 58 } 59 60 func (suite *CustomTestSuite) TestEnv() { 61 suite.Equal("test", os.Getenv("GOYAVE_ENV")) 62 suite.Equal("test", config.GetString("app.environment")) 63 suite.Equal("Malformed JSON", lang.Get("en-US", "malformed-json")) 64 } 65 66 func (suite *CustomTestSuite) TestCreateTestResponse() { 67 writer := httptest.NewRecorder() 68 response := suite.CreateTestResponse(writer) 69 suite.Equal(writer, response.writer) 70 suite.Equal(writer, response.responseWriter) 71 72 rawRequest := httptest.NewRequest("POST", "/test-route", strings.NewReader("body")) 73 response = suite.CreateTestResponseWithRequest(writer, rawRequest) 74 suite.Equal(writer, response.writer) 75 suite.Equal(writer, response.responseWriter) 76 suite.Equal(rawRequest, response.httpRequest) 77 } 78 79 func (suite *CustomTestSuite) TestCreateTestRequest() { 80 request := suite.CreateTestRequest(nil) 81 suite.Nil(request.route) 82 suite.Nil(request.Data) 83 suite.Nil(request.Rules) 84 suite.Equal("en-US", request.Lang) 85 suite.NotNil(request.Params) 86 suite.NotNil(request.Extra) 87 suite.NotNil(request.httpRequest) 88 suite.Equal("GET", request.httpRequest.Method) 89 suite.Equal("/", request.httpRequest.RequestURI) 90 91 rawRequest := httptest.NewRequest("POST", "/test-route", nil) 92 request = suite.CreateTestRequest(rawRequest) 93 suite.Equal("POST", request.httpRequest.Method) 94 suite.Equal("/test-route", request.httpRequest.RequestURI) 95 } 96 97 func (suite *CustomTestSuite) TestRunServer() { 98 RegisterStartupHook(func() {}) 99 RegisterShutdownHook(func() {}) 100 suite.RunServer(func(router *Router) { 101 router.Route("GET", "/hello", func(response *Response, request *Request) { 102 response.String(http.StatusOK, "Hi!") 103 }) 104 }, func() { 105 resp, err := suite.Get("/hello", nil) 106 suite.Nil(err) 107 if err != nil { 108 suite.Fail(err.Error()) 109 } 110 111 suite.NotNil(resp) 112 if resp != nil { 113 defer resp.Body.Close() 114 suite.Equal(200, resp.StatusCode) 115 suite.Equal("Hi!", string(suite.GetBody(resp))) 116 } 117 }) 118 suite.Empty(startupHooks) 119 suite.Empty(shutdownHooks) 120 } 121 122 func (suite *CustomTestSuite) TestRunServerTimeout() { 123 RegisterStartupHook(func() {}) 124 RegisterShutdownHook(func() {}) 125 suite.SetTimeout(time.Second) 126 oldT := suite.T() 127 suite.SetT(new(testing.T)) 128 suite.RunServer(func(router *Router) {}, func() { 129 time.Sleep(suite.Timeout() + 1*time.Second) 130 }) 131 assert.True(oldT, suite.T().Failed()) 132 suite.SetTimeout(5 * time.Second) 133 suite.SetT(oldT) 134 suite.Empty(startupHooks) 135 suite.Empty(shutdownHooks) 136 } 137 138 func (suite *CustomTestSuite) TestRunServerError() { 139 RegisterStartupHook(func() {}) 140 RegisterShutdownHook(func() {}) 141 config.Clear() 142 oldT := suite.T() 143 suite.SetT(new(testing.T)) 144 prevEnv := os.Getenv("GOYAVE_ENV") 145 if err := os.Setenv("GOYAVE_ENV", "notanenv"); err != nil { 146 suite.Fail(err.Error()) 147 } 148 suite.RunServer(func(router *Router) {}, func() {}) 149 assert.True(oldT, suite.T().Failed()) 150 suite.SetT(oldT) 151 if err := os.Setenv("GOYAVE_ENV", prevEnv); err != nil { 152 suite.Fail(err.Error()) 153 } 154 suite.Empty(startupHooks) 155 suite.Empty(shutdownHooks) 156 } 157 158 func (suite *CustomTestSuite) TestMiddleware() { 159 rawRequest := httptest.NewRequest("GET", "/test-route", nil) 160 rawRequest.Header.Set("Content-Type", "application/json") 161 request := suite.CreateTestRequest(rawRequest) 162 163 result := suite.Middleware(func(next Handler) Handler { 164 return func(response *Response, request *Request) { 165 response.Status(http.StatusTeapot) 166 next(response, request) 167 } 168 }, request, func(response *Response, request *Request) { 169 suite.Equal("application/json", request.Header().Get("Content-Type")) 170 }) 171 172 result.Body.Close() 173 suite.Equal(418, result.StatusCode) 174 } 175 176 func (suite *CustomTestSuite) TestRequests() { 177 suite.RunServer(func(router *Router) { 178 router.Route("GET", "/get", genericHandler("get")) 179 router.Route("POST", "/post", genericHandler("post")) 180 router.Route("PUT", "/put", genericHandler("put")) 181 router.Route("PATCH", "/patch", genericHandler("patch")) 182 router.Route("DELETE", "/delete", genericHandler("delete")) 183 router.Route("GET", "/headers", func(response *Response, request *Request) { 184 response.String(http.StatusOK, request.Header().Get("Accept-Language")) 185 }) 186 }, func() { 187 resp, err := suite.Get("/get", nil) 188 suite.Nil(err) 189 if err == nil { 190 suite.Equal("get", string(suite.GetBody(resp))) 191 resp.Body.Close() 192 } 193 resp, err = suite.Get("/post", nil) 194 suite.Nil(err) 195 if err == nil { 196 suite.Equal(http.StatusMethodNotAllowed, resp.StatusCode) 197 resp.Body.Close() 198 } 199 resp, err = suite.Get("/nonexistent-route", nil) 200 suite.Nil(err) 201 if err == nil { 202 suite.Equal(http.StatusNotFound, resp.StatusCode) 203 resp.Body.Close() 204 } 205 resp, err = suite.Post("/post", nil, strings.NewReader("field=value")) 206 suite.Nil(err) 207 if err == nil { 208 suite.Equal("post", string(suite.GetBody(resp))) 209 } 210 resp.Body.Close() 211 resp, err = suite.Put("/put", nil, strings.NewReader("field=value")) 212 suite.Nil(err) 213 if err == nil { 214 suite.Equal("put", string(suite.GetBody(resp))) 215 } 216 resp.Body.Close() 217 resp, err = suite.Patch("/patch", nil, strings.NewReader("field=value")) 218 suite.Nil(err) 219 if err == nil { 220 suite.Equal("patch", string(suite.GetBody(resp))) 221 } 222 resp.Body.Close() 223 resp, err = suite.Delete("/delete", nil, strings.NewReader("field=value")) 224 suite.Nil(err) 225 if err == nil { 226 suite.Equal("delete", string(suite.GetBody(resp))) 227 } 228 resp.Body.Close() 229 230 // Headers 231 resp, err = suite.Get("/headers", map[string]string{"Accept-Language": "en-US"}) 232 suite.Nil(err) 233 if err == nil { 234 suite.Equal("en-US", string(suite.GetBody(resp))) 235 } 236 resp.Body.Close() 237 238 // Errors 239 resp, err = suite.Get("invalid", nil) 240 suite.NotNil(err) 241 suite.Nil(resp) 242 if resp != nil { 243 resp.Body.Close() 244 } 245 }) 246 } 247 248 func (suite *CustomTestSuite) TestJSON() { 249 suite.RunServer(func(router *Router) { 250 router.Route("GET", "/invalid", genericHandler("get")) 251 router.Route("GET", "/get", func(response *Response, request *Request) { 252 response.JSON(http.StatusOK, map[string]interface{}{ 253 "field": "value", 254 "number": 42, 255 }) 256 }) 257 }, func() { 258 resp, err := suite.Get("/get", nil) 259 suite.Nil(err) 260 if err == nil { 261 defer resp.Body.Close() 262 json := map[string]interface{}{} 263 err := suite.GetJSONBody(resp, &json) 264 suite.Nil(err) 265 if err == nil { 266 suite.Equal("value", json["field"]) 267 suite.Equal(float64(42), json["number"]) 268 } 269 } 270 271 resp, err = suite.Get("/invalid", nil) 272 suite.Nil(err) 273 if err == nil { 274 defer resp.Body.Close() 275 oldT := suite.T() 276 suite.SetT(new(testing.T)) 277 json := map[string]interface{}{} 278 err := suite.GetJSONBody(resp, &json) 279 assert.True(oldT, suite.T().Failed()) 280 suite.SetT(oldT) 281 suite.NotNil(err) 282 } 283 }) 284 } 285 286 func (suite *CustomTestSuite) TestJSONSlice() { 287 suite.RunServer(func(router *Router) { 288 router.Route("GET", "/get", func(response *Response, request *Request) { 289 response.JSON(http.StatusOK, []map[string]interface{}{ 290 {"field": "value", "number": 42}, 291 {"field": "other value", "number": 12}, 292 }) 293 }) 294 }, func() { 295 resp, err := suite.Get("/get", nil) 296 suite.Nil(err) 297 if err == nil { 298 defer resp.Body.Close() 299 json := []map[string]interface{}{} 300 err := suite.GetJSONBody(resp, &json) 301 suite.Nil(err) 302 suite.Len(json, 2) 303 if err == nil { 304 suite.Equal("value", json[0]["field"]) 305 suite.Equal(float64(42), json[0]["number"]) 306 suite.Equal("other value", json[1]["field"]) 307 suite.Equal(float64(12), json[1]["number"]) 308 } 309 } 310 }) 311 } 312 313 func (suite *CustomTestSuite) TestCreateTestFiles() { 314 err := os.WriteFile("test-file.txt", []byte("test-content"), 0644) 315 if err != nil { 316 panic(err) 317 } 318 defer fsutil.Delete("test-file.txt") 319 files := suite.CreateTestFiles("test-file.txt") 320 suite.Equal(1, len(files)) 321 suite.Equal("test-file.txt", files[0].Header.Filename) 322 body, err := io.ReadAll(files[0].Data) 323 if err != nil { 324 panic(err) 325 } 326 suite.Equal("test-content", string(body)) 327 328 oldT := suite.T() 329 suite.SetT(new(testing.T)) 330 files = suite.CreateTestFiles("doesn't exist") 331 assert.True(oldT, suite.T().Failed()) 332 suite.SetT(oldT) 333 suite.Equal(0, len(files)) 334 } 335 336 func (suite *CustomTestSuite) TestMultipartForm() { 337 const path = "test-file.txt" 338 err := os.WriteFile(path, []byte("test-content"), 0644) 339 if err != nil { 340 panic(err) 341 } 342 defer fsutil.Delete(path) 343 344 suite.RunServer(func(router *Router) { 345 router.Route("POST", "/post", func(response *Response, request *Request) { 346 content, err := io.ReadAll(request.File("file")[0].Data) 347 if err != nil { 348 panic(err) 349 } 350 response.JSON(http.StatusOK, map[string]interface{}{ 351 "file": string(content), 352 "field": request.String("field"), 353 }) 354 }) 355 }, func() { 356 body := &bytes.Buffer{} 357 writer := multipart.NewWriter(body) 358 359 suite.WriteFile(writer, path, "file", filepath.Base(path)) 360 suite.WriteField(writer, "field", "hello world") 361 if err := writer.Close(); err != nil { 362 panic(err) 363 } 364 resp, err := suite.Post("/post", map[string]string{"Content-Type": writer.FormDataContentType()}, body) 365 suite.Nil(err) 366 if err == nil { 367 json := map[string]interface{}{} 368 err := suite.GetJSONBody(resp, &json) 369 suite.Nil(err) 370 if err == nil { 371 suite.Equal("test-content", json["file"]) 372 suite.Equal("hello world", json["field"]) 373 } 374 } 375 resp.Body.Close() 376 }) 377 } 378 379 func (suite *CustomTestSuite) TestClearDatabase() { 380 config.Set("database.connection", "mysql") 381 db := database.GetConnection() 382 db.AutoMigrate(&TestModel{}) 383 384 for i := 0; i < 5; i++ { 385 db.Create(&TestModel{Name: fmt.Sprintf("Test %d", i)}) 386 } 387 count := int64(0) 388 db.Model(&TestModel{}).Count(&count) 389 suite.Equal(int64(5), count) 390 391 database.RegisterModel(&TestModel{}) 392 suite.ClearDatabase() 393 database.ClearRegisteredModels() 394 395 db.Model(&TestModel{}).Count(&count) 396 suite.Equal(int64(0), count) 397 398 db.Migrator().DropTable(&TestModel{}) 399 config.Set("database.connection", "none") 400 } 401 402 func (suite *CustomTestSuite) TestClearDatabaseView() { 403 config.Set("database.connection", "mysql") 404 db := database.GetConnection() 405 db.AutoMigrate(&TestViewModel{}) 406 407 for i := 0; i < 5; i++ { 408 db.Create(&TestViewModel{Name: fmt.Sprintf("Test %d", i)}) 409 } 410 defer func() { 411 if err := db.Unscoped().Session(&gorm.Session{AllowGlobalUpdate: true}).Delete(&TestViewModel{}).Error; err != nil { 412 panic(err) 413 } 414 }() 415 count := int64(0) 416 db.Model(&TestViewModel{}).Count(&count) 417 suite.Equal(int64(5), count) 418 419 database.RegisterModel(&TestViewModel{}) 420 suite.ClearDatabase() 421 database.ClearRegisteredModels() 422 423 db.Model(&TestViewModel{}).Count(&count) 424 suite.Equal(int64(5), count) 425 } 426 427 func (suite *CustomTestSuite) TestClearDatabaseTables() { 428 config.Set("database.connection", "mysql") 429 db := database.GetConnection() 430 db.AutoMigrate(&TestModel{}) 431 432 database.RegisterModel(&TestModel{}) 433 suite.ClearDatabaseTables() 434 database.ClearRegisteredModels() 435 436 found := false 437 rows, err := db.Raw("SHOW TABLES;").Rows() 438 if err != nil { 439 panic(err) 440 } 441 defer rows.Close() 442 for rows.Next() { 443 name := "" 444 if err := rows.Scan(&name); err != nil { 445 panic(err) 446 } 447 if name == "test_models" { 448 found = true 449 } 450 } 451 452 suite.False(found) 453 454 config.Set("database.connection", "none") 455 } 456 457 func TestConcurrentSuiteExecution(t *testing.T) { // Suites should not execute in parallel 458 // This test is only useful if the race detector is enabled 459 res := 0 460 suite1 := new(ConcurrentTestSuite) 461 suite2 := new(ConcurrentTestSuite) 462 suite1.res = &res 463 suite2.res = &res 464 465 c := make(chan bool, 1) 466 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 467 defer cancel() 468 469 var wg sync.WaitGroup 470 for i := 0; i < 10; i++ { 471 // Executing this ten times almost guarantees 472 // there WILL be a race condition. 473 wg.Add(2) 474 go func() { 475 defer wg.Done() 476 RunTest(t, suite1) 477 }() 478 go func() { 479 defer wg.Done() 480 RunTest(t, suite2) 481 }() 482 } 483 484 go func() { 485 wg.Wait() 486 c <- true 487 }() 488 489 select { 490 case <-ctx.Done(): 491 assert.Fail(t, "Timeout exceeded in concurrent suites test") 492 case val := <-c: 493 assert.True(t, val) 494 } 495 496 } 497 498 func (suite *ConcurrentTestSuite) TestExecutionOrder() { 499 *suite.res++ 500 } 501 502 func TestTestSuite(t *testing.T) { 503 suite := new(CustomTestSuite) 504 RunTest(t, suite) 505 assert.Equal(t, 5*time.Second, suite.Timeout()) 506 } 507 508 func (s *FailingTestSuite) TestRunServerTimeout() { 509 s.RunServer(func(router *Router) {}, func() { 510 time.Sleep(s.Timeout() + 1) 511 }) 512 } 513 514 func TestTestSuiteFail(t *testing.T) { 515 if err := os.Rename("config.test.json", "config.test.json.bak"); err != nil { 516 panic(err) 517 } 518 defer func() { 519 if err := os.Rename("config.test.json.bak", "config.test.json"); err != nil { 520 panic(err) 521 } 522 }() 523 mockT := new(testing.T) 524 RunTest(mockT, new(FailingTestSuite)) 525 assert.True(t, mockT.Failed()) 526 } 527 528 func (suite *MigratingTestSuite) TearDownSuite() { 529 suite.ClearDatabaseTables() 530 } 531 532 func TestMigrate(t *testing.T) { 533 if err := config.LoadFrom("resources/config.migration-test.json"); err != nil { 534 assert.Fail(t, "Failed to load config", err) 535 return 536 } 537 suite := new(MigratingTestSuite) 538 RunTest(t, suite) 539 }