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  }