github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/cmd/hmac/main_test.go (about)

     1  /*
     2  Copyright 2020 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 main
    18  
    19  import (
    20  	"flag"
    21  	"reflect"
    22  	"strings"
    23  	"testing"
    24  	"time"
    25  
    26  	"github.com/google/go-cmp/cmp"
    27  	"k8s.io/apimachinery/pkg/util/sets"
    28  
    29  	"sigs.k8s.io/prow/cmd/hmac/fakeghhook"
    30  	"sigs.k8s.io/prow/pkg/config"
    31  	"sigs.k8s.io/prow/pkg/flagutil"
    32  	configflagutil "sigs.k8s.io/prow/pkg/flagutil/config"
    33  	"sigs.k8s.io/prow/pkg/github"
    34  )
    35  
    36  func TestGatherOptions(t *testing.T) {
    37  	cases := []struct {
    38  		name     string
    39  		args     map[string]string
    40  		del      sets.Set[string]
    41  		expected func(*options)
    42  		err      bool
    43  	}{
    44  		{
    45  			name: "minimal flags work",
    46  		},
    47  		{
    48  			name: "explicitly set --config-path",
    49  			args: map[string]string{
    50  				"--config-path": "/random/value",
    51  			},
    52  			expected: func(o *options) {
    53  				o.config.ConfigPath = "/random/value"
    54  			},
    55  		},
    56  		{
    57  			name: "explicitly set --dry-run=false",
    58  			args: map[string]string{
    59  				"--dry-run": "false",
    60  			},
    61  			expected: func(o *options) {
    62  				o.dryRun = false
    63  			},
    64  		},
    65  	}
    66  	for _, tc := range cases {
    67  		t.Run(tc.name, func(t *testing.T) {
    68  			ghoptions := flagutil.GitHubOptions{}
    69  			ghoptions.AddFlags(&flag.FlagSet{})
    70  			ghoptions.Validate(false)
    71  			expected := &options{
    72  				config: configflagutil.ConfigOptions{
    73  					ConfigPathFlagName:                    "config-path",
    74  					JobConfigPathFlagName:                 "job-config-path",
    75  					ConfigPath:                            "yo",
    76  					SupplementalProwConfigsFileNameSuffix: "_prowconfig.yaml",
    77  					InRepoConfigCacheSize:                 200,
    78  				},
    79  				dryRun:                   true,
    80  				github:                   ghoptions,
    81  				kubeconfigCtx:            "whatever-kubeconfig-context",
    82  				hookUrl:                  "http://whatever-hook-url",
    83  				hmacTokenSecretNamespace: "default",
    84  				hmacTokenSecretName:      "hmac-token",
    85  				hmacTokenKey:             "hmac",
    86  			}
    87  			if tc.expected != nil {
    88  				tc.expected(expected)
    89  			}
    90  
    91  			argMap := map[string]string{
    92  				"--config-path":            "yo",
    93  				"--hook-url":               "http://whatever-hook-url",
    94  				"--kubeconfig-context":     "whatever-kubeconfig-context",
    95  				"--hmac-token-secret-name": "hmac-token",
    96  				"--hmac-token-key":         "hmac",
    97  			}
    98  			for k, v := range tc.args {
    99  				argMap[k] = v
   100  			}
   101  			for k := range tc.del {
   102  				delete(argMap, k)
   103  			}
   104  
   105  			var args []string
   106  			for k, v := range argMap {
   107  				args = append(args, k+"="+v)
   108  			}
   109  			fs := flag.NewFlagSet("fake-flags", flag.PanicOnError)
   110  			actual := gatherOptions(fs, args...)
   111  			switch err := actual.validate(); {
   112  			case err != nil:
   113  				if !tc.err {
   114  					t.Errorf("unexpected error: %v", err)
   115  				}
   116  			case tc.err:
   117  				t.Errorf("failed to receive expected error")
   118  			case !reflect.DeepEqual(*expected, actual):
   119  				t.Errorf("\n%#v\n != expected \n%#v\n", actual, *expected)
   120  			}
   121  		})
   122  	}
   123  }
   124  
   125  func TestPruneOldTokens(t *testing.T) {
   126  	// "2006-01-02T15:04:05+07:00"
   127  	time1, _ := time.Parse(time.RFC3339, "2020-01-05T19:07:08+00:00")
   128  	time2, _ := time.Parse(time.RFC3339, "2020-02-05T19:07:08+00:00")
   129  	time3, _ := time.Parse(time.RFC3339, "2020-03-05T19:07:08+00:00")
   130  
   131  	cases := []struct {
   132  		name     string
   133  		current  map[string]github.HMACsForRepo
   134  		repo     string
   135  		expected map[string]github.HMACsForRepo
   136  	}{
   137  		{
   138  			name: "three hmacs, only the latest one is left after pruning",
   139  			current: map[string]github.HMACsForRepo{
   140  				"org1/repo1": []github.HMACToken{
   141  					{
   142  						Value:     "rand-val1",
   143  						CreatedAt: time1,
   144  					},
   145  					{
   146  						Value:     "rand-val2",
   147  						CreatedAt: time2,
   148  					},
   149  					{
   150  						Value:     "rand-val3",
   151  						CreatedAt: time3,
   152  					},
   153  				},
   154  			},
   155  			repo: "org1/repo1",
   156  			expected: map[string]github.HMACsForRepo{
   157  				"org1/repo1": []github.HMACToken{
   158  					{
   159  						Value:     "rand-val3",
   160  						CreatedAt: time3,
   161  					},
   162  				},
   163  			},
   164  		},
   165  		{
   166  			name: "two hmacs, only the latest one is left after pruning",
   167  			current: map[string]github.HMACsForRepo{
   168  				"org1/repo1": []github.HMACToken{
   169  					{
   170  						Value:     "rand-val1",
   171  						CreatedAt: time1,
   172  					},
   173  					{
   174  						Value:     "rand-val2",
   175  						CreatedAt: time2,
   176  					},
   177  				},
   178  			},
   179  			repo: "org1/repo1",
   180  			expected: map[string]github.HMACsForRepo{
   181  				"org1/repo1": []github.HMACToken{
   182  					{
   183  						Value:     "rand-val2",
   184  						CreatedAt: time2,
   185  					},
   186  				},
   187  			},
   188  		},
   189  		{
   190  			name: "nothing will be changed if the repo is not in the map",
   191  			current: map[string]github.HMACsForRepo{
   192  				"org1/repo1": []github.HMACToken{
   193  					{
   194  						Value:     "rand-val1",
   195  						CreatedAt: time1,
   196  					},
   197  				},
   198  			},
   199  			repo: "org2/repo2",
   200  			expected: map[string]github.HMACsForRepo{
   201  				"org1/repo1": []github.HMACToken{
   202  					{
   203  						Value:     "rand-val1",
   204  						CreatedAt: time1,
   205  					},
   206  				},
   207  			},
   208  		},
   209  	}
   210  
   211  	for _, tc := range cases {
   212  		t.Run(tc.name, func(t *testing.T) {
   213  			c := &client{currentHMACMap: tc.current}
   214  			c.pruneOldTokens(tc.repo)
   215  			if !reflect.DeepEqual(tc.expected, c.currentHMACMap) {
   216  				t.Errorf("%#v != expected %#v", c.currentHMACMap, tc.expected)
   217  			}
   218  		})
   219  	}
   220  }
   221  
   222  func TestGenerateNewHMACToken(t *testing.T) {
   223  	token1, err := generateNewHMACToken()
   224  	if err != nil {
   225  		t.Errorf("error generating new hmac token1: %v", err)
   226  	}
   227  
   228  	token2, err := generateNewHMACToken()
   229  	if err != nil {
   230  		t.Errorf("error generating new hmac token2: %v", err)
   231  	}
   232  	if token1 == token2 {
   233  		t.Error("the generated hmac token should be random, but the two are equal")
   234  	}
   235  }
   236  
   237  func TestHandleRemovedRepo(t *testing.T) {
   238  	cases := []struct {
   239  		name          string
   240  		toRemove      map[string]bool
   241  		expectedHMACs map[string]github.HMACsForRepo
   242  		expectedHooks map[string][]github.Hook
   243  	}{
   244  		{
   245  			name:     "delete hmac and hook for one repo",
   246  			toRemove: map[string]bool{"repo1": true},
   247  			expectedHMACs: map[string]github.HMACsForRepo{
   248  				"repo2": {
   249  					github.HMACToken{
   250  						Value: "val2",
   251  					},
   252  				},
   253  			},
   254  			expectedHooks: map[string][]github.Hook{
   255  				"repo2": {
   256  					github.Hook{
   257  						ID:     0,
   258  						Name:   "hook2",
   259  						Active: true,
   260  						Config: github.HookConfig{
   261  							URL: "http://whatever-hook-url",
   262  						},
   263  					},
   264  				},
   265  			},
   266  		},
   267  		{
   268  			name:          "delete hmac and hook for multiple repos",
   269  			toRemove:      map[string]bool{"repo1": true, "repo2": true},
   270  			expectedHMACs: map[string]github.HMACsForRepo{},
   271  			expectedHooks: map[string][]github.Hook{},
   272  		},
   273  		{
   274  			name:     "delete hmac and hook for non-existed repo",
   275  			toRemove: map[string]bool{"repo1": true, "whatever-repo": true},
   276  			expectedHMACs: map[string]github.HMACsForRepo{
   277  				"repo2": {
   278  					github.HMACToken{
   279  						Value: "val2",
   280  					},
   281  				},
   282  			},
   283  			expectedHooks: map[string][]github.Hook{
   284  				"repo2": {
   285  					github.Hook{
   286  						Name:   "hook2",
   287  						Active: true,
   288  						Config: github.HookConfig{
   289  							URL: "http://whatever-hook-url",
   290  						},
   291  					},
   292  				},
   293  			},
   294  		},
   295  	}
   296  	for _, tc := range cases {
   297  		t.Run(tc.name, func(t *testing.T) {
   298  			fakeClient := &fakeghhook.FakeClient{
   299  				OrgHooks: map[string][]github.Hook{
   300  					"repo1": {
   301  						github.Hook{
   302  							ID:     0,
   303  							Name:   "hook1",
   304  							Active: true,
   305  							Config: github.HookConfig{
   306  								URL: "http://whatever-hook-url",
   307  							},
   308  						},
   309  					},
   310  					"repo2": {
   311  						github.Hook{
   312  							ID:     0,
   313  							Name:   "hook2",
   314  							Active: true,
   315  							Config: github.HookConfig{
   316  								URL: "http://whatever-hook-url",
   317  							},
   318  						},
   319  					},
   320  				},
   321  			}
   322  			c := &client{
   323  				currentHMACMap: map[string]github.HMACsForRepo{
   324  					"repo1": {
   325  						github.HMACToken{
   326  							Value: "val1",
   327  						},
   328  					},
   329  					"repo2": {
   330  						github.HMACToken{
   331  							Value: "val2",
   332  						},
   333  					},
   334  				},
   335  				githubHookClient: fakeClient,
   336  				options:          options{hookUrl: "http://whatever-hook-url"},
   337  			}
   338  			if err := c.handleRemovedRepo(tc.toRemove); err != nil {
   339  				t.Errorf("unexpected error: %v", err)
   340  			}
   341  			if !reflect.DeepEqual(tc.expectedHMACs, c.currentHMACMap) {
   342  				t.Errorf("hmacs %#v != expected %#v", c.currentHMACMap, tc.expectedHMACs)
   343  			}
   344  			if !reflect.DeepEqual(tc.expectedHooks, fakeClient.OrgHooks) {
   345  				t.Errorf("hooks %#v != expected %#v", fakeClient.OrgHooks, tc.expectedHooks)
   346  			}
   347  		})
   348  	}
   349  }
   350  
   351  func TestHandleAddedRepo(t *testing.T) {
   352  	globalToken := []github.HMACToken{
   353  		{
   354  			Value:     "global-rand-val1",
   355  			CreatedAt: time.Now().Add(-time.Hour),
   356  		},
   357  	}
   358  
   359  	cases := []struct {
   360  		name                         string
   361  		toAdd                        map[string]config.ManagedWebhookInfo
   362  		currentHMACs                 map[string]github.HMACsForRepo
   363  		currentHMACMapForBatchUpdate map[string]string
   364  		expectedHMACsSize            map[string]int
   365  	}{
   366  		{
   367  			name: "add repos when global token does not exist",
   368  			toAdd: map[string]config.ManagedWebhookInfo{
   369  				"repo1": {TokenCreatedAfter: time.Now()},
   370  				"repo2": {TokenCreatedAfter: time.Now()},
   371  			},
   372  			currentHMACs:                 map[string]github.HMACsForRepo{},
   373  			currentHMACMapForBatchUpdate: map[string]string{"whatever-repo": "whatever-token"},
   374  			expectedHMACsSize:            map[string]int{"repo1": 1, "repo2": 1},
   375  		},
   376  		{
   377  			name: "add repos when global token exists",
   378  			toAdd: map[string]config.ManagedWebhookInfo{
   379  				"repo1": {TokenCreatedAfter: time.Now()},
   380  				"repo2": {TokenCreatedAfter: time.Now()},
   381  			},
   382  			currentHMACs: map[string]github.HMACsForRepo{
   383  				"*": globalToken,
   384  			},
   385  			currentHMACMapForBatchUpdate: map[string]string{"whatever-repo": "whatever-token"},
   386  			expectedHMACsSize:            map[string]int{"repo1": 2, "repo2": 2},
   387  		},
   388  	}
   389  
   390  	for _, tc := range cases {
   391  		t.Run(tc.name, func(t *testing.T) {
   392  			c := &client{
   393  				currentHMACMap:        tc.currentHMACs,
   394  				hmacMapForBatchUpdate: tc.currentHMACMapForBatchUpdate,
   395  			}
   396  			if err := c.handleAddedRepo(tc.toAdd); err != nil {
   397  				t.Errorf("unexpected error: %v", err)
   398  			}
   399  			for repo, size := range tc.expectedHMACsSize {
   400  				if _, ok := c.currentHMACMap[repo]; !ok {
   401  					t.Errorf("repo %q does not exist in the updated HMAC map", repo)
   402  				} else if len(c.currentHMACMap[repo]) != size {
   403  					t.Errorf("repo %q hmac size %d != expected %d", repo, len(c.currentHMACMap[repo]), size)
   404  				}
   405  			}
   406  			for repo := range tc.toAdd {
   407  				if _, ok := c.hmacMapForBatchUpdate[repo]; !ok {
   408  					t.Errorf("repo %q is expected to be added to the batch update map, but not", repo)
   409  				}
   410  			}
   411  		})
   412  	}
   413  }
   414  
   415  func TestHandleRotatedRepo(t *testing.T) {
   416  	pastTime, _ := time.Parse(time.RFC3339Nano, "2020-01-01T00:00:50Z")
   417  
   418  	globalToken := []github.HMACToken{
   419  		{
   420  			Value:     "global-rand-val1",
   421  			CreatedAt: pastTime,
   422  		},
   423  	}
   424  	commonTokens := []github.HMACToken{
   425  		{
   426  			Value:     "rand-val1",
   427  			CreatedAt: pastTime,
   428  		},
   429  		{
   430  			Value:     "rand-val2",
   431  			CreatedAt: pastTime,
   432  		},
   433  	}
   434  
   435  	cases := []struct {
   436  		name                         string
   437  		toRotate                     map[string]config.ManagedWebhookInfo
   438  		currentHMACs                 map[string]github.HMACsForRepo
   439  		currentHMACMapForBatchUpdate map[string]string
   440  		expectedHMACsSize            map[string]int
   441  		expectedReposForBatchUpdate  []string
   442  		expectedHMACMapForRecovery   map[string]github.HMACsForRepo
   443  	}{
   444  		{
   445  			name: "test a repo that needs its hmac to be rotated, and global token does not exist",
   446  			toRotate: map[string]config.ManagedWebhookInfo{
   447  				"repo1": {TokenCreatedAfter: time.Now()},
   448  				"repo2": {TokenCreatedAfter: time.Now()},
   449  			},
   450  			currentHMACs: map[string]github.HMACsForRepo{
   451  				"repo1": commonTokens,
   452  				"repo2": commonTokens,
   453  			},
   454  			currentHMACMapForBatchUpdate: map[string]string{"whatever-repo": "whatever-token"},
   455  			expectedHMACsSize:            map[string]int{"repo1": 3, "repo2": 3},
   456  			expectedReposForBatchUpdate:  []string{"repo1", "repo2"},
   457  			expectedHMACMapForRecovery: map[string]github.HMACsForRepo{
   458  				"repo1": commonTokens,
   459  				"repo2": commonTokens,
   460  			},
   461  		},
   462  		{
   463  			name: "test a repo that needs its hmac to be rotated, and global token exists",
   464  			toRotate: map[string]config.ManagedWebhookInfo{
   465  				"repo1": {TokenCreatedAfter: time.Now()},
   466  				"repo2": {TokenCreatedAfter: time.Now()},
   467  			},
   468  			currentHMACs: map[string]github.HMACsForRepo{
   469  				"*":     globalToken,
   470  				"repo1": commonTokens,
   471  				"repo2": commonTokens,
   472  			},
   473  			currentHMACMapForBatchUpdate: map[string]string{"whatever-repo": "whatever-token"},
   474  			expectedHMACsSize:            map[string]int{"repo1": 3, "repo2": 3},
   475  			expectedReposForBatchUpdate:  []string{"repo1", "repo2"},
   476  			expectedHMACMapForRecovery: map[string]github.HMACsForRepo{
   477  				"repo1": commonTokens,
   478  				"repo2": commonTokens,
   479  			},
   480  		},
   481  		{
   482  			name: "test a repo that does not need its hmac to be rotated",
   483  			toRotate: map[string]config.ManagedWebhookInfo{
   484  				"repo1": {TokenCreatedAfter: pastTime},
   485  				"repo2": {TokenCreatedAfter: pastTime},
   486  			},
   487  			currentHMACs: map[string]github.HMACsForRepo{
   488  				"repo1": []github.HMACToken{
   489  					{
   490  						Value:     "rand-val1",
   491  						CreatedAt: pastTime.Add(-1 * time.Hour),
   492  					},
   493  				},
   494  				"repo2": []github.HMACToken{
   495  					{
   496  						Value:     "rand-val2",
   497  						CreatedAt: pastTime.Add(1 * time.Hour),
   498  					},
   499  				},
   500  			},
   501  			currentHMACMapForBatchUpdate: map[string]string{"whatever-repo": "whatever-token"},
   502  			expectedHMACsSize:            map[string]int{"repo1": 2, "repo2": 1},
   503  			expectedReposForBatchUpdate:  []string{"repo1"},
   504  			expectedHMACMapForRecovery: map[string]github.HMACsForRepo{
   505  				"repo1": []github.HMACToken{
   506  					{
   507  						Value:     "rand-val1",
   508  						CreatedAt: pastTime.Add(-1 * time.Hour),
   509  					},
   510  				},
   511  			},
   512  		},
   513  	}
   514  
   515  	for _, tc := range cases {
   516  		t.Run(tc.name, func(t *testing.T) {
   517  			c := &client{
   518  				currentHMACMap:        tc.currentHMACs,
   519  				hmacMapForBatchUpdate: tc.currentHMACMapForBatchUpdate,
   520  				hmacMapForRecovery:    map[string]github.HMACsForRepo{},
   521  			}
   522  			if err := c.handledRotatedRepo(tc.toRotate); err != nil {
   523  				t.Errorf("unexpected error: %v", err)
   524  			}
   525  			for repo, size := range tc.expectedHMACsSize {
   526  				if _, ok := c.currentHMACMap[repo]; !ok {
   527  					t.Errorf("repo %q does not exist in the updated HMAC map", repo)
   528  				} else if len(c.currentHMACMap[repo]) != size {
   529  					t.Errorf("repo %q hmac size %d != expected %d", repo, len(c.currentHMACMap[repo]), size)
   530  				}
   531  			}
   532  			for _, repo := range tc.expectedReposForBatchUpdate {
   533  				if _, ok := c.hmacMapForBatchUpdate[repo]; !ok {
   534  					t.Errorf("repo %q is expected to be added to the batch update map, but not", repo)
   535  				}
   536  			}
   537  			if !reflect.DeepEqual(tc.expectedHMACMapForRecovery, c.hmacMapForRecovery) {
   538  				t.Errorf("The hmacMapForRecovery %#v != expected %#v", c.hmacMapForRecovery, tc.expectedHMACMapForRecovery)
   539  			}
   540  		})
   541  	}
   542  }
   543  
   544  func TestBatchOnboardNewTokenForRepos(t *testing.T) {
   545  	name := "web"
   546  	contentType := "json"
   547  	secretBeforeUpdate := "whatever-secret-before-update"
   548  	secretAfterUpdate := "whatever-secret-after-update"
   549  	hookBeforeUpdate := github.Hook{
   550  		ID:     0,
   551  		Name:   name,
   552  		Active: true,
   553  		Events: github.AllHookEvents,
   554  		Config: github.HookConfig{
   555  			URL:         "http://whatever-hook-url",
   556  			ContentType: &contentType,
   557  			Secret:      &secretBeforeUpdate,
   558  		},
   559  	}
   560  	hookAfterUpdate := github.Hook{
   561  		ID:     0,
   562  		Name:   name,
   563  		Active: true,
   564  		Events: github.AllHookEvents,
   565  		Config: github.HookConfig{
   566  			URL:         "http://whatever-hook-url",
   567  			ContentType: &contentType,
   568  			Secret:      &secretAfterUpdate,
   569  		},
   570  	}
   571  
   572  	cases := []struct {
   573  		name                  string
   574  		hmacMapForBatchUpdate map[string]string
   575  		currentOrgHooks       map[string][]github.Hook
   576  		currentRepoHooks      map[string][]github.Hook
   577  		expectedOrgHooks      map[string][]github.Hook
   578  		expectedRepoHooks     map[string][]github.Hook
   579  	}{
   580  		{
   581  			name:                  "add hook for one repo",
   582  			hmacMapForBatchUpdate: map[string]string{"org/repo1": secretBeforeUpdate},
   583  			currentRepoHooks:      map[string][]github.Hook{},
   584  			expectedRepoHooks: map[string][]github.Hook{
   585  				"org/repo1": {hookBeforeUpdate},
   586  			},
   587  		},
   588  		{
   589  			name:                  "add hook for one org",
   590  			hmacMapForBatchUpdate: map[string]string{"org1": secretBeforeUpdate},
   591  			currentOrgHooks:       map[string][]github.Hook{},
   592  			expectedOrgHooks: map[string][]github.Hook{
   593  				"org1": {hookBeforeUpdate},
   594  			},
   595  		},
   596  		{
   597  			name:                  "update hook for one org",
   598  			hmacMapForBatchUpdate: map[string]string{"org1": secretAfterUpdate},
   599  			currentOrgHooks: map[string][]github.Hook{
   600  				"org1": {hookBeforeUpdate},
   601  			},
   602  			expectedOrgHooks: map[string][]github.Hook{
   603  				"org1": {hookAfterUpdate},
   604  			},
   605  		},
   606  		{
   607  			name:                  "update hook for one repo",
   608  			hmacMapForBatchUpdate: map[string]string{"org/repo1": secretAfterUpdate},
   609  			currentRepoHooks: map[string][]github.Hook{
   610  				"org/repo1": {hookBeforeUpdate},
   611  			},
   612  			expectedRepoHooks: map[string][]github.Hook{
   613  				"org/repo1": {hookAfterUpdate},
   614  			},
   615  		},
   616  		{
   617  			name:                  "add hook for one org, and update hook for one repo",
   618  			hmacMapForBatchUpdate: map[string]string{"org1": secretAfterUpdate, "org2/repo": secretAfterUpdate},
   619  			currentOrgHooks:       map[string][]github.Hook{},
   620  			expectedOrgHooks: map[string][]github.Hook{
   621  				"org1": {hookAfterUpdate},
   622  			},
   623  			currentRepoHooks: map[string][]github.Hook{
   624  				"org2/repo": {hookBeforeUpdate},
   625  			},
   626  			expectedRepoHooks: map[string][]github.Hook{
   627  				"org2/repo": {hookAfterUpdate},
   628  			},
   629  		},
   630  	}
   631  
   632  	for _, tc := range cases {
   633  		t.Run(tc.name, func(t *testing.T) {
   634  			fakeclient := &fakeghhook.FakeClient{
   635  				OrgHooks:  tc.currentOrgHooks,
   636  				RepoHooks: tc.currentRepoHooks,
   637  			}
   638  			c := &client{
   639  				githubHookClient:      fakeclient,
   640  				hmacMapForBatchUpdate: tc.hmacMapForBatchUpdate,
   641  				options:               options{hookUrl: "http://whatever-hook-url"},
   642  			}
   643  			if err := c.batchOnboardNewTokenForRepos(); err != nil {
   644  				t.Errorf("unexpected error: %v", err)
   645  			}
   646  			if !reflect.DeepEqual(fakeclient.OrgHooks, tc.expectedOrgHooks) {
   647  				t.Errorf("org hooks %#v != expected %#v", fakeclient.OrgHooks, tc.expectedOrgHooks)
   648  			}
   649  			if !reflect.DeepEqual(fakeclient.RepoHooks, tc.expectedRepoHooks) {
   650  				t.Errorf("repo hooks %#v != expected %#v", fakeclient.RepoHooks, tc.expectedRepoHooks)
   651  			}
   652  		})
   653  	}
   654  }
   655  
   656  func TestHandleInvitation(t *testing.T) {
   657  	tests := []struct {
   658  		name          string
   659  		urivs         []github.UserRepoInvitation
   660  		uoivs         []github.UserOrgInvitation
   661  		newHMACConfig config.ManagedWebhooks
   662  		wantUrivs     []github.UserRepoInvitation
   663  		wantUoivs     []github.UserOrgInvitation
   664  		wantErr       error
   665  	}{
   666  		{
   667  			name: "accept repo invitation",
   668  			urivs: []github.UserRepoInvitation{
   669  				{
   670  					Repository: &github.Repo{
   671  						FullName: "org1/repo1",
   672  					},
   673  					Permission: "admin",
   674  				},
   675  			},
   676  			newHMACConfig: config.ManagedWebhooks{
   677  				AutoAcceptInvitation: true,
   678  				OrgRepoConfig: map[string]config.ManagedWebhookInfo{
   679  					"org1/repo1": {},
   680  				},
   681  			},
   682  			wantUrivs: []github.UserRepoInvitation{},
   683  		},
   684  		{
   685  			name: "accept org invitation",
   686  			uoivs: []github.UserOrgInvitation{
   687  				{
   688  					Org: github.UserOrganization{
   689  						Login: "org1",
   690  					},
   691  					Role: "admin",
   692  				},
   693  			},
   694  			newHMACConfig: config.ManagedWebhooks{
   695  				AutoAcceptInvitation: true,
   696  				OrgRepoConfig: map[string]config.ManagedWebhookInfo{
   697  					"org1": {},
   698  				},
   699  			},
   700  			wantUoivs: []github.UserOrgInvitation{},
   701  		},
   702  		{
   703  			name: "accept org invitation with single repo webhook",
   704  			uoivs: []github.UserOrgInvitation{
   705  				{
   706  					Org: github.UserOrganization{
   707  						Login: "org1",
   708  					},
   709  					Role: "admin",
   710  				},
   711  			},
   712  			newHMACConfig: config.ManagedWebhooks{
   713  				AutoAcceptInvitation: true,
   714  				OrgRepoConfig: map[string]config.ManagedWebhookInfo{
   715  					"org1/repo1": {},
   716  				},
   717  			},
   718  			wantUoivs: []github.UserOrgInvitation{},
   719  		},
   720  		{
   721  			name: "dont accept repo invitation with org webhook",
   722  			urivs: []github.UserRepoInvitation{
   723  				{
   724  					Repository: &github.Repo{
   725  						FullName: "org1/repo1",
   726  					},
   727  					Permission: "admin",
   728  				},
   729  			},
   730  			newHMACConfig: config.ManagedWebhooks{
   731  				AutoAcceptInvitation: true,
   732  				OrgRepoConfig: map[string]config.ManagedWebhookInfo{
   733  					"org1": {},
   734  				},
   735  			},
   736  			wantUrivs: []github.UserRepoInvitation{
   737  				{
   738  					Repository: &github.Repo{
   739  						FullName: "org1/repo1",
   740  					},
   741  					Permission: "admin",
   742  				},
   743  			},
   744  		},
   745  		{
   746  			name: "dont accept invitation when opt out",
   747  			urivs: []github.UserRepoInvitation{
   748  				{
   749  					Repository: &github.Repo{
   750  						FullName: "org1/repo1",
   751  					},
   752  					Permission: "admin",
   753  				},
   754  			},
   755  			uoivs: []github.UserOrgInvitation{
   756  				{
   757  					Org: github.UserOrganization{
   758  						Login: "org2",
   759  					},
   760  					Role: "admin",
   761  				},
   762  			},
   763  			newHMACConfig: config.ManagedWebhooks{
   764  				AutoAcceptInvitation: false,
   765  				OrgRepoConfig: map[string]config.ManagedWebhookInfo{
   766  					"org2":       {},
   767  					"org1/repo1": {},
   768  				},
   769  			},
   770  			wantUrivs: []github.UserRepoInvitation{
   771  				{
   772  					Repository: &github.Repo{
   773  						FullName: "org1/repo1",
   774  					},
   775  					Permission: "admin",
   776  				},
   777  			},
   778  			wantUoivs: []github.UserOrgInvitation{
   779  				{
   780  					Org: github.UserOrganization{
   781  						Login: "org2",
   782  					},
   783  					Role: "admin",
   784  				},
   785  			},
   786  		},
   787  	}
   788  
   789  	for _, tc := range tests {
   790  		t.Run(tc.name, func(t *testing.T) {
   791  			fgc := fakeghhook.FakeClient{
   792  				UserRepoInvitations: tc.urivs,
   793  				UserOrgInvitations:  tc.uoivs,
   794  			}
   795  			c := client{
   796  				newHMACConfig:    tc.newHMACConfig,
   797  				githubHookClient: &fgc,
   798  			}
   799  
   800  			if wantErr, gotErr := tc.wantErr, c.handleInvitation(); (wantErr == nil && gotErr != nil) || (wantErr != nil && gotErr == nil) ||
   801  				(wantErr != nil && gotErr != nil && !strings.Contains(gotErr.Error(), wantErr.Error())) {
   802  				t.Fatalf("Error mismatch. Want: %v, got: %v", wantErr, gotErr)
   803  			}
   804  			if diff := cmp.Diff(tc.wantUrivs, fgc.UserRepoInvitations); diff != "" {
   805  				t.Fatalf("User repo invitation mismatch. Want(-), got(+): %s", diff)
   806  			}
   807  			if diff := cmp.Diff(tc.wantUoivs, fgc.UserOrgInvitations); diff != "" {
   808  				t.Fatalf("User org invitation mismatch. Want(-), got(+): %s", diff)
   809  			}
   810  		})
   811  	}
   812  }