code.gitea.io/gitea@v1.22.3/tests/integration/integration_test.go (about)

     1  // Copyright 2017 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  //nolint:forbidigo
     5  package integration
     6  
     7  import (
     8  	"bytes"
     9  	"context"
    10  	"fmt"
    11  	"hash"
    12  	"hash/fnv"
    13  	"io"
    14  	"net/http"
    15  	"net/http/cookiejar"
    16  	"net/http/httptest"
    17  	"net/url"
    18  	"os"
    19  	"path/filepath"
    20  	"strings"
    21  	"sync/atomic"
    22  	"testing"
    23  	"time"
    24  
    25  	"code.gitea.io/gitea/models/auth"
    26  	"code.gitea.io/gitea/models/unittest"
    27  	"code.gitea.io/gitea/modules/graceful"
    28  	"code.gitea.io/gitea/modules/json"
    29  	"code.gitea.io/gitea/modules/log"
    30  	"code.gitea.io/gitea/modules/setting"
    31  	"code.gitea.io/gitea/modules/testlogger"
    32  	"code.gitea.io/gitea/modules/util"
    33  	"code.gitea.io/gitea/modules/web"
    34  	"code.gitea.io/gitea/routers"
    35  	gitea_context "code.gitea.io/gitea/services/context"
    36  	"code.gitea.io/gitea/tests"
    37  
    38  	"github.com/PuerkitoBio/goquery"
    39  	"github.com/stretchr/testify/assert"
    40  	"github.com/stretchr/testify/require"
    41  	"github.com/xeipuuv/gojsonschema"
    42  )
    43  
    44  var testWebRoutes *web.Route
    45  
    46  type NilResponseRecorder struct {
    47  	httptest.ResponseRecorder
    48  	Length int
    49  }
    50  
    51  func (n *NilResponseRecorder) Write(b []byte) (int, error) {
    52  	n.Length += len(b)
    53  	return len(b), nil
    54  }
    55  
    56  // NewRecorder returns an initialized ResponseRecorder.
    57  func NewNilResponseRecorder() *NilResponseRecorder {
    58  	return &NilResponseRecorder{
    59  		ResponseRecorder: *httptest.NewRecorder(),
    60  	}
    61  }
    62  
    63  type NilResponseHashSumRecorder struct {
    64  	httptest.ResponseRecorder
    65  	Hash   hash.Hash
    66  	Length int
    67  }
    68  
    69  func (n *NilResponseHashSumRecorder) Write(b []byte) (int, error) {
    70  	_, _ = n.Hash.Write(b)
    71  	n.Length += len(b)
    72  	return len(b), nil
    73  }
    74  
    75  // NewRecorder returns an initialized ResponseRecorder.
    76  func NewNilResponseHashSumRecorder() *NilResponseHashSumRecorder {
    77  	return &NilResponseHashSumRecorder{
    78  		Hash:             fnv.New32(),
    79  		ResponseRecorder: *httptest.NewRecorder(),
    80  	}
    81  }
    82  
    83  func TestMain(m *testing.M) {
    84  	defer log.GetManager().Close()
    85  
    86  	managerCtx, cancel := context.WithCancel(context.Background())
    87  	graceful.InitManager(managerCtx)
    88  	defer cancel()
    89  
    90  	tests.InitTest(true)
    91  	testWebRoutes = routers.NormalRoutes()
    92  
    93  	// integration test settings...
    94  	if setting.CfgProvider != nil {
    95  		testingCfg := setting.CfgProvider.Section("integration-tests")
    96  		testlogger.SlowTest = testingCfg.Key("SLOW_TEST").MustDuration(testlogger.SlowTest)
    97  		testlogger.SlowFlush = testingCfg.Key("SLOW_FLUSH").MustDuration(testlogger.SlowFlush)
    98  	}
    99  
   100  	if os.Getenv("GITEA_SLOW_TEST_TIME") != "" {
   101  		duration, err := time.ParseDuration(os.Getenv("GITEA_SLOW_TEST_TIME"))
   102  		if err == nil {
   103  			testlogger.SlowTest = duration
   104  		}
   105  	}
   106  
   107  	if os.Getenv("GITEA_SLOW_FLUSH_TIME") != "" {
   108  		duration, err := time.ParseDuration(os.Getenv("GITEA_SLOW_FLUSH_TIME"))
   109  		if err == nil {
   110  			testlogger.SlowFlush = duration
   111  		}
   112  	}
   113  
   114  	os.Unsetenv("GIT_AUTHOR_NAME")
   115  	os.Unsetenv("GIT_AUTHOR_EMAIL")
   116  	os.Unsetenv("GIT_AUTHOR_DATE")
   117  	os.Unsetenv("GIT_COMMITTER_NAME")
   118  	os.Unsetenv("GIT_COMMITTER_EMAIL")
   119  	os.Unsetenv("GIT_COMMITTER_DATE")
   120  
   121  	err := unittest.InitFixtures(
   122  		unittest.FixturesOptions{
   123  			Dir: filepath.Join(filepath.Dir(setting.AppPath), "models/fixtures/"),
   124  		},
   125  	)
   126  	if err != nil {
   127  		fmt.Printf("Error initializing test database: %v\n", err)
   128  		os.Exit(1)
   129  	}
   130  
   131  	// FIXME: the console logger is deleted by mistake, so if there is any `log.Fatal`, developers won't see any error message.
   132  	// Instead, "No tests were found",  last nonsense log is "According to the configuration, subsequent logs will not be printed to the console"
   133  	exitCode := m.Run()
   134  
   135  	testlogger.WriterCloser.Reset()
   136  
   137  	if err = util.RemoveAll(setting.Indexer.IssuePath); err != nil {
   138  		fmt.Printf("util.RemoveAll: %v\n", err)
   139  		os.Exit(1)
   140  	}
   141  	if err = util.RemoveAll(setting.Indexer.RepoPath); err != nil {
   142  		fmt.Printf("Unable to remove repo indexer: %v\n", err)
   143  		os.Exit(1)
   144  	}
   145  
   146  	os.Exit(exitCode)
   147  }
   148  
   149  type TestSession struct {
   150  	jar http.CookieJar
   151  }
   152  
   153  func (s *TestSession) GetCookie(name string) *http.Cookie {
   154  	baseURL, err := url.Parse(setting.AppURL)
   155  	if err != nil {
   156  		return nil
   157  	}
   158  
   159  	for _, c := range s.jar.Cookies(baseURL) {
   160  		if c.Name == name {
   161  			return c
   162  		}
   163  	}
   164  	return nil
   165  }
   166  
   167  func (s *TestSession) MakeRequest(t testing.TB, rw *RequestWrapper, expectedStatus int) *httptest.ResponseRecorder {
   168  	t.Helper()
   169  	req := rw.Request
   170  	baseURL, err := url.Parse(setting.AppURL)
   171  	assert.NoError(t, err)
   172  	for _, c := range s.jar.Cookies(baseURL) {
   173  		req.AddCookie(c)
   174  	}
   175  	resp := MakeRequest(t, rw, expectedStatus)
   176  
   177  	ch := http.Header{}
   178  	ch.Add("Cookie", strings.Join(resp.Header()["Set-Cookie"], ";"))
   179  	cr := http.Request{Header: ch}
   180  	s.jar.SetCookies(baseURL, cr.Cookies())
   181  
   182  	return resp
   183  }
   184  
   185  func (s *TestSession) MakeRequestNilResponseRecorder(t testing.TB, rw *RequestWrapper, expectedStatus int) *NilResponseRecorder {
   186  	t.Helper()
   187  	req := rw.Request
   188  	baseURL, err := url.Parse(setting.AppURL)
   189  	assert.NoError(t, err)
   190  	for _, c := range s.jar.Cookies(baseURL) {
   191  		req.AddCookie(c)
   192  	}
   193  	resp := MakeRequestNilResponseRecorder(t, rw, expectedStatus)
   194  
   195  	ch := http.Header{}
   196  	ch.Add("Cookie", strings.Join(resp.Header()["Set-Cookie"], ";"))
   197  	cr := http.Request{Header: ch}
   198  	s.jar.SetCookies(baseURL, cr.Cookies())
   199  
   200  	return resp
   201  }
   202  
   203  func (s *TestSession) MakeRequestNilResponseHashSumRecorder(t testing.TB, rw *RequestWrapper, expectedStatus int) *NilResponseHashSumRecorder {
   204  	t.Helper()
   205  	req := rw.Request
   206  	baseURL, err := url.Parse(setting.AppURL)
   207  	assert.NoError(t, err)
   208  	for _, c := range s.jar.Cookies(baseURL) {
   209  		req.AddCookie(c)
   210  	}
   211  	resp := MakeRequestNilResponseHashSumRecorder(t, rw, expectedStatus)
   212  
   213  	ch := http.Header{}
   214  	ch.Add("Cookie", strings.Join(resp.Header()["Set-Cookie"], ";"))
   215  	cr := http.Request{Header: ch}
   216  	s.jar.SetCookies(baseURL, cr.Cookies())
   217  
   218  	return resp
   219  }
   220  
   221  const userPassword = "password"
   222  
   223  func emptyTestSession(t testing.TB) *TestSession {
   224  	t.Helper()
   225  	jar, err := cookiejar.New(nil)
   226  	assert.NoError(t, err)
   227  
   228  	return &TestSession{jar: jar}
   229  }
   230  
   231  func getUserToken(t testing.TB, userName string, scope ...auth.AccessTokenScope) string {
   232  	return getTokenForLoggedInUser(t, loginUser(t, userName), scope...)
   233  }
   234  
   235  func loginUser(t testing.TB, userName string) *TestSession {
   236  	t.Helper()
   237  
   238  	return loginUserWithPassword(t, userName, userPassword)
   239  }
   240  
   241  func loginUserWithPassword(t testing.TB, userName, password string) *TestSession {
   242  	t.Helper()
   243  	req := NewRequest(t, "GET", "/user/login")
   244  	resp := MakeRequest(t, req, http.StatusOK)
   245  
   246  	doc := NewHTMLParser(t, resp.Body)
   247  	req = NewRequestWithValues(t, "POST", "/user/login", map[string]string{
   248  		"_csrf":     doc.GetCSRF(),
   249  		"user_name": userName,
   250  		"password":  password,
   251  	})
   252  	resp = MakeRequest(t, req, http.StatusSeeOther)
   253  
   254  	ch := http.Header{}
   255  	ch.Add("Cookie", strings.Join(resp.Header()["Set-Cookie"], ";"))
   256  	cr := http.Request{Header: ch}
   257  
   258  	session := emptyTestSession(t)
   259  
   260  	baseURL, err := url.Parse(setting.AppURL)
   261  	assert.NoError(t, err)
   262  	session.jar.SetCookies(baseURL, cr.Cookies())
   263  
   264  	return session
   265  }
   266  
   267  // token has to be unique this counter take care of
   268  var tokenCounter int64
   269  
   270  // getTokenForLoggedInUser returns a token for a logged in user.
   271  // The scope is an optional list of snake_case strings like the frontend form fields,
   272  // but without the "scope_" prefix.
   273  func getTokenForLoggedInUser(t testing.TB, session *TestSession, scopes ...auth.AccessTokenScope) string {
   274  	t.Helper()
   275  	var token string
   276  	req := NewRequest(t, "GET", "/user/settings/applications")
   277  	resp := session.MakeRequest(t, req, http.StatusOK)
   278  	var csrf string
   279  	for _, cookie := range resp.Result().Cookies() {
   280  		if cookie.Name != "_csrf" {
   281  			continue
   282  		}
   283  		csrf = cookie.Value
   284  		break
   285  	}
   286  	if csrf == "" {
   287  		doc := NewHTMLParser(t, resp.Body)
   288  		csrf = doc.GetCSRF()
   289  	}
   290  	assert.NotEmpty(t, csrf)
   291  	urlValues := url.Values{}
   292  	urlValues.Add("_csrf", csrf)
   293  	urlValues.Add("name", fmt.Sprintf("api-testing-token-%d", atomic.AddInt64(&tokenCounter, 1)))
   294  	for _, scope := range scopes {
   295  		urlValues.Add("scope", string(scope))
   296  	}
   297  	req = NewRequestWithURLValues(t, "POST", "/user/settings/applications", urlValues)
   298  	resp = session.MakeRequest(t, req, http.StatusSeeOther)
   299  
   300  	// Log the flash values on failure
   301  	if !assert.Equal(t, resp.Result().Header["Location"], []string{"/user/settings/applications"}) {
   302  		for _, cookie := range resp.Result().Cookies() {
   303  			if cookie.Name != gitea_context.CookieNameFlash {
   304  				continue
   305  			}
   306  			flash, _ := url.ParseQuery(cookie.Value)
   307  			for key, value := range flash {
   308  				t.Logf("Flash %q: %q", key, value)
   309  			}
   310  		}
   311  	}
   312  
   313  	req = NewRequest(t, "GET", "/user/settings/applications")
   314  	resp = session.MakeRequest(t, req, http.StatusOK)
   315  	htmlDoc := NewHTMLParser(t, resp.Body)
   316  	token = htmlDoc.doc.Find(".ui.info p").Text()
   317  	assert.NotEmpty(t, token)
   318  	return token
   319  }
   320  
   321  type RequestWrapper struct {
   322  	*http.Request
   323  }
   324  
   325  func (req *RequestWrapper) AddBasicAuth(username string) *RequestWrapper {
   326  	req.Request.SetBasicAuth(username, userPassword)
   327  	return req
   328  }
   329  
   330  func (req *RequestWrapper) AddTokenAuth(token string) *RequestWrapper {
   331  	if token == "" {
   332  		return req
   333  	}
   334  	if !strings.HasPrefix(token, "Bearer ") {
   335  		token = "Bearer " + token
   336  	}
   337  	req.Request.Header.Set("Authorization", token)
   338  	return req
   339  }
   340  
   341  func (req *RequestWrapper) SetHeader(name, value string) *RequestWrapper {
   342  	req.Request.Header.Set(name, value)
   343  	return req
   344  }
   345  
   346  func NewRequest(t testing.TB, method, urlStr string) *RequestWrapper {
   347  	t.Helper()
   348  	return NewRequestWithBody(t, method, urlStr, nil)
   349  }
   350  
   351  func NewRequestf(t testing.TB, method, urlFormat string, args ...any) *RequestWrapper {
   352  	t.Helper()
   353  	return NewRequest(t, method, fmt.Sprintf(urlFormat, args...))
   354  }
   355  
   356  func NewRequestWithValues(t testing.TB, method, urlStr string, values map[string]string) *RequestWrapper {
   357  	t.Helper()
   358  	urlValues := url.Values{}
   359  	for key, value := range values {
   360  		urlValues[key] = []string{value}
   361  	}
   362  	return NewRequestWithURLValues(t, method, urlStr, urlValues)
   363  }
   364  
   365  func NewRequestWithURLValues(t testing.TB, method, urlStr string, urlValues url.Values) *RequestWrapper {
   366  	t.Helper()
   367  	return NewRequestWithBody(t, method, urlStr, bytes.NewBufferString(urlValues.Encode())).
   368  		SetHeader("Content-Type", "application/x-www-form-urlencoded")
   369  }
   370  
   371  func NewRequestWithJSON(t testing.TB, method, urlStr string, v any) *RequestWrapper {
   372  	t.Helper()
   373  
   374  	jsonBytes, err := json.Marshal(v)
   375  	assert.NoError(t, err)
   376  	return NewRequestWithBody(t, method, urlStr, bytes.NewBuffer(jsonBytes)).
   377  		SetHeader("Content-Type", "application/json")
   378  }
   379  
   380  func NewRequestWithBody(t testing.TB, method, urlStr string, body io.Reader) *RequestWrapper {
   381  	t.Helper()
   382  	if !strings.HasPrefix(urlStr, "http") && !strings.HasPrefix(urlStr, "/") {
   383  		urlStr = "/" + urlStr
   384  	}
   385  	req, err := http.NewRequest(method, urlStr, body)
   386  	assert.NoError(t, err)
   387  	req.RequestURI = urlStr
   388  
   389  	return &RequestWrapper{req}
   390  }
   391  
   392  const NoExpectedStatus = -1
   393  
   394  func MakeRequest(t testing.TB, rw *RequestWrapper, expectedStatus int) *httptest.ResponseRecorder {
   395  	t.Helper()
   396  	req := rw.Request
   397  	recorder := httptest.NewRecorder()
   398  	if req.RemoteAddr == "" {
   399  		req.RemoteAddr = "test-mock:12345"
   400  	}
   401  	testWebRoutes.ServeHTTP(recorder, req)
   402  	if expectedStatus != NoExpectedStatus {
   403  		if !assert.EqualValues(t, expectedStatus, recorder.Code, "Request: %s %s", req.Method, req.URL.String()) {
   404  			logUnexpectedResponse(t, recorder)
   405  		}
   406  	}
   407  	return recorder
   408  }
   409  
   410  func MakeRequestNilResponseRecorder(t testing.TB, rw *RequestWrapper, expectedStatus int) *NilResponseRecorder {
   411  	t.Helper()
   412  	req := rw.Request
   413  	recorder := NewNilResponseRecorder()
   414  	testWebRoutes.ServeHTTP(recorder, req)
   415  	if expectedStatus != NoExpectedStatus {
   416  		if !assert.EqualValues(t, expectedStatus, recorder.Code,
   417  			"Request: %s %s", req.Method, req.URL.String()) {
   418  			logUnexpectedResponse(t, &recorder.ResponseRecorder)
   419  		}
   420  	}
   421  	return recorder
   422  }
   423  
   424  func MakeRequestNilResponseHashSumRecorder(t testing.TB, rw *RequestWrapper, expectedStatus int) *NilResponseHashSumRecorder {
   425  	t.Helper()
   426  	req := rw.Request
   427  	recorder := NewNilResponseHashSumRecorder()
   428  	testWebRoutes.ServeHTTP(recorder, req)
   429  	if expectedStatus != NoExpectedStatus {
   430  		if !assert.EqualValues(t, expectedStatus, recorder.Code,
   431  			"Request: %s %s", req.Method, req.URL.String()) {
   432  			logUnexpectedResponse(t, &recorder.ResponseRecorder)
   433  		}
   434  	}
   435  	return recorder
   436  }
   437  
   438  // logUnexpectedResponse logs the contents of an unexpected response.
   439  func logUnexpectedResponse(t testing.TB, recorder *httptest.ResponseRecorder) {
   440  	t.Helper()
   441  	respBytes := recorder.Body.Bytes()
   442  	if len(respBytes) == 0 {
   443  		return
   444  	} else if len(respBytes) < 500 {
   445  		// if body is short, just log the whole thing
   446  		t.Log("Response: ", string(respBytes))
   447  		return
   448  	}
   449  	t.Log("Response length: ", len(respBytes))
   450  
   451  	// log the "flash" error message, if one exists
   452  	// we must create a new buffer, so that we don't "use up" resp.Body
   453  	htmlDoc, err := goquery.NewDocumentFromReader(bytes.NewBuffer(respBytes))
   454  	if err != nil {
   455  		return // probably a non-HTML response
   456  	}
   457  	errMsg := htmlDoc.Find(".ui.negative.message").Text()
   458  	if len(errMsg) > 0 {
   459  		t.Log("A flash error message was found:", errMsg)
   460  	}
   461  }
   462  
   463  func DecodeJSON(t testing.TB, resp *httptest.ResponseRecorder, v any) {
   464  	t.Helper()
   465  
   466  	decoder := json.NewDecoder(resp.Body)
   467  	assert.NoError(t, decoder.Decode(v))
   468  }
   469  
   470  func VerifyJSONSchema(t testing.TB, resp *httptest.ResponseRecorder, schemaFile string) {
   471  	t.Helper()
   472  
   473  	schemaFilePath := filepath.Join(filepath.Dir(setting.AppPath), "tests", "integration", "schemas", schemaFile)
   474  	_, schemaFileErr := os.Stat(schemaFilePath)
   475  	assert.Nil(t, schemaFileErr)
   476  
   477  	schema, schemaFileReadErr := os.ReadFile(schemaFilePath)
   478  	assert.Nil(t, schemaFileReadErr)
   479  	assert.True(t, len(schema) > 0)
   480  
   481  	nodeinfoSchema := gojsonschema.NewStringLoader(string(schema))
   482  	nodeinfoString := gojsonschema.NewStringLoader(resp.Body.String())
   483  	result, schemaValidationErr := gojsonschema.Validate(nodeinfoSchema, nodeinfoString)
   484  	assert.Nil(t, schemaValidationErr)
   485  	assert.Empty(t, result.Errors())
   486  	assert.True(t, result.Valid())
   487  }
   488  
   489  // GetCSRF returns CSRF token from body
   490  // If it fails, it means the CSRF token is not found in the response body returned by the url with the given session.
   491  // In this case, you should find a better url to get it.
   492  func GetCSRF(t testing.TB, session *TestSession, urlStr string) string {
   493  	t.Helper()
   494  	req := NewRequest(t, "GET", urlStr)
   495  	resp := session.MakeRequest(t, req, http.StatusOK)
   496  	doc := NewHTMLParser(t, resp.Body)
   497  	csrf := doc.GetCSRF()
   498  	require.NotEmpty(t, csrf)
   499  	return csrf
   500  }
   501  
   502  // GetCSRFFrom returns CSRF token from body
   503  func GetCSRFFromCookie(t testing.TB, session *TestSession, urlStr string) string {
   504  	t.Helper()
   505  	req := NewRequest(t, "GET", urlStr)
   506  	session.MakeRequest(t, req, http.StatusOK)
   507  	return session.GetCookie("_csrf").Value
   508  }