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  }