sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/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  	"github.com/google/go-cmp/cmp"
    25  	"k8s.io/apimachinery/pkg/util/diff"
    26  	utilpointer "k8s.io/utils/pointer"
    27  )
    28  
    29  var (
    30  	y   = true
    31  	n   = false
    32  	yes = &y
    33  	no  = &n
    34  )
    35  
    36  func normalize(policy *Policy) {
    37  	if policy == nil || policy.RequiredStatusChecks == nil {
    38  		return
    39  	}
    40  	sort.Strings(policy.RequiredStatusChecks.Contexts)
    41  	sort.Strings(policy.Exclude)
    42  }
    43  
    44  func TestSelectBool(t *testing.T) {
    45  	cases := []struct {
    46  		name     string
    47  		parent   *bool
    48  		child    *bool
    49  		expected *bool
    50  	}{
    51  		{
    52  			name: "default is nil",
    53  		},
    54  		{
    55  			name:     "use child if set",
    56  			child:    yes,
    57  			expected: yes,
    58  		},
    59  		{
    60  			name:     "child overrides parent",
    61  			child:    yes,
    62  			parent:   no,
    63  			expected: yes,
    64  		},
    65  		{
    66  			name:     "use parent if child unset",
    67  			parent:   no,
    68  			expected: no,
    69  		},
    70  	}
    71  	for _, tc := range cases {
    72  		t.Run(tc.name, func(t *testing.T) {
    73  			actual := selectBool(tc.parent, tc.child)
    74  			if !reflect.DeepEqual(actual, tc.expected) {
    75  				t.Errorf("actual %v != expected %v", actual, tc.expected)
    76  			}
    77  		})
    78  	}
    79  }
    80  
    81  func TestSelectInt(t *testing.T) {
    82  	one := 1
    83  	two := 2
    84  	cases := []struct {
    85  		name     string
    86  		parent   *int
    87  		child    *int
    88  		expected *int
    89  	}{
    90  		{
    91  			name: "default is nil",
    92  		},
    93  		{
    94  			name:     "use child if set",
    95  			child:    &one,
    96  			expected: &one,
    97  		},
    98  		{
    99  			name:     "child overrides parent",
   100  			child:    &one,
   101  			parent:   &two,
   102  			expected: &one,
   103  		},
   104  		{
   105  			name:     "use parent if child unset",
   106  			parent:   &two,
   107  			expected: &two,
   108  		},
   109  	}
   110  	for _, tc := range cases {
   111  		t.Run(tc.name, func(t *testing.T) {
   112  			actual := selectInt(tc.parent, tc.child)
   113  			if !reflect.DeepEqual(actual, tc.expected) {
   114  				t.Errorf("actual %v != expected %v", actual, tc.expected)
   115  			}
   116  		})
   117  	}
   118  }
   119  
   120  func TestUnionStrings(t *testing.T) {
   121  	cases := []struct {
   122  		name     string
   123  		parent   []string
   124  		child    []string
   125  		expected []string
   126  	}{
   127  		{
   128  			name: "empty list",
   129  		},
   130  		{
   131  			name:     "all parent items",
   132  			parent:   []string{"hi", "there"},
   133  			expected: []string{"hi", "there"},
   134  		},
   135  		{
   136  			name:     "all child items",
   137  			child:    []string{"hi", "there"},
   138  			expected: []string{"hi", "there"},
   139  		},
   140  		{
   141  			name:     "both child and parent items, no duplicates",
   142  			child:    []string{"hi", "world"},
   143  			parent:   []string{"hi", "there"},
   144  			expected: []string{"hi", "there", "world"},
   145  		},
   146  	}
   147  	for _, tc := range cases {
   148  		t.Run(tc.name, func(t *testing.T) {
   149  			actual := unionStrings(tc.parent, tc.child)
   150  			sort.Strings(actual)
   151  			sort.Strings(tc.expected)
   152  			if !reflect.DeepEqual(actual, tc.expected) {
   153  				t.Errorf("actual %v != expected %v", actual, tc.expected)
   154  			}
   155  		})
   156  	}
   157  }
   158  
   159  func TestApply(test *testing.T) {
   160  	t := true
   161  	f := false
   162  	basic := Policy{
   163  		Protect: &t,
   164  	}
   165  	ebasic := Policy{
   166  		Protect: &t,
   167  	}
   168  	cases := []struct {
   169  		name     string
   170  		parent   Policy
   171  		child    Policy
   172  		expected Policy
   173  	}{
   174  		{
   175  			name:     "nil child",
   176  			parent:   basic,
   177  			expected: ebasic,
   178  		},
   179  		{
   180  			name: "merge parent and child",
   181  			parent: Policy{
   182  				Protect: &t,
   183  			},
   184  			child: Policy{
   185  				Admins:                &f,
   186  				RequiredLinearHistory: &t,
   187  				AllowForcePushes:      &t,
   188  				AllowDeletions:        &t,
   189  			},
   190  			expected: Policy{
   191  				Protect:               &t,
   192  				Admins:                &f,
   193  				RequiredLinearHistory: &t,
   194  				AllowForcePushes:      &t,
   195  				AllowDeletions:        &t,
   196  			},
   197  		},
   198  		{
   199  			name: "child overrides parent",
   200  			parent: Policy{
   201  				Protect: &t,
   202  			},
   203  			child: Policy{
   204  				Protect: &f,
   205  			},
   206  			expected: Policy{
   207  				Protect: &f,
   208  			},
   209  		},
   210  		{
   211  			name: "append strings",
   212  			parent: Policy{
   213  				RequiredStatusChecks: &ContextPolicy{
   214  					Contexts: []string{"hello", "world"},
   215  				},
   216  			},
   217  			child: Policy{
   218  				RequiredStatusChecks: &ContextPolicy{
   219  					Contexts: []string{"world", "of", "thrones"},
   220  				},
   221  			},
   222  			expected: Policy{
   223  				RequiredStatusChecks: &ContextPolicy{
   224  					Contexts: []string{"hello", "of", "thrones", "world"},
   225  				},
   226  			},
   227  		},
   228  		{
   229  			name: "merge struct",
   230  			parent: Policy{
   231  				RequiredStatusChecks: &ContextPolicy{
   232  					Contexts: []string{"hi"},
   233  				},
   234  			},
   235  			child: Policy{
   236  				RequiredStatusChecks: &ContextPolicy{
   237  					Strict: &t,
   238  				},
   239  			},
   240  			expected: Policy{
   241  				RequiredStatusChecks: &ContextPolicy{
   242  					Contexts: []string{"hi"},
   243  					Strict:   &t,
   244  				},
   245  			},
   246  		},
   247  		{
   248  			name: "nil child struct",
   249  			parent: Policy{
   250  				RequiredStatusChecks: &ContextPolicy{
   251  					Strict: &f,
   252  				},
   253  			},
   254  			child: Policy{
   255  				Protect: &t,
   256  			},
   257  			expected: Policy{
   258  				RequiredStatusChecks: &ContextPolicy{
   259  					Strict: &f,
   260  				},
   261  				Protect: &t,
   262  			},
   263  		},
   264  		{
   265  			name: "nil parent struct",
   266  			child: Policy{
   267  				RequiredStatusChecks: &ContextPolicy{
   268  					Strict: &f,
   269  				},
   270  			},
   271  			parent: Policy{
   272  				Protect: &t,
   273  			},
   274  			expected: Policy{
   275  				RequiredStatusChecks: &ContextPolicy{
   276  					Strict: &f,
   277  				},
   278  				Protect: &t,
   279  			},
   280  		},
   281  		{
   282  			name: "merge exclusion strings",
   283  			child: Policy{
   284  				Exclude: []string{"foo*"},
   285  			},
   286  			parent: Policy{
   287  				Exclude: []string{"bar*"},
   288  			},
   289  			expected: Policy{
   290  				Exclude: []string{"bar*", "foo*"},
   291  			},
   292  		},
   293  		{
   294  			name: "merge inclusion strings",
   295  			child: Policy{
   296  				Include: []string{"foo*"},
   297  			},
   298  			parent: Policy{
   299  				Include: []string{"bar*"},
   300  			},
   301  			expected: Policy{
   302  				Include: []string{"bar*", "foo*"},
   303  			},
   304  		},
   305  	}
   306  
   307  	for _, tc := range cases {
   308  		test.Run(tc.name, func(test *testing.T) {
   309  			defer func() {
   310  				if r := recover(); r != nil {
   311  					test.Errorf("unexpected panic: %s", r)
   312  				}
   313  			}()
   314  			actual := tc.parent.Apply(tc.child)
   315  			normalize(&actual)
   316  			normalize(&tc.expected)
   317  			if !reflect.DeepEqual(actual, tc.expected) {
   318  				test.Errorf("bad merged policy:\n%s", diff.ObjectReflectDiff(tc.expected, actual))
   319  			}
   320  		})
   321  	}
   322  }
   323  
   324  func TestBranchRequirements(t *testing.T) {
   325  	cases := []struct {
   326  		name                            string
   327  		requireManuallyTriggeredJobs    bool
   328  		config                          []Presubmit
   329  		masterExpected, otherExpected   []string
   330  		masterOptional, otherOptional   []string
   331  		masterIfPresent, otherIfPresent []string
   332  	}{
   333  		{
   334  			name: "basic",
   335  			config: []Presubmit{
   336  				{
   337  					AlwaysRun: true,
   338  					Reporter: Reporter{
   339  						Context:    "always-run",
   340  						SkipReport: false,
   341  					},
   342  				},
   343  				{
   344  					RegexpChangeMatcher: RegexpChangeMatcher{
   345  						RunIfChanged: "foo",
   346  					},
   347  					AlwaysRun: false,
   348  					Reporter: Reporter{
   349  						Context:    "run-if-changed",
   350  						SkipReport: false,
   351  					},
   352  				},
   353  				{
   354  					RegexpChangeMatcher: RegexpChangeMatcher{
   355  						SkipIfOnlyChanged: "foo",
   356  					},
   357  					AlwaysRun: false,
   358  					Reporter: Reporter{
   359  						Context:    "skip-if-only-changed",
   360  						SkipReport: false,
   361  					},
   362  				},
   363  				{
   364  					AlwaysRun: false,
   365  					Reporter: Reporter{
   366  						Context:    "not-always",
   367  						SkipReport: false,
   368  					},
   369  				},
   370  				{
   371  					AlwaysRun: true,
   372  					Reporter: Reporter{
   373  						Context:    "skip-report",
   374  						SkipReport: true,
   375  					},
   376  					Brancher: Brancher{
   377  						SkipBranches: []string{"master"},
   378  					},
   379  				},
   380  				{
   381  					AlwaysRun: true,
   382  					Reporter: Reporter{
   383  						Context:    "optional",
   384  						SkipReport: false,
   385  					},
   386  					Optional: true,
   387  				},
   388  			},
   389  			masterExpected:  []string{"always-run"},
   390  			masterIfPresent: []string{"run-if-changed", "skip-if-only-changed", "not-always"},
   391  			masterOptional:  []string{"optional"},
   392  			otherExpected:   []string{"always-run"},
   393  			otherIfPresent:  []string{"run-if-changed", "skip-if-only-changed", "not-always"},
   394  			otherOptional:   []string{"skip-report", "optional"},
   395  		},
   396  		{
   397  			name:                         "require non optional jobs",
   398  			requireManuallyTriggeredJobs: true,
   399  			config: []Presubmit{
   400  				{
   401  					AlwaysRun: false,
   402  					Reporter: Reporter{
   403  						Context:    "always-run false",
   404  						SkipReport: false,
   405  					},
   406  				},
   407  				{
   408  					AlwaysRun: false,
   409  					Reporter: Reporter{
   410  						Context:    "always-run false skip master",
   411  						SkipReport: false,
   412  					},
   413  					Brancher: Brancher{
   414  						SkipBranches: []string{"master"},
   415  					},
   416  				},
   417  				{
   418  					RegexpChangeMatcher: RegexpChangeMatcher{
   419  						RunIfChanged: "foo",
   420  					},
   421  					AlwaysRun: false,
   422  					Reporter: Reporter{
   423  						Context:    "run-if-changed",
   424  						SkipReport: false,
   425  					},
   426  				},
   427  				{
   428  					RegexpChangeMatcher: RegexpChangeMatcher{
   429  						SkipIfOnlyChanged: "foo",
   430  					},
   431  					AlwaysRun: false,
   432  					Reporter: Reporter{
   433  						Context:    "skip-if-only-changed",
   434  						SkipReport: false,
   435  					},
   436  				},
   437  				{
   438  					AlwaysRun: true,
   439  					Reporter: Reporter{
   440  						Context:    "skip-report",
   441  						SkipReport: true,
   442  					},
   443  					Brancher: Brancher{
   444  						SkipBranches: []string{"master"},
   445  					},
   446  				},
   447  				{
   448  					AlwaysRun: true,
   449  					Reporter: Reporter{
   450  						Context:    "optional",
   451  						SkipReport: false,
   452  					},
   453  					Optional: true,
   454  				},
   455  			},
   456  			masterExpected:  []string{"always-run false"},
   457  			masterIfPresent: []string{"run-if-changed", "skip-if-only-changed"},
   458  			masterOptional:  []string{"optional"},
   459  			otherExpected:   []string{"always-run false", "always-run false skip master"},
   460  			otherIfPresent:  []string{"run-if-changed", "skip-if-only-changed"},
   461  			otherOptional:   []string{"skip-report", "optional"},
   462  		},
   463  	}
   464  
   465  	for _, tc := range cases {
   466  		if err := SetPresubmitRegexes(tc.config); err != nil {
   467  			t.Fatalf("could not set regexes: %v", err)
   468  		}
   469  		presubmits := map[string][]Presubmit{
   470  			"o/r": tc.config,
   471  		}
   472  		masterActual, masterActualIfPresent, masterOptional := BranchRequirements("master", presubmits["o/r"], &tc.requireManuallyTriggeredJobs)
   473  		if !reflect.DeepEqual(masterActual, tc.masterExpected) {
   474  			t.Errorf("%s: identified incorrect required contexts on branch master: %s", tc.name, diff.ObjectReflectDiff(masterActual, tc.masterExpected))
   475  		}
   476  		if !reflect.DeepEqual(masterOptional, tc.masterOptional) {
   477  			t.Errorf("%s: identified incorrect optional contexts on branch master: %s", tc.name, diff.ObjectReflectDiff(masterOptional, tc.masterOptional))
   478  		}
   479  		if !reflect.DeepEqual(masterActualIfPresent, tc.masterIfPresent) {
   480  			t.Errorf("%s: identified incorrect if-present contexts on branch master: %s", tc.name, diff.ObjectReflectDiff(masterActualIfPresent, tc.masterIfPresent))
   481  		}
   482  		otherActual, otherActualIfPresent, otherOptional := BranchRequirements("other", presubmits["o/r"], &tc.requireManuallyTriggeredJobs)
   483  		if !reflect.DeepEqual(masterActual, tc.masterExpected) {
   484  			t.Errorf("%s: identified incorrect required contexts on branch other: : %s", tc.name, diff.ObjectReflectDiff(otherActual, tc.otherExpected))
   485  		}
   486  		if !reflect.DeepEqual(otherOptional, tc.otherOptional) {
   487  			t.Errorf("%s: identified incorrect optional contexts on branch other: %s", tc.name, diff.ObjectReflectDiff(otherOptional, tc.otherOptional))
   488  		}
   489  		if !reflect.DeepEqual(otherActualIfPresent, tc.otherIfPresent) {
   490  			t.Errorf("%s: identified incorrect if-present contexts on branch other: %s", tc.name, diff.ObjectReflectDiff(otherActualIfPresent, tc.otherIfPresent))
   491  		}
   492  	}
   493  }
   494  
   495  func TestConfig_GetBranchProtection(t *testing.T) {
   496  	testCases := []struct {
   497  		name     string
   498  		config   Config
   499  		err      bool
   500  		expected *Policy
   501  	}{
   502  		{
   503  			name: "unprotected by default",
   504  		},
   505  		{
   506  			name: "undefined org not protected",
   507  			config: Config{
   508  				ProwConfig: ProwConfig{
   509  					BranchProtection: BranchProtection{
   510  						Policy: Policy{
   511  							Protect: yes,
   512  						},
   513  						Orgs: map[string]Org{
   514  							"unknown": {},
   515  						},
   516  					},
   517  				},
   518  			},
   519  		},
   520  		{
   521  			name: "protect via config default",
   522  			config: Config{
   523  				ProwConfig: ProwConfig{
   524  					BranchProtection: BranchProtection{
   525  						Policy: Policy{
   526  							Protect: yes,
   527  						},
   528  						Orgs: map[string]Org{
   529  							"org": {},
   530  						},
   531  					},
   532  				},
   533  			},
   534  			expected: &Policy{Protect: yes},
   535  		},
   536  		{
   537  			name: "protect via org default",
   538  			config: Config{
   539  				ProwConfig: ProwConfig{
   540  					BranchProtection: BranchProtection{
   541  						Orgs: map[string]Org{
   542  							"org": {
   543  								Policy: Policy{
   544  									Protect: yes,
   545  								},
   546  							},
   547  						},
   548  					},
   549  				},
   550  			},
   551  			expected: &Policy{Protect: yes},
   552  		},
   553  		{
   554  			name: "protect via repo default",
   555  			config: Config{
   556  				ProwConfig: ProwConfig{
   557  					BranchProtection: BranchProtection{
   558  						Orgs: map[string]Org{
   559  							"org": {
   560  								Repos: map[string]Repo{
   561  									"repo": {
   562  										Policy: Policy{
   563  											Protect: yes,
   564  										},
   565  									},
   566  								},
   567  							},
   568  						},
   569  					},
   570  				},
   571  			},
   572  			expected: &Policy{Protect: yes},
   573  		},
   574  		{
   575  			name: "protect specific branch",
   576  			config: Config{
   577  				ProwConfig: ProwConfig{
   578  					BranchProtection: BranchProtection{
   579  						Orgs: map[string]Org{
   580  							"org": {
   581  								Repos: map[string]Repo{
   582  									"repo": {
   583  										Branches: map[string]Branch{
   584  											"branch": {
   585  												Policy: Policy{
   586  													Protect: yes,
   587  												},
   588  											},
   589  										},
   590  									},
   591  								},
   592  							},
   593  						},
   594  					},
   595  				},
   596  			},
   597  			expected: &Policy{Protect: yes},
   598  		},
   599  		{
   600  			name: "ignore other org settings",
   601  			config: Config{
   602  				ProwConfig: ProwConfig{
   603  					BranchProtection: BranchProtection{
   604  						Policy: Policy{
   605  							Protect: no,
   606  						},
   607  						Orgs: map[string]Org{
   608  							"other": {
   609  								Policy: Policy{Protect: yes},
   610  							},
   611  							"org": {},
   612  						},
   613  					},
   614  				},
   615  			},
   616  			expected: &Policy{Protect: no},
   617  		},
   618  		{
   619  			name: "defined branches must make a protection decision",
   620  			config: Config{
   621  				ProwConfig: ProwConfig{
   622  					BranchProtection: BranchProtection{
   623  						Orgs: map[string]Org{
   624  							"org": {
   625  								Repos: map[string]Repo{
   626  									"repo": {
   627  										Branches: map[string]Branch{
   628  											"branch": {},
   629  										},
   630  									},
   631  								},
   632  							},
   633  						},
   634  					},
   635  				},
   636  			},
   637  			err: true,
   638  		},
   639  		{
   640  			name: "pushers require protection",
   641  			config: Config{
   642  				ProwConfig: ProwConfig{
   643  					BranchProtection: BranchProtection{
   644  						Policy: Policy{
   645  							Protect: no,
   646  							Restrictions: &Restrictions{
   647  								Teams: []string{"oncall"},
   648  							},
   649  						},
   650  						Orgs: map[string]Org{
   651  							"org": {},
   652  						},
   653  					},
   654  				},
   655  			},
   656  			err: true,
   657  		},
   658  		{
   659  			name: "required contexts require protection",
   660  			config: Config{
   661  				ProwConfig: ProwConfig{
   662  					BranchProtection: BranchProtection{
   663  						Policy: Policy{
   664  							Protect: no,
   665  							RequiredStatusChecks: &ContextPolicy{
   666  								Contexts: []string{"test-foo"},
   667  							},
   668  						},
   669  						Orgs: map[string]Org{
   670  							"org": {},
   671  						},
   672  					},
   673  				},
   674  			},
   675  			err: true,
   676  		},
   677  		{
   678  			name: "child policy with defined parent can disable protection",
   679  			config: Config{
   680  				ProwConfig: ProwConfig{
   681  					BranchProtection: BranchProtection{
   682  						AllowDisabledPolicies: utilpointer.Bool(true),
   683  						Policy: Policy{
   684  							Protect: yes,
   685  							Restrictions: &Restrictions{
   686  								Teams: []string{"oncall"},
   687  							},
   688  						},
   689  						Orgs: map[string]Org{
   690  							"org": {
   691  								Policy: Policy{
   692  									Protect: no,
   693  								},
   694  							},
   695  						},
   696  					},
   697  				},
   698  			},
   699  			expected: &Policy{
   700  				Protect: no,
   701  				Restrictions: &Restrictions{
   702  					Teams: []string{"oncall"},
   703  				},
   704  			},
   705  		},
   706  		{
   707  			name: "Make required presubmits required",
   708  			config: Config{
   709  				ProwConfig: ProwConfig{
   710  					BranchProtection: BranchProtection{
   711  						Policy: Policy{
   712  							Protect: yes,
   713  							RequiredStatusChecks: &ContextPolicy{
   714  								Contexts: []string{"cla"},
   715  							},
   716  						},
   717  						Orgs: map[string]Org{
   718  							"org": {},
   719  						},
   720  					},
   721  				},
   722  				JobConfig: JobConfig{
   723  					PresubmitsStatic: map[string][]Presubmit{
   724  						"org/repo": {
   725  							{
   726  								JobBase: JobBase{
   727  									Name: "required presubmit",
   728  								},
   729  								Reporter: Reporter{
   730  									Context: "required presubmit",
   731  								},
   732  								AlwaysRun: true,
   733  							},
   734  						},
   735  					},
   736  				},
   737  			},
   738  			expected: &Policy{
   739  				Protect: yes,
   740  				RequiredStatusChecks: &ContextPolicy{
   741  					Contexts: []string{"required presubmit", "cla"},
   742  				},
   743  			},
   744  		},
   745  		{
   746  			name: "ProtectTested opts into protection",
   747  			config: Config{
   748  				ProwConfig: ProwConfig{
   749  					BranchProtection: BranchProtection{
   750  						ProtectTested: utilpointer.Bool(true),
   751  						Orgs: map[string]Org{
   752  							"org": {},
   753  						},
   754  					},
   755  				},
   756  				JobConfig: JobConfig{
   757  					PresubmitsStatic: map[string][]Presubmit{
   758  						"org/repo": {
   759  							{
   760  								JobBase: JobBase{
   761  									Name: "required presubmit",
   762  								},
   763  								Reporter: Reporter{
   764  									Context: "required presubmit",
   765  								},
   766  								AlwaysRun: true,
   767  							},
   768  						},
   769  					},
   770  				},
   771  			},
   772  			expected: &Policy{
   773  				Protect: yes,
   774  				RequiredStatusChecks: &ContextPolicy{
   775  					Contexts: []string{"required presubmit"},
   776  				},
   777  			},
   778  		},
   779  		{
   780  			name: "required presubmits require protection",
   781  			config: Config{
   782  				ProwConfig: ProwConfig{
   783  					BranchProtection: BranchProtection{
   784  						Policy: Policy{
   785  							Protect: no,
   786  						},
   787  						Orgs: map[string]Org{
   788  							"org": {},
   789  						},
   790  					},
   791  				},
   792  				JobConfig: JobConfig{
   793  					PresubmitsStatic: map[string][]Presubmit{
   794  						"org/repo": {
   795  							{
   796  								JobBase: JobBase{
   797  									Name: "required presubmit",
   798  								},
   799  								Reporter: Reporter{
   800  									Context: "required presubmit",
   801  								},
   802  								AlwaysRun: true,
   803  							},
   804  						},
   805  					},
   806  				},
   807  			},
   808  			err: true,
   809  		},
   810  		{
   811  			name: "Optional presubmits do not force protection",
   812  			config: Config{
   813  				ProwConfig: ProwConfig{
   814  					BranchProtection: BranchProtection{
   815  						ProtectTested: utilpointer.Bool(true),
   816  						Orgs: map[string]Org{
   817  							"org": {},
   818  						},
   819  					},
   820  				},
   821  				JobConfig: JobConfig{
   822  					PresubmitsStatic: map[string][]Presubmit{
   823  						"org/repo": {
   824  							{
   825  								JobBase: JobBase{
   826  									Name: "optional presubmit",
   827  								},
   828  								Reporter: Reporter{
   829  									Context: "optional presubmit",
   830  								},
   831  								AlwaysRun: true,
   832  								Optional:  true,
   833  							},
   834  						},
   835  					},
   836  				},
   837  			},
   838  		},
   839  		{
   840  			name: "Optional presubmits force protection if ProtectReposWithOptionalJobs is true",
   841  			config: Config{
   842  				ProwConfig: ProwConfig{
   843  					BranchProtection: BranchProtection{
   844  						ProtectTested:                utilpointer.Bool(true),
   845  						ProtectReposWithOptionalJobs: utilpointer.Bool(true),
   846  						Orgs: map[string]Org{
   847  							"org": {},
   848  						},
   849  					},
   850  				},
   851  				JobConfig: JobConfig{
   852  					PresubmitsStatic: map[string][]Presubmit{
   853  						"org/repo": {
   854  							{
   855  								JobBase: JobBase{
   856  									Name: "optional presubmit",
   857  								},
   858  								Reporter: Reporter{
   859  									Context: "optional presubmit",
   860  								},
   861  								AlwaysRun: true,
   862  								Optional:  true,
   863  							},
   864  						},
   865  					},
   866  				},
   867  			},
   868  			expected: &Policy{
   869  				Protect:              yes,
   870  				RequiredStatusChecks: &ContextPolicy{},
   871  			},
   872  		},
   873  		{
   874  			name: "Explicit configuration takes precedence over ProtectTested",
   875  			config: Config{
   876  				ProwConfig: ProwConfig{
   877  					BranchProtection: BranchProtection{
   878  						ProtectTested: utilpointer.Bool(true),
   879  						Orgs: map[string]Org{
   880  							"org": {
   881  								Policy: Policy{
   882  									Protect: yes,
   883  								},
   884  							},
   885  						},
   886  					},
   887  				},
   888  				JobConfig: JobConfig{
   889  					PresubmitsStatic: map[string][]Presubmit{
   890  						"org/repo": {
   891  							{
   892  								JobBase: JobBase{
   893  									Name: "optional presubmit",
   894  								},
   895  								Reporter: Reporter{
   896  									Context: "optional presubmit",
   897  								},
   898  								AlwaysRun: true,
   899  								Optional:  true,
   900  							},
   901  						},
   902  					},
   903  				},
   904  			},
   905  			expected: &Policy{Protect: yes},
   906  		},
   907  		{
   908  			name: "Explicit non-configuration takes precedence over ProtectTested",
   909  			config: Config{
   910  				ProwConfig: ProwConfig{
   911  					BranchProtection: BranchProtection{
   912  						AllowDisabledJobPolicies: utilpointer.Bool(true),
   913  						ProtectTested:            utilpointer.Bool(true),
   914  						Orgs: map[string]Org{
   915  							"org": {
   916  								Repos: map[string]Repo{
   917  									"repo": {
   918  										Policy: Policy{
   919  											Protect: no,
   920  										},
   921  									},
   922  								},
   923  							},
   924  						},
   925  					},
   926  				},
   927  				JobConfig: JobConfig{
   928  					PresubmitsStatic: map[string][]Presubmit{
   929  						"org/repo": {
   930  							{
   931  								JobBase: JobBase{
   932  									Name: "required presubmit",
   933  								},
   934  								Reporter: Reporter{
   935  									Context: "required presubmit",
   936  								},
   937  								AlwaysRun: true,
   938  							},
   939  						},
   940  					},
   941  				},
   942  			},
   943  			expected: nil,
   944  		},
   945  	}
   946  
   947  	for _, tc := range testCases {
   948  		t.Run(tc.name, func(t *testing.T) {
   949  			actual, err := tc.config.GetBranchProtection("org", "repo", "branch", tc.config.PresubmitsStatic["org/repo"])
   950  			switch {
   951  			case err != nil:
   952  				if !tc.err {
   953  					t.Errorf("unexpected error: %v", err)
   954  				}
   955  			case tc.err:
   956  				t.Errorf("failed to receive an error")
   957  			default:
   958  				normalize(actual)
   959  				normalize(tc.expected)
   960  				if diff := cmp.Diff(actual, tc.expected); diff != "" {
   961  					t.Errorf("actual differs from expected: %s", diff)
   962  				}
   963  			}
   964  		})
   965  	}
   966  }
   967  
   968  func TestReposWithDisabledPolicy(t *testing.T) {
   969  	testCases := []struct {
   970  		name              string
   971  		config            Config
   972  		expectedRepoWarns []string
   973  	}{
   974  		{
   975  			name: "Warning is generated for repos with disabled policies",
   976  			config: Config{
   977  				ProwConfig: ProwConfig{
   978  					BranchProtection: BranchProtection{
   979  						Policy: Policy{
   980  							Protect: no,
   981  							RequiredStatusChecks: &ContextPolicy{
   982  								Contexts: []string{"hello", "world"},
   983  							},
   984  						},
   985  						AllowDisabledPolicies: utilpointer.Bool(true),
   986  						Orgs: map[string]Org{
   987  							"org1": {
   988  								Repos: map[string]Repo{
   989  									"repo1": {},
   990  									"repo2": {},
   991  								},
   992  							},
   993  						},
   994  					},
   995  				},
   996  			},
   997  			expectedRepoWarns: []string{"org1/repo1", "org1/repo2"},
   998  		},
   999  		{
  1000  			name: "No warnings if disabled policies are not allowed",
  1001  			config: Config{
  1002  				ProwConfig: ProwConfig{
  1003  					BranchProtection: BranchProtection{
  1004  						Policy: Policy{
  1005  							Protect: no,
  1006  							RequiredStatusChecks: &ContextPolicy{
  1007  								Contexts: []string{"hello", "world"},
  1008  							},
  1009  						},
  1010  						Orgs: map[string]Org{
  1011  							"org1": {
  1012  								Repos: map[string]Repo{
  1013  									"repo1": {},
  1014  								},
  1015  							},
  1016  						},
  1017  					},
  1018  				},
  1019  			},
  1020  			expectedRepoWarns: []string{},
  1021  		},
  1022  		{
  1023  			name: "No warnings if repo has no policies",
  1024  			config: Config{
  1025  				ProwConfig: ProwConfig{
  1026  					BranchProtection: BranchProtection{
  1027  						Orgs: map[string]Org{
  1028  							"org1": {
  1029  								Repos: map[string]Repo{
  1030  									"repo1": {},
  1031  								},
  1032  							},
  1033  						},
  1034  					},
  1035  				},
  1036  			},
  1037  			expectedRepoWarns: []string{},
  1038  		},
  1039  		{
  1040  			name: "No warnings if repo's defined policy is protected",
  1041  			config: Config{
  1042  				ProwConfig: ProwConfig{
  1043  					BranchProtection: BranchProtection{
  1044  						Orgs: map[string]Org{
  1045  							"org1": {
  1046  								Repos: map[string]Repo{
  1047  									"repo1": {
  1048  										Policy: Policy{
  1049  											Protect: yes,
  1050  											RequiredStatusChecks: &ContextPolicy{
  1051  												Contexts: []string{"hello", "world"},
  1052  											},
  1053  										},
  1054  									},
  1055  								},
  1056  							},
  1057  						},
  1058  					},
  1059  				},
  1060  			},
  1061  			expectedRepoWarns: []string{},
  1062  		},
  1063  	}
  1064  
  1065  	for _, tc := range testCases {
  1066  		t.Run(tc.name, func(t *testing.T) {
  1067  			repoWarns := tc.config.reposWithDisabledPolicy()
  1068  			if !reflect.DeepEqual(repoWarns, tc.expectedRepoWarns) {
  1069  				t.Errorf("actual repo warnings %+v != expected %+v", repoWarns, tc.expectedRepoWarns)
  1070  			}
  1071  		})
  1072  	}
  1073  }
  1074  
  1075  func TestUnprotectedBranches(t *testing.T) {
  1076  	testCases := []struct {
  1077  		name                string
  1078  		config              Config
  1079  		presubmits          map[string][]Presubmit
  1080  		expectedBranchWarns []string
  1081  	}{
  1082  		{
  1083  			name: "Repos with unprotected branches are added to the warning list",
  1084  			config: Config{
  1085  				ProwConfig: ProwConfig{
  1086  					BranchProtection: BranchProtection{
  1087  						Policy: Policy{
  1088  							RequiredStatusChecks: &ContextPolicy{
  1089  								Contexts: []string{"hello", "world"},
  1090  							},
  1091  						},
  1092  						AllowDisabledPolicies: utilpointer.Bool(true),
  1093  						Orgs: map[string]Org{
  1094  							"org1": {
  1095  								Repos: map[string]Repo{
  1096  									"repo1": {
  1097  										Branches: map[string]Branch{
  1098  											"branch1": {
  1099  												Policy{
  1100  													Protect: no,
  1101  												},
  1102  											},
  1103  										},
  1104  									},
  1105  									"repo2": {
  1106  										Branches: map[string]Branch{
  1107  											"branch1": {
  1108  												Policy{
  1109  													Protect: no,
  1110  												},
  1111  											},
  1112  										},
  1113  									},
  1114  								},
  1115  							},
  1116  						},
  1117  					},
  1118  				},
  1119  			},
  1120  			expectedBranchWarns: []string{"org1/repo1=branch1", "org1/repo2=branch1"},
  1121  		},
  1122  		{
  1123  			name: "Warn only once about repos with multiple unprotected branches",
  1124  			config: Config{
  1125  				ProwConfig: ProwConfig{
  1126  					BranchProtection: BranchProtection{
  1127  						Policy: Policy{
  1128  							RequiredStatusChecks: &ContextPolicy{
  1129  								Contexts: []string{"hello", "world"},
  1130  							},
  1131  						},
  1132  						AllowDisabledPolicies: utilpointer.Bool(true),
  1133  						Orgs: map[string]Org{
  1134  							"org1": {
  1135  								Repos: map[string]Repo{
  1136  									"repo1": {
  1137  										Branches: map[string]Branch{
  1138  											"branch1": {
  1139  												Policy{
  1140  													Protect: no,
  1141  												},
  1142  											},
  1143  											"branch2": {
  1144  												Policy{
  1145  													Protect: no,
  1146  												},
  1147  											},
  1148  										},
  1149  									},
  1150  									"repo2": {
  1151  										Branches: map[string]Branch{
  1152  											"branch1": {
  1153  												Policy{
  1154  													Protect: no,
  1155  												},
  1156  											},
  1157  										},
  1158  									},
  1159  								},
  1160  							},
  1161  						},
  1162  					},
  1163  				},
  1164  			},
  1165  			expectedBranchWarns: []string{"org1/repo1=branch1,branch2", "org1/repo2=branch1"},
  1166  		},
  1167  		{
  1168  			name: "No warnings if repo has no policies",
  1169  			config: Config{
  1170  				ProwConfig: ProwConfig{
  1171  					BranchProtection: BranchProtection{
  1172  						Orgs: map[string]Org{
  1173  							"org1": {
  1174  								Repos: map[string]Repo{
  1175  									"repo1": {
  1176  										Branches: map[string]Branch{
  1177  											"branch1": {
  1178  												Policy{
  1179  													Protect: no,
  1180  												},
  1181  											},
  1182  										},
  1183  									},
  1184  								},
  1185  							},
  1186  						},
  1187  					},
  1188  				},
  1189  			},
  1190  			expectedBranchWarns: []string{},
  1191  		},
  1192  		{
  1193  			name: "No warnings if repo's defined policy is protected",
  1194  			config: Config{
  1195  				ProwConfig: ProwConfig{
  1196  					BranchProtection: BranchProtection{
  1197  						Orgs: map[string]Org{
  1198  							"org1": {
  1199  								Repos: map[string]Repo{
  1200  									"repo1": {
  1201  										Policy: Policy{
  1202  											Protect: yes,
  1203  											RequiredStatusChecks: &ContextPolicy{
  1204  												Contexts: []string{"hello", "world"},
  1205  											},
  1206  										},
  1207  										Branches: map[string]Branch{
  1208  											"branch1": {
  1209  												Policy{
  1210  													Protect: no,
  1211  												},
  1212  											},
  1213  										},
  1214  									},
  1215  								},
  1216  							},
  1217  						},
  1218  					},
  1219  				},
  1220  			},
  1221  			expectedBranchWarns: []string{},
  1222  		},
  1223  		{
  1224  			name: "Warning if a branch has a required context but has protect: false",
  1225  			config: Config{
  1226  				ProwConfig: ProwConfig{
  1227  					BranchProtection: BranchProtection{
  1228  						AllowDisabledJobPolicies: utilpointer.Bool(true),
  1229  						Orgs: map[string]Org{
  1230  							"org1": {
  1231  								Repos: map[string]Repo{
  1232  									"repo1": {
  1233  										Branches: map[string]Branch{
  1234  											"branch1": {
  1235  												Policy{
  1236  													Protect: no,
  1237  												},
  1238  											},
  1239  										},
  1240  									},
  1241  								},
  1242  							},
  1243  						},
  1244  					},
  1245  				},
  1246  			},
  1247  			presubmits: map[string][]Presubmit{
  1248  				"org1/repo1": {
  1249  					{
  1250  						JobBase: JobBase{
  1251  							Name: "always-run",
  1252  						},
  1253  						AlwaysRun: true,
  1254  					},
  1255  				},
  1256  			},
  1257  			expectedBranchWarns: []string{"org1/repo1=branch1"},
  1258  		},
  1259  		{
  1260  			name: "No warnings for a branch with no required context and protect: false",
  1261  			config: Config{
  1262  				ProwConfig: ProwConfig{
  1263  					BranchProtection: BranchProtection{
  1264  						AllowDisabledJobPolicies: utilpointer.Bool(true),
  1265  						Orgs: map[string]Org{
  1266  							"org1": {
  1267  								Repos: map[string]Repo{
  1268  									"repo1": {
  1269  										Branches: map[string]Branch{
  1270  											"branch1": {
  1271  												Policy{
  1272  													Protect: no,
  1273  												},
  1274  											},
  1275  										},
  1276  									},
  1277  								},
  1278  							},
  1279  						},
  1280  					},
  1281  				},
  1282  			},
  1283  			presubmits: map[string][]Presubmit{
  1284  				"org1/repo1": {
  1285  					{
  1286  						JobBase: JobBase{
  1287  							Name: "optional",
  1288  						},
  1289  						Optional: true,
  1290  					},
  1291  				},
  1292  			},
  1293  			expectedBranchWarns: []string{},
  1294  		},
  1295  		{
  1296  			name: "No warnings if allow_disabled_job_policies is not set",
  1297  			config: Config{
  1298  				ProwConfig: ProwConfig{
  1299  					BranchProtection: BranchProtection{
  1300  						Orgs: map[string]Org{
  1301  							"org1": {
  1302  								Repos: map[string]Repo{
  1303  									"repo1": {
  1304  										Branches: map[string]Branch{
  1305  											"branch1": {
  1306  												Policy{
  1307  													Protect: no,
  1308  												},
  1309  											},
  1310  										},
  1311  									},
  1312  								},
  1313  							},
  1314  						},
  1315  					},
  1316  				},
  1317  			},
  1318  			presubmits: map[string][]Presubmit{
  1319  				"org1/repo1": {
  1320  					{
  1321  						JobBase: JobBase{
  1322  							Name: "always-run",
  1323  						},
  1324  						AlwaysRun: true,
  1325  					},
  1326  				},
  1327  			},
  1328  			expectedBranchWarns: []string{},
  1329  		},
  1330  	}
  1331  
  1332  	for _, tc := range testCases {
  1333  		t.Run(tc.name, func(t *testing.T) {
  1334  			branchWarns := tc.config.unprotectedBranches(tc.presubmits)
  1335  			if !reflect.DeepEqual(branchWarns, tc.expectedBranchWarns) {
  1336  				t.Errorf("actual branch warnings %+v != expected %+v", branchWarns, tc.expectedBranchWarns)
  1337  			}
  1338  		})
  1339  	}
  1340  }