sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/cmd/checkconfig/main_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 main
    18  
    19  import (
    20  	"context"
    21  	"flag"
    22  	"fmt"
    23  	stdio "io"
    24  	"os"
    25  	"path/filepath"
    26  	"reflect"
    27  	"regexp"
    28  	"strings"
    29  	"testing"
    30  	"testing/fstest"
    31  	"time"
    32  
    33  	"github.com/google/go-cmp/cmp"
    34  	"k8s.io/apimachinery/pkg/util/diff"
    35  	utilerrors "k8s.io/apimachinery/pkg/util/errors"
    36  	"k8s.io/apimachinery/pkg/util/sets"
    37  	utilpointer "k8s.io/utils/pointer"
    38  	"sigs.k8s.io/yaml"
    39  
    40  	prowapi "sigs.k8s.io/prow/pkg/apis/prowjobs/v1"
    41  	"sigs.k8s.io/prow/pkg/config"
    42  	"sigs.k8s.io/prow/pkg/flagutil"
    43  	configflagutil "sigs.k8s.io/prow/pkg/flagutil/config"
    44  	pluginsflagutil "sigs.k8s.io/prow/pkg/flagutil/plugins"
    45  	"sigs.k8s.io/prow/pkg/github"
    46  	"sigs.k8s.io/prow/pkg/io"
    47  	"sigs.k8s.io/prow/pkg/plank"
    48  	"sigs.k8s.io/prow/pkg/plugins"
    49  )
    50  
    51  func TestEnsureValidConfiguration(t *testing.T) {
    52  	var testCases = []struct {
    53  		name                                    string
    54  		tideSubSet, tideSuperSet, pluginsSubSet *orgRepoConfig
    55  		expectedErr                             bool
    56  	}{
    57  		{
    58  			name:          "nothing enabled makes no error",
    59  			tideSubSet:    newOrgRepoConfig(nil, nil),
    60  			tideSuperSet:  newOrgRepoConfig(nil, nil),
    61  			pluginsSubSet: newOrgRepoConfig(nil, nil),
    62  			expectedErr:   false,
    63  		},
    64  		{
    65  			name:          "plugin enabled on org without tide makes no error",
    66  			tideSubSet:    newOrgRepoConfig(nil, nil),
    67  			tideSuperSet:  newOrgRepoConfig(nil, nil),
    68  			pluginsSubSet: newOrgRepoConfig(map[string]sets.Set[string]{"org": nil}, nil),
    69  			expectedErr:   false,
    70  		},
    71  		{
    72  			name:          "plugin enabled on repo without tide makes no error",
    73  			tideSubSet:    newOrgRepoConfig(nil, nil),
    74  			tideSuperSet:  newOrgRepoConfig(nil, nil),
    75  			pluginsSubSet: newOrgRepoConfig(nil, sets.New[string]("org/repo")),
    76  			expectedErr:   false,
    77  		},
    78  		{
    79  			name:          "plugin enabled on repo with tide on repo makes no error",
    80  			tideSubSet:    newOrgRepoConfig(nil, sets.New[string]("org/repo")),
    81  			tideSuperSet:  newOrgRepoConfig(nil, sets.New[string]("org/repo")),
    82  			pluginsSubSet: newOrgRepoConfig(nil, sets.New[string]("org/repo")),
    83  			expectedErr:   false,
    84  		},
    85  		{
    86  			name:          "plugin enabled on repo with tide on org makes error",
    87  			tideSubSet:    newOrgRepoConfig(map[string]sets.Set[string]{"org": nil}, nil),
    88  			tideSuperSet:  newOrgRepoConfig(map[string]sets.Set[string]{"org": nil}, nil),
    89  			pluginsSubSet: newOrgRepoConfig(nil, sets.New[string]("org/repo")),
    90  			expectedErr:   true,
    91  		},
    92  		{
    93  			name:          "plugin enabled on org with tide on repo makes no error",
    94  			tideSubSet:    newOrgRepoConfig(nil, sets.New[string]("org/repo")),
    95  			tideSuperSet:  newOrgRepoConfig(nil, sets.New[string]("org/repo")),
    96  			pluginsSubSet: newOrgRepoConfig(map[string]sets.Set[string]{"org": nil}, nil),
    97  			expectedErr:   false,
    98  		},
    99  		{
   100  			name:          "plugin enabled on org with tide on org makes no error",
   101  			tideSubSet:    newOrgRepoConfig(map[string]sets.Set[string]{"org": nil}, nil),
   102  			tideSuperSet:  newOrgRepoConfig(map[string]sets.Set[string]{"org": nil}, nil),
   103  			pluginsSubSet: newOrgRepoConfig(map[string]sets.Set[string]{"org": nil}, nil),
   104  			expectedErr:   false,
   105  		},
   106  		{
   107  			name:          "tide enabled on org without plugin makes error",
   108  			tideSubSet:    newOrgRepoConfig(map[string]sets.Set[string]{"org": nil}, nil),
   109  			tideSuperSet:  newOrgRepoConfig(map[string]sets.Set[string]{"org": nil}, nil),
   110  			pluginsSubSet: newOrgRepoConfig(nil, nil),
   111  			expectedErr:   true,
   112  		},
   113  		{
   114  			name:          "tide enabled on repo without plugin makes error",
   115  			tideSubSet:    newOrgRepoConfig(nil, sets.New[string]("org/repo")),
   116  			tideSuperSet:  newOrgRepoConfig(nil, sets.New[string]("org/repo")),
   117  			pluginsSubSet: newOrgRepoConfig(nil, nil),
   118  			expectedErr:   true,
   119  		},
   120  		{
   121  			name:          "plugin enabled on org with any tide record but no specific tide requirement makes error",
   122  			tideSubSet:    newOrgRepoConfig(nil, nil),
   123  			tideSuperSet:  newOrgRepoConfig(map[string]sets.Set[string]{"org": nil}, nil),
   124  			pluginsSubSet: newOrgRepoConfig(map[string]sets.Set[string]{"org": nil}, nil),
   125  			expectedErr:   true,
   126  		},
   127  		{
   128  			name:          "plugin enabled on repo with any tide record but no specific tide requirement makes error",
   129  			tideSubSet:    newOrgRepoConfig(nil, nil),
   130  			tideSuperSet:  newOrgRepoConfig(nil, sets.New[string]("org/repo")),
   131  			pluginsSubSet: newOrgRepoConfig(nil, sets.New[string]("org/repo")),
   132  			expectedErr:   true,
   133  		},
   134  		{
   135  			name:          "any tide org record but no specific tide requirement or plugin makes no error",
   136  			tideSubSet:    newOrgRepoConfig(nil, nil),
   137  			tideSuperSet:  newOrgRepoConfig(map[string]sets.Set[string]{"org": nil}, nil),
   138  			pluginsSubSet: newOrgRepoConfig(nil, nil),
   139  			expectedErr:   false,
   140  		},
   141  		{
   142  			name:          "any tide repo record but no specific tide requirement or plugin makes no error",
   143  			tideSubSet:    newOrgRepoConfig(nil, nil),
   144  			tideSuperSet:  newOrgRepoConfig(nil, sets.New[string]("org/repo")),
   145  			pluginsSubSet: newOrgRepoConfig(nil, nil),
   146  			expectedErr:   false,
   147  		},
   148  		{
   149  			name:          "irrelevant repo exception in tide superset doesn't stop missing req error",
   150  			tideSubSet:    newOrgRepoConfig(nil, nil),
   151  			tideSuperSet:  newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo")}, nil),
   152  			pluginsSubSet: newOrgRepoConfig(map[string]sets.Set[string]{"org": nil}, nil),
   153  			expectedErr:   true,
   154  		},
   155  		{
   156  			name:          "repo exception in tide superset (no missing req error)",
   157  			tideSubSet:    newOrgRepoConfig(nil, nil),
   158  			tideSuperSet:  newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo")}, nil),
   159  			pluginsSubSet: newOrgRepoConfig(nil, sets.New[string]("org/repo")),
   160  			expectedErr:   false,
   161  		},
   162  		{
   163  			name:          "repo exception in tide subset (new missing req error)",
   164  			tideSubSet:    newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo")}, nil),
   165  			tideSuperSet:  newOrgRepoConfig(map[string]sets.Set[string]{"org": nil}, nil),
   166  			pluginsSubSet: newOrgRepoConfig(map[string]sets.Set[string]{"org": nil}, nil),
   167  			expectedErr:   true,
   168  		},
   169  	}
   170  
   171  	for _, testCase := range testCases {
   172  		t.Run(testCase.name, func(t *testing.T) {
   173  			err := ensureValidConfiguration("plugin", "label", "verb", testCase.tideSubSet, testCase.tideSuperSet, testCase.pluginsSubSet)
   174  			if testCase.expectedErr && err == nil {
   175  				t.Errorf("%s: expected an error but got none", testCase.name)
   176  			}
   177  			if !testCase.expectedErr && err != nil {
   178  				t.Errorf("%s: expected no error but got one: %v", testCase.name, err)
   179  			}
   180  		})
   181  	}
   182  }
   183  
   184  func TestOrgRepoDifference(t *testing.T) {
   185  	testCases := []struct {
   186  		name           string
   187  		a, b, expected *orgRepoConfig
   188  	}{
   189  		{
   190  			name:     "subtract nil",
   191  			a:        newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo")}, sets.New[string]("4/1", "4/2")),
   192  			b:        newOrgRepoConfig(map[string]sets.Set[string]{}, sets.New[string]()),
   193  			expected: newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo")}, sets.New[string]("4/1", "4/2")),
   194  		},
   195  		{
   196  			name:     "no overlap",
   197  			a:        newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo")}, sets.New[string]("4/1", "4/2")),
   198  			b:        newOrgRepoConfig(map[string]sets.Set[string]{"2": nil}, sets.New[string]("3/1")),
   199  			expected: newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo")}, sets.New[string]("4/1", "4/2")),
   200  		},
   201  		{
   202  			name:     "subtract self",
   203  			a:        newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo")}, sets.New[string]("4/1", "4/2")),
   204  			b:        newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo")}, sets.New[string]("4/1", "4/2")),
   205  			expected: newOrgRepoConfig(map[string]sets.Set[string]{}, sets.New[string]()),
   206  		},
   207  		{
   208  			name:     "subtract superset",
   209  			a:        newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo")}, sets.New[string]("4/1", "4/2")),
   210  			b:        newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo"), "org2": nil}, sets.New[string]("4/1", "4/2", "5/1")),
   211  			expected: newOrgRepoConfig(map[string]sets.Set[string]{}, sets.New[string]()),
   212  		},
   213  		{
   214  			name:     "remove org with org",
   215  			a:        newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo", "org/foo")}, sets.New[string]("4/1", "4/2")),
   216  			b:        newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/foo"), "2": nil}, sets.New[string]("3/1")),
   217  			expected: newOrgRepoConfig(map[string]sets.Set[string]{}, sets.New[string]("4/1", "4/2")),
   218  		},
   219  		{
   220  			name:     "shrink org with org",
   221  			a:        newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo")}, sets.New[string]("4/1", "4/2")),
   222  			b:        newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo", "org/foo"), "2": nil}, sets.New[string]("3/1")),
   223  			expected: newOrgRepoConfig(map[string]sets.Set[string]{}, sets.New[string]("org/foo", "4/1", "4/2")),
   224  		},
   225  		{
   226  			name:     "shrink org with repo",
   227  			a:        newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo")}, sets.New[string]("4/1", "4/2")),
   228  			b:        newOrgRepoConfig(map[string]sets.Set[string]{"2": nil}, sets.New[string]("org/foo", "3/1")),
   229  			expected: newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo", "org/foo")}, sets.New[string]("4/1", "4/2")),
   230  		},
   231  		{
   232  			name:     "remove repo with org",
   233  			a:        newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo")}, sets.New[string]("4/1", "4/2", "4/3", "5/1")),
   234  			b:        newOrgRepoConfig(map[string]sets.Set[string]{"2": nil, "4": sets.New[string]("4/2")}, sets.New[string]("3/1")),
   235  			expected: newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo")}, sets.New[string]("4/2", "5/1")),
   236  		},
   237  		{
   238  			name:     "remove repo with repo",
   239  			a:        newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo")}, sets.New[string]("4/1", "4/2", "4/3", "5/1")),
   240  			b:        newOrgRepoConfig(map[string]sets.Set[string]{"2": nil}, sets.New[string]("3/1", "4/2", "4/3")),
   241  			expected: newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo")}, sets.New[string]("4/1", "5/1")),
   242  		},
   243  	}
   244  
   245  	for _, tc := range testCases {
   246  		t.Run(tc.name, func(t *testing.T) {
   247  			got := tc.a.difference(tc.b)
   248  			if !reflect.DeepEqual(got, tc.expected) {
   249  				t.Errorf("expected config: %#v, but got config: %#v", tc.expected, got)
   250  			}
   251  		})
   252  	}
   253  }
   254  
   255  func TestOrgRepoIntersection(t *testing.T) {
   256  	testCases := []struct {
   257  		name           string
   258  		a, b, expected *orgRepoConfig
   259  	}{
   260  		{
   261  			name:     "intersect empty",
   262  			a:        newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo")}, sets.New[string]("4/1", "4/2")),
   263  			b:        newOrgRepoConfig(map[string]sets.Set[string]{}, sets.New[string]()),
   264  			expected: newOrgRepoConfig(map[string]sets.Set[string]{}, sets.New[string]()),
   265  		},
   266  		{
   267  			name:     "no overlap",
   268  			a:        newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo")}, sets.New[string]("4/1", "4/2")),
   269  			b:        newOrgRepoConfig(map[string]sets.Set[string]{"2": nil}, sets.New[string]("3/1")),
   270  			expected: newOrgRepoConfig(map[string]sets.Set[string]{}, sets.New[string]()),
   271  		},
   272  		{
   273  			name:     "intersect self",
   274  			a:        newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo")}, sets.New[string]("4/1", "4/2")),
   275  			b:        newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo")}, sets.New[string]("4/1", "4/2")),
   276  			expected: newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo")}, sets.New[string]("4/1", "4/2")),
   277  		},
   278  		{
   279  			name:     "intersect superset",
   280  			a:        newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo")}, sets.New[string]("4/1", "4/2")),
   281  			b:        newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo"), "org2": nil}, sets.New[string]("4/1", "4/2", "5/1")),
   282  			expected: newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo")}, sets.New[string]("4/1", "4/2")),
   283  		},
   284  		{
   285  			name:     "remove org",
   286  			a:        newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo", "org/foo")}, sets.New[string]("4/1", "4/2")),
   287  			b:        newOrgRepoConfig(map[string]sets.Set[string]{"org2": sets.New[string]("org2/repo1")}, sets.New[string]("4/1", "4/2", "5/1")),
   288  			expected: newOrgRepoConfig(map[string]sets.Set[string]{}, sets.New[string]("4/1", "4/2")),
   289  		},
   290  		{
   291  			name:     "shrink org with org",
   292  			a:        newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo", "org/bar")}, sets.New[string]("4/1", "4/2")),
   293  			b:        newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo", "org/foo"), "2": nil}, sets.New[string]("3/1")),
   294  			expected: newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo", "org/foo", "org/bar")}, sets.New[string]()),
   295  		},
   296  		{
   297  			name:     "shrink org with repo",
   298  			a:        newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo")}, sets.New[string]("4/1", "4/2")),
   299  			b:        newOrgRepoConfig(map[string]sets.Set[string]{"2": nil}, sets.New[string]("org/repo", "org/foo", "3/1", "4/1")),
   300  			expected: newOrgRepoConfig(map[string]sets.Set[string]{}, sets.New[string]("org/foo", "4/1")),
   301  		},
   302  		{
   303  			name:     "remove repo with org",
   304  			a:        newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo")}, sets.New[string]("4/1", "4/2", "4/3", "5/1")),
   305  			b:        newOrgRepoConfig(map[string]sets.Set[string]{"2": nil, "4": sets.New[string]("4/2")}, sets.New[string]("3/1")),
   306  			expected: newOrgRepoConfig(map[string]sets.Set[string]{}, sets.New[string]("4/1", "4/3")),
   307  		},
   308  		{
   309  			name:     "remove repo with repo",
   310  			a:        newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo")}, sets.New[string]("4/1", "4/2", "4/3", "5/1")),
   311  			b:        newOrgRepoConfig(map[string]sets.Set[string]{"2": nil}, sets.New[string]("3/1", "4/2", "4/3")),
   312  			expected: newOrgRepoConfig(map[string]sets.Set[string]{}, sets.New[string]("4/2", "4/3")),
   313  		},
   314  	}
   315  
   316  	for _, tc := range testCases {
   317  		t.Run(tc.name, func(t *testing.T) {
   318  			got := tc.a.intersection(tc.b)
   319  			if !reflect.DeepEqual(got, tc.expected) {
   320  				t.Errorf("expected config: %#v, but got config: %#v", tc.expected, got)
   321  			}
   322  		})
   323  	}
   324  }
   325  
   326  func TestOrgRepoUnion(t *testing.T) {
   327  	testCases := []struct {
   328  		name           string
   329  		a, b, expected *orgRepoConfig
   330  	}{
   331  		{
   332  			name:     "second set empty, get first set back",
   333  			a:        newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo")}, sets.New[string]("4/1", "4/2")),
   334  			b:        newOrgRepoConfig(map[string]sets.Set[string]{}, sets.New[string]()),
   335  			expected: newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo")}, sets.New[string]("4/1", "4/2")),
   336  		},
   337  		{
   338  			name:     "no overlap, simple union",
   339  			a:        newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo")}, sets.New[string]("4/1", "4/2")),
   340  			b:        newOrgRepoConfig(map[string]sets.Set[string]{"2": sets.New[string]()}, sets.New[string]("3/1")),
   341  			expected: newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo"), "2": sets.New[string]()}, sets.New[string]("4/1", "4/2", "3/1")),
   342  		},
   343  		{
   344  			name:     "union self, get self back",
   345  			a:        newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo")}, sets.New[string]("4/1", "4/2")),
   346  			b:        newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo")}, sets.New[string]("4/1", "4/2")),
   347  			expected: newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo")}, sets.New[string]("4/1", "4/2")),
   348  		},
   349  		{
   350  			name:     "union superset, get superset back",
   351  			a:        newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo")}, sets.New[string]("4/1", "4/2")),
   352  			b:        newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo"), "org2": sets.New[string]()}, sets.New[string]("4/1", "4/2", "5/1")),
   353  			expected: newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo"), "org2": sets.New[string]()}, sets.New[string]("4/1", "4/2", "5/1")),
   354  		},
   355  		{
   356  			name:     "keep only common denied items for an org",
   357  			a:        newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo", "org/bar")}, sets.New[string]()),
   358  			b:        newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo", "org/foo")}, sets.New[string]()),
   359  			expected: newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo")}, sets.New[string]()),
   360  		},
   361  		{
   362  			name:     "remove items from an org denylist if they're in a repo allowlist",
   363  			a:        newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]("org/repo")}, sets.New[string]()),
   364  			b:        newOrgRepoConfig(map[string]sets.Set[string]{}, sets.New[string]("org/repo")),
   365  			expected: newOrgRepoConfig(map[string]sets.Set[string]{"org": sets.New[string]()}, sets.New[string]()),
   366  		},
   367  		{
   368  			name:     "remove repos when they're covered by an org allowlist",
   369  			a:        newOrgRepoConfig(map[string]sets.Set[string]{}, sets.New[string]("4/1", "4/2", "4/3")),
   370  			b:        newOrgRepoConfig(map[string]sets.Set[string]{"4": sets.New[string]("4/2")}, sets.New[string]()),
   371  			expected: newOrgRepoConfig(map[string]sets.Set[string]{"4": sets.New[string]()}, sets.New[string]()),
   372  		},
   373  	}
   374  
   375  	for _, tc := range testCases {
   376  		t.Run(tc.name, func(t *testing.T) {
   377  			got := tc.a.union(tc.b)
   378  			if !reflect.DeepEqual(got, tc.expected) {
   379  				t.Errorf("%s: did not get expected config:\n%v", tc.name, cmp.Diff(tc.expected, got))
   380  			}
   381  		})
   382  	}
   383  }
   384  
   385  func TestValidateUnknownFields(t *testing.T) {
   386  	testCases := []struct {
   387  		name, filename string
   388  		cfg            interface{}
   389  		configBytes    []byte
   390  		expectedErr    string
   391  	}{
   392  		{
   393  			name:     "valid config",
   394  			filename: "valid-conf.yaml",
   395  			cfg:      &plugins.Configuration{},
   396  			configBytes: []byte(`plugins:
   397    kube/kube:
   398    - size
   399    - config-updater
   400  config_updater:
   401    maps:
   402      # Update the plugins configmap whenever plugins.yaml changes
   403      kube/plugins.yaml:
   404        name: plugins
   405  size:
   406    s: 1`),
   407  			expectedErr: "",
   408  		},
   409  		{
   410  			name:     "invalid top-level property",
   411  			filename: "toplvl.yaml",
   412  			cfg:      &plugins.Configuration{},
   413  			configBytes: []byte(`plugins:
   414    kube/kube:
   415    - size
   416    - config-updater
   417  notconfig_updater:
   418    maps:
   419      # Update the plugins configmap whenever plugins.yaml changes
   420      kube/plugins.yaml:
   421        name: plugins
   422  size:
   423    s: 1`),
   424  			expectedErr: "notconfig_updater",
   425  		},
   426  		{
   427  			name:     "invalid second-level property",
   428  			filename: "seclvl.yaml",
   429  			cfg:      &plugins.Configuration{},
   430  			configBytes: []byte(`plugins:
   431    kube/kube:
   432    - size
   433    - config-updater
   434  size:
   435    xs: 1
   436    s: 5`),
   437  			expectedErr: "xs",
   438  		},
   439  		{
   440  			name:     "invalid array element",
   441  			filename: "home/array.yaml",
   442  			cfg:      &plugins.Configuration{},
   443  			configBytes: []byte(`plugins:
   444    kube/kube:
   445    - size
   446    - trigger
   447  triggers:
   448  - repos:
   449    - kube/kube
   450  - repoz:
   451    - kube/kubez`),
   452  			expectedErr: "repoz",
   453  		},
   454  		// Options like DisallowUnknownFields can not be passed when using
   455  		// a custon json.Unmarshaler like we do here for defaulting:
   456  		// https://github.com/golang/go/issues/41144
   457  		//		{
   458  		//			name:     "invalid map entry",
   459  		//			filename: "map.yaml",
   460  		//			cfg:      &plugins.Configuration{},
   461  		//			configBytes: []byte(`plugins:
   462  		//  kube/kube:
   463  		//  - size
   464  		//  - config-updater
   465  		// config_updater:
   466  		//  maps:
   467  		//    # Update the plugins configmap whenever plugins.yaml changes
   468  		//    kube/plugins.yaml:
   469  		//      name: plugins
   470  		//    kube/config.yaml:
   471  		//      validation: config
   472  		// size:
   473  		//  s: 1`),
   474  		//			expectedErr: "validation",
   475  		//		},
   476  		{
   477  			// only one invalid element is printed in the error
   478  			name:     "multiple invalid elements",
   479  			filename: "multiple.yaml",
   480  			cfg:      &plugins.Configuration{},
   481  			configBytes: []byte(`plugins:
   482    kube/kube:
   483    - size
   484    - trigger
   485  triggers:
   486  - repoz:
   487    - kube/kubez
   488  - repos:
   489    - kube/kube
   490  size:
   491    s: 1
   492    xs: 1`),
   493  			expectedErr: "xs",
   494  		},
   495  		{
   496  			name:     "embedded structs - kube",
   497  			filename: "embedded.yaml",
   498  			cfg:      &config.Config{},
   499  			configBytes: []byte(`presubmits:
   500    kube/kube:
   501    - name: test-presubmit
   502      decorate: true
   503      always_run: true
   504      never_run: false
   505      skip_report: true
   506      spec:
   507        containers:
   508        - image: alpine
   509          command: ["/bin/printenv"]`),
   510  			expectedErr: "never_run",
   511  		},
   512  		{
   513  			name:     "embedded structs - tide",
   514  			filename: "embedded.yaml",
   515  			cfg:      &config.Config{},
   516  			configBytes: []byte(`tide:
   517    squash_label: sq
   518    not-a-property: true`),
   519  			expectedErr: "not-a-property",
   520  		},
   521  		{
   522  			name:     "embedded structs - size",
   523  			filename: "embedded.yaml",
   524  			cfg:      &config.Config{},
   525  			configBytes: []byte(`size:
   526    s: 1
   527    xs: 1`),
   528  			expectedErr: "size",
   529  		},
   530  		{
   531  			name:     "pointer to a slice",
   532  			filename: "pointer.yaml",
   533  			cfg:      &plugins.Configuration{},
   534  			configBytes: []byte(`bugzilla:
   535    default:
   536      '*':
   537        statuses:
   538        - foobar
   539        extra: oops`),
   540  			expectedErr: "extra",
   541  		},
   542  	}
   543  
   544  	for i := range testCases {
   545  		tc := testCases[i]
   546  		t.Run(tc.name, func(t *testing.T) {
   547  			t.Parallel()
   548  			if err := yaml.Unmarshal(tc.configBytes, tc.cfg); err != nil {
   549  				t.Fatalf("Unable to unmarhsal yaml: %v", err)
   550  			}
   551  			got := validateUnknownFields(tc.cfg, tc.configBytes, tc.filename)
   552  
   553  			if tc.expectedErr == "" {
   554  				if got != nil {
   555  					t.Errorf("%s: expected nil error but got:\n%v", tc.name, got)
   556  				}
   557  			} else { // check substrings in case yaml lib changes err fmt
   558  				var errMsg string
   559  				if got != nil {
   560  					errMsg = got.Error()
   561  				}
   562  				for _, s := range []string{"unknown field", tc.filename, tc.expectedErr} {
   563  					if !strings.Contains(errMsg, s) {
   564  						t.Errorf("%s: did not get expected validation error: expected substring in error message:\n%s\n but got:\n%v", tc.name, s, got)
   565  					}
   566  				}
   567  			}
   568  		})
   569  	}
   570  }
   571  
   572  func TestValidateUnknownFieldsAll(t *testing.T) {
   573  	testcases := []struct {
   574  		name             string
   575  		configContent    string
   576  		jobConfigContent map[string]string
   577  		expectedErr      bool
   578  	}{
   579  		{
   580  			name: "no separate job-config, all known fields",
   581  			configContent: `
   582  plank:
   583    default_decoration_config_entries:
   584      - config:
   585          timeout: 2h
   586          grace_period: 15s
   587          utility_images:
   588            clonerefs: "clonerefs:default"
   589            initupload: "initupload:default"
   590            entrypoint: "entrypoint:default"
   591            sidecar: "sidecar:default"
   592          gcs_configuration:
   593            bucket: "default-bucket"
   594            path_strategy: "legacy"
   595            default_org: "kubernetes"
   596            default_repo: "kubernetes"
   597          gcs_credentials_secret: "default-service-account"
   598  
   599  presubmits:
   600    kube/kube:
   601    - name: test-presubmit
   602      decorate: true
   603      spec:
   604        containers:
   605        - image: alpine
   606          command: ["/bin/printenv"]
   607  `,
   608  		},
   609  		{
   610  			name: "no separate job-config, unknown field",
   611  			configContent: `
   612  presubmits:
   613    kube/kube:
   614    - name: test-presubmit
   615      never_run: true      // I'm unknown
   616      spec:
   617        containers:
   618        - image: alpine
   619  `,
   620  			expectedErr: true,
   621  		},
   622  		{
   623  			name: "separate job-configs, all known field",
   624  			configContent: `
   625  presubmits:
   626    kube/kube:
   627    - name: kube-presubmit
   628      run_if_changed: "^src/"
   629      spec:
   630        containers:
   631        - image: alpine
   632  `,
   633  			jobConfigContent: map[string]string{
   634  				"org-repo-presubmits.yaml": `
   635  presubmits:
   636    org/repo:
   637    - name: org-repo-presubmit
   638      always_run: true
   639      spec:
   640        containers:
   641        - image: alpine
   642  `,
   643  				"org-repo2-presubmits.yaml": `
   644  presubmits:
   645    org/repo2:
   646    - name: org-repo2-presubmit
   647      always_run: true
   648      spec:
   649        containers:
   650        - image: alpine
   651  `,
   652  			},
   653  		},
   654  		{
   655  			name: "separate job-configs, unknown field in second job config",
   656  			configContent: `
   657  presubmits:
   658    kube/kube:
   659    - name: kube-presubmit
   660      never_run: true      // I'm unknown
   661      spec:
   662        containers:
   663        - image: alpine
   664  `,
   665  			jobConfigContent: map[string]string{
   666  				"org-repo-presubmits.yaml": `
   667  presubmits:
   668    org/repo:
   669    - name: org-repo-presubmit
   670      always_run: true
   671      spec:
   672        containers:
   673        - image: alpine
   674  `,
   675  				"org-repo2-presubmits.yaml": `
   676  presubmits:
   677    org/repo2:
   678    - name: org-repo2-presubmit
   679      never_run: true       // I'm unknown
   680      spec:
   681        containers:
   682        - image: alpine
   683  `,
   684  			},
   685  			expectedErr: true,
   686  		},
   687  	}
   688  	for i := range testcases {
   689  		tc := testcases[i]
   690  		t.Run(tc.name, func(t *testing.T) {
   691  			// Set up config files
   692  			root := t.TempDir()
   693  
   694  			prowConfigFile := filepath.Join(root, "config.yaml")
   695  			if err := os.WriteFile(prowConfigFile, []byte(tc.configContent), 0666); err != nil {
   696  				t.Fatalf("Error writing config.yaml file: %v.", err)
   697  			}
   698  			var jobConfigDir string
   699  			if len(tc.jobConfigContent) > 0 {
   700  				jobConfigDir = filepath.Join(root, "job-config")
   701  				if err := os.Mkdir(jobConfigDir, 0777); err != nil {
   702  					t.Fatalf("Error creating job-config directory: %v.", err)
   703  				}
   704  				for file, content := range tc.jobConfigContent {
   705  					file = filepath.Join(jobConfigDir, file)
   706  					if err := os.WriteFile(file, []byte(content), 0666); err != nil {
   707  						t.Fatalf("Error writing %q file: %v.", file, err)
   708  					}
   709  				}
   710  			}
   711  			// Test validation
   712  			_, err := config.LoadStrict(prowConfigFile, jobConfigDir, nil, "")
   713  			if (err != nil) != tc.expectedErr {
   714  				if tc.expectedErr {
   715  					t.Error("Expected an error, but did not receive one.")
   716  				} else {
   717  					content, _ := os.ReadFile(prowConfigFile)
   718  					t.Log(string(content))
   719  					t.Errorf("Unexpected error: %v.", err)
   720  				}
   721  			}
   722  		})
   723  	}
   724  }
   725  
   726  func TestValidateStrictBranches(t *testing.T) {
   727  	trueVal := true
   728  	falseVal := false
   729  	testcases := []struct {
   730  		name   string
   731  		config config.ProwConfig
   732  
   733  		errItems []string
   734  		okItems  []string
   735  	}{
   736  		{
   737  			name: "no conflict: no strict config",
   738  			config: config.ProwConfig{
   739  				Tide: config.Tide{
   740  					TideGitHubConfig: config.TideGitHubConfig{
   741  						Queries: []config.TideQuery{
   742  							{
   743  								Orgs: []string{"kubernetes"},
   744  							},
   745  						},
   746  					},
   747  				},
   748  			},
   749  			errItems: []string{},
   750  			okItems:  []string{"kubernetes"},
   751  		},
   752  		{
   753  			name: "no conflict: no tide config",
   754  			config: config.ProwConfig{
   755  				BranchProtection: config.BranchProtection{
   756  					Orgs: map[string]config.Org{
   757  						"kubernetes": {
   758  							Policy: config.Policy{
   759  								Protect: &trueVal,
   760  								RequiredStatusChecks: &config.ContextPolicy{
   761  									Strict: &trueVal,
   762  								},
   763  							},
   764  						},
   765  					},
   766  				},
   767  			},
   768  			errItems: []string{},
   769  			okItems:  []string{"kubernetes"},
   770  		},
   771  		{
   772  			name: "no conflict: tide repo exclusion",
   773  			config: config.ProwConfig{
   774  				Tide: config.Tide{
   775  					TideGitHubConfig: config.TideGitHubConfig{
   776  						Queries: []config.TideQuery{
   777  							{
   778  								Orgs:          []string{"kubernetes"},
   779  								ExcludedRepos: []string{"kubernetes/test-infra"},
   780  							},
   781  						},
   782  					},
   783  				},
   784  				BranchProtection: config.BranchProtection{
   785  					Orgs: map[string]config.Org{
   786  						"kubernetes": {
   787  							Policy: config.Policy{
   788  								Protect: &falseVal,
   789  							},
   790  							Repos: map[string]config.Repo{
   791  								"test-infra": {
   792  									Policy: config.Policy{
   793  										Protect: &trueVal,
   794  										RequiredStatusChecks: &config.ContextPolicy{
   795  											Strict: &trueVal,
   796  										},
   797  									},
   798  								},
   799  							},
   800  						},
   801  					},
   802  				},
   803  			},
   804  			errItems: []string{},
   805  			okItems:  []string{"kubernetes", "kubernetes/test-infra"},
   806  		},
   807  		{
   808  			name: "no conflict: protection repo exclusion",
   809  			config: config.ProwConfig{
   810  				Tide: config.Tide{
   811  					TideGitHubConfig: config.TideGitHubConfig{
   812  						Queries: []config.TideQuery{
   813  							{
   814  								Repos: []string{"kubernetes/test-infra"},
   815  							},
   816  						},
   817  					},
   818  				},
   819  				BranchProtection: config.BranchProtection{
   820  					Orgs: map[string]config.Org{
   821  						"kubernetes": {
   822  							Policy: config.Policy{
   823  								Protect: &trueVal,
   824  								RequiredStatusChecks: &config.ContextPolicy{
   825  									Strict: &trueVal,
   826  								},
   827  							},
   828  							Repos: map[string]config.Repo{
   829  								"test-infra": {
   830  									Policy: config.Policy{
   831  										Protect: &falseVal,
   832  									},
   833  								},
   834  							},
   835  						},
   836  					},
   837  				},
   838  			},
   839  			errItems: []string{},
   840  			okItems:  []string{"kubernetes", "kubernetes/test-infra"},
   841  		},
   842  		{
   843  			name: "conflict: tide more general",
   844  			config: config.ProwConfig{
   845  				Tide: config.Tide{
   846  					TideGitHubConfig: config.TideGitHubConfig{
   847  						Queries: []config.TideQuery{
   848  							{
   849  								Orgs: []string{"kubernetes"},
   850  							},
   851  						},
   852  					},
   853  				},
   854  				BranchProtection: config.BranchProtection{
   855  					Policy: config.Policy{
   856  						Protect: &trueVal,
   857  					},
   858  					Orgs: map[string]config.Org{
   859  						"kubernetes": {
   860  							Repos: map[string]config.Repo{
   861  								"test-infra": {
   862  									Policy: config.Policy{
   863  										Protect: &trueVal,
   864  										RequiredStatusChecks: &config.ContextPolicy{
   865  											Strict: &trueVal,
   866  										},
   867  									},
   868  								},
   869  							},
   870  						},
   871  					},
   872  				},
   873  			},
   874  			errItems: []string{"kubernetes/test-infra"},
   875  			okItems:  []string{"kubernetes"},
   876  		},
   877  		{
   878  			name: "conflict: tide more specific",
   879  			config: config.ProwConfig{
   880  				Tide: config.Tide{
   881  					TideGitHubConfig: config.TideGitHubConfig{
   882  						Queries: []config.TideQuery{
   883  							{
   884  								Repos: []string{"kubernetes/test-infra"},
   885  							},
   886  						},
   887  					},
   888  				},
   889  				BranchProtection: config.BranchProtection{
   890  					Policy: config.Policy{
   891  						Protect: &trueVal,
   892  					},
   893  					Orgs: map[string]config.Org{
   894  						"kubernetes": {
   895  							Policy: config.Policy{
   896  								RequiredStatusChecks: &config.ContextPolicy{
   897  									Strict: &trueVal,
   898  								},
   899  							},
   900  						},
   901  					},
   902  				},
   903  			},
   904  			errItems: []string{"kubernetes/test-infra"},
   905  			okItems:  []string{"kubernetes"},
   906  		},
   907  		{
   908  			name: "conflict: org level",
   909  			config: config.ProwConfig{
   910  				Tide: config.Tide{
   911  					TideGitHubConfig: config.TideGitHubConfig{
   912  						Queries: []config.TideQuery{
   913  							{
   914  								Orgs: []string{"kubernetes", "k8s"},
   915  							},
   916  						},
   917  					},
   918  				},
   919  				BranchProtection: config.BranchProtection{
   920  					Policy: config.Policy{
   921  						Protect: &trueVal,
   922  					},
   923  					Orgs: map[string]config.Org{
   924  						"kubernetes": {
   925  							Policy: config.Policy{
   926  								RequiredStatusChecks: &config.ContextPolicy{
   927  									Strict: &trueVal,
   928  								},
   929  							},
   930  						},
   931  					},
   932  				},
   933  			},
   934  			errItems: []string{"kubernetes"},
   935  			okItems:  []string{},
   936  		},
   937  		{
   938  			name: "conflict: repo level",
   939  			config: config.ProwConfig{
   940  				Tide: config.Tide{
   941  					TideGitHubConfig: config.TideGitHubConfig{
   942  						Queries: []config.TideQuery{
   943  							{
   944  								Repos: []string{"kubernetes/kubernetes"},
   945  							},
   946  							{
   947  								Repos: []string{"kubernetes/test-infra"},
   948  							},
   949  						},
   950  					},
   951  				},
   952  				BranchProtection: config.BranchProtection{
   953  					Policy: config.Policy{
   954  						Protect: &trueVal,
   955  					},
   956  					Orgs: map[string]config.Org{
   957  						"kubernetes": {
   958  							Repos: map[string]config.Repo{
   959  								"kubernetes": {
   960  									Policy: config.Policy{
   961  										RequiredStatusChecks: &config.ContextPolicy{
   962  											Strict: &trueVal,
   963  										},
   964  									},
   965  								},
   966  							},
   967  						},
   968  					},
   969  				},
   970  			},
   971  			errItems: []string{"kubernetes/kubernetes"},
   972  			okItems:  []string{"kubernetes", "kubernetes/test-infra"},
   973  		},
   974  		{
   975  			name: "conflict: branch level",
   976  			config: config.ProwConfig{
   977  				Tide: config.Tide{
   978  					TideGitHubConfig: config.TideGitHubConfig{
   979  						Queries: []config.TideQuery{
   980  							{
   981  								Repos:            []string{"kubernetes/test-infra"},
   982  								IncludedBranches: []string{"master"},
   983  							},
   984  							{
   985  								Repos: []string{"kubernetes/kubernetes"},
   986  							},
   987  						},
   988  					},
   989  				},
   990  				BranchProtection: config.BranchProtection{
   991  					Policy: config.Policy{
   992  						Protect: &trueVal,
   993  					},
   994  					Orgs: map[string]config.Org{
   995  						"kubernetes": {
   996  							Repos: map[string]config.Repo{
   997  								"test-infra": {
   998  									Branches: map[string]config.Branch{
   999  										"master": {
  1000  											Policy: config.Policy{
  1001  												RequiredStatusChecks: &config.ContextPolicy{
  1002  													Strict: &trueVal,
  1003  												},
  1004  											},
  1005  										},
  1006  									},
  1007  								},
  1008  							},
  1009  						},
  1010  					},
  1011  				},
  1012  			},
  1013  			errItems: []string{"kubernetes/test-infra"},
  1014  			okItems:  []string{"kubernetes", "kubernetes/kubernetes"},
  1015  		},
  1016  		{
  1017  			name: "conflict: global strict",
  1018  			config: config.ProwConfig{
  1019  				Tide: config.Tide{
  1020  					TideGitHubConfig: config.TideGitHubConfig{
  1021  						Queries: []config.TideQuery{
  1022  							{
  1023  								Repos: []string{"kubernetes/test-infra"},
  1024  							},
  1025  						},
  1026  					},
  1027  				},
  1028  				BranchProtection: config.BranchProtection{
  1029  					Policy: config.Policy{
  1030  						Protect: &trueVal,
  1031  						RequiredStatusChecks: &config.ContextPolicy{
  1032  							Strict: &trueVal,
  1033  						},
  1034  					},
  1035  				},
  1036  			},
  1037  			errItems: []string{"global"},
  1038  			okItems:  []string{},
  1039  		},
  1040  		{
  1041  			name: "no conflict: global strict, Tide disabled",
  1042  			config: config.ProwConfig{
  1043  				BranchProtection: config.BranchProtection{
  1044  					Policy: config.Policy{
  1045  						Protect: &trueVal,
  1046  						RequiredStatusChecks: &config.ContextPolicy{
  1047  							Strict: &trueVal,
  1048  						},
  1049  					},
  1050  				},
  1051  			},
  1052  			errItems: []string{},
  1053  			okItems:  []string{"global"},
  1054  		},
  1055  	}
  1056  	for i := range testcases {
  1057  		t.Run(testcases[i].name, func(t *testing.T) {
  1058  			tc := testcases[i]
  1059  			t.Parallel()
  1060  			err := validateStrictBranches(tc.config)
  1061  			if err == nil && len(tc.errItems) > 0 {
  1062  				t.Errorf("Expected errors for the following items, but didn't see an error: %v.", tc.errItems)
  1063  			} else if err != nil && len(tc.errItems) == 0 {
  1064  				t.Errorf("Unexpected error: %v.", err)
  1065  			}
  1066  			if err == nil {
  1067  				return
  1068  			}
  1069  			errText := err.Error()
  1070  			for _, errItem := range tc.errItems {
  1071  				// Search for the token while explicitly forbidding neighboring slashes
  1072  				// so that orgs don't match member repos.
  1073  				re, err := regexp.Compile(fmt.Sprintf("[^/]%s[^/]", errItem))
  1074  				if err != nil {
  1075  					t.Fatalf("Unexpected error compiling regexp: %v.", err)
  1076  				}
  1077  				if !re.MatchString(errText) {
  1078  					t.Errorf("Error did not reference expected error item %q: %q.", errItem, errText)
  1079  				}
  1080  			}
  1081  			for _, okItem := range tc.okItems {
  1082  				re, err := regexp.Compile(fmt.Sprintf("[^/]%s[^/]", okItem))
  1083  				if err != nil {
  1084  					t.Fatalf("Unexpected error compiling regexp: %v.", err)
  1085  				}
  1086  				if re.MatchString(errText) {
  1087  					t.Errorf("Error unexpectedly included ok item %q: %q.", okItem, errText)
  1088  				}
  1089  			}
  1090  		})
  1091  	}
  1092  }
  1093  
  1094  func TestValidateManagedWebhooks(t *testing.T) {
  1095  	testCases := []struct {
  1096  		name      string
  1097  		config    config.ProwConfig
  1098  		expectErr bool
  1099  	}{
  1100  		{
  1101  			name:      "empty config",
  1102  			config:    config.ProwConfig{},
  1103  			expectErr: false,
  1104  		},
  1105  		{
  1106  			name: "no duplicate webhooks",
  1107  			config: config.ProwConfig{
  1108  				ManagedWebhooks: config.ManagedWebhooks{
  1109  					RespectLegacyGlobalToken: false,
  1110  					OrgRepoConfig: map[string]config.ManagedWebhookInfo{
  1111  						"foo1":     {TokenCreatedAfter: time.Now()},
  1112  						"foo2":     {TokenCreatedAfter: time.Now()},
  1113  						"foo/bar":  {TokenCreatedAfter: time.Now()},
  1114  						"foo/bar1": {TokenCreatedAfter: time.Now()},
  1115  						"foo/bar2": {TokenCreatedAfter: time.Now()},
  1116  					},
  1117  				},
  1118  			},
  1119  			expectErr: false,
  1120  		},
  1121  		{
  1122  			name: "has duplicate webhooks",
  1123  			config: config.ProwConfig{
  1124  				ManagedWebhooks: config.ManagedWebhooks{
  1125  					OrgRepoConfig: map[string]config.ManagedWebhookInfo{
  1126  						"foo":      {TokenCreatedAfter: time.Now()},
  1127  						"foo1":     {TokenCreatedAfter: time.Now()},
  1128  						"foo2":     {TokenCreatedAfter: time.Now()},
  1129  						"foo/bar":  {TokenCreatedAfter: time.Now()},
  1130  						"foo/bar1": {TokenCreatedAfter: time.Now()},
  1131  						"foo/bar2": {TokenCreatedAfter: time.Now()},
  1132  					},
  1133  				},
  1134  			},
  1135  			expectErr: true,
  1136  		},
  1137  		{
  1138  			name: "has multiple duplicate webhooks",
  1139  			config: config.ProwConfig{
  1140  				ManagedWebhooks: config.ManagedWebhooks{
  1141  					RespectLegacyGlobalToken: true,
  1142  					OrgRepoConfig: map[string]config.ManagedWebhookInfo{
  1143  						"foo":       {TokenCreatedAfter: time.Now()},
  1144  						"foo1":      {TokenCreatedAfter: time.Now()},
  1145  						"foo2":      {TokenCreatedAfter: time.Now()},
  1146  						"foo/bar":   {TokenCreatedAfter: time.Now()},
  1147  						"foo/bar1":  {TokenCreatedAfter: time.Now()},
  1148  						"foo1/bar1": {TokenCreatedAfter: time.Now()},
  1149  					},
  1150  				},
  1151  			},
  1152  			expectErr: true,
  1153  		},
  1154  	}
  1155  
  1156  	for _, testCase := range testCases {
  1157  		err := validateManagedWebhooks(&config.Config{ProwConfig: testCase.config})
  1158  		if testCase.expectErr && err == nil {
  1159  			t.Errorf("%s: expected the config %+v to have errors but not", testCase.name, testCase.config)
  1160  		}
  1161  		if !testCase.expectErr && err != nil {
  1162  			t.Errorf("%s: expected the config %+v to be correct but got an error in validation: %v",
  1163  				testCase.name, testCase.config, err)
  1164  		}
  1165  	}
  1166  }
  1167  
  1168  func TestWarningEnabled(t *testing.T) {
  1169  	var testCases = []struct {
  1170  		name      string
  1171  		warnings  []string
  1172  		excludes  []string
  1173  		candidate string
  1174  		expected  bool
  1175  	}{
  1176  		{
  1177  			name:      "nothing is found in empty sets",
  1178  			warnings:  []string{},
  1179  			excludes:  []string{},
  1180  			candidate: "missing",
  1181  			expected:  false,
  1182  		},
  1183  		{
  1184  			name:      "explicit warning is found",
  1185  			warnings:  []string{"found"},
  1186  			excludes:  []string{},
  1187  			candidate: "found",
  1188  			expected:  true,
  1189  		},
  1190  		{
  1191  			name:      "explicit warning that is excluded is not found",
  1192  			warnings:  []string{"found"},
  1193  			excludes:  []string{"found"},
  1194  			candidate: "found",
  1195  			expected:  false,
  1196  		},
  1197  	}
  1198  
  1199  	for _, testCase := range testCases {
  1200  		opt := options{
  1201  			warnings:        flagutil.NewStrings(testCase.warnings...),
  1202  			excludeWarnings: flagutil.NewStrings(testCase.excludes...),
  1203  		}
  1204  		if actual, expected := opt.warningEnabled(testCase.candidate), testCase.expected; actual != expected {
  1205  			t.Errorf("%s: expected warning %s enablement to be %v but got %v", testCase.name, testCase.candidate, expected, actual)
  1206  		}
  1207  	}
  1208  }
  1209  
  1210  type fakeGHContent map[string]map[string]map[string]bool // org[repo][path] -> exist/does not exist
  1211  
  1212  type fakeGH struct {
  1213  	files    fakeGHContent
  1214  	archived map[string]bool // org/repo -> true/false
  1215  }
  1216  
  1217  func (f fakeGH) GetFile(org, repo, filepath, _ string) ([]byte, error) {
  1218  	if _, hasOrg := f.files[org]; !hasOrg {
  1219  		return nil, &github.FileNotFound{}
  1220  	}
  1221  	if _, hasRepo := f.files[org][repo]; !hasRepo {
  1222  		return nil, &github.FileNotFound{}
  1223  	}
  1224  	if _, hasPath := f.files[org][repo][filepath]; !hasPath {
  1225  		return nil, &github.FileNotFound{}
  1226  	}
  1227  
  1228  	return []byte("CONTENT"), nil
  1229  }
  1230  
  1231  func (f fakeGH) GetRepos(org string, isUser bool) ([]github.Repo, error) {
  1232  	if _, hasOrg := f.files[org]; !hasOrg {
  1233  		return nil, fmt.Errorf("no such org")
  1234  	}
  1235  	var repos []github.Repo
  1236  	for repo := range f.files[org] {
  1237  		fullname := fmt.Sprintf("%s/%s", org, repo)
  1238  		_, archived := f.archived[fullname]
  1239  		repos = append(
  1240  			repos,
  1241  			github.Repo{
  1242  				Owner:    github.User{Login: org},
  1243  				Name:     repo,
  1244  				FullName: fullname,
  1245  				Archived: archived,
  1246  			})
  1247  	}
  1248  	return repos, nil
  1249  }
  1250  
  1251  func TestVerifyOwnersPresence(t *testing.T) {
  1252  	testCases := []struct {
  1253  		description string
  1254  		cfg         *plugins.Configuration
  1255  		gh          fakeGH
  1256  
  1257  		expected string
  1258  	}{
  1259  		{
  1260  			description: "org with blunderbuss enabled contains a repo without OWNERS (legacy config)",
  1261  			cfg:         &plugins.Configuration{Plugins: plugins.OldToNewPlugins(map[string][]string{"org": {"blunderbuss"}})},
  1262  			gh:          fakeGH{files: fakeGHContent{"org": {"repo": {"NOOWNERS": true}}}},
  1263  			expected: "the following orgs or repos enable at least one" +
  1264  				" plugin that uses OWNERS files (approve, blunderbuss, owners-label), but" +
  1265  				" its master branch does not contain a root level OWNERS file: [org/repo]",
  1266  		}, {
  1267  			description: "org with approve enable contains a repo without OWNERS (legacy config)",
  1268  			cfg:         &plugins.Configuration{Plugins: plugins.OldToNewPlugins(map[string][]string{"org": {"approve"}})},
  1269  			gh:          fakeGH{files: fakeGHContent{"org": {"repo": {"NOOWNERS": true}}}},
  1270  			expected: "the following orgs or repos enable at least one" +
  1271  				" plugin that uses OWNERS files (approve, blunderbuss, owners-label), but" +
  1272  				" its master branch does not contain a root level OWNERS file: [org/repo]",
  1273  		}, {
  1274  			description: "org with owners-label enabled contains a repo without OWNERS (legacy config)",
  1275  			cfg:         &plugins.Configuration{Plugins: plugins.OldToNewPlugins(map[string][]string{"org": {"owners-label"}})},
  1276  			gh:          fakeGH{files: fakeGHContent{"org": {"repo": {"NOOWNERS": true}}}},
  1277  			expected: "the following orgs or repos enable at least one" +
  1278  				" plugin that uses OWNERS files (approve, blunderbuss, owners-label), but" +
  1279  				" its master branch does not contain a root level OWNERS file: [org/repo]",
  1280  		}, {
  1281  			description: "org with owners-label enabled contains an *archived* repo without OWNERS (legacy config)",
  1282  			cfg:         &plugins.Configuration{Plugins: plugins.OldToNewPlugins(map[string][]string{"org": {"owners-label"}})},
  1283  			gh: fakeGH{
  1284  				files:    fakeGHContent{"org": {"repo": {"NOOWNERS": true}}},
  1285  				archived: map[string]bool{"org/repo": true},
  1286  			},
  1287  			expected: "",
  1288  		}, {
  1289  			description: "repo with owners-label enabled does not contain OWNERS (legacy config)",
  1290  			cfg:         &plugins.Configuration{Plugins: plugins.OldToNewPlugins(map[string][]string{"org": {"owners-label"}})},
  1291  			gh:          fakeGH{files: fakeGHContent{"org": {"repo": {"NOOWNERS": true}}}},
  1292  			expected: "the following orgs or repos enable at least one" +
  1293  				" plugin that uses OWNERS files (approve, blunderbuss, owners-label), but" +
  1294  				" its master branch does not contain a root level OWNERS file: [org/repo]",
  1295  		}, {
  1296  			description: "org with owners-label enabled contains only repos with OWNERS (legacy config)",
  1297  			cfg:         &plugins.Configuration{Plugins: plugins.OldToNewPlugins(map[string][]string{"org": {"owners-label"}})},
  1298  			gh:          fakeGH{files: fakeGHContent{"org": {"repo": {"OWNERS": true}}}},
  1299  			expected:    "",
  1300  		}, {
  1301  			description: "repo with owners-label enabled contains OWNERS (legacy config)",
  1302  			cfg:         &plugins.Configuration{Plugins: plugins.OldToNewPlugins(map[string][]string{"org": {"owners-label"}})},
  1303  			gh:          fakeGH{files: fakeGHContent{"org": {"repo": {"OWNERS": true}}}},
  1304  			expected:    "",
  1305  		}, {
  1306  			description: "repo with unrelated plugin enabled does not contain OWNERS (legacy config)",
  1307  			cfg:         &plugins.Configuration{Plugins: plugins.OldToNewPlugins(map[string][]string{"org/repo": {"cat"}})},
  1308  			gh:          fakeGH{files: fakeGHContent{"org": {"repo": {"NOOWNERS": true}}}},
  1309  			expected:    "",
  1310  		}, {
  1311  			description: "org with blunderbuss enabled contains a repo without OWNERS",
  1312  			cfg:         &plugins.Configuration{Plugins: plugins.Plugins{"org": {Plugins: []string{"blunderbuss"}}}},
  1313  			gh:          fakeGH{files: fakeGHContent{"org": {"repo": {"NOOWNERS": true}}}},
  1314  			expected: "the following orgs or repos enable at least one" +
  1315  				" plugin that uses OWNERS files (approve, blunderbuss, owners-label), but" +
  1316  				" its master branch does not contain a root level OWNERS file: [org/repo]",
  1317  		}, {
  1318  			description: "org with approve enable contains a repo without OWNERS",
  1319  			cfg:         &plugins.Configuration{Plugins: plugins.Plugins{"org": {Plugins: []string{"approve"}}}},
  1320  			gh:          fakeGH{files: fakeGHContent{"org": {"repo": {"NOOWNERS": true}}}},
  1321  			expected: "the following orgs or repos enable at least one" +
  1322  				" plugin that uses OWNERS files (approve, blunderbuss, owners-label), but" +
  1323  				" its master branch does not contain a root level OWNERS file: [org/repo]",
  1324  		}, {
  1325  			description: "org with approve excluded contains a repo without OWNERS",
  1326  			cfg: &plugins.Configuration{Plugins: plugins.Plugins{"org": {
  1327  				Plugins:       []string{"approve"},
  1328  				ExcludedRepos: []string{"repo"},
  1329  			}}},
  1330  			gh:       fakeGH{files: fakeGHContent{"org": {"repo": {"NOOWNERS": true}}}},
  1331  			expected: "",
  1332  		}, {
  1333  			description: "org with approve repo-enabled contains a repo without OWNERS",
  1334  			cfg: &plugins.Configuration{Plugins: plugins.Plugins{
  1335  				"org": {
  1336  					Plugins:       []string{"approve"},
  1337  					ExcludedRepos: []string{"repo"},
  1338  				},
  1339  				"org/repo": {Plugins: []string{"approve"}},
  1340  			}},
  1341  			gh: fakeGH{files: fakeGHContent{"org": {"repo": {"NOOWNERS": true}}}},
  1342  			expected: "the following orgs or repos enable at least one" +
  1343  				" plugin that uses OWNERS files (approve, blunderbuss, owners-label), but" +
  1344  				" its master branch does not contain a root level OWNERS file: [org/repo]",
  1345  		}, {
  1346  			description: "org with owners-label enabled contains a repo without OWNERS",
  1347  			cfg:         &plugins.Configuration{Plugins: plugins.Plugins{"org": {Plugins: []string{"owners-label"}}}},
  1348  			gh:          fakeGH{files: fakeGHContent{"org": {"repo": {"NOOWNERS": true}}}},
  1349  			expected: "the following orgs or repos enable at least one" +
  1350  				" plugin that uses OWNERS files (approve, blunderbuss, owners-label), but" +
  1351  				" its master branch does not contain a root level OWNERS file: [org/repo]",
  1352  		}, {
  1353  			description: "org with owners-label enabled contains an *archived* repo without OWNERS",
  1354  			cfg:         &plugins.Configuration{Plugins: plugins.Plugins{"org": {Plugins: []string{"owners-label"}}}},
  1355  			gh: fakeGH{
  1356  				files:    fakeGHContent{"org": {"repo": {"NOOWNERS": true}}},
  1357  				archived: map[string]bool{"org/repo": true},
  1358  			},
  1359  			expected: "",
  1360  		}, {
  1361  			description: "repo with owners-label enabled does not contain OWNERS",
  1362  			cfg:         &plugins.Configuration{Plugins: plugins.Plugins{"org/repo": {Plugins: []string{"owners-label"}}}},
  1363  			gh:          fakeGH{files: fakeGHContent{"org": {"repo": {"NOOWNERS": true}}}},
  1364  			expected: "the following orgs or repos enable at least one" +
  1365  				" plugin that uses OWNERS files (approve, blunderbuss, owners-label), but" +
  1366  				" its master branch does not contain a root level OWNERS file: [org/repo]",
  1367  		}, {
  1368  			description: "org with owners-label enabled contains only repos with OWNERS",
  1369  			cfg:         &plugins.Configuration{Plugins: plugins.Plugins{"org": {Plugins: []string{"owners-label"}}}},
  1370  			gh:          fakeGH{files: fakeGHContent{"org": {"repo": {"OWNERS": true}}}},
  1371  			expected:    "",
  1372  		}, {
  1373  			description: "repo with owners-label enabled contains OWNERS",
  1374  			cfg:         &plugins.Configuration{Plugins: plugins.Plugins{"org/repo": {Plugins: []string{"owners-label"}}}},
  1375  			gh:          fakeGH{files: fakeGHContent{"org": {"repo": {"OWNERS": true}}}},
  1376  			expected:    "",
  1377  		}, {
  1378  			description: "repo with unrelated plugin enabled does not contain OWNERS",
  1379  			cfg:         &plugins.Configuration{Plugins: plugins.Plugins{"org/repo": {Plugins: []string{"cat"}}}},
  1380  			gh:          fakeGH{files: fakeGHContent{"org": {"repo": {"NOOWNERS": true}}}},
  1381  			expected:    "",
  1382  		},
  1383  	}
  1384  
  1385  	for _, tc := range testCases {
  1386  		t.Run(tc.description, func(t *testing.T) {
  1387  			var errMessage string
  1388  			if err := verifyOwnersPresence(tc.cfg, tc.gh); err != nil {
  1389  				errMessage = err.Error()
  1390  			}
  1391  			if errMessage != tc.expected {
  1392  				t.Errorf("result differs:\n%s", diff.StringDiff(tc.expected, errMessage))
  1393  			}
  1394  		})
  1395  	}
  1396  }
  1397  
  1398  func TestOptions(t *testing.T) {
  1399  
  1400  	var defaultGitHubOptions flagutil.GitHubOptions
  1401  	defaultGitHubOptions.AddCustomizedFlags(flag.NewFlagSet("", flag.ContinueOnError), throttlerDefaults)
  1402  	defaultGitHubOptions.AllowAnonymous = true
  1403  
  1404  	StringsFlag := func(vals []string) flagutil.Strings {
  1405  		var flag flagutil.Strings
  1406  		for _, val := range vals {
  1407  			flag.Set(val)
  1408  		}
  1409  		return flag
  1410  	}
  1411  
  1412  	testCases := []struct {
  1413  		name            string
  1414  		args            []string
  1415  		expectedOptions *options
  1416  		expectedError   bool
  1417  	}{
  1418  		{
  1419  			name: "cannot parse argument, reject",
  1420  			args: []string{
  1421  				"--config-path=prow/config.yaml",
  1422  				"--strict=non-boolean-string",
  1423  			},
  1424  			expectedOptions: nil,
  1425  			expectedError:   true,
  1426  		},
  1427  		{
  1428  			name:            "forgot config-path, reject",
  1429  			args:            []string{"--job-config-path=config/jobs/org/job.yaml"},
  1430  			expectedOptions: nil,
  1431  			expectedError:   true,
  1432  		},
  1433  		{
  1434  			name: "config-path with two warnings but one unknown, reject",
  1435  			args: []string{
  1436  				"--config-path=prow/config.yaml",
  1437  				"--warnings=mismatched-tide",
  1438  				"--warnings=unknown-warning",
  1439  			},
  1440  			expectedOptions: nil,
  1441  			expectedError:   true,
  1442  		},
  1443  		{
  1444  			name: "config-path with many valid options",
  1445  			args: []string{
  1446  				"--config-path=prow/config.yaml",
  1447  				"--plugin-config=prow/plugins/plugin.yaml",
  1448  				"--job-config-path=config/jobs/org/job.yaml",
  1449  				"--warnings=mismatched-tide",
  1450  				"--warnings=mismatched-tide-lenient",
  1451  				"--exclude-warning=tide-strict-branch",
  1452  				"--exclude-warning=mismatched-tide",
  1453  				"--exclude-warning=ok-if-unknown-warning",
  1454  				"--strict=true",
  1455  				"--expensive-checks=false",
  1456  			},
  1457  			expectedOptions: &options{
  1458  				config: configflagutil.ConfigOptions{
  1459  					ConfigPathFlagName:                    "config-path",
  1460  					JobConfigPathFlagName:                 "job-config-path",
  1461  					ConfigPath:                            "prow/config.yaml",
  1462  					JobConfigPath:                         "config/jobs/org/job.yaml",
  1463  					SupplementalProwConfigsFileNameSuffix: "_prowconfig.yaml",
  1464  					InRepoConfigCacheSize:                 200,
  1465  				},
  1466  				pluginsConfig: pluginsflagutil.PluginOptions{
  1467  					PluginConfigPath:                         "prow/plugins/plugin.yaml",
  1468  					SupplementalPluginsConfigsFileNameSuffix: "_pluginconfig.yaml",
  1469  					CheckUnknownPlugins:                      true,
  1470  				},
  1471  				warnings:        StringsFlag([]string{"mismatched-tide", "mismatched-tide-lenient"}),
  1472  				excludeWarnings: StringsFlag([]string{"tide-strict-branch", "mismatched-tide", "ok-if-unknown-warning"}),
  1473  				strict:          true,
  1474  				expensive:       false,
  1475  				github:          defaultGitHubOptions,
  1476  			},
  1477  			expectedError: false,
  1478  		},
  1479  		{
  1480  			name: "prow-yaml-path without prow-yaml-repo-name is invalid",
  1481  			args: []string{
  1482  				"--prow-yaml-path=my-file",
  1483  			},
  1484  			expectedError: true,
  1485  		},
  1486  	}
  1487  
  1488  	for _, tc := range testCases {
  1489  		t.Run(tc.name, func(t *testing.T) {
  1490  			flags := flag.NewFlagSet(tc.name, flag.ContinueOnError)
  1491  			var actualOptions options
  1492  			switch actualErr := actualOptions.gatherOptions(flags, tc.args); {
  1493  			case tc.expectedError:
  1494  				if actualErr == nil {
  1495  					t.Error("failed to receive an error")
  1496  				}
  1497  			case actualErr != nil:
  1498  				t.Errorf("unexpected error: %v", actualErr)
  1499  			case !reflect.DeepEqual(&actualOptions, tc.expectedOptions):
  1500  				t.Errorf("actual differs from expected: %s", cmp.Diff(actualOptions, *tc.expectedOptions, cmp.Exporter(func(_ reflect.Type) bool { return true })))
  1501  			}
  1502  		})
  1503  	}
  1504  }
  1505  
  1506  func TestValidateJobExtraRefs(t *testing.T) {
  1507  	testCases := []struct {
  1508  		name      string
  1509  		extraRefs []prowapi.Refs
  1510  		expected  error
  1511  	}{
  1512  		{
  1513  			name: "validation error if extra ref specifies the repo for which the job is configured",
  1514  			extraRefs: []prowapi.Refs{
  1515  				{
  1516  					Org:  "org",
  1517  					Repo: "repo",
  1518  				},
  1519  			},
  1520  			expected: fmt.Errorf("invalid job test on repo org/repo: the following refs specified more than once: %s",
  1521  				"org/repo"),
  1522  		},
  1523  		{
  1524  			name: "no errors if there are no duplications",
  1525  			extraRefs: []prowapi.Refs{
  1526  				{
  1527  					Org:  "foo",
  1528  					Repo: "bar",
  1529  				},
  1530  			},
  1531  		},
  1532  	}
  1533  
  1534  	for _, tc := range testCases {
  1535  		t.Run(tc.name, func(t *testing.T) {
  1536  			config := config.JobConfig{
  1537  				PresubmitsStatic: map[string][]config.Presubmit{
  1538  					"org/repo": {
  1539  						{
  1540  							JobBase: config.JobBase{
  1541  								Name: "test",
  1542  								UtilityConfig: config.UtilityConfig{
  1543  									ExtraRefs: tc.extraRefs,
  1544  								},
  1545  							},
  1546  						},
  1547  					},
  1548  				},
  1549  			}
  1550  			if err := validateJobExtraRefs(config); !reflect.DeepEqual(err, utilerrors.NewAggregate([]error{tc.expected})) {
  1551  				t.Errorf("%s: did not get expected validation error:\n%v", tc.name,
  1552  					cmp.Diff(tc.expected, err))
  1553  			}
  1554  		})
  1555  	}
  1556  }
  1557  
  1558  func TestValidateInRepoConfig(t *testing.T) {
  1559  	testCases := []struct {
  1560  		name         string
  1561  		prowYAMLData []byte
  1562  		strict       bool
  1563  		expectedErr  string
  1564  	}{
  1565  		{
  1566  			name:         "Valid prowYAML, no err",
  1567  			prowYAMLData: []byte(`presubmits: [{"name": "hans", "spec": {"containers": [{}]}}]`),
  1568  		},
  1569  		{
  1570  			name:         "Invalid prowYAML presubmit, err",
  1571  			prowYAMLData: []byte(`presubmits: [{"name": "hans"}]`),
  1572  			expectedErr:  "failed to validate Prow YAML: invalid presubmit job hans: kubernetes jobs require a spec",
  1573  		},
  1574  		{
  1575  			name:         "Invalid prowYAML postsubmit, err",
  1576  			prowYAMLData: []byte(`postsubmits: [{"name": "hans"}]`),
  1577  			expectedErr:  "failed to validate Prow YAML: invalid postsubmit job hans: kubernetes jobs require a spec",
  1578  		},
  1579  		{
  1580  			name: "Absent prowYAML, no err",
  1581  		},
  1582  		{
  1583  			name:         "unknown field prowYAML fails strict validation",
  1584  			strict:       true,
  1585  			prowYAMLData: []byte(`presubmits: [{"name": "hans", "never_run": "true", "spec": {"containers": [{}]}}]`),
  1586  			expectedErr:  "error unmarshaling JSON: while decoding JSON: json: unknown field \"never_run\"",
  1587  		},
  1588  	}
  1589  
  1590  	for _, tc := range testCases {
  1591  		prowYAMLFileName := "/this-must-not-exist"
  1592  
  1593  		if tc.prowYAMLData != nil {
  1594  			fileName := filepath.Join(t.TempDir(), ".prow.yaml")
  1595  			if err := os.WriteFile(fileName, tc.prowYAMLData, 0666); err != nil {
  1596  				t.Fatalf("failed to write to tempfile: %v", err)
  1597  			}
  1598  
  1599  			prowYAMLFileName = fileName
  1600  		}
  1601  
  1602  		// Need an empty file to load the config from so we go through its defaulting
  1603  		tempConfig, err := os.CreateTemp("", "prow-test")
  1604  		if err != nil {
  1605  			t.Fatalf("failed to get tempfile: %v", err)
  1606  		}
  1607  		defer func() {
  1608  			if err := os.Remove(tempConfig.Name()); err != nil {
  1609  				t.Errorf("failed to remove tempfile: %v", err)
  1610  			}
  1611  		}()
  1612  		if err := tempConfig.Close(); err != nil {
  1613  			t.Errorf("failed to close tempFile: %v", err)
  1614  		}
  1615  
  1616  		cfg, err := config.Load(tempConfig.Name(), "", nil, "")
  1617  		if err != nil {
  1618  			t.Fatalf("failed to load config: %v", err)
  1619  		}
  1620  		err = validateInRepoConfig(cfg, prowYAMLFileName, "my/repo", tc.strict)
  1621  		var errString string
  1622  		if err != nil {
  1623  			errString = err.Error()
  1624  		}
  1625  
  1626  		if errString != tc.expectedErr && !strings.Contains(errString, tc.expectedErr) {
  1627  			t.Errorf("expected error %q does not match actual error %q", tc.expectedErr, errString)
  1628  		}
  1629  	}
  1630  }
  1631  
  1632  func TestValidateTideContextPolicy(t *testing.T) {
  1633  	cfg := func(m ...func(*config.Config)) *config.Config {
  1634  		cfg := &config.Config{}
  1635  		cfg.PresubmitsStatic = map[string][]config.Presubmit{}
  1636  		for _, mod := range m {
  1637  			mod(cfg)
  1638  		}
  1639  		return cfg
  1640  	}
  1641  
  1642  	testCases := []struct {
  1643  		name          string
  1644  		cfg           *config.Config
  1645  		expectedError string
  1646  	}{
  1647  		{
  1648  			name: "overlapping branch config, error",
  1649  			cfg: cfg(func(c *config.Config) {
  1650  				c.PresubmitsStatic["a/b"] = []config.Presubmit{
  1651  					{Reporter: config.Reporter{Context: "a"}, Brancher: config.Brancher{Branches: []string{"a"}}},
  1652  					{AlwaysRun: true, Reporter: config.Reporter{Context: "a"}},
  1653  				}
  1654  			}),
  1655  			expectedError: "context policy for a branch in a/b is invalid: contexts a are defined as required and required if present",
  1656  		},
  1657  		{
  1658  			name: "overlapping branch config with empty branch configs, error",
  1659  			cfg: cfg(func(c *config.Config) {
  1660  				c.PresubmitsStatic["a/b"] = []config.Presubmit{
  1661  					{Reporter: config.Reporter{Context: "a"}},
  1662  					{AlwaysRun: true, Reporter: config.Reporter{Context: "a"}},
  1663  				}
  1664  			}),
  1665  			expectedError: "context policy for master branch in a/b is invalid: contexts a are defined as required and required if present",
  1666  		},
  1667  		{
  1668  			name: "overlapping branch config, inrepoconfig enabled, error",
  1669  			cfg: cfg(func(c *config.Config) {
  1670  				c.InRepoConfig.Enabled = map[string]*bool{"*": utilpointer.Bool(true)}
  1671  				c.PresubmitsStatic["a/b"] = []config.Presubmit{
  1672  					{Reporter: config.Reporter{Context: "a"}, Brancher: config.Brancher{Branches: []string{"a"}}},
  1673  					{AlwaysRun: true, Reporter: config.Reporter{Context: "a"}},
  1674  				}
  1675  			}),
  1676  			expectedError: "context policy for a branch in a/b is invalid: contexts a are defined as required and required if present",
  1677  		},
  1678  		{
  1679  			name: "no overlapping branch config, no error",
  1680  			cfg: cfg(func(c *config.Config) {
  1681  				c.PresubmitsStatic["a/b"] = []config.Presubmit{
  1682  					{Reporter: config.Reporter{Context: "a"}, Brancher: config.Brancher{Branches: []string{"a"}}},
  1683  					{AlwaysRun: true, Reporter: config.Reporter{Context: "a"}, Brancher: config.Brancher{Branches: []string{"b"}}},
  1684  				}
  1685  			}),
  1686  		},
  1687  		{
  1688  			name: "repo key is not in org/repo format, no error",
  1689  			cfg: cfg(func(c *config.Config) {
  1690  				c.PresubmitsStatic["https://kunit-review.googlesource.com/linux"] = []config.Presubmit{
  1691  					{Reporter: config.Reporter{Context: "a"}, Brancher: config.Brancher{Branches: []string{"a"}}},
  1692  					{AlwaysRun: true, Reporter: config.Reporter{Context: "a"}, Brancher: config.Brancher{Branches: []string{"b"}}},
  1693  				}
  1694  			}),
  1695  		},
  1696  	}
  1697  
  1698  	for _, tc := range testCases {
  1699  		t.Run(tc.name, func(t *testing.T) {
  1700  			// Needed so regexes get compiled
  1701  			tc.cfg.SetPresubmits(tc.cfg.PresubmitsStatic)
  1702  
  1703  			errMsg := ""
  1704  			if err := validateTideContextPolicy(tc.cfg); err != nil {
  1705  				errMsg = err.Error()
  1706  			}
  1707  			if errMsg != tc.expectedError {
  1708  				t.Errorf("expected error %q, got error %q", tc.expectedError, errMsg)
  1709  			}
  1710  		})
  1711  	}
  1712  }
  1713  
  1714  func TestValidate(t *testing.T) {
  1715  	testCases := []struct {
  1716  		name string
  1717  		opts options
  1718  	}{
  1719  		{
  1720  			name: "combined config",
  1721  			opts: options{
  1722  				config: configflagutil.ConfigOptions{ConfigPath: "testdata/combined.yaml"},
  1723  			},
  1724  		},
  1725  	}
  1726  
  1727  	for _, tc := range testCases {
  1728  		t.Run(tc.name, func(t *testing.T) {
  1729  			if err := validate(tc.opts); err != nil {
  1730  				t.Fatalf("validation failed: %v", err)
  1731  			}
  1732  		})
  1733  	}
  1734  }
  1735  
  1736  type fakeOpener struct {
  1737  	io.Opener
  1738  	content   string
  1739  	readError error
  1740  }
  1741  
  1742  func (fo *fakeOpener) Reader(ctx context.Context, path string) (io.ReadCloser, error) {
  1743  	if fo.readError != nil {
  1744  		return nil, fo.readError
  1745  	}
  1746  	return stdio.NopCloser(strings.NewReader(fo.content)), nil
  1747  }
  1748  
  1749  func (fo *fakeOpener) Close() error {
  1750  	return nil
  1751  }
  1752  
  1753  func TestValidateClusterField(t *testing.T) {
  1754  	testCases := []struct {
  1755  		name              string
  1756  		cfg               *config.Config
  1757  		clusterStatusFile string
  1758  		readError         error
  1759  		expectedError     string
  1760  	}{
  1761  		{
  1762  			name: "Jenkins job with unset cluster",
  1763  			cfg: &config.Config{
  1764  				JobConfig: config.JobConfig{
  1765  					PresubmitsStatic: map[string][]config.Presubmit{
  1766  						"org1/repo1": {
  1767  							{
  1768  								JobBase: config.JobBase{
  1769  									Agent: "jenkins",
  1770  								},
  1771  							}}}}},
  1772  		},
  1773  		{
  1774  			name: "jenkins job with defaulted cluster",
  1775  			cfg: &config.Config{
  1776  				JobConfig: config.JobConfig{
  1777  					PresubmitsStatic: map[string][]config.Presubmit{
  1778  						"org1/repo1": {
  1779  							{
  1780  								JobBase: config.JobBase{
  1781  									Agent:   "jenkins",
  1782  									Cluster: "default",
  1783  									Name:    "some-job",
  1784  								},
  1785  							}}}}},
  1786  		},
  1787  		{
  1788  			name: "jenkins job must not set cluster",
  1789  			cfg: &config.Config{
  1790  				JobConfig: config.JobConfig{
  1791  					PresubmitsStatic: map[string][]config.Presubmit{
  1792  						"org1/repo1": {
  1793  							{
  1794  								JobBase: config.JobBase{
  1795  									Agent:   "jenkins",
  1796  									Cluster: "build1",
  1797  									Name:    "some-job",
  1798  								},
  1799  							}}}}},
  1800  			expectedError: "org1/repo1: some-job: cannot set cluster field if agent is jenkins",
  1801  		},
  1802  		{
  1803  			name: "k8s job can set cluster",
  1804  			cfg: &config.Config{
  1805  				JobConfig: config.JobConfig{
  1806  					PresubmitsStatic: map[string][]config.Presubmit{
  1807  						"org1/repo1": {
  1808  							{
  1809  								JobBase: config.JobBase{
  1810  									Agent:   "kubernetes",
  1811  									Cluster: "default",
  1812  								},
  1813  							}}}}},
  1814  		},
  1815  		{
  1816  			name: "empty agent job can set cluster",
  1817  			cfg: &config.Config{
  1818  				JobConfig: config.JobConfig{
  1819  					PresubmitsStatic: map[string][]config.Presubmit{
  1820  						"org1/repo1": {
  1821  							{
  1822  								JobBase: config.JobBase{
  1823  									Cluster: "default",
  1824  								},
  1825  							}}}}},
  1826  		},
  1827  		{
  1828  			name: "cluster validates with lone reachable default cluster",
  1829  			cfg: &config.Config{
  1830  				ProwConfig: config.ProwConfig{
  1831  					Plank: config.Plank{BuildClusterStatusFile: "gs://my-bucket/build-cluster-status.json"},
  1832  				},
  1833  				JobConfig: config.JobConfig{
  1834  					PresubmitsStatic: map[string][]config.Presubmit{
  1835  						"org1/repo1": {
  1836  							{
  1837  								JobBase: config.JobBase{
  1838  									Cluster: "default",
  1839  								},
  1840  							}}}}},
  1841  			clusterStatusFile: fmt.Sprintf(`{"default": %q}`, plank.ClusterStatusReachable),
  1842  		},
  1843  		{
  1844  			name: "cluster validates with multiple clusters, specified is reachable",
  1845  			cfg: &config.Config{
  1846  				ProwConfig: config.ProwConfig{
  1847  					Plank: config.Plank{BuildClusterStatusFile: "gs://my-bucket/build-cluster-status.json"},
  1848  				},
  1849  				JobConfig: config.JobConfig{
  1850  					PresubmitsStatic: map[string][]config.Presubmit{
  1851  						"org1/repo1": {
  1852  							{
  1853  								JobBase: config.JobBase{
  1854  									Cluster: "build1",
  1855  								},
  1856  							}}}}},
  1857  			clusterStatusFile: fmt.Sprintf(`{"default": %q, "build1": %q, "build2": %q}`, plank.ClusterStatusReachable, plank.ClusterStatusReachable, plank.ClusterStatusError),
  1858  		},
  1859  		{
  1860  			name: "cluster validates with multiple clusters, specified is unreachable (just warn)",
  1861  			cfg: &config.Config{
  1862  				ProwConfig: config.ProwConfig{
  1863  					Plank: config.Plank{BuildClusterStatusFile: "gs://my-bucket/build-cluster-status.json"},
  1864  				},
  1865  				JobConfig: config.JobConfig{
  1866  					PresubmitsStatic: map[string][]config.Presubmit{
  1867  						"org1/repo1": {
  1868  							{
  1869  								JobBase: config.JobBase{
  1870  									Name:    "my-job",
  1871  									Cluster: "build2",
  1872  								},
  1873  							}}}}},
  1874  			clusterStatusFile: fmt.Sprintf(`{"default": %q, "build1": %q, "build2": %q}`, plank.ClusterStatusReachable, plank.ClusterStatusReachable, plank.ClusterStatusError),
  1875  		},
  1876  		{
  1877  			name: "cluster fails validation with multiple clusters, specified is unrecognized",
  1878  			cfg: &config.Config{
  1879  				ProwConfig: config.ProwConfig{
  1880  					Plank: config.Plank{BuildClusterStatusFile: "gs://my-bucket/build-cluster-status.json"},
  1881  				},
  1882  				JobConfig: config.JobConfig{
  1883  					PresubmitsStatic: map[string][]config.Presubmit{
  1884  						"org1/repo1": {
  1885  							{
  1886  								JobBase: config.JobBase{
  1887  									Name:    "my-job",
  1888  									Cluster: "build3",
  1889  								},
  1890  							}}}}},
  1891  			clusterStatusFile: fmt.Sprintf(`{"default": %q, "build1": %q, "build2": %q}`, plank.ClusterStatusReachable, plank.ClusterStatusReachable, plank.ClusterStatusError),
  1892  			expectedError:     "org1/repo1: job configuration for \"my-job\" specifies unknown 'cluster' value \"build3\"",
  1893  		},
  1894  		{
  1895  			name: "cluster validation skipped if status file does not exist yet",
  1896  			cfg: &config.Config{
  1897  				ProwConfig: config.ProwConfig{
  1898  					Plank: config.Plank{BuildClusterStatusFile: "gs://my-bucket/build-cluster-status.json"},
  1899  				},
  1900  				JobConfig: config.JobConfig{
  1901  					PresubmitsStatic: map[string][]config.Presubmit{
  1902  						"org1/repo1": {
  1903  							{
  1904  								JobBase: config.JobBase{
  1905  									Cluster: "build1",
  1906  								},
  1907  							}}}}},
  1908  			readError: os.ErrNotExist,
  1909  		},
  1910  	}
  1911  
  1912  	for i := range testCases {
  1913  		tc := testCases[i]
  1914  		t.Run(tc.name, func(t *testing.T) {
  1915  			t.Parallel()
  1916  			opener := fakeOpener{content: tc.clusterStatusFile, readError: tc.readError}
  1917  			errMsg := ""
  1918  			if err := validateCluster(tc.cfg, &opener); err != nil {
  1919  				errMsg = err.Error()
  1920  			}
  1921  			if errMsg != tc.expectedError {
  1922  				t.Errorf("expected error %q, got error %q", tc.expectedError, errMsg)
  1923  			}
  1924  		})
  1925  	}
  1926  }
  1927  
  1928  func TestValidateAdditionalProwConfigIsInOrgRepoDirectoryStructure(t *testing.T) {
  1929  	t.Parallel()
  1930  	const root = "root"
  1931  	const invalidConfig = `[]`
  1932  	const validGlobalConfig = `
  1933  sinker:
  1934    exclude_clusters:
  1935      - default
  1936  slack_reporter_configs:
  1937    '*':
  1938      channel: '#general-announcements'
  1939      job_states_to_report:
  1940        - failure
  1941        - error
  1942  	  - success
  1943  	report_template: Job {{.Spec.Job}} ended with status {{.Status.State}}.`
  1944  	const validOrgConfig = `
  1945  branch-protection:
  1946    orgs:
  1947      my-org:
  1948        protect: true
  1949  tide:
  1950    merge_method:
  1951      my-org: squash
  1952  slack_reporter_configs:
  1953    my-org:
  1954      channel: '#my-org-announcements'
  1955      job_states_to_report:
  1956        - failure
  1957        - error
  1958      report_template: Job {{.Spec.Job}} needs my-org maintainers attention.`
  1959  	const validRepoConfig = `
  1960  branch-protection:
  1961    orgs:
  1962      my-org:
  1963        repos:
  1964          my-repo:
  1965            protect: true
  1966  tide:
  1967    merge_method:
  1968      my-org/my-repo: squash
  1969  slack_reporter_configs:
  1970    my-org/my-repo:
  1971      channel: '#my-repo-announcements'
  1972      job_states_to_report:
  1973        - failure
  1974      report_template: Job {{.Spec.Job}} needs my-repo maintainers attention.`
  1975  	const validGlobalPluginsConfig = `
  1976  blunderbuss:
  1977    max_request_count: 2
  1978    request_count: 2
  1979    use_status_availability: true`
  1980  	const validOrgPluginsConfig = `
  1981  label:
  1982    restricted_labels:
  1983      my-org:
  1984      - label: cherry-pick-approved
  1985        allowed_teams:
  1986        - patch-managers
  1987  plugins:
  1988    my-org:
  1989      plugins:
  1990      - assign`
  1991  	const validRepoPluginsConfig = `
  1992  plugins:
  1993    my-org/my-repo:
  1994      plugins:
  1995      - assign`
  1996  
  1997  	tests := []struct {
  1998  		name string
  1999  		fs   fstest.MapFS
  2000  
  2001  		expectedErrorMessage string
  2002  	}{
  2003  		{
  2004  			name: "No configs, no error",
  2005  			fs:   testfs(map[string]string{root + "/OWNERS": "some-owners"}),
  2006  		},
  2007  		{
  2008  			name: "Config directly below root, no error",
  2009  			fs: testfs(map[string]string{
  2010  				root + "/cfg.yaml":     validGlobalConfig,
  2011  				root + "/plugins.yaml": validGlobalPluginsConfig,
  2012  			}),
  2013  		},
  2014  		{
  2015  			name: "Valid org config",
  2016  			fs: testfs(map[string]string{
  2017  				root + "/my-org/cfg.yaml":     validOrgConfig,
  2018  				root + "/my-org/plugins.yaml": validOrgPluginsConfig,
  2019  			}),
  2020  		},
  2021  		{
  2022  			name: "Valid org config for wrong org",
  2023  			fs: testfs(map[string]string{
  2024  				root + "/my-other-org/cfg.yaml":     validOrgConfig,
  2025  				root + "/my-other-org/plugins.yaml": validOrgPluginsConfig,
  2026  			}),
  2027  			expectedErrorMessage: `[config root/my-other-org/cfg.yaml is invalid: Must contain only config for org my-other-org, but contains config for org my-org, config root/my-other-org/plugins.yaml is invalid: Must contain only config for org my-other-org, but contains config for org my-org]`,
  2028  		},
  2029  		{
  2030  			name: "Invalid org config",
  2031  			fs: testfs(map[string]string{
  2032  				root + "/my-org/cfg.yaml":     invalidConfig,
  2033  				root + "/my-org/plugins.yaml": invalidConfig,
  2034  			}),
  2035  			expectedErrorMessage: `[failed to unmarshal root/my-org/cfg.yaml into *config.Config: error unmarshaling JSON: while decoding JSON: json: cannot unmarshal array into Go value of type config.Config, failed to unmarshal root/my-org/plugins.yaml into *plugins.Configuration: error unmarshaling JSON: while decoding JSON: json: cannot unmarshal array into Go value of type plugins.Configuration]`,
  2036  		},
  2037  		{
  2038  			name: "Repo config at org level",
  2039  			fs: testfs(map[string]string{
  2040  				root + "/my-org/cfg.yaml":     validRepoConfig,
  2041  				root + "/my-org/plugins.yaml": validRepoPluginsConfig,
  2042  			}),
  2043  			expectedErrorMessage: `[config root/my-org/cfg.yaml is invalid: Must contain only config for org my-org, but contains config for repo my-org/my-repo, config root/my-org/plugins.yaml is invalid: Must contain only config for org my-org, but contains config for repo my-org/my-repo]`,
  2044  		},
  2045  		{
  2046  			name: "Valid repo config",
  2047  			fs: testfs(map[string]string{
  2048  				root + "/my-org/my-repo/cfg.yaml":     validRepoConfig,
  2049  				root + "/my-org/my-repo/plugins.yaml": validRepoPluginsConfig,
  2050  			}),
  2051  		},
  2052  		{
  2053  			name: "Valid repo config for wrong repo",
  2054  			fs: testfs(map[string]string{
  2055  				root + "/my-org/my-other-repo/cfg.yaml":     validRepoConfig,
  2056  				root + "/my-org/my-other-repo/plugins.yaml": validRepoPluginsConfig,
  2057  			}),
  2058  			expectedErrorMessage: `[config root/my-org/my-other-repo/cfg.yaml is invalid: Must only contain config for repo my-org/my-other-repo, but contains config for repo my-org/my-repo, config root/my-org/my-other-repo/plugins.yaml is invalid: Must only contain config for repo my-org/my-other-repo, but contains config for repo my-org/my-repo]`,
  2059  		},
  2060  		{
  2061  			name: "Invalid repo config",
  2062  			fs: testfs(map[string]string{
  2063  				root + "/my-org/my-repo/cfg.yaml":     invalidConfig,
  2064  				root + "/my-org/my-repo/plugins.yaml": invalidConfig,
  2065  			}),
  2066  			expectedErrorMessage: `[failed to unmarshal root/my-org/my-repo/cfg.yaml into *config.Config: error unmarshaling JSON: while decoding JSON: json: cannot unmarshal array into Go value of type config.Config, failed to unmarshal root/my-org/my-repo/plugins.yaml into *plugins.Configuration: error unmarshaling JSON: while decoding JSON: json: cannot unmarshal array into Go value of type plugins.Configuration]`,
  2067  		},
  2068  		{
  2069  			name: "Org config at repo level",
  2070  			fs: testfs(map[string]string{
  2071  				root + "/my-org/my-repo/cfg.yaml":     validOrgConfig,
  2072  				root + "/my-org/my-repo/plugins.yaml": validOrgPluginsConfig,
  2073  			}),
  2074  			expectedErrorMessage: `[config root/my-org/my-repo/cfg.yaml is invalid: Must only contain config for repo my-org/my-repo, but contains config for org my-org, config root/my-org/my-repo/plugins.yaml is invalid: Must only contain config for repo my-org/my-repo, but contains config for org my-org]`,
  2075  		},
  2076  		{
  2077  			name: "Nested too deeply",
  2078  			fs: testfs(map[string]string{
  2079  				root + "/my-org/my-repo/nest/cfg.yaml":     validOrgConfig,
  2080  				root + "/my-org/my-repo/nest/plugins.yaml": validOrgPluginsConfig,
  2081  			}),
  2082  
  2083  			expectedErrorMessage: `[config root/my-org/my-repo/nest/cfg.yaml is at an invalid location. All configs must be below root. If they are org-specific, they must be in a folder named like the org. If they are repo-specific, they must be in a folder named like the repo below a folder named like the org., config root/my-org/my-repo/nest/plugins.yaml is at an invalid location. All configs must be below root. If they are org-specific, they must be in a folder named like the org. If they are repo-specific, they must be in a folder named like the repo below a folder named like the org.]`,
  2084  		},
  2085  	}
  2086  
  2087  	for _, tc := range tests {
  2088  		t.Run(tc.name, func(t *testing.T) {
  2089  			var errMsg string
  2090  			err := validateAdditionalProwConfigIsInOrgRepoDirectoryStructure(tc.fs, []string{root}, []string{root}, "cfg.yaml", "plugins.yaml")
  2091  			if err != nil {
  2092  				errMsg = err.Error()
  2093  			}
  2094  			if tc.expectedErrorMessage != errMsg {
  2095  				t.Errorf("expected error %s, got %s", tc.expectedErrorMessage, errMsg)
  2096  			}
  2097  		})
  2098  	}
  2099  }
  2100  
  2101  func testfs(files map[string]string) fstest.MapFS {
  2102  	filesystem := fstest.MapFS{}
  2103  	for path, content := range files {
  2104  		filesystem[path] = &fstest.MapFile{Data: []byte(content)}
  2105  	}
  2106  	return filesystem
  2107  }
  2108  
  2109  func TestValidateUnmanagedBranchprotectionConfigDoesntHaveSubconfig(t *testing.T) {
  2110  	t.Parallel()
  2111  	bpConfigWithSettingsOnAllLayers := func(m ...func(*config.BranchProtection)) config.BranchProtection {
  2112  		cfg := config.BranchProtection{
  2113  			Policy: config.Policy{Exclude: []string{"some-regex"}},
  2114  			Orgs: map[string]config.Org{
  2115  				"my-org": {
  2116  					Policy: config.Policy{Exclude: []string{"some-regex"}},
  2117  					Repos: map[string]config.Repo{
  2118  						"my-repo": {
  2119  							Policy: config.Policy{Exclude: []string{"some-regex"}},
  2120  							Branches: map[string]config.Branch{
  2121  								"my-branch": {
  2122  									Policy: config.Policy{Exclude: []string{"some-regex"}},
  2123  								},
  2124  							},
  2125  						},
  2126  					},
  2127  				},
  2128  			},
  2129  		}
  2130  
  2131  		for _, modify := range m {
  2132  			modify(&cfg)
  2133  		}
  2134  
  2135  		return cfg
  2136  	}
  2137  
  2138  	testCases := []struct {
  2139  		name   string
  2140  		config config.BranchProtection
  2141  
  2142  		expectedErrorMsg string
  2143  	}{
  2144  		{
  2145  			name: "Empty config, no error",
  2146  		},
  2147  		{
  2148  			name: "Globally disabled, errors for global and org config",
  2149  			config: bpConfigWithSettingsOnAllLayers(func(bp *config.BranchProtection) {
  2150  				bp.Unmanaged = utilpointer.Bool(true)
  2151  			}),
  2152  
  2153  			expectedErrorMsg: `[branch protection is globally set to unmanaged, but has configuration, branch protection config is globally set to unmanaged but has configuration for org my-org without setting the org to unmanaged: false]`,
  2154  		},
  2155  		{
  2156  			name: "Org-level disabled, errors for org policy and repos",
  2157  			config: bpConfigWithSettingsOnAllLayers(func(bp *config.BranchProtection) {
  2158  				p := bp.Orgs["my-org"]
  2159  				p.Unmanaged = utilpointer.Bool(true)
  2160  				bp.Orgs["my-org"] = p
  2161  			}),
  2162  
  2163  			expectedErrorMsg: `[branch protection config for org my-org is set to unmanaged, but it defines settings, branch protection config for repo my-org/my-repo is defined, but branch protection is unmanaged for org my-org without setting the repo to unmanaged: false]`,
  2164  		},
  2165  
  2166  		{
  2167  			name: "Repo-level disabled, errors for repo policy and branches",
  2168  			config: bpConfigWithSettingsOnAllLayers(func(bp *config.BranchProtection) {
  2169  				p := bp.Orgs["my-org"].Repos["my-repo"]
  2170  				p.Unmanaged = utilpointer.Bool(true)
  2171  				bp.Orgs["my-org"].Repos["my-repo"] = p
  2172  			}),
  2173  
  2174  			expectedErrorMsg: `[branch protection config for repo my-org/my-repo is set to unmanaged, but it defines settings, branch protection for repo my-org/my-repo is set to unmanaged, but it defines settings for branch my-branch without setting the branch to unmanaged: false]`,
  2175  		},
  2176  
  2177  		{
  2178  			name: "Branch-level disabled, errors for branch policy",
  2179  			config: bpConfigWithSettingsOnAllLayers(func(bp *config.BranchProtection) {
  2180  				p := bp.Orgs["my-org"].Repos["my-repo"].Branches["my-branch"]
  2181  				p.Unmanaged = utilpointer.Bool(true)
  2182  				bp.Orgs["my-org"].Repos["my-repo"].Branches["my-branch"] = p
  2183  			}),
  2184  
  2185  			expectedErrorMsg: `branch protection config for branch my-branch in repo my-org/my-repo is set to unmanaged but defines settings`,
  2186  		},
  2187  		{
  2188  			name: "unmanaged repo level is overridden by branch level, no errors",
  2189  			config: bpConfigWithSettingsOnAllLayers(func(bp *config.BranchProtection) {
  2190  				repoP := bp.Orgs["my-org"].Repos["my-repo"]
  2191  				repoP.Unmanaged = utilpointer.Bool(true)
  2192  				bp.Orgs["my-org"].Repos["my-repo"] = repoP
  2193  				p := bp.Orgs["my-org"].Repos["my-repo"].Branches["my-branch"]
  2194  				p.Unmanaged = utilpointer.Bool(false)
  2195  				bp.Orgs["my-org"].Repos["my-repo"].Branches["my-branch"] = p
  2196  			}),
  2197  		},
  2198  	}
  2199  
  2200  	for _, tc := range testCases {
  2201  		var errMsg string
  2202  		err := validateUnmanagedBranchprotectionConfigDoesntHaveSubconfig(tc.config)
  2203  		if err != nil {
  2204  			errMsg = err.Error()
  2205  		}
  2206  		if tc.expectedErrorMsg != errMsg {
  2207  			t.Errorf("expected error message\n%s\ngot error message\n%s", tc.expectedErrorMsg, errMsg)
  2208  		}
  2209  	}
  2210  }
  2211  
  2212  type fakeGhAppListingClient struct {
  2213  	installations []github.AppInstallation
  2214  }
  2215  
  2216  func (f *fakeGhAppListingClient) ListAppInstallations() ([]github.AppInstallation, error) {
  2217  	return f.installations, nil
  2218  }
  2219  
  2220  func TestValidateGitHubAppIsInstalled(t *testing.T) {
  2221  	t.Parallel()
  2222  	testCases := []struct {
  2223  		name          string
  2224  		allRepos      sets.Set[string]
  2225  		installations []github.AppInstallation
  2226  
  2227  		expectedErrorMsg string
  2228  	}{
  2229  		{
  2230  			name:     "Installations exist",
  2231  			allRepos: sets.New[string]("org/repo", "org-a/repo-a", "org-b/repo-b"),
  2232  			installations: []github.AppInstallation{
  2233  				{Account: github.User{Login: "org"}},
  2234  				{Account: github.User{Login: "org-a"}},
  2235  				{Account: github.User{Login: "org-b"}},
  2236  			},
  2237  		},
  2238  		{
  2239  			name:     "Some installations exist",
  2240  			allRepos: sets.New[string]("org/repo", "org-a/repo-a", "org-b/repo-b"),
  2241  			installations: []github.AppInstallation{
  2242  				{Account: github.User{Login: "org"}},
  2243  				{Account: github.User{Login: "org-a"}},
  2244  			},
  2245  
  2246  			expectedErrorMsg: `There is configuration for the GitHub org "org-b" but the GitHub app is not installed there`,
  2247  		},
  2248  		{
  2249  			name:     "No installations exist",
  2250  			allRepos: sets.New[string]("org/repo", "org-a/repo-a", "org-b/repo-b"),
  2251  
  2252  			expectedErrorMsg: `[There is configuration for the GitHub org "org-a" but the GitHub app is not installed there, There is configuration for the GitHub org "org-b" but the GitHub app is not installed there, There is configuration for the GitHub org "org" but the GitHub app is not installed there]`,
  2253  		},
  2254  	}
  2255  
  2256  	for _, tc := range testCases {
  2257  		t.Run(tc.name, func(t *testing.T) {
  2258  			var actualErrMsg string
  2259  			if err := validateGitHubAppIsInstalled(&fakeGhAppListingClient{installations: tc.installations}, tc.allRepos); err != nil {
  2260  				actualErrMsg = err.Error()
  2261  			}
  2262  
  2263  			if actualErrMsg != tc.expectedErrorMsg {
  2264  				t.Errorf("expected error %q, got error %q", tc.expectedErrorMsg, actualErrMsg)
  2265  			}
  2266  		})
  2267  	}
  2268  }
  2269  
  2270  func TestVerifyLabelPlugin(t *testing.T) {
  2271  	t.Parallel()
  2272  	testCases := []struct {
  2273  		name             string
  2274  		label            plugins.Label
  2275  		expectedErrorMsg string
  2276  	}{
  2277  		{
  2278  			name: "empty label config is valid",
  2279  		},
  2280  		{
  2281  			name: "cannot use the empty string as label name",
  2282  			label: plugins.Label{
  2283  				RestrictedLabels: map[string][]plugins.RestrictedLabel{
  2284  					"openshift/machine-config-operator": {
  2285  						{
  2286  							Label:        "",
  2287  							AllowedTeams: []string{"openshift-patch-managers"},
  2288  						},
  2289  						{
  2290  							Label:        "backport-risk-assessed",
  2291  							AllowedUsers: []string{"kikisdeliveryservice", "sinnykumari", "yuqi-zhang"},
  2292  						},
  2293  					},
  2294  				},
  2295  			},
  2296  			expectedErrorMsg: "the following orgs or repos have configuration of label plugin using the empty string as label name in restricted labels: openshift/machine-config-operator",
  2297  		},
  2298  		{
  2299  			name: "valid after removing the restricted labels for the empty string",
  2300  			label: plugins.Label{
  2301  				RestrictedLabels: map[string][]plugins.RestrictedLabel{
  2302  					"openshift/machine-config-operator": {
  2303  						{
  2304  							Label:        "backport-risk-assessed",
  2305  							AllowedUsers: []string{"kikisdeliveryservice", "sinnykumari", "yuqi-zhang"},
  2306  						},
  2307  					},
  2308  				},
  2309  			},
  2310  		},
  2311  		{
  2312  			name: "two invalid label configs",
  2313  			label: plugins.Label{
  2314  				RestrictedLabels: map[string][]plugins.RestrictedLabel{
  2315  					"orgRepo1": {
  2316  						{
  2317  							Label:        "",
  2318  							AllowedTeams: []string{"some-team"},
  2319  						},
  2320  					},
  2321  					"orgRepo2": {
  2322  						{
  2323  							Label:        "",
  2324  							AllowedUsers: []string{"some-user"},
  2325  						},
  2326  					},
  2327  				},
  2328  			},
  2329  			expectedErrorMsg: "the following orgs or repos have configuration of label plugin using the empty string as label name in restricted labels: orgRepo1, orgRepo2",
  2330  		},
  2331  		{
  2332  			name: "invalid when additional and restricted labels are the same",
  2333  			label: plugins.Label{
  2334  				AdditionalLabels: []string{"cherry-pick-approved"},
  2335  				RestrictedLabels: map[string][]plugins.RestrictedLabel{
  2336  					"orgRepo1": {
  2337  						{
  2338  							Label: "cherry-pick-approved",
  2339  						},
  2340  					},
  2341  				},
  2342  			},
  2343  			expectedErrorMsg: "the following orgs or repos have configuration of label plugin using the restricted label cherry-pick-approved which is also configured as an additional label: orgRepo1",
  2344  		},
  2345  		{
  2346  			name: "invalid when additional and restricted labels are the same in multiple orgRepos and empty string",
  2347  			label: plugins.Label{
  2348  				AdditionalLabels: []string{"cherry-pick-approved"},
  2349  				RestrictedLabels: map[string][]plugins.RestrictedLabel{
  2350  					"orgRepo1": {
  2351  						{
  2352  							Label: "cherry-pick-approved",
  2353  						},
  2354  					},
  2355  					"orgRepo2": {
  2356  						{
  2357  							Label: "",
  2358  						},
  2359  					},
  2360  					"orgRepo3": {
  2361  						{
  2362  							Label: "cherry-pick-approved",
  2363  						},
  2364  					},
  2365  				},
  2366  			},
  2367  			expectedErrorMsg: "[the following orgs or repos have configuration of label plugin using the restricted label cherry-pick-approved which is also configured as an additional label: orgRepo1, orgRepo3, " +
  2368  				"the following orgs or repos have configuration of label plugin using the empty string as label name in restricted labels: orgRepo2]",
  2369  		},
  2370  	}
  2371  
  2372  	for _, tc := range testCases {
  2373  		t.Run(tc.name, func(t *testing.T) {
  2374  			var actualErrMsg string
  2375  			if err := verifyLabelPlugin(tc.label); err != nil {
  2376  				actualErrMsg = err.Error()
  2377  			}
  2378  			if actualErrMsg != tc.expectedErrorMsg {
  2379  				t.Errorf("expected error %q, got error %q", tc.expectedErrorMsg, actualErrMsg)
  2380  			}
  2381  		})
  2382  	}
  2383  }
  2384  
  2385  func TestValidateRequiredJobAnnotations(t *testing.T) {
  2386  	tc := []struct {
  2387  		name                string
  2388  		presubmits          []config.Presubmit
  2389  		postsubmits         []config.Postsubmit
  2390  		periodics           []config.Periodic
  2391  		expectedErr         bool
  2392  		expectedAnnotations []string
  2393  	}{
  2394  		{
  2395  			name: "no annotation is required, pass",
  2396  			presubmits: []config.Presubmit{
  2397  				{
  2398  					JobBase: config.JobBase{},
  2399  				},
  2400  			},
  2401  			postsubmits: []config.Postsubmit{
  2402  				{
  2403  					JobBase: config.JobBase{
  2404  						Annotations: map[string]string{"prow.k8s.io/cat": "meow"},
  2405  					},
  2406  				},
  2407  			},
  2408  			periodics: []config.Periodic{
  2409  				{
  2410  					JobBase: config.JobBase{},
  2411  				},
  2412  			},
  2413  			expectedErr:         false,
  2414  			expectedAnnotations: nil,
  2415  		},
  2416  		{
  2417  			name: "jobs don't have required annotation, fail",
  2418  			presubmits: []config.Presubmit{
  2419  				{
  2420  					JobBase: config.JobBase{},
  2421  				},
  2422  			},
  2423  			postsubmits: []config.Postsubmit{
  2424  				{
  2425  					JobBase: config.JobBase{
  2426  						Annotations: map[string]string{"prow.k8s.io/cat": "meow"},
  2427  					},
  2428  				},
  2429  			},
  2430  			periodics: []config.Periodic{
  2431  				{
  2432  					JobBase: config.JobBase{},
  2433  				},
  2434  			},
  2435  			expectedAnnotations: []string{"prow.k8s.io/maintainer"},
  2436  			expectedErr:         true,
  2437  		},
  2438  		{
  2439  			name: "jobs have required annotations, pass",
  2440  			presubmits: []config.Presubmit{
  2441  				{
  2442  					JobBase: config.JobBase{
  2443  						Annotations: map[string]string{"prow.k8s.io/maintainer": "job-maintainer"},
  2444  					},
  2445  				},
  2446  			},
  2447  			postsubmits: []config.Postsubmit{
  2448  				{
  2449  					JobBase: config.JobBase{
  2450  						Annotations: map[string]string{"prow.k8s.io/maintainer": "job-maintainer"},
  2451  					},
  2452  				},
  2453  			},
  2454  			periodics: []config.Periodic{
  2455  				{
  2456  					JobBase: config.JobBase{
  2457  						Annotations: map[string]string{"prow.k8s.io/maintainer": "job-maintainer"},
  2458  					},
  2459  				},
  2460  			},
  2461  			expectedAnnotations: []string{"prow.k8s.io/maintainer"},
  2462  			expectedErr:         false,
  2463  		},
  2464  	}
  2465  
  2466  	for _, c := range tc {
  2467  		t.Run(c.name, func(t *testing.T) {
  2468  			jcfg := config.JobConfig{
  2469  				PresubmitsStatic:  map[string][]config.Presubmit{"org/repo": c.presubmits},
  2470  				PostsubmitsStatic: map[string][]config.Postsubmit{"org/repo": c.postsubmits},
  2471  				Periodics:         c.periodics,
  2472  			}
  2473  			err := validateRequiredJobAnnotations(c.expectedAnnotations, jcfg)
  2474  			if c.expectedErr && err == nil {
  2475  				t.Errorf("Expected error but got none")
  2476  			}
  2477  			if !c.expectedErr && err != nil {
  2478  				t.Errorf("Got error but didn't expect one: %v", err)
  2479  			}
  2480  		})
  2481  	}
  2482  }