github.com/jenkins-x/test-infra@v0.0.7/prow/config/branch_protection_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 config
    18  
    19  import (
    20  	"reflect"
    21  	"sort"
    22  	"testing"
    23  
    24  	"k8s.io/apimachinery/pkg/util/diff"
    25  )
    26  
    27  var (
    28  	y   = true
    29  	n   = false
    30  	yes = &y
    31  	no  = &n
    32  )
    33  
    34  func normalize(policy *Policy) {
    35  	if policy == nil || policy.RequiredStatusChecks == nil {
    36  		return
    37  	}
    38  	sort.Strings(policy.RequiredStatusChecks.Contexts)
    39  }
    40  
    41  func TestSelectBool(t *testing.T) {
    42  	cases := []struct {
    43  		name     string
    44  		parent   *bool
    45  		child    *bool
    46  		expected *bool
    47  	}{
    48  		{
    49  			name: "default is nil",
    50  		},
    51  		{
    52  			name:     "use child if set",
    53  			child:    yes,
    54  			expected: yes,
    55  		},
    56  		{
    57  			name:     "child overrides parent",
    58  			child:    yes,
    59  			parent:   no,
    60  			expected: yes,
    61  		},
    62  		{
    63  			name:     "use parent if child unset",
    64  			parent:   no,
    65  			expected: no,
    66  		},
    67  	}
    68  	for _, tc := range cases {
    69  		t.Run(tc.name, func(t *testing.T) {
    70  			actual := selectBool(tc.parent, tc.child)
    71  			if !reflect.DeepEqual(actual, tc.expected) {
    72  				t.Errorf("actual %v != expected %v", actual, tc.expected)
    73  			}
    74  		})
    75  	}
    76  }
    77  
    78  func TestSelectInt(t *testing.T) {
    79  	one := 1
    80  	two := 2
    81  	cases := []struct {
    82  		name     string
    83  		parent   *int
    84  		child    *int
    85  		expected *int
    86  	}{
    87  		{
    88  			name: "default is nil",
    89  		},
    90  		{
    91  			name:     "use child if set",
    92  			child:    &one,
    93  			expected: &one,
    94  		},
    95  		{
    96  			name:     "child overrides parent",
    97  			child:    &one,
    98  			parent:   &two,
    99  			expected: &one,
   100  		},
   101  		{
   102  			name:     "use parent if child unset",
   103  			parent:   &two,
   104  			expected: &two,
   105  		},
   106  	}
   107  	for _, tc := range cases {
   108  		t.Run(tc.name, func(t *testing.T) {
   109  			actual := selectInt(tc.parent, tc.child)
   110  			if !reflect.DeepEqual(actual, tc.expected) {
   111  				t.Errorf("actual %v != expected %v", actual, tc.expected)
   112  			}
   113  		})
   114  	}
   115  }
   116  
   117  func TestUnionStrings(t *testing.T) {
   118  	cases := []struct {
   119  		name     string
   120  		parent   []string
   121  		child    []string
   122  		expected []string
   123  	}{
   124  		{
   125  			name: "empty list",
   126  		},
   127  		{
   128  			name:     "all parent items",
   129  			parent:   []string{"hi", "there"},
   130  			expected: []string{"hi", "there"},
   131  		},
   132  		{
   133  			name:     "all child items",
   134  			child:    []string{"hi", "there"},
   135  			expected: []string{"hi", "there"},
   136  		},
   137  		{
   138  			name:     "both child and parent items, no duplicates",
   139  			child:    []string{"hi", "world"},
   140  			parent:   []string{"hi", "there"},
   141  			expected: []string{"hi", "there", "world"},
   142  		},
   143  	}
   144  	for _, tc := range cases {
   145  		t.Run(tc.name, func(t *testing.T) {
   146  			actual := unionStrings(tc.parent, tc.child)
   147  			sort.Strings(actual)
   148  			sort.Strings(tc.expected)
   149  			if !reflect.DeepEqual(actual, tc.expected) {
   150  				t.Errorf("actual %v != expected %v", actual, tc.expected)
   151  			}
   152  		})
   153  	}
   154  }
   155  
   156  func TestApply(test *testing.T) {
   157  	t := true
   158  	f := false
   159  	basic := Policy{
   160  		Protect: &t,
   161  	}
   162  	ebasic := Policy{
   163  		Protect: &t,
   164  	}
   165  	cases := []struct {
   166  		name     string
   167  		parent   Policy
   168  		child    Policy
   169  		expected Policy
   170  	}{
   171  		{
   172  			name:     "nil child",
   173  			parent:   basic,
   174  			expected: ebasic,
   175  		},
   176  		{
   177  			name: "merge parent and child",
   178  			parent: Policy{
   179  				Protect: &t,
   180  			},
   181  			child: Policy{
   182  				Admins: &f,
   183  			},
   184  			expected: Policy{
   185  				Protect: &t,
   186  				Admins:  &f,
   187  			},
   188  		},
   189  		{
   190  			name: "child overrides parent",
   191  			parent: Policy{
   192  				Protect: &t,
   193  			},
   194  			child: Policy{
   195  				Protect: &f,
   196  			},
   197  			expected: Policy{
   198  				Protect: &f,
   199  			},
   200  		},
   201  		{
   202  			name: "append strings",
   203  			parent: Policy{
   204  				RequiredStatusChecks: &ContextPolicy{
   205  					Contexts: []string{"hello", "world"},
   206  				},
   207  			},
   208  			child: Policy{
   209  				RequiredStatusChecks: &ContextPolicy{
   210  					Contexts: []string{"world", "of", "thrones"},
   211  				},
   212  			},
   213  			expected: Policy{
   214  				RequiredStatusChecks: &ContextPolicy{
   215  					Contexts: []string{"hello", "of", "thrones", "world"},
   216  				},
   217  			},
   218  		},
   219  		{
   220  			name: "merge struct",
   221  			parent: Policy{
   222  				RequiredStatusChecks: &ContextPolicy{
   223  					Contexts: []string{"hi"},
   224  				},
   225  			},
   226  			child: Policy{
   227  				RequiredStatusChecks: &ContextPolicy{
   228  					Strict: &t,
   229  				},
   230  			},
   231  			expected: Policy{
   232  				RequiredStatusChecks: &ContextPolicy{
   233  					Contexts: []string{"hi"},
   234  					Strict:   &t,
   235  				},
   236  			},
   237  		},
   238  		{
   239  			name: "nil child struct",
   240  			parent: Policy{
   241  				RequiredStatusChecks: &ContextPolicy{
   242  					Strict: &f,
   243  				},
   244  			},
   245  			child: Policy{
   246  				Protect: &t,
   247  			},
   248  			expected: Policy{
   249  				RequiredStatusChecks: &ContextPolicy{
   250  					Strict: &f,
   251  				},
   252  				Protect: &t,
   253  			},
   254  		},
   255  		{
   256  			name: "nil parent struct",
   257  			child: Policy{
   258  				RequiredStatusChecks: &ContextPolicy{
   259  					Strict: &f,
   260  				},
   261  			},
   262  			parent: Policy{
   263  				Protect: &t,
   264  			},
   265  			expected: Policy{
   266  				RequiredStatusChecks: &ContextPolicy{
   267  					Strict: &f,
   268  				},
   269  				Protect: &t,
   270  			},
   271  		},
   272  	}
   273  
   274  	for _, tc := range cases {
   275  		test.Run(tc.name, func(test *testing.T) {
   276  			defer func() {
   277  				if r := recover(); r != nil {
   278  					test.Errorf("unexpected panic: %s", r)
   279  				}
   280  			}()
   281  			actual, err := tc.parent.Apply(tc.child)
   282  			if err != nil {
   283  				test.Fatalf("unexpected error: %v", err)
   284  			}
   285  			normalize(&actual)
   286  			normalize(&tc.expected)
   287  			if !reflect.DeepEqual(actual, tc.expected) {
   288  				test.Errorf("bad merged policy:\n%s", diff.ObjectReflectDiff(tc.expected, actual))
   289  			}
   290  		})
   291  	}
   292  }
   293  
   294  func TestJobRequirements(t *testing.T) {
   295  	cases := []struct {
   296  		name                          string
   297  		config                        []Presubmit
   298  		masterExpected, otherExpected []string
   299  		masterOptional, otherOptional []string
   300  	}{
   301  		{
   302  			name: "basic",
   303  			config: []Presubmit{
   304  				{
   305  					Context:    "always-run",
   306  					AlwaysRun:  true,
   307  					SkipReport: false,
   308  				},
   309  				{
   310  					Context: "run-if-changed",
   311  					RegexpChangeMatcher: RegexpChangeMatcher{
   312  						RunIfChanged: "foo",
   313  					},
   314  					AlwaysRun:  false,
   315  					SkipReport: false,
   316  				},
   317  				{
   318  					Context:    "not-always",
   319  					AlwaysRun:  false,
   320  					SkipReport: false,
   321  				},
   322  				{
   323  					Context:    "skip-report",
   324  					AlwaysRun:  true,
   325  					SkipReport: true,
   326  					Brancher: Brancher{
   327  						SkipBranches: []string{"master"},
   328  					},
   329  				},
   330  				{
   331  					Context:    "optional",
   332  					AlwaysRun:  true,
   333  					SkipReport: false,
   334  					Optional:   true,
   335  				},
   336  			},
   337  			masterExpected: []string{"always-run", "run-if-changed"},
   338  			masterOptional: []string{"optional"},
   339  			otherExpected:  []string{"always-run", "run-if-changed"},
   340  			otherOptional:  []string{"skip-report", "optional"},
   341  		},
   342  		{
   343  			name: "children",
   344  			config: []Presubmit{
   345  				{
   346  					Context:    "always-run",
   347  					AlwaysRun:  true,
   348  					SkipReport: false,
   349  					RunAfterSuccess: []Presubmit{
   350  						{
   351  							Context: "include-me",
   352  						},
   353  					},
   354  				},
   355  				{
   356  					Context: "run-if-changed",
   357  					RegexpChangeMatcher: RegexpChangeMatcher{
   358  						RunIfChanged: "foo",
   359  					},
   360  					SkipReport: true,
   361  					AlwaysRun:  false,
   362  					RunAfterSuccess: []Presubmit{
   363  						{
   364  							Context: "me2",
   365  						},
   366  					},
   367  				},
   368  				{
   369  					Context:    "run-and-skip",
   370  					AlwaysRun:  true,
   371  					SkipReport: true,
   372  					RunAfterSuccess: []Presubmit{
   373  						{
   374  							Context: "also-me-3",
   375  						},
   376  					},
   377  				},
   378  				{
   379  					Context:    "optional",
   380  					AlwaysRun:  false,
   381  					SkipReport: false,
   382  					RunAfterSuccess: []Presubmit{
   383  						{
   384  							Context: "no thanks",
   385  						},
   386  					},
   387  				},
   388  				{
   389  					Context:    "hidden-grandpa",
   390  					AlwaysRun:  true,
   391  					SkipReport: true,
   392  					RunAfterSuccess: []Presubmit{
   393  						{
   394  							Context:   "hidden-parent",
   395  							Optional:  true,
   396  							AlwaysRun: false,
   397  							Brancher: Brancher{
   398  								Branches: []string{"master"},
   399  							},
   400  							RunAfterSuccess: []Presubmit{
   401  								{
   402  									Context: "visible-kid",
   403  									Brancher: Brancher{
   404  										Branches: []string{"master"},
   405  									},
   406  								},
   407  							},
   408  						},
   409  					},
   410  				},
   411  			},
   412  			masterExpected: []string{
   413  				"always-run", "include-me",
   414  				"me2",
   415  				"also-me-3",
   416  				"visible-kid",
   417  			},
   418  			masterOptional: []string{
   419  				"run-if-changed",
   420  				"run-and-skip",
   421  				"hidden-grandpa",
   422  				"hidden-parent"},
   423  			otherExpected: []string{
   424  				"always-run", "include-me",
   425  				"me2",
   426  				"also-me-3",
   427  			},
   428  			otherOptional: []string{
   429  				"run-if-changed",
   430  				"run-and-skip",
   431  				"hidden-grandpa"},
   432  		},
   433  	}
   434  
   435  	for _, tc := range cases {
   436  		if err := SetPresubmitRegexes(tc.config); err != nil {
   437  			t.Fatalf("could not set regexes: %v", err)
   438  		}
   439  		masterActual, masterOptional := jobRequirements(tc.config, "master", false)
   440  		if !reflect.DeepEqual(masterActual, tc.masterExpected) {
   441  			t.Errorf("branch: master - %s: actual %v != expected %v", tc.name, masterActual, tc.masterExpected)
   442  		}
   443  		if !reflect.DeepEqual(masterOptional, tc.masterOptional) {
   444  			t.Errorf("branch: master - optional - %s: actual %v != expected %v", tc.name, masterOptional, tc.masterOptional)
   445  		}
   446  		otherActual, otherOptional := jobRequirements(tc.config, "other", false)
   447  		if !reflect.DeepEqual(masterActual, tc.masterExpected) {
   448  			t.Errorf("branch: other - %s: actual %v != expected %v", tc.name, otherActual, tc.otherExpected)
   449  		}
   450  		if !reflect.DeepEqual(otherOptional, tc.otherOptional) {
   451  			t.Errorf("branch: other - optional - %s: actual %v != expected %v", tc.name, otherOptional, tc.otherOptional)
   452  		}
   453  	}
   454  }
   455  
   456  func TestConfig_GetBranchProtection(t *testing.T) {
   457  	testCases := []struct {
   458  		name              string
   459  		config            Config
   460  		org, repo, branch string
   461  		err               bool
   462  		expected          *Policy
   463  	}{
   464  		{
   465  			name: "unprotected by default",
   466  		},
   467  		{
   468  			name: "undefined org not protected",
   469  			config: Config{
   470  				ProwConfig: ProwConfig{
   471  					BranchProtection: BranchProtection{
   472  						Policy: Policy{
   473  							Protect: yes,
   474  						},
   475  						Orgs: map[string]Org{
   476  							"unknown": {},
   477  						},
   478  					},
   479  				},
   480  			},
   481  		},
   482  		{
   483  			name: "protect via config default",
   484  			config: Config{
   485  				ProwConfig: ProwConfig{
   486  					BranchProtection: BranchProtection{
   487  						Policy: Policy{
   488  							Protect: yes,
   489  						},
   490  						Orgs: map[string]Org{
   491  							"org": {},
   492  						},
   493  					},
   494  				},
   495  			},
   496  			expected: &Policy{Protect: yes},
   497  		},
   498  		{
   499  			name: "protect via org default",
   500  			config: Config{
   501  				ProwConfig: ProwConfig{
   502  					BranchProtection: BranchProtection{
   503  						Orgs: map[string]Org{
   504  							"org": {
   505  								Policy: Policy{
   506  									Protect: yes,
   507  								},
   508  							},
   509  						},
   510  					},
   511  				},
   512  			},
   513  			expected: &Policy{Protect: yes},
   514  		},
   515  		{
   516  			name: "protect via repo default",
   517  			config: Config{
   518  				ProwConfig: ProwConfig{
   519  					BranchProtection: BranchProtection{
   520  						Orgs: map[string]Org{
   521  							"org": {
   522  								Repos: map[string]Repo{
   523  									"repo": {
   524  										Policy: Policy{
   525  											Protect: yes,
   526  										},
   527  									},
   528  								},
   529  							},
   530  						},
   531  					},
   532  				},
   533  			},
   534  			expected: &Policy{Protect: yes},
   535  		},
   536  		{
   537  			name: "protect specific branch",
   538  			config: Config{
   539  				ProwConfig: ProwConfig{
   540  					BranchProtection: BranchProtection{
   541  						Orgs: map[string]Org{
   542  							"org": {
   543  								Repos: map[string]Repo{
   544  									"repo": {
   545  										Branches: map[string]Branch{
   546  											"branch": {
   547  												Policy: Policy{
   548  													Protect: yes,
   549  												},
   550  											},
   551  										},
   552  									},
   553  								},
   554  							},
   555  						},
   556  					},
   557  				},
   558  			},
   559  			expected: &Policy{Protect: yes},
   560  		},
   561  		{
   562  			name: "ignore other org settings",
   563  			config: Config{
   564  				ProwConfig: ProwConfig{
   565  					BranchProtection: BranchProtection{
   566  						Policy: Policy{
   567  							Protect: no,
   568  						},
   569  						Orgs: map[string]Org{
   570  							"other": {
   571  								Policy: Policy{Protect: yes},
   572  							},
   573  							"org": {},
   574  						},
   575  					},
   576  				},
   577  			},
   578  			expected: &Policy{Protect: no},
   579  		},
   580  		{
   581  			name: "defined branches must make a protection decision",
   582  			config: Config{
   583  				ProwConfig: ProwConfig{
   584  					BranchProtection: BranchProtection{
   585  						Orgs: map[string]Org{
   586  							"org": {
   587  								Repos: map[string]Repo{
   588  									"repo": {
   589  										Branches: map[string]Branch{
   590  											"branch": {},
   591  										},
   592  									},
   593  								},
   594  							},
   595  						},
   596  					},
   597  				},
   598  			},
   599  			err: true,
   600  		},
   601  		{
   602  			name: "pushers require protection",
   603  			config: Config{
   604  				ProwConfig: ProwConfig{
   605  					BranchProtection: BranchProtection{
   606  						Policy: Policy{
   607  							Protect: no,
   608  							Restrictions: &Restrictions{
   609  								Teams: []string{"oncall"},
   610  							},
   611  						},
   612  						Orgs: map[string]Org{
   613  							"org": {},
   614  						},
   615  					},
   616  				},
   617  			},
   618  			err: true,
   619  		},
   620  		{
   621  			name: "required contexts require protection",
   622  			config: Config{
   623  				ProwConfig: ProwConfig{
   624  					BranchProtection: BranchProtection{
   625  						Policy: Policy{
   626  							Protect: no,
   627  							RequiredStatusChecks: &ContextPolicy{
   628  								Contexts: []string{"test-foo"},
   629  							},
   630  						},
   631  						Orgs: map[string]Org{
   632  							"org": {},
   633  						},
   634  					},
   635  				},
   636  			},
   637  			err: true,
   638  		},
   639  		{
   640  			name: "child policy with defined parent can disable protection",
   641  			config: Config{
   642  				ProwConfig: ProwConfig{
   643  					BranchProtection: BranchProtection{
   644  						AllowDisabledPolicies: true,
   645  						Policy: Policy{
   646  							Protect: yes,
   647  							Restrictions: &Restrictions{
   648  								Teams: []string{"oncall"},
   649  							},
   650  						},
   651  						Orgs: map[string]Org{
   652  							"org": {
   653  								Policy: Policy{
   654  									Protect: no,
   655  								},
   656  							},
   657  						},
   658  					},
   659  				},
   660  			},
   661  			expected: &Policy{
   662  				Protect: no,
   663  			},
   664  		},
   665  		{
   666  			name: "Make required presubmits required",
   667  			config: Config{
   668  				ProwConfig: ProwConfig{
   669  					BranchProtection: BranchProtection{
   670  						Policy: Policy{
   671  							Protect: yes,
   672  							RequiredStatusChecks: &ContextPolicy{
   673  								Contexts: []string{"cla"},
   674  							},
   675  						},
   676  						Orgs: map[string]Org{
   677  							"org": {},
   678  						},
   679  					},
   680  				},
   681  				JobConfig: JobConfig{
   682  					Presubmits: map[string][]Presubmit{
   683  						"org/repo": {
   684  							{
   685  								JobBase: JobBase{
   686  									Name: "required presubmit",
   687  								},
   688  								Context:   "required presubmit",
   689  								AlwaysRun: true,
   690  							},
   691  						},
   692  					},
   693  				},
   694  			},
   695  			expected: &Policy{
   696  				Protect: yes,
   697  				RequiredStatusChecks: &ContextPolicy{
   698  					Contexts: []string{"required presubmit", "cla"},
   699  				},
   700  			},
   701  		},
   702  		{
   703  			name: "ProtectTested opts into protection",
   704  			config: Config{
   705  				ProwConfig: ProwConfig{
   706  					BranchProtection: BranchProtection{
   707  						ProtectTested: true,
   708  						Orgs: map[string]Org{
   709  							"org": {},
   710  						},
   711  					},
   712  				},
   713  				JobConfig: JobConfig{
   714  					Presubmits: map[string][]Presubmit{
   715  						"org/repo": {
   716  							{
   717  								JobBase: JobBase{
   718  									Name: "required presubmit",
   719  								},
   720  								Context:   "required presubmit",
   721  								AlwaysRun: true,
   722  							},
   723  						},
   724  					},
   725  				},
   726  			},
   727  			expected: &Policy{
   728  				Protect: yes,
   729  				RequiredStatusChecks: &ContextPolicy{
   730  					Contexts: []string{"required presubmit"},
   731  				},
   732  			},
   733  		},
   734  		{
   735  			name: "required presubmits require protection",
   736  			config: Config{
   737  				ProwConfig: ProwConfig{
   738  					BranchProtection: BranchProtection{
   739  						Policy: Policy{
   740  							Protect: no,
   741  						},
   742  						Orgs: map[string]Org{
   743  							"org": {},
   744  						},
   745  					},
   746  				},
   747  				JobConfig: JobConfig{
   748  					Presubmits: map[string][]Presubmit{
   749  						"org/repo": {
   750  							{
   751  								JobBase: JobBase{
   752  									Name: "required presubmit",
   753  								},
   754  								Context:   "required presubmit",
   755  								AlwaysRun: true,
   756  							},
   757  						},
   758  					},
   759  				},
   760  			},
   761  			err: true,
   762  		},
   763  		{
   764  			name: "Optional presubmits do not force protection",
   765  			config: Config{
   766  				ProwConfig: ProwConfig{
   767  					BranchProtection: BranchProtection{
   768  						ProtectTested: true,
   769  						Orgs: map[string]Org{
   770  							"org": {},
   771  						},
   772  					},
   773  				},
   774  				JobConfig: JobConfig{
   775  					Presubmits: map[string][]Presubmit{
   776  						"org/repo": {
   777  							{
   778  								JobBase: JobBase{
   779  									Name: "optional presubmit",
   780  								},
   781  								Context:   "optional presubmit",
   782  								AlwaysRun: true,
   783  								Optional:  true,
   784  							},
   785  						},
   786  					},
   787  				},
   788  			},
   789  		},
   790  		{
   791  			name: "Explicit configuration takes precedence over ProtectTested",
   792  			config: Config{
   793  				ProwConfig: ProwConfig{
   794  					BranchProtection: BranchProtection{
   795  						ProtectTested: true,
   796  						Orgs: map[string]Org{
   797  							"org": {
   798  								Policy: Policy{
   799  									Protect: yes,
   800  								},
   801  							},
   802  						},
   803  					},
   804  				},
   805  				JobConfig: JobConfig{
   806  					Presubmits: map[string][]Presubmit{
   807  						"org/repo": {
   808  							{
   809  								JobBase: JobBase{
   810  									Name: "optional presubmit",
   811  								},
   812  								Context:   "optional presubmit",
   813  								AlwaysRun: true,
   814  								Optional:  true,
   815  							},
   816  						},
   817  					},
   818  				},
   819  			},
   820  			expected: &Policy{Protect: yes},
   821  		},
   822  	}
   823  
   824  	for _, tc := range testCases {
   825  		t.Run(tc.name, func(t *testing.T) {
   826  			actual, err := tc.config.GetBranchProtection("org", "repo", "branch")
   827  			switch {
   828  			case err != nil:
   829  				if !tc.err {
   830  					t.Errorf("unexpected error: %v", err)
   831  				}
   832  			case err == nil && tc.err:
   833  				t.Errorf("failed to receive an error")
   834  			default:
   835  				normalize(actual)
   836  				normalize(tc.expected)
   837  				if !reflect.DeepEqual(actual, tc.expected) {
   838  					t.Errorf("actual %+v != expected %+v", actual, tc.expected)
   839  				}
   840  			}
   841  		})
   842  	}
   843  }