github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/pkg/hook/server_test.go (about)

     1  /*
     2  Copyright 2016 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 hook
    18  
    19  import (
    20  	"bytes"
    21  	"io"
    22  	"net/http"
    23  	"net/http/httptest"
    24  	"reflect"
    25  	"strings"
    26  	"sync"
    27  	"testing"
    28  
    29  	"github.com/google/go-cmp/cmp"
    30  	"github.com/google/go-cmp/cmp/cmpopts"
    31  
    32  	"sigs.k8s.io/prow/pkg/githubeventserver"
    33  	"sigs.k8s.io/prow/pkg/plugins"
    34  )
    35  
    36  func TestServeHTTPErrors(t *testing.T) {
    37  	metrics := githubeventserver.NewMetrics()
    38  	pa := &plugins.ConfigAgent{}
    39  	pa.Set(&plugins.Configuration{})
    40  
    41  	getSecret := func() []byte {
    42  		var repoLevelSecret = `
    43  '*':
    44    - value: abc
    45      created_at: 2019-10-02T15:00:00Z
    46    - value: key2
    47      created_at: 2020-10-02T15:00:00Z
    48  foo/bar:
    49    - value: 123abc
    50      created_at: 2019-10-02T15:00:00Z
    51    - value: key6
    52      created_at: 2020-10-02T15:00:00Z
    53  `
    54  		return []byte(repoLevelSecret)
    55  	}
    56  
    57  	s := &Server{
    58  		Metrics:        metrics,
    59  		Plugins:        pa,
    60  		TokenGenerator: getSecret,
    61  		RepoEnabled:    func(org, repo string) bool { return true },
    62  	}
    63  	// This is the SHA1 signature for payload "{}" and signature "abc"
    64  	// echo -n '{}' | openssl dgst -sha1 -hmac abc
    65  	const hmac string = "sha1=db5c76f4264d0ad96cf21baec394964b4b8ce580"
    66  	const body string = "{}"
    67  	var testcases = []struct {
    68  		name string
    69  
    70  		Method string
    71  		Header map[string]string
    72  		Body   string
    73  		Code   int
    74  	}{
    75  		{
    76  			name: "Delete",
    77  
    78  			Method: http.MethodDelete,
    79  			Header: map[string]string{
    80  				"X-GitHub-Event":    "ping",
    81  				"X-GitHub-Delivery": "I am unique",
    82  				"X-Hub-Signature":   hmac,
    83  				"content-type":      "application/json",
    84  			},
    85  			Body: body,
    86  			Code: http.StatusMethodNotAllowed,
    87  		},
    88  		{
    89  			name: "No event",
    90  
    91  			Method: http.MethodPost,
    92  			Header: map[string]string{
    93  				"X-GitHub-Delivery": "I am unique",
    94  				"X-Hub-Signature":   hmac,
    95  				"content-type":      "application/json",
    96  			},
    97  			Body: body,
    98  			Code: http.StatusBadRequest,
    99  		},
   100  		{
   101  			name: "No content type",
   102  
   103  			Method: http.MethodPost,
   104  			Header: map[string]string{
   105  				"X-GitHub-Event":    "ping",
   106  				"X-GitHub-Delivery": "I am unique",
   107  				"X-Hub-Signature":   hmac,
   108  			},
   109  			Body: body,
   110  			Code: http.StatusBadRequest,
   111  		},
   112  		{
   113  			name: "No event guid",
   114  
   115  			Method: http.MethodPost,
   116  			Header: map[string]string{
   117  				"X-GitHub-Event":  "ping",
   118  				"X-Hub-Signature": hmac,
   119  				"content-type":    "application/json",
   120  			},
   121  			Body: body,
   122  			Code: http.StatusBadRequest,
   123  		},
   124  		{
   125  			name: "No signature",
   126  
   127  			Method: http.MethodPost,
   128  			Header: map[string]string{
   129  				"X-GitHub-Event":    "ping",
   130  				"X-GitHub-Delivery": "I am unique",
   131  				"content-type":      "application/json",
   132  			},
   133  			Body: body,
   134  			Code: http.StatusForbidden,
   135  		},
   136  		{
   137  			name: "Bad signature",
   138  
   139  			Method: http.MethodPost,
   140  			Header: map[string]string{
   141  				"X-GitHub-Event":    "ping",
   142  				"X-GitHub-Delivery": "I am unique",
   143  				"X-Hub-Signature":   "this doesn't work",
   144  				"content-type":      "application/json",
   145  			},
   146  			Body: body,
   147  			Code: http.StatusForbidden,
   148  		},
   149  		{
   150  			name: "Good",
   151  
   152  			Method: http.MethodPost,
   153  			Header: map[string]string{
   154  				"X-GitHub-Event":    "ping",
   155  				"X-GitHub-Delivery": "I am unique",
   156  				"X-Hub-Signature":   hmac,
   157  				"content-type":      "application/json",
   158  			},
   159  			Body: body,
   160  			Code: http.StatusOK,
   161  		},
   162  		{
   163  			name: "Good, again",
   164  
   165  			Method: http.MethodGet,
   166  			Header: map[string]string{
   167  				"content-type": "application/json",
   168  			},
   169  			Body: body,
   170  			Code: http.StatusMethodNotAllowed,
   171  		},
   172  	}
   173  
   174  	for _, tc := range testcases {
   175  		t.Logf("Running scenario %q", tc.name)
   176  
   177  		w := httptest.NewRecorder()
   178  		r, err := http.NewRequest(tc.Method, "", strings.NewReader(tc.Body))
   179  		if err != nil {
   180  			t.Fatal(err)
   181  		}
   182  		for k, v := range tc.Header {
   183  			r.Header.Set(k, v)
   184  		}
   185  		s.ServeHTTP(w, r)
   186  		if w.Code != tc.Code {
   187  			t.Errorf("For test case: %+v\nExpected code %v, got code %v", tc, tc.Code, w.Code)
   188  		}
   189  	}
   190  }
   191  
   192  func TestNeedDemux(t *testing.T) {
   193  	tests := []struct {
   194  		name string
   195  
   196  		eventType   string
   197  		srcRepo     string
   198  		repoEnabled func(org, repo string) bool
   199  		plugins     map[string][]plugins.ExternalPlugin
   200  
   201  		expected []plugins.ExternalPlugin
   202  	}{
   203  		{
   204  			name: "no external plugins",
   205  
   206  			eventType: "issue_comment",
   207  			srcRepo:   "kubernetes/test-infra",
   208  			plugins:   nil,
   209  
   210  			expected: nil,
   211  		},
   212  		{
   213  			name: "we have variety",
   214  
   215  			eventType: "issue_comment",
   216  			srcRepo:   "kubernetes/test-infra",
   217  			plugins: map[string][]plugins.ExternalPlugin{
   218  				"kubernetes/test-infra": {
   219  					{
   220  						Name:   "sandwich",
   221  						Events: []string{"pull_request"},
   222  					},
   223  					{
   224  						Name: "coffee",
   225  					},
   226  				},
   227  				"kubernetes/kubernetes": {
   228  					{
   229  						Name:   "gumbo",
   230  						Events: []string{"issue_comment"},
   231  					},
   232  				},
   233  				"kubernetes": {
   234  					{
   235  						Name:   "chicken",
   236  						Events: []string{"push"},
   237  					},
   238  					{
   239  						Name: "water",
   240  					},
   241  					{
   242  						Name:   "chocolate",
   243  						Events: []string{"pull_request", "issue_comment", "issues"},
   244  					},
   245  				},
   246  			},
   247  
   248  			expected: []plugins.ExternalPlugin{
   249  				{
   250  					Name: "coffee",
   251  				},
   252  				{
   253  					Name: "water",
   254  				},
   255  				{
   256  					Name:   "chocolate",
   257  					Events: []string{"pull_request", "issue_comment", "issues"},
   258  				},
   259  			},
   260  		},
   261  		{
   262  			name: "external plugins handling other events",
   263  
   264  			eventType: "repository",
   265  			srcRepo:   "kubernetes/test-infra",
   266  			plugins: map[string][]plugins.ExternalPlugin{
   267  				"kubernetes/test-infra": {
   268  					{
   269  						Name: "coffee",
   270  					},
   271  				},
   272  				"kubernetes/kubernetes": {
   273  					{
   274  						Name:   "gumbo",
   275  						Events: []string{"issue_comment"},
   276  					},
   277  				},
   278  				"kubernetes": {
   279  					{
   280  						Name:   "chicken",
   281  						Events: []string{"repository"},
   282  					},
   283  					{
   284  						Name: "water",
   285  					},
   286  					{
   287  						Name:   "chocolate",
   288  						Events: []string{"pull_request", "issue_comment", "repository"},
   289  					},
   290  				},
   291  			},
   292  
   293  			expected: []plugins.ExternalPlugin{
   294  				{
   295  					Name: "coffee",
   296  				},
   297  				{
   298  					Name: "water",
   299  				},
   300  				{
   301  					Name:   "chicken",
   302  					Events: []string{"repository"},
   303  				},
   304  				{
   305  					Name:   "chocolate",
   306  					Events: []string{"pull_request", "issue_comment", "repository"},
   307  				},
   308  			},
   309  		},
   310  		{
   311  			name: "we have variety but disabled that repo",
   312  
   313  			eventType: "issue_comment",
   314  			srcRepo:   "kubernetes/test-infra",
   315  			repoEnabled: func(org, repo string) bool {
   316  				if org == "kubernetes" && repo == "test-infra" {
   317  					return false
   318  				}
   319  				return true
   320  			},
   321  			plugins: map[string][]plugins.ExternalPlugin{
   322  				"kubernetes/test-infra": {
   323  					{
   324  						Name:   "sandwich",
   325  						Events: []string{"pull_request"},
   326  					},
   327  					{
   328  						Name: "coffee",
   329  					},
   330  				},
   331  				"kubernetes/kubernetes": {
   332  					{
   333  						Name:   "gumbo",
   334  						Events: []string{"issue_comment"},
   335  					},
   336  				},
   337  				"kubernetes": {
   338  					{
   339  						Name:   "chicken",
   340  						Events: []string{"push"},
   341  					},
   342  					{
   343  						Name: "water",
   344  					},
   345  					{
   346  						Name:   "chocolate",
   347  						Events: []string{"pull_request", "issue_comment", "issues"},
   348  					},
   349  				},
   350  			},
   351  		},
   352  	}
   353  
   354  	for _, test := range tests {
   355  		t.Run(test.name, func(t *testing.T) {
   356  
   357  			t.Logf("Running scenario %q", test.name)
   358  
   359  			pa := &plugins.ConfigAgent{}
   360  			pa.Set(&plugins.Configuration{
   361  				ExternalPlugins: test.plugins,
   362  			})
   363  
   364  			if test.repoEnabled == nil {
   365  				test.repoEnabled = func(_, _ string) bool { return true }
   366  			}
   367  			s := &Server{Plugins: pa, RepoEnabled: test.repoEnabled}
   368  
   369  			gotPlugins := s.needDemux(test.eventType, test.srcRepo)
   370  			if len(gotPlugins) != len(test.expected) {
   371  				t.Fatalf("expected plugins: %+v, got: %+v", test.expected, gotPlugins)
   372  			}
   373  			for _, expected := range test.expected {
   374  				var found bool
   375  				for _, got := range gotPlugins {
   376  					if got.Name != expected.Name {
   377  						continue
   378  					}
   379  					if !reflect.DeepEqual(expected, got) {
   380  						t.Errorf("expected plugin: %+v, got: %+v", expected, got)
   381  					}
   382  					found = true
   383  				}
   384  				if !found {
   385  					t.Errorf("expected plugins: %+v, got: %+v", test.expected, gotPlugins)
   386  					break
   387  				}
   388  			}
   389  		})
   390  	}
   391  }
   392  
   393  type roundTripFunc func(req *http.Request) *http.Response
   394  
   395  // RoundTrip .
   396  func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
   397  	return f(req), nil
   398  }
   399  
   400  // newTestClient returns *http.Client with Transport replaced to avoid making real calls
   401  func newTestClient(fn roundTripFunc) *http.Client {
   402  	return &http.Client{
   403  		Transport: fn,
   404  	}
   405  }
   406  
   407  func TestDemuxEvent(t *testing.T) {
   408  
   409  	getSecret := func() []byte {
   410  		var repoLevelSecret = `
   411  '*':
   412    - value: abc
   413      created_at: 2019-10-02T15:00:00Z
   414    - value: key2
   415      created_at: 2020-10-02T15:00:00Z
   416  foo/bar:
   417    - value: 123abc
   418      created_at: 2019-10-02T15:00:00Z
   419    - value: key6
   420      created_at: 2020-10-02T15:00:00Z
   421  `
   422  		return []byte(repoLevelSecret)
   423  	}
   424  
   425  	externalPlugins := map[string][]plugins.ExternalPlugin{
   426  		"kubernetes/test-infra": {
   427  			{
   428  				Name:     "coffee",
   429  				Endpoint: "/coffee",
   430  			},
   431  		},
   432  		"kubernetes/kubernetes": {
   433  			{
   434  				Name:     "gumbo",
   435  				Endpoint: "/gumbo",
   436  				Events:   []string{"issue_comment"},
   437  			},
   438  		},
   439  		"kubernetes": {
   440  			{
   441  				Name:     "chicken",
   442  				Endpoint: "/chicken",
   443  				Events:   []string{"repository"},
   444  			},
   445  			{
   446  				Name:     "water",
   447  				Endpoint: "/water",
   448  			},
   449  			{
   450  				Name:     "chocolate",
   451  				Endpoint: "/chocolate",
   452  				Events:   []string{"pull_request", "issue_comment", "repository"},
   453  			},
   454  			{
   455  				Name:     "unknown_event_handler",
   456  				Endpoint: "/unknown",
   457  				Events:   []string{"unknown_event"},
   458  			},
   459  		},
   460  	}
   461  
   462  	// This is the SHA1 signature for payload "$BODY" and signature "abc"
   463  	// echo -n $BODY | openssl dgst -sha1 -hmac abc
   464  	const hmac string = "sha1=d5f926df2d39006bdb5b6acb18f8fcdebad7a052"
   465  	const body string = `{
   466    "action": "edited",
   467    "changes": {
   468      "default_branch": {
   469        "from": "master"
   470      }
   471    },
   472    "repository": {
   473      "full_name": "kubernetes/test-infra",
   474      "default_branch": "master"
   475    }
   476  }`
   477  
   478  	metrics := githubeventserver.NewMetrics()
   479  	pa := &plugins.ConfigAgent{}
   480  	pa.Set(&plugins.Configuration{
   481  		ExternalPlugins: externalPlugins,
   482  	})
   483  
   484  	var testcases = []struct {
   485  		name string
   486  
   487  		Method string
   488  		Header map[string]string
   489  		Body   string
   490  
   491  		ExpectedDispatch []string
   492  	}{
   493  		{
   494  			name: "Repository event",
   495  
   496  			Method: http.MethodPost,
   497  			Header: map[string]string{
   498  				"X-GitHub-Event":    "repository",
   499  				"X-GitHub-Delivery": "I am unique",
   500  				"X-Hub-Signature":   hmac,
   501  				"content-type":      "application/json",
   502  			},
   503  			Body: body,
   504  
   505  			ExpectedDispatch: []string{"/coffee", "/water", "/chicken", "/chocolate"},
   506  		},
   507  		{
   508  			name: "Issue comment event",
   509  
   510  			Method: http.MethodPost,
   511  			Header: map[string]string{
   512  				"X-GitHub-Event":    "issue_comment",
   513  				"X-GitHub-Delivery": "I am unique",
   514  				"X-Hub-Signature":   hmac,
   515  				"content-type":      "application/json",
   516  			},
   517  			Body: body,
   518  
   519  			ExpectedDispatch: []string{"/coffee", "/water", "/chocolate"},
   520  		},
   521  		{
   522  			name: "Unknown event type gets dispatched to external plugin",
   523  
   524  			Method: http.MethodPost,
   525  			Header: map[string]string{
   526  				"X-GitHub-Event":    "unknown_event",
   527  				"X-GitHub-Delivery": "I am unique",
   528  				"X-Hub-Signature":   hmac,
   529  				"content-type":      "application/json",
   530  			},
   531  			Body: body,
   532  
   533  			ExpectedDispatch: []string{"/coffee", "/water", "/unknown"},
   534  		},
   535  	}
   536  
   537  	for _, tc := range testcases {
   538  		t.Run(tc.name, func(t *testing.T) {
   539  			t.Logf("Running scenario %q", tc.name)
   540  
   541  			var calledExternalPlugins []string
   542  			var m sync.Mutex
   543  
   544  			client := newTestClient(func(req *http.Request) *http.Response {
   545  				m.Lock()
   546  				calledExternalPlugins = append(calledExternalPlugins, req.URL.String())
   547  				m.Unlock()
   548  				return &http.Response{
   549  					StatusCode: 200,
   550  					Body:       io.NopCloser(bytes.NewBufferString(`OK`)),
   551  					Header:     make(http.Header),
   552  				}
   553  			})
   554  
   555  			s := &Server{
   556  				Metrics:        metrics,
   557  				Plugins:        pa,
   558  				TokenGenerator: getSecret,
   559  				RepoEnabled:    func(org, repo string) bool { return true },
   560  				c:              *client,
   561  			}
   562  			w := httptest.NewRecorder()
   563  			r, err := http.NewRequest(tc.Method, "", strings.NewReader(tc.Body))
   564  			if err != nil {
   565  				t.Fatal(err)
   566  			}
   567  			for k, v := range tc.Header {
   568  				r.Header.Set(k, v)
   569  			}
   570  			s.ServeHTTP(w, r)
   571  			s.wg.Wait()
   572  
   573  			if diff := cmp.Diff(tc.ExpectedDispatch, calledExternalPlugins, cmpopts.SortSlices(func(a, b string) bool {
   574  				return a < b
   575  			})); diff != "" {
   576  				t.Fatalf("Expected plugins calls mismatch. got(+), want(-):\n%s", diff)
   577  			}
   578  		})
   579  	}
   580  }