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  }