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 }