sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/prstatus/prstatus_test.go (about) 1 /* 2 Copyright 2018 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package prstatus 18 19 import ( 20 "context" 21 "encoding/gob" 22 "errors" 23 "io" 24 "net/http" 25 "net/http/httptest" 26 "reflect" 27 "strconv" 28 "testing" 29 "time" 30 31 "github.com/google/go-cmp/cmp" 32 "github.com/gorilla/sessions" 33 "github.com/sirupsen/logrus" 34 "golang.org/x/oauth2" 35 "sigs.k8s.io/yaml" 36 37 "sigs.k8s.io/prow/pkg/github" 38 "sigs.k8s.io/prow/pkg/githuboauth" 39 ) 40 41 type MockQueryHandler struct { 42 prs []PullRequest 43 contextMap map[int][]Context 44 } 45 46 func (mh *MockQueryHandler) queryPullRequests(ctx context.Context, ghc githubQuerier, query string) ([]PullRequest, error) { 47 return mh.prs, nil 48 } 49 50 func (mh *MockQueryHandler) getHeadContexts(ghc githubStatusFetcher, pr PullRequest) ([]Context, error) { 51 return mh.contextMap[int(pr.Number)], nil 52 } 53 54 type fgc struct { 55 combinedStatus *github.CombinedStatus 56 checkruns *github.CheckRunList 57 botName string 58 } 59 60 func (c fgc) QueryWithGitHubAppsSupport(context.Context, interface{}, map[string]interface{}, string) error { 61 return nil 62 } 63 64 func (c fgc) GetCombinedStatus(org, repo, ref string) (*github.CombinedStatus, error) { 65 return c.combinedStatus, nil 66 } 67 68 func (c fgc) ListCheckRuns(org, repo, ref string) (*github.CheckRunList, error) { 69 if c.checkruns != nil { 70 return c.checkruns, nil 71 } 72 return &github.CheckRunList{}, nil 73 } 74 75 func (c fgc) BotUser() (*github.UserData, error) { 76 if c.botName == "error" { 77 return nil, errors.New("injected BotUser() error") 78 } 79 return &github.UserData{Login: c.botName}, nil 80 } 81 82 func newGitHubClientCreator(tokenUsers map[string]fgc) githubClientCreator { 83 return func(accessToken string) (GitHubClient, error) { 84 who, ok := tokenUsers[accessToken] 85 if !ok { 86 panic("unexpected access token: " + accessToken) 87 } 88 return who, nil 89 } 90 } 91 92 func newMockQueryHandler(prs []PullRequest, contextMap map[int][]Context) *MockQueryHandler { 93 return &MockQueryHandler{ 94 prs: prs, 95 contextMap: contextMap, 96 } 97 } 98 99 func createMockAgent(repos []string, config *githuboauth.Config) *DashboardAgent { 100 return &DashboardAgent{ 101 repos: repos, 102 goac: config, 103 log: logrus.WithField("unit-test", "dashboard-agent"), 104 } 105 } 106 107 func TestHandlePrStatusWithoutLogin(t *testing.T) { 108 repos := []string{"mock/repo", "kubernetes/test-infra", "foo/bar"} 109 mockCookieStore := sessions.NewCookieStore([]byte("secret-key")) 110 mockConfig := &githuboauth.Config{ 111 CookieStore: mockCookieStore, 112 } 113 mockAgent := createMockAgent(repos, mockConfig) 114 mockData := UserData{ 115 Login: false, 116 } 117 118 rr := httptest.NewRecorder() 119 request := httptest.NewRequest(http.MethodGet, "/pr-data.js", nil) 120 121 mockQueryHandler := newMockQueryHandler(nil, nil) 122 123 ghClientCreator := newGitHubClientCreator(map[string]fgc{"should-not-find-me": {}}) 124 prHandler := mockAgent.HandlePrStatus(mockQueryHandler, ghClientCreator) 125 prHandler.ServeHTTP(rr, request) 126 if rr.Code != http.StatusOK { 127 t.Fatalf("Bad status code: %d", rr.Code) 128 } 129 response := rr.Result() 130 defer response.Body.Close() 131 body, err := io.ReadAll(response.Body) 132 if err != nil { 133 t.Fatalf("Error with reading response body: %v", err) 134 } 135 var dataReturned UserData 136 if err := yaml.Unmarshal(body, &dataReturned); err != nil { 137 t.Errorf("Error with unmarshaling response: %v", err) 138 } 139 if !reflect.DeepEqual(dataReturned, mockData) { 140 t.Errorf("Invalid user data. Got %v, expected %v", dataReturned, mockData) 141 } 142 } 143 144 func TestHandlePrStatusWithInvalidToken(t *testing.T) { 145 logrus.SetLevel(logrus.ErrorLevel) 146 repos := []string{"mock/repo", "kubernetes/test-infra", "foo/bar"} 147 mockCookieStore := sessions.NewCookieStore([]byte("secret-key")) 148 mockConfig := &githuboauth.Config{ 149 CookieStore: mockCookieStore, 150 } 151 mockAgent := createMockAgent(repos, mockConfig) 152 mockQueryHandler := newMockQueryHandler([]PullRequest{}, map[int][]Context{}) 153 154 rr := httptest.NewRecorder() 155 request := httptest.NewRequest(http.MethodGet, "/pr-data.js", nil) 156 request.AddCookie(&http.Cookie{Name: tokenSession, Value: "garbage"}) 157 ghClientCreator := newGitHubClientCreator(map[string]fgc{"should-not-find-me": {}}) 158 prHandler := mockAgent.HandlePrStatus(mockQueryHandler, ghClientCreator) 159 prHandler.ServeHTTP(rr, request) 160 if rr.Code != http.StatusOK { 161 t.Fatalf("Bad status code: %d", rr.Code) 162 } 163 response := rr.Result() 164 defer response.Body.Close() 165 166 body, err := io.ReadAll(response.Body) 167 if err != nil { 168 t.Fatalf("Error with reading response body: %v", err) 169 } 170 171 var dataReturned UserData 172 if err := yaml.Unmarshal(body, &dataReturned); err != nil { 173 t.Errorf("Error with unmarshaling response: %v", err) 174 } 175 176 expectedData := UserData{Login: false} 177 if !reflect.DeepEqual(dataReturned, expectedData) { 178 t.Fatalf("Invalid user data. Got %v, expected %v.", dataReturned, expectedData) 179 } 180 } 181 182 func TestHandlePrStatusWithLogin(t *testing.T) { 183 repos := []string{"mock/repo", "kubernetes/test-infra", "foo/bar"} 184 mockCookieStore := sessions.NewCookieStore([]byte("secret-key")) 185 mockConfig := &githuboauth.Config{ 186 CookieStore: mockCookieStore, 187 } 188 mockAgent := createMockAgent(repos, mockConfig) 189 190 testCases := []struct { 191 prs []PullRequest 192 contextMap map[int][]Context 193 expectedData UserData 194 }{ 195 { 196 prs: []PullRequest{}, 197 contextMap: map[int][]Context{}, 198 expectedData: UserData{ 199 Login: true, 200 }, 201 }, 202 { 203 prs: []PullRequest{ 204 { 205 Number: 0, 206 Title: "random pull request", 207 }, 208 { 209 Number: 1, 210 Title: "This is a test", 211 }, 212 { 213 Number: 2, 214 Title: "test pull request", 215 }, 216 }, 217 contextMap: map[int][]Context{ 218 0: { 219 { 220 Context: "gofmt-job", 221 Description: "job succeed", 222 State: "SUCCESS", 223 }, 224 }, 225 1: { 226 { 227 Context: "verify-bazel-job", 228 Description: "job failed", 229 State: "FAILURE", 230 }, 231 }, 232 2: { 233 { 234 Context: "gofmt-job", 235 Description: "job succeed", 236 State: "SUCCESS", 237 }, 238 { 239 Context: "verify-bazel-job", 240 Description: "job failed", 241 State: "FAILURE", 242 }, 243 }, 244 }, 245 expectedData: UserData{ 246 Login: true, 247 PullRequestsWithContexts: []PullRequestWithContexts{ 248 { 249 PullRequest: PullRequest{ 250 Number: 0, 251 Title: "random pull request", 252 }, 253 Contexts: []Context{ 254 { 255 Context: "gofmt-job", 256 Description: "job succeed", 257 State: "SUCCESS", 258 }, 259 }, 260 }, 261 { 262 PullRequest: PullRequest{ 263 Number: 1, 264 Title: "This is a test", 265 }, 266 Contexts: []Context{ 267 { 268 Context: "verify-bazel-job", 269 Description: "job failed", 270 State: "FAILURE", 271 }, 272 }, 273 }, 274 { 275 PullRequest: PullRequest{ 276 Number: 2, 277 Title: "test pull request", 278 }, 279 Contexts: []Context{ 280 { 281 Context: "gofmt-job", 282 Description: "job succeed", 283 State: "SUCCESS", 284 }, 285 { 286 Context: "verify-bazel-job", 287 Description: "job failed", 288 State: "FAILURE", 289 }, 290 }, 291 }, 292 }, 293 }, 294 }, 295 } 296 for id, testcase := range testCases { 297 t.Run(strconv.Itoa(id), func(t *testing.T) { 298 rr := httptest.NewRecorder() 299 request := httptest.NewRequest(http.MethodGet, "/pr-data.js", nil) 300 mockSession, err := sessions.GetRegistry(request).Get(mockCookieStore, tokenSession) 301 if err != nil { 302 t.Errorf("Error with creating mock session: %v", err) 303 } 304 gob.Register(oauth2.Token{}) 305 const ( 306 accessToken = "secret-token" 307 botName = "random_user" 308 ) 309 token := &oauth2.Token{AccessToken: accessToken, Expiry: time.Now().Add(time.Duration(24*365) * time.Hour)} 310 mockSession.Values[tokenKey] = token 311 mockSession.Values[loginKey] = botName 312 mockQueryHandler := newMockQueryHandler(testcase.prs, testcase.contextMap) 313 ghClientCreator := newGitHubClientCreator(map[string]fgc{accessToken: {botName: botName}}) 314 prHandler := mockAgent.HandlePrStatus(mockQueryHandler, ghClientCreator) 315 prHandler.ServeHTTP(rr, request) 316 if rr.Code != http.StatusOK { 317 t.Fatalf("Bad status code: %d", rr.Code) 318 } 319 response := rr.Result() 320 defer response.Body.Close() 321 body, err := io.ReadAll(response.Body) 322 if err != nil { 323 t.Fatalf("Error with reading response body: %v", err) 324 } 325 var dataReturned UserData 326 if err := yaml.Unmarshal(body, &dataReturned); err != nil { 327 t.Errorf("Error with unmarshaling response: %v", err) 328 } 329 if !reflect.DeepEqual(dataReturned, testcase.expectedData) { 330 t.Fatalf("Invalid user data. Got %v, expected %v.", dataReturned, testcase.expectedData) 331 } 332 t.Logf("Passed") 333 }) 334 } 335 } 336 337 func TestGetHeadContexts(t *testing.T) { 338 repos := []string{"mock/repo", "kubernetes/test-infra", "foo/bar"} 339 mockCookieStore := sessions.NewCookieStore([]byte("secret-key")) 340 mockConfig := &githuboauth.Config{ 341 CookieStore: mockCookieStore, 342 } 343 mockAgent := createMockAgent(repos, mockConfig) 344 testCases := []struct { 345 combinedStatus *github.CombinedStatus 346 checkruns *github.CheckRunList 347 expectedContexts []Context 348 }{ 349 { 350 combinedStatus: &github.CombinedStatus{}, 351 expectedContexts: []Context{}, 352 }, 353 { 354 combinedStatus: &github.CombinedStatus{ 355 Statuses: []github.Status{ 356 { 357 State: "FAILURE", 358 Description: "job failed", 359 Context: "gofmt-job", 360 }, 361 { 362 State: "SUCCESS", 363 Description: "job succeed", 364 Context: "k8s-job", 365 }, 366 { 367 State: "PENDING", 368 Description: "triggered", 369 Context: "test-job", 370 }, 371 }, 372 }, 373 expectedContexts: []Context{ 374 { 375 State: "FAILURE", 376 Context: "gofmt-job", 377 Description: "job failed", 378 }, 379 { 380 State: "SUCCESS", 381 Description: "job succeed", 382 Context: "k8s-job", 383 }, 384 { 385 State: "PENDING", 386 Description: "triggered", 387 Context: "test-job", 388 }, 389 }, 390 }, 391 { 392 combinedStatus: &github.CombinedStatus{ 393 Statuses: []github.Status{ 394 { 395 State: "FAILURE", 396 Description: "job failed", 397 Context: "gofmt-job", 398 }, 399 { 400 State: "SUCCESS", 401 Description: "job succeed", 402 Context: "k8s-job", 403 }, 404 { 405 State: "PENDING", 406 Description: "triggered", 407 Context: "test-job", 408 }, 409 }, 410 }, 411 checkruns: &github.CheckRunList{ 412 CheckRuns: []github.CheckRun{ 413 {Name: "incomplete-checkrun"}, 414 {Name: "neutral-is-considered-success-checkrun", CompletedAt: "2000 BC", Conclusion: "neutral"}, 415 {Name: "success-checkrun", CompletedAt: "1900 BC", Conclusion: "success"}, 416 {Name: "failure-checkrun", CompletedAt: "1800 BC", Conclusion: "failure"}, 417 }, 418 }, 419 expectedContexts: []Context{ 420 { 421 State: "FAILURE", 422 Context: "gofmt-job", 423 Description: "job failed", 424 }, 425 { 426 State: "SUCCESS", 427 Description: "job succeed", 428 Context: "k8s-job", 429 }, 430 { 431 State: "PENDING", 432 Description: "triggered", 433 Context: "test-job", 434 }, 435 { 436 State: "PENDING", 437 Context: "incomplete-checkrun", 438 }, 439 { 440 State: "SUCCESS", 441 Context: "neutral-is-considered-success-checkrun", 442 }, 443 { 444 State: "SUCCESS", 445 Context: "success-checkrun", 446 }, 447 { 448 State: "FAILURE", 449 Context: "failure-checkrun", 450 }, 451 }, 452 }, 453 } 454 for id, testcase := range testCases { 455 t.Run(strconv.Itoa(id), func(t *testing.T) { 456 contexts, err := mockAgent.getHeadContexts(&fgc{ 457 combinedStatus: testcase.combinedStatus, 458 checkruns: testcase.checkruns, 459 }, PullRequest{}) 460 if err != nil { 461 t.Fatalf("Error with getting head contexts") 462 } 463 if diff := cmp.Diff(contexts, testcase.expectedContexts); diff != "" { 464 t.Fatalf("contexts differ from expected: %s", diff) 465 } 466 }) 467 } 468 } 469 470 func TestConstructSearchQuery(t *testing.T) { 471 repos := []string{"mock/repo", "kubernetes/test-infra", "foo/bar"} 472 mockCookieStore := sessions.NewCookieStore([]byte("secret-key")) 473 mockConfig := &githuboauth.Config{ 474 CookieStore: mockCookieStore, 475 } 476 mockAgent := createMockAgent(repos, mockConfig) 477 query := mockAgent.ConstructSearchQuery("random_username") 478 mockQuery := "is:pr state:open author:random_username repo:\"mock/repo\" repo:\"kubernetes/test-infra\" repo:\"foo/bar\"" 479 if query != mockQuery { 480 t.Errorf("Invalid query. Got: %v, expected %v", query, mockQuery) 481 } 482 }