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