sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/plugins/config_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 plugins
    18  
    19  import (
    20  	"errors"
    21  	"fmt"
    22  	"reflect"
    23  	"strings"
    24  	"testing"
    25  	"time"
    26  
    27  	"github.com/google/go-cmp/cmp"
    28  	fuzz "github.com/google/gofuzz"
    29  
    30  	apiequality "k8s.io/apimachinery/pkg/api/equality"
    31  	"k8s.io/apimachinery/pkg/util/diff"
    32  	"k8s.io/apimachinery/pkg/util/sets"
    33  	utilpointer "k8s.io/utils/pointer"
    34  	"sigs.k8s.io/yaml"
    35  
    36  	"sigs.k8s.io/prow/pkg/bugzilla"
    37  	"sigs.k8s.io/prow/pkg/plugins/ownersconfig"
    38  )
    39  
    40  func TestValidateExternalPlugins(t *testing.T) {
    41  	tests := []struct {
    42  		name        string
    43  		plugins     map[string][]ExternalPlugin
    44  		expectedErr error
    45  	}{
    46  		{
    47  			name: "valid config",
    48  			plugins: map[string][]ExternalPlugin{
    49  				"kubernetes/test-infra": {
    50  					{
    51  						Name: "cherrypick",
    52  					},
    53  					{
    54  						Name: "configupdater",
    55  					},
    56  					{
    57  						Name: "tetris",
    58  					},
    59  				},
    60  				"kubernetes": {
    61  					{
    62  						Name: "coffeemachine",
    63  					},
    64  					{
    65  						Name: "blender",
    66  					},
    67  				},
    68  			},
    69  			expectedErr: nil,
    70  		},
    71  		{
    72  			name: "invalid config",
    73  			plugins: map[string][]ExternalPlugin{
    74  				"kubernetes/test-infra": {
    75  					{
    76  						Name: "cherrypick",
    77  					},
    78  					{
    79  						Name: "configupdater",
    80  					},
    81  					{
    82  						Name: "tetris",
    83  					},
    84  				},
    85  				"kubernetes": {
    86  					{
    87  						Name: "coffeemachine",
    88  					},
    89  					{
    90  						Name: "tetris",
    91  					},
    92  				},
    93  			},
    94  			expectedErr: errors.New("invalid plugin configuration:\n\texternal plugins [tetris] are duplicated for kubernetes/test-infra and kubernetes"),
    95  		},
    96  	}
    97  
    98  	for _, test := range tests {
    99  		t.Logf("Running scenario %q", test.name)
   100  
   101  		err := validateExternalPlugins(test.plugins)
   102  		if !reflect.DeepEqual(err, test.expectedErr) {
   103  			t.Errorf("unexpected error: %v, expected: %v", err, test.expectedErr)
   104  		}
   105  	}
   106  }
   107  
   108  func TestOwnersFilenames(t *testing.T) {
   109  	cases := []struct {
   110  		org      string
   111  		repo     string
   112  		config   Owners
   113  		expected ownersconfig.Filenames
   114  	}{
   115  		{
   116  			org:  "kubernetes",
   117  			repo: "test-infra",
   118  			config: Owners{
   119  				Filenames: map[string]ownersconfig.Filenames{
   120  					"kubernetes":            {Owners: "OWNERS", OwnersAliases: "OWNERS_ALIASES"},
   121  					"kubernetes/test-infra": {Owners: ".OWNERS", OwnersAliases: ".OWNERS_ALIASES"},
   122  				},
   123  			},
   124  			expected: ownersconfig.Filenames{
   125  				Owners: ".OWNERS", OwnersAliases: ".OWNERS_ALIASES",
   126  			},
   127  		},
   128  		{
   129  			org:  "kubernetes",
   130  			repo: "",
   131  			config: Owners{
   132  				Filenames: map[string]ownersconfig.Filenames{
   133  					"kubernetes":            {Owners: "OWNERS", OwnersAliases: "OWNERS_ALIASES"},
   134  					"kubernetes/test-infra": {Owners: ".OWNERS", OwnersAliases: ".OWNERS_ALIASES"},
   135  				},
   136  			},
   137  			expected: ownersconfig.Filenames{
   138  				Owners: "OWNERS", OwnersAliases: "OWNERS_ALIASES",
   139  			},
   140  		},
   141  	}
   142  
   143  	for _, tc := range cases {
   144  		cfg := Configuration{
   145  			Owners: tc.config,
   146  		}
   147  		actual := cfg.OwnersFilenames(tc.org, tc.repo)
   148  		if actual != tc.expected {
   149  			t.Errorf("%s/%s: unexpected value. Diff: %v", tc.org, tc.repo, diff.ObjectDiff(actual, tc.expected))
   150  		}
   151  	}
   152  }
   153  
   154  func TestSetDefault_Maps(t *testing.T) {
   155  	cases := []struct {
   156  		name     string
   157  		config   ConfigUpdater
   158  		expected map[string]ConfigMapSpec
   159  	}{
   160  		{
   161  			name: "nothing",
   162  			expected: map[string]ConfigMapSpec{
   163  				"config/prow/config.yaml":  {Name: "config", Clusters: map[string][]string{"default": {""}}},
   164  				"config/prow/plugins.yaml": {Name: "plugins", Clusters: map[string][]string{"default": {""}}},
   165  			},
   166  		},
   167  		{
   168  			name: "basic",
   169  			config: ConfigUpdater{
   170  				Maps: map[string]ConfigMapSpec{
   171  					"hello.yaml": {Name: "my-cm"},
   172  					"world.yaml": {Name: "you-cm"},
   173  				},
   174  			},
   175  			expected: map[string]ConfigMapSpec{
   176  				"hello.yaml": {Name: "my-cm", Clusters: map[string][]string{"default": {""}}},
   177  				"world.yaml": {Name: "you-cm", Clusters: map[string][]string{"default": {""}}},
   178  			},
   179  		},
   180  		{
   181  			name: "both current and deprecated",
   182  			config: ConfigUpdater{
   183  				Maps: map[string]ConfigMapSpec{
   184  					"config.yaml":        {Name: "overwrite-config"},
   185  					"plugins.yaml":       {Name: "overwrite-plugins"},
   186  					"unconflicting.yaml": {Name: "ignored"},
   187  				},
   188  			},
   189  			expected: map[string]ConfigMapSpec{
   190  				"config.yaml":        {Name: "overwrite-config", Clusters: map[string][]string{"default": {""}}},
   191  				"plugins.yaml":       {Name: "overwrite-plugins", Clusters: map[string][]string{"default": {""}}},
   192  				"unconflicting.yaml": {Name: "ignored", Clusters: map[string][]string{"default": {""}}},
   193  			},
   194  		},
   195  	}
   196  	for _, tc := range cases {
   197  		cfg := Configuration{
   198  			ConfigUpdater: tc.config,
   199  		}
   200  		cfg.setDefaults()
   201  		actual := cfg.ConfigUpdater.Maps
   202  		if len(actual) != len(tc.expected) {
   203  			t.Errorf("%s: actual and expected have different keys: %v %v", tc.name, actual, tc.expected)
   204  			continue
   205  		}
   206  		for k, n := range tc.expected {
   207  			if an := actual[k]; !reflect.DeepEqual(an, n) {
   208  				t.Errorf("%s - %s: unexpected value. Diff: %v", tc.name, k, diff.ObjectReflectDiff(an, n))
   209  			}
   210  		}
   211  	}
   212  }
   213  
   214  func TestTriggerFor(t *testing.T) {
   215  	config := Configuration{
   216  		Triggers: []Trigger{
   217  			{
   218  				Repos:      []string{"kuber"},
   219  				TrustedOrg: "org1",
   220  			},
   221  			{
   222  				Repos:      []string{"k8s/k8s", "k8s/kuber"},
   223  				TrustedOrg: "org2",
   224  			},
   225  			{
   226  				Repos:      []string{"k8s/t-i"},
   227  				TrustedOrg: "org3",
   228  			},
   229  			{
   230  				Repos:      []string{"kuber/utils"},
   231  				TrustedOrg: "org4",
   232  			},
   233  		},
   234  	}
   235  	config.setDefaults()
   236  
   237  	testCases := []struct {
   238  		name            string
   239  		org, repo       string
   240  		expectedTrusted string
   241  		check           func(Trigger) error
   242  	}{
   243  		{
   244  			name:            "org trigger",
   245  			org:             "kuber",
   246  			repo:            "kuber",
   247  			expectedTrusted: "org1",
   248  		},
   249  		{
   250  			name:            "repo trigger",
   251  			org:             "k8s",
   252  			repo:            "t-i",
   253  			expectedTrusted: "org3",
   254  		},
   255  		{
   256  			name:            "repo trigger",
   257  			org:             "kuber",
   258  			repo:            "utils",
   259  			expectedTrusted: "org4",
   260  		},
   261  		{
   262  			name: "default trigger",
   263  			org:  "other",
   264  			repo: "other",
   265  		},
   266  	}
   267  	for i := range testCases {
   268  		tc := testCases[i]
   269  		t.Run(tc.name, func(t *testing.T) {
   270  			actual := config.TriggerFor(tc.org, tc.repo)
   271  			if tc.expectedTrusted != actual.TrustedOrg {
   272  				t.Errorf("expected TrustedOrg to be %q, but got %q", tc.expectedTrusted, actual.TrustedOrg)
   273  			}
   274  		})
   275  	}
   276  }
   277  
   278  func TestSetApproveDefaults(t *testing.T) {
   279  	c := &Configuration{
   280  		Approve: []Approve{
   281  			{
   282  				Repos: []string{
   283  					"kubernetes/kubernetes",
   284  					"kubernetes-client",
   285  				},
   286  			},
   287  			{
   288  				Repos: []string{
   289  					"kubernetes-sigs/cluster-api",
   290  				},
   291  				CommandHelpLink: "https://prow.k8s.io/command-help",
   292  				PrProcessLink:   "https://github.com/kubernetes/community/blob/427ccfbc7d423d8763ed756f3b8c888b7de3cf34/contributors/guide/pull-requests.md",
   293  			},
   294  		},
   295  	}
   296  
   297  	tests := []struct {
   298  		name                    string
   299  		org                     string
   300  		repo                    string
   301  		expectedCommandHelpLink string
   302  		expectedPrProcessLink   string
   303  	}{
   304  		{
   305  			name:                    "default",
   306  			org:                     "kubernetes",
   307  			repo:                    "kubernetes",
   308  			expectedCommandHelpLink: "https://go.k8s.io/bot-commands",
   309  			expectedPrProcessLink:   "https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process",
   310  		},
   311  		{
   312  			name:                    "overwrite",
   313  			org:                     "kubernetes-sigs",
   314  			repo:                    "cluster-api",
   315  			expectedCommandHelpLink: "https://prow.k8s.io/command-help",
   316  			expectedPrProcessLink:   "https://github.com/kubernetes/community/blob/427ccfbc7d423d8763ed756f3b8c888b7de3cf34/contributors/guide/pull-requests.md",
   317  		},
   318  		{
   319  			name:                    "default for repo without approve plugin config",
   320  			org:                     "kubernetes",
   321  			repo:                    "website",
   322  			expectedCommandHelpLink: "https://go.k8s.io/bot-commands",
   323  			expectedPrProcessLink:   "https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process",
   324  		},
   325  	}
   326  
   327  	for _, test := range tests {
   328  
   329  		a := c.ApproveFor(test.org, test.repo)
   330  
   331  		if a.CommandHelpLink != test.expectedCommandHelpLink {
   332  			t.Errorf("unexpected commandHelpLink: %s, expected: %s", a.CommandHelpLink, test.expectedCommandHelpLink)
   333  		}
   334  
   335  		if a.PrProcessLink != test.expectedPrProcessLink {
   336  			t.Errorf("unexpected prProcessLink: %s, expected: %s", a.PrProcessLink, test.expectedPrProcessLink)
   337  		}
   338  	}
   339  }
   340  
   341  func TestSetHelpDefaults(t *testing.T) {
   342  	tests := []struct {
   343  		name              string
   344  		helpGuidelinesURL string
   345  
   346  		expectedHelpGuidelinesURL string
   347  	}{
   348  		{
   349  			name:                      "default",
   350  			helpGuidelinesURL:         "",
   351  			expectedHelpGuidelinesURL: "https://git.k8s.io/community/contributors/guide/help-wanted.md",
   352  		},
   353  		{
   354  			name:                      "overwrite",
   355  			helpGuidelinesURL:         "https://github.com/kubernetes/community/blob/master/contributors/guide/help-wanted.md",
   356  			expectedHelpGuidelinesURL: "https://github.com/kubernetes/community/blob/master/contributors/guide/help-wanted.md",
   357  		},
   358  	}
   359  
   360  	for _, test := range tests {
   361  		c := &Configuration{
   362  			Help: Help{
   363  				HelpGuidelinesURL: test.helpGuidelinesURL,
   364  			},
   365  		}
   366  
   367  		c.setDefaults()
   368  
   369  		if c.Help.HelpGuidelinesURL != test.expectedHelpGuidelinesURL {
   370  			t.Errorf("unexpected help_guidelines_url: %s, expected: %s", c.Help.HelpGuidelinesURL, test.expectedHelpGuidelinesURL)
   371  		}
   372  	}
   373  }
   374  
   375  func TestSetTriggerDefaults(t *testing.T) {
   376  	tests := []struct {
   377  		name string
   378  
   379  		trustedOrg string
   380  		joinOrgURL string
   381  
   382  		expectedTrustedOrg string
   383  		expectedJoinOrgURL string
   384  	}{
   385  		{
   386  			name: "url defaults to org",
   387  
   388  			trustedOrg: "kubernetes",
   389  			joinOrgURL: "",
   390  
   391  			expectedTrustedOrg: "kubernetes",
   392  			expectedJoinOrgURL: "https://github.com/orgs/kubernetes/people",
   393  		},
   394  		{
   395  			name: "both org and url are set",
   396  
   397  			trustedOrg: "kubernetes",
   398  			joinOrgURL: "https://git.k8s.io/community/community-membership.md#member",
   399  
   400  			expectedTrustedOrg: "kubernetes",
   401  			expectedJoinOrgURL: "https://git.k8s.io/community/community-membership.md#member",
   402  		},
   403  		{
   404  			name: "only url is set",
   405  
   406  			trustedOrg: "",
   407  			joinOrgURL: "https://git.k8s.io/community/community-membership.md#member",
   408  
   409  			expectedTrustedOrg: "",
   410  			expectedJoinOrgURL: "https://git.k8s.io/community/community-membership.md#member",
   411  		},
   412  		{
   413  			name: "nothing is set",
   414  
   415  			trustedOrg: "",
   416  			joinOrgURL: "",
   417  
   418  			expectedTrustedOrg: "",
   419  			expectedJoinOrgURL: "",
   420  		},
   421  	}
   422  
   423  	for _, test := range tests {
   424  		c := &Configuration{
   425  			Triggers: []Trigger{
   426  				{
   427  					TrustedOrg: test.trustedOrg,
   428  					JoinOrgURL: test.joinOrgURL,
   429  				},
   430  			},
   431  		}
   432  
   433  		c.setDefaults()
   434  
   435  		if c.Triggers[0].TrustedOrg != test.expectedTrustedOrg {
   436  			t.Errorf("unexpected trusted_org: %s, expected: %s", c.Triggers[0].TrustedOrg, test.expectedTrustedOrg)
   437  		}
   438  		if c.Triggers[0].JoinOrgURL != test.expectedJoinOrgURL {
   439  			t.Errorf("unexpected join_org_url: %s, expected: %s", c.Triggers[0].JoinOrgURL, test.expectedJoinOrgURL)
   440  		}
   441  	}
   442  }
   443  
   444  func TestSetCherryPickUnapprovedDefaults(t *testing.T) {
   445  	defaultBranchRegexp := `^release-.*$`
   446  	defaultComment := `This PR is not for the master branch but does not have the ` + "`cherry-pick-approved`" + `  label. Adding the ` + "`do-not-merge/cherry-pick-not-approved`" + `  label.`
   447  
   448  	testcases := []struct {
   449  		name string
   450  
   451  		branchRegexp string
   452  		comment      string
   453  
   454  		expectedBranchRegexp string
   455  		expectedComment      string
   456  	}{
   457  		{
   458  			name:                 "none of branchRegexp and comment are set",
   459  			branchRegexp:         "",
   460  			comment:              "",
   461  			expectedBranchRegexp: defaultBranchRegexp,
   462  			expectedComment:      defaultComment,
   463  		},
   464  		{
   465  			name:                 "only branchRegexp is set",
   466  			branchRegexp:         `release-1.1.*$`,
   467  			comment:              "",
   468  			expectedBranchRegexp: `release-1.1.*$`,
   469  			expectedComment:      defaultComment,
   470  		},
   471  		{
   472  			name:                 "only comment is set",
   473  			branchRegexp:         "",
   474  			comment:              "custom comment",
   475  			expectedBranchRegexp: defaultBranchRegexp,
   476  			expectedComment:      "custom comment",
   477  		},
   478  		{
   479  			name:                 "both branchRegexp and comment are set",
   480  			branchRegexp:         `release-1.1.*$`,
   481  			comment:              "custom comment",
   482  			expectedBranchRegexp: `release-1.1.*$`,
   483  			expectedComment:      "custom comment",
   484  		},
   485  	}
   486  
   487  	for _, tc := range testcases {
   488  		c := &Configuration{
   489  			CherryPickUnapproved: CherryPickUnapproved{
   490  				BranchRegexp: tc.branchRegexp,
   491  				Comment:      tc.comment,
   492  			},
   493  		}
   494  
   495  		c.setDefaults()
   496  
   497  		if c.CherryPickUnapproved.BranchRegexp != tc.expectedBranchRegexp {
   498  			t.Errorf("unexpected branchRegexp: %s, expected: %s", c.CherryPickUnapproved.BranchRegexp, tc.expectedBranchRegexp)
   499  		}
   500  		if c.CherryPickUnapproved.Comment != tc.expectedComment {
   501  			t.Errorf("unexpected comment: %s, expected: %s", c.CherryPickUnapproved.Comment, tc.expectedComment)
   502  		}
   503  	}
   504  }
   505  
   506  func TestOptionsForItem(t *testing.T) {
   507  	open := true
   508  	one, two := "v1", "v2"
   509  	var testCases = []struct {
   510  		name     string
   511  		item     string
   512  		config   map[string]BugzillaBranchOptions
   513  		expected BugzillaBranchOptions
   514  	}{
   515  		{
   516  			name:     "no config means no options",
   517  			item:     "item",
   518  			config:   map[string]BugzillaBranchOptions{},
   519  			expected: BugzillaBranchOptions{},
   520  		},
   521  		{
   522  			name:     "unrelated config means no options",
   523  			item:     "item",
   524  			config:   map[string]BugzillaBranchOptions{"other": {IsOpen: &open, TargetRelease: &one}},
   525  			expected: BugzillaBranchOptions{},
   526  		},
   527  		{
   528  			name:     "global config resolves to options",
   529  			item:     "item",
   530  			config:   map[string]BugzillaBranchOptions{"*": {IsOpen: &open, TargetRelease: &one}},
   531  			expected: BugzillaBranchOptions{IsOpen: &open, TargetRelease: &one},
   532  		},
   533  		{
   534  			name:     "specific config resolves to options",
   535  			item:     "item",
   536  			config:   map[string]BugzillaBranchOptions{"item": {IsOpen: &open, TargetRelease: &one}},
   537  			expected: BugzillaBranchOptions{IsOpen: &open, TargetRelease: &one},
   538  		},
   539  		{
   540  			name: "global and specific config resolves to options that favor specificity",
   541  			item: "item",
   542  			config: map[string]BugzillaBranchOptions{
   543  				"*":    {IsOpen: &open, TargetRelease: &one},
   544  				"item": {TargetRelease: &two},
   545  			},
   546  			expected: BugzillaBranchOptions{IsOpen: &open, TargetRelease: &two},
   547  		},
   548  	}
   549  
   550  	for _, testCase := range testCases {
   551  		t.Run(testCase.name, func(t *testing.T) {
   552  			if actual, expected := OptionsForItem(testCase.item, testCase.config), testCase.expected; !reflect.DeepEqual(actual, expected) {
   553  				t.Errorf("%s: got incorrect options for item %q: %v", testCase.name, testCase.item, diff.ObjectReflectDiff(actual, expected))
   554  			}
   555  		})
   556  	}
   557  }
   558  
   559  func TestResolveBugzillaOptions(t *testing.T) {
   560  	open, closed := true, false
   561  	yes, no := true, false
   562  	one, two := "v1", "v2"
   563  	modified, verified, post, pre := "MODIFIED", "VERIFIED", "POST", "PRE"
   564  	modifiedState := BugzillaBugState{Status: modified}
   565  	verifiedState := BugzillaBugState{Status: verified}
   566  	postState := BugzillaBugState{Status: post}
   567  	preState := BugzillaBugState{Status: pre}
   568  	var testCases = []struct {
   569  		name          string
   570  		parent, child BugzillaBranchOptions
   571  		expected      BugzillaBranchOptions
   572  	}{
   573  		{
   574  			name: "no parent or child means no output",
   575  		},
   576  		{
   577  			name:   "no child means a copy of parent is the output",
   578  			parent: BugzillaBranchOptions{ValidateByDefault: &yes, IsOpen: &open, TargetRelease: &one, ValidStates: &[]BugzillaBugState{modifiedState}, DependentBugStates: &[]BugzillaBugState{verifiedState}, DependentBugTargetReleases: &[]string{one}, StateAfterValidation: &postState},
   579  			expected: BugzillaBranchOptions{
   580  				ValidateByDefault:          &yes,
   581  				IsOpen:                     &open,
   582  				TargetRelease:              &one,
   583  				ValidStates:                &[]BugzillaBugState{modifiedState},
   584  				DependentBugStates:         &[]BugzillaBugState{verifiedState},
   585  				DependentBugTargetReleases: &[]string{one},
   586  				StateAfterValidation:       &postState,
   587  			},
   588  		},
   589  		{
   590  			name:  "no parent means a copy of child is the output",
   591  			child: BugzillaBranchOptions{ValidateByDefault: &yes, IsOpen: &open, TargetRelease: &one, ValidStates: &[]BugzillaBugState{modifiedState}, DependentBugStates: &[]BugzillaBugState{verifiedState}, DependentBugTargetReleases: &[]string{one}, StateAfterValidation: &postState},
   592  			expected: BugzillaBranchOptions{
   593  				ValidateByDefault:          &yes,
   594  				IsOpen:                     &open,
   595  				TargetRelease:              &one,
   596  				ValidStates:                &[]BugzillaBugState{modifiedState},
   597  				DependentBugStates:         &[]BugzillaBugState{verifiedState},
   598  				DependentBugTargetReleases: &[]string{one},
   599  				StateAfterValidation:       &postState,
   600  			},
   601  		},
   602  		{
   603  			name:     "child overrides parent on IsOpen",
   604  			parent:   BugzillaBranchOptions{IsOpen: &open, TargetRelease: &one, ValidStates: &[]BugzillaBugState{modifiedState}, StateAfterValidation: &postState},
   605  			child:    BugzillaBranchOptions{IsOpen: &closed},
   606  			expected: BugzillaBranchOptions{IsOpen: &closed, TargetRelease: &one, ValidStates: &[]BugzillaBugState{modifiedState}, StateAfterValidation: &postState},
   607  		},
   608  		{
   609  			name:     "child overrides parent on target release",
   610  			parent:   BugzillaBranchOptions{IsOpen: &open, TargetRelease: &one, ValidStates: &[]BugzillaBugState{modifiedState}, StateAfterValidation: &postState},
   611  			child:    BugzillaBranchOptions{TargetRelease: &two},
   612  			expected: BugzillaBranchOptions{IsOpen: &open, TargetRelease: &two, ValidStates: &[]BugzillaBugState{modifiedState}, StateAfterValidation: &postState},
   613  		},
   614  		{
   615  			name:     "child overrides parent on states",
   616  			parent:   BugzillaBranchOptions{IsOpen: &open, TargetRelease: &one, ValidStates: &[]BugzillaBugState{modifiedState}, StateAfterValidation: &postState},
   617  			child:    BugzillaBranchOptions{ValidStates: &[]BugzillaBugState{verifiedState}},
   618  			expected: BugzillaBranchOptions{IsOpen: &open, TargetRelease: &one, ValidStates: &[]BugzillaBugState{verifiedState}, StateAfterValidation: &postState},
   619  		},
   620  		{
   621  			name:     "child overrides parent on state after validation",
   622  			parent:   BugzillaBranchOptions{IsOpen: &open, TargetRelease: &one, ValidStates: &[]BugzillaBugState{modifiedState}, StateAfterValidation: &postState},
   623  			child:    BugzillaBranchOptions{StateAfterValidation: &preState},
   624  			expected: BugzillaBranchOptions{IsOpen: &open, TargetRelease: &one, ValidStates: &[]BugzillaBugState{modifiedState}, StateAfterValidation: &preState},
   625  		},
   626  		{
   627  			name:     "child overrides parent on validation by default",
   628  			parent:   BugzillaBranchOptions{IsOpen: &open, TargetRelease: &one, ValidStates: &[]BugzillaBugState{modifiedState}, StateAfterValidation: &postState},
   629  			child:    BugzillaBranchOptions{ValidateByDefault: &yes},
   630  			expected: BugzillaBranchOptions{ValidateByDefault: &yes, IsOpen: &open, TargetRelease: &one, ValidStates: &[]BugzillaBugState{modifiedState}, StateAfterValidation: &postState},
   631  		},
   632  		{
   633  			name:   "child overrides parent on dependent bug states",
   634  			parent: BugzillaBranchOptions{IsOpen: &open, TargetRelease: &one, ValidStates: &[]BugzillaBugState{modifiedState}, DependentBugStates: &[]BugzillaBugState{verifiedState}, StateAfterValidation: &postState},
   635  			child:  BugzillaBranchOptions{DependentBugStates: &[]BugzillaBugState{modifiedState}},
   636  			expected: BugzillaBranchOptions{
   637  				IsOpen:               &open,
   638  				TargetRelease:        &one,
   639  				ValidStates:          &[]BugzillaBugState{modifiedState},
   640  				DependentBugStates:   &[]BugzillaBugState{modifiedState},
   641  				StateAfterValidation: &postState,
   642  			},
   643  		},
   644  		{
   645  			name:     "child overrides parent on dependent bug target releases",
   646  			parent:   BugzillaBranchOptions{IsOpen: &open, TargetRelease: &one, ValidStates: &[]BugzillaBugState{modifiedState}, StateAfterValidation: &postState, DependentBugTargetReleases: &[]string{one}},
   647  			child:    BugzillaBranchOptions{DependentBugTargetReleases: &[]string{two}},
   648  			expected: BugzillaBranchOptions{IsOpen: &open, TargetRelease: &one, ValidStates: &[]BugzillaBugState{modifiedState}, StateAfterValidation: &postState, DependentBugTargetReleases: &[]string{two}},
   649  		},
   650  		{
   651  			name:   "child overrides parent on state after merge",
   652  			parent: BugzillaBranchOptions{IsOpen: &open, TargetRelease: &one, ValidStates: &[]BugzillaBugState{modifiedState}, StateAfterValidation: &postState, StateAfterMerge: &postState},
   653  			child:  BugzillaBranchOptions{StateAfterMerge: &preState},
   654  			expected: BugzillaBranchOptions{
   655  				IsOpen:               &open,
   656  				TargetRelease:        &one,
   657  				ValidStates:          &[]BugzillaBugState{modifiedState},
   658  				StateAfterValidation: &postState,
   659  				StateAfterMerge:      &preState,
   660  			},
   661  		},
   662  		{
   663  			name:     "status slices are correctly merged with states slices on parent",
   664  			parent:   BugzillaBranchOptions{Statuses: &[]string{modified}, ValidStates: &[]BugzillaBugState{verifiedState}, DependentBugStatuses: &[]string{pre}, DependentBugStates: &[]BugzillaBugState{postState}},
   665  			expected: BugzillaBranchOptions{ValidStates: &[]BugzillaBugState{modifiedState, verifiedState}, DependentBugStates: &[]BugzillaBugState{postState, preState}},
   666  		},
   667  		{
   668  			name:     "status slices are correctly merged with states slices on child",
   669  			child:    BugzillaBranchOptions{Statuses: &[]string{modified}, ValidStates: &[]BugzillaBugState{verifiedState}, DependentBugStatuses: &[]string{pre}, DependentBugStates: &[]BugzillaBugState{postState}},
   670  			expected: BugzillaBranchOptions{ValidStates: &[]BugzillaBugState{modifiedState, verifiedState}, DependentBugStates: &[]BugzillaBugState{postState, preState}},
   671  		},
   672  		{
   673  			name:     "state fields when not present re inferred from status fields on parent",
   674  			parent:   BugzillaBranchOptions{StatusAfterMerge: &modified, StatusAfterValidation: &verified},
   675  			expected: BugzillaBranchOptions{StateAfterMerge: &modifiedState, StateAfterValidation: &verifiedState},
   676  		},
   677  		{
   678  			name:     "state fields when not present are inferred from status fields on child",
   679  			child:    BugzillaBranchOptions{StatusAfterMerge: &modified, StatusAfterValidation: &verified},
   680  			expected: BugzillaBranchOptions{StateAfterMerge: &modifiedState, StateAfterValidation: &verifiedState},
   681  		},
   682  		{
   683  			name:     "child status overrides all statuses and states of the parent",
   684  			parent:   BugzillaBranchOptions{Statuses: &[]string{modified}, ValidStates: &[]BugzillaBugState{verifiedState}, DependentBugStatuses: &[]string{modified}, DependentBugStates: &[]BugzillaBugState{verifiedState}, StatusAfterMerge: &pre, StateAfterMerge: &preState, StatusAfterValidation: &pre, StateAfterValidation: &preState},
   685  			child:    BugzillaBranchOptions{Statuses: &[]string{post}, DependentBugStatuses: &[]string{post}, StatusAfterMerge: &post, StatusAfterValidation: &post},
   686  			expected: BugzillaBranchOptions{ValidStates: &[]BugzillaBugState{postState}, DependentBugStates: &[]BugzillaBugState{postState}, StateAfterMerge: &postState, StateAfterValidation: &postState},
   687  		},
   688  		{
   689  			name:     "parent dependent target release is merged on child",
   690  			parent:   BugzillaBranchOptions{DeprecatedDependentBugTargetRelease: &one},
   691  			child:    BugzillaBranchOptions{},
   692  			expected: BugzillaBranchOptions{DependentBugTargetReleases: &[]string{one}},
   693  		},
   694  		{
   695  			name:     "parent dependent target release is merged into target releases",
   696  			parent:   BugzillaBranchOptions{DependentBugTargetReleases: &[]string{one}, DeprecatedDependentBugTargetRelease: &two},
   697  			child:    BugzillaBranchOptions{},
   698  			expected: BugzillaBranchOptions{DependentBugTargetReleases: &[]string{one, two}},
   699  		},
   700  		{
   701  			name:   "child overrides parent on all fields",
   702  			parent: BugzillaBranchOptions{ValidateByDefault: &yes, IsOpen: &open, TargetRelease: &one, ValidStates: &[]BugzillaBugState{verifiedState}, DependentBugStates: &[]BugzillaBugState{verifiedState}, DependentBugTargetReleases: &[]string{one}, StateAfterValidation: &postState, StateAfterMerge: &postState},
   703  			child:  BugzillaBranchOptions{ValidateByDefault: &no, IsOpen: &closed, TargetRelease: &two, ValidStates: &[]BugzillaBugState{modifiedState}, DependentBugStates: &[]BugzillaBugState{modifiedState}, DependentBugTargetReleases: &[]string{two}, StateAfterValidation: &preState, StateAfterMerge: &preState},
   704  			expected: BugzillaBranchOptions{
   705  				ValidateByDefault:          &no,
   706  				IsOpen:                     &closed,
   707  				TargetRelease:              &two,
   708  				ValidStates:                &[]BugzillaBugState{modifiedState},
   709  				DependentBugStates:         &[]BugzillaBugState{modifiedState},
   710  				DependentBugTargetReleases: &[]string{two},
   711  				StateAfterValidation:       &preState,
   712  				StateAfterMerge:            &preState,
   713  			},
   714  		},
   715  		{
   716  			name:     "parent target release is excluded on child",
   717  			parent:   BugzillaBranchOptions{TargetRelease: &one},
   718  			child:    BugzillaBranchOptions{ExcludeDefaults: &yes},
   719  			expected: BugzillaBranchOptions{ExcludeDefaults: &yes},
   720  		},
   721  		{
   722  			name:     "parent target release is excluded on child with other options",
   723  			parent:   BugzillaBranchOptions{DependentBugTargetReleases: &[]string{one}},
   724  			child:    BugzillaBranchOptions{TargetRelease: &one, ExcludeDefaults: &yes},
   725  			expected: BugzillaBranchOptions{TargetRelease: &one, ExcludeDefaults: &yes},
   726  		},
   727  		{
   728  			name:     "parent exclude merges with child options",
   729  			parent:   BugzillaBranchOptions{DependentBugTargetReleases: &[]string{one}, ExcludeDefaults: &yes},
   730  			child:    BugzillaBranchOptions{TargetRelease: &one},
   731  			expected: BugzillaBranchOptions{DependentBugTargetReleases: &[]string{one}, TargetRelease: &one, ExcludeDefaults: &yes},
   732  		},
   733  	}
   734  	for _, testCase := range testCases {
   735  		t.Run(testCase.name, func(t *testing.T) {
   736  			if actual, expected := ResolveBugzillaOptions(testCase.parent, testCase.child), testCase.expected; !reflect.DeepEqual(actual, expected) {
   737  				t.Errorf("%s: resolved incorrect options for parent and child: %v", testCase.name, diff.ObjectReflectDiff(actual, expected))
   738  			}
   739  		})
   740  	}
   741  
   742  	var i int = 0
   743  	managedCol1 := ManagedColumn{ID: &i, Name: "col1", State: "open", Labels: []string{"area/conformance", "area/testing"}, Org: "org1"}
   744  	managedCol3 := ManagedColumn{ID: &i, Name: "col2", State: "open", Labels: []string{}, Org: "org2"}
   745  	managedColx := ManagedColumn{ID: &i, Name: "col2", State: "open", Labels: []string{"area/conformance", "area/testing"}, Org: "org2"}
   746  	invalidCol := ManagedColumn{State: "open", Labels: []string{"area/conformance", "area/testing2"}, Org: "org2"}
   747  	invalidOrg := ManagedColumn{Name: "col1", State: "open", Labels: []string{"area/conformance", "area/testing2"}, Org: ""}
   748  	managedProj2 := ManagedProject{Columns: []ManagedColumn{managedCol3}}
   749  	managedProjx := ManagedProject{Columns: []ManagedColumn{managedCol1, managedColx}}
   750  	managedOrgRepo2 := ManagedOrgRepo{Projects: map[string]ManagedProject{"project1": managedProj2}}
   751  	managedOrgRepox := ManagedOrgRepo{Projects: map[string]ManagedProject{"project1": managedProjx}}
   752  
   753  	projectManagerTestcases := []struct {
   754  		name        string
   755  		config      *Configuration
   756  		expectedErr string
   757  	}{
   758  		{
   759  			name: "No projects configured in a repo",
   760  			config: &Configuration{
   761  				ProjectManager: ProjectManager{
   762  					OrgRepos: map[string]ManagedOrgRepo{"org1": {Projects: map[string]ManagedProject{}}},
   763  				},
   764  			},
   765  			expectedErr: fmt.Sprintf("Org/repo: %s, has no projects configured", "org1"),
   766  		},
   767  		{
   768  			name: "No columns configured for a project",
   769  			config: &Configuration{
   770  				ProjectManager: ProjectManager{
   771  					OrgRepos: map[string]ManagedOrgRepo{"org1": {Projects: map[string]ManagedProject{"project1": {Columns: []ManagedColumn{}}}}},
   772  				},
   773  			},
   774  			expectedErr: fmt.Sprintf("Org/repo: %s, project %s, has no columns configured", "org1", "project1"),
   775  		},
   776  		{
   777  			name: "Columns does not have name or ID",
   778  			config: &Configuration{
   779  				ProjectManager: ProjectManager{
   780  					OrgRepos: map[string]ManagedOrgRepo{"org1": {Projects: map[string]ManagedProject{"project1": {Columns: []ManagedColumn{invalidCol}}}}},
   781  				},
   782  			},
   783  			expectedErr: fmt.Sprintf("Org/repo: %s, project %s, column %v, has no name/id configured", "org1", "project1", invalidCol),
   784  		},
   785  		{
   786  			name: "Columns does not have owner Org/repo",
   787  			config: &Configuration{
   788  				ProjectManager: ProjectManager{
   789  					OrgRepos: map[string]ManagedOrgRepo{"org1": {Projects: map[string]ManagedProject{"project1": {Columns: []ManagedColumn{invalidOrg}}}}},
   790  				},
   791  			},
   792  			expectedErr: fmt.Sprintf("Org/repo: %s, project %s, column %s, has no org configured", "org1", "project1", "col1"),
   793  		},
   794  		{
   795  			name: "No Labels specified in the column of the project",
   796  			config: &Configuration{
   797  				ProjectManager: ProjectManager{
   798  					OrgRepos: map[string]ManagedOrgRepo{"org1": managedOrgRepo2},
   799  				},
   800  			},
   801  			expectedErr: fmt.Sprintf("Org/repo: %s, project %s, column %s, has no labels configured", "org1", "project1", "col2"),
   802  		},
   803  		{
   804  			name: "Same Label specified to multiple column in a project",
   805  			config: &Configuration{
   806  				ProjectManager: ProjectManager{
   807  					OrgRepos: map[string]ManagedOrgRepo{"org1": managedOrgRepox},
   808  				},
   809  			},
   810  			expectedErr: fmt.Sprintf("Org/repo: %s, project %s, column %s has same labels configured as another column", "org1", "project1", "col2"),
   811  		},
   812  	}
   813  
   814  	for _, c := range projectManagerTestcases {
   815  		t.Run(c.name, func(t *testing.T) {
   816  			err := validateProjectManager(c.config.ProjectManager)
   817  			if err != nil && len(c.expectedErr) == 0 {
   818  				t.Fatalf("config validation error: %v", err)
   819  			}
   820  			if err == nil && len(c.expectedErr) > 0 {
   821  				t.Fatalf("config validation error: %v but expecting %v", err, c.expectedErr)
   822  			}
   823  			if err != nil && c.expectedErr != err.Error() {
   824  				t.Fatalf("Error running the test %s, \nexpected: %s, \nreceived: %s", c.name, c.expectedErr, err.Error())
   825  			}
   826  		})
   827  	}
   828  }
   829  
   830  func TestOptionsForBranch(t *testing.T) {
   831  	open, closed := true, false
   832  	yes, no := true, false
   833  	globalDefault, globalBranchDefault, orgDefault, orgBranchDefault, repoDefault, repoBranch, legacyBranch := "global-default", "global-branch-default", "my-org-default", "my-org-branch-default", "my-repo-default", "my-repo-branch", "my-legacy-branch"
   834  	post, pre, release, notabug, new, reset := "POST", "PRE", "RELEASE_PENDING", "NOTABUG", "NEW", "RESET"
   835  	verifiedState, modifiedState := BugzillaBugState{Status: "VERIFIED"}, BugzillaBugState{Status: "MODIFIED"}
   836  	postState, preState, releaseState, notabugState, newState, resetState := BugzillaBugState{Status: post}, BugzillaBugState{Status: pre}, BugzillaBugState{Status: release}, BugzillaBugState{Status: notabug}, BugzillaBugState{Status: new}, BugzillaBugState{Status: reset}
   837  	closedErrata := BugzillaBugState{Status: "CLOSED", Resolution: "ERRATA"}
   838  	orgAllowedGroups, repoAllowedGroups := []string{"test"}, []string{"security", "test"}
   839  
   840  	rawConfig := `default:
   841    "*":
   842      target_release: global-default
   843    "global-branch":
   844      is_open: false
   845      target_release: global-branch-default
   846  orgs:
   847    my-org:
   848      default:
   849        "*":
   850          is_open: true
   851          target_release: my-org-default
   852          state_after_validation:
   853            status: "PRE"
   854          state_after_close:
   855            status: "NEW"
   856          allowed_groups:
   857          - test
   858        "my-org-branch":
   859          target_release: my-org-branch-default
   860          state_after_validation:
   861            status: "POST"
   862      repos:
   863        my-repo:
   864          branches:
   865            "*":
   866              is_open: false
   867              target_release: my-repo-default
   868              valid_states:
   869              - status: VERIFIED
   870              validate_by_default: false
   871              state_after_merge:
   872                status: RELEASE_PENDING
   873            "my-repo-branch":
   874              target_release: my-repo-branch
   875              valid_states:
   876              - status: MODIFIED
   877              - status: CLOSED
   878                resolution: ERRATA
   879              validate_by_default: true
   880              state_after_merge:
   881                status: NOTABUG
   882              state_after_close:
   883                status: RESET
   884              allowed_groups:
   885              - security
   886            "my-legacy-branch":
   887              target_release: my-legacy-branch
   888              statuses:
   889              - MODIFIED
   890              dependent_bug_statuses:
   891              - VERIFIED
   892              validate_by_default: true
   893              status_after_validation: MODIFIED
   894              status_after_merge: NOTABUG
   895            "my-special-branch":
   896              exclude_defaults: true
   897              validate_by_default: false
   898        another-repo:
   899          branches:
   900            "*":
   901              exclude_defaults: true
   902            "my-org-branch":
   903              target_release: my-repo-branch`
   904  	var config Bugzilla
   905  	if err := yaml.Unmarshal([]byte(rawConfig), &config); err != nil {
   906  		t.Fatalf("couldn't unmarshal config: %v", err)
   907  	}
   908  
   909  	var testCases = []struct {
   910  		name              string
   911  		org, repo, branch string
   912  		expected          BugzillaBranchOptions
   913  	}{
   914  		{
   915  			name:     "unconfigured branch gets global default",
   916  			org:      "some-org",
   917  			repo:     "some-repo",
   918  			branch:   "some-branch",
   919  			expected: BugzillaBranchOptions{TargetRelease: &globalDefault},
   920  		},
   921  		{
   922  			name:     "branch on unconfigured org/repo gets global default",
   923  			org:      "some-org",
   924  			repo:     "some-repo",
   925  			branch:   "global-branch",
   926  			expected: BugzillaBranchOptions{IsOpen: &closed, TargetRelease: &globalBranchDefault},
   927  		},
   928  		{
   929  			name:     "branch on configured org but not repo gets org default",
   930  			org:      "my-org",
   931  			repo:     "some-repo",
   932  			branch:   "some-branch",
   933  			expected: BugzillaBranchOptions{IsOpen: &open, TargetRelease: &orgDefault, StateAfterValidation: &preState, AllowedGroups: orgAllowedGroups, StateAfterClose: &newState},
   934  		},
   935  		{
   936  			name:     "branch on configured org but not repo gets org branch default",
   937  			org:      "my-org",
   938  			repo:     "some-repo",
   939  			branch:   "my-org-branch",
   940  			expected: BugzillaBranchOptions{IsOpen: &open, TargetRelease: &orgBranchDefault, StateAfterValidation: &postState, AllowedGroups: orgAllowedGroups, StateAfterClose: &newState},
   941  		},
   942  		{
   943  			name:     "branch on configured org and repo gets repo default",
   944  			org:      "my-org",
   945  			repo:     "my-repo",
   946  			branch:   "some-branch",
   947  			expected: BugzillaBranchOptions{ValidateByDefault: &no, IsOpen: &closed, TargetRelease: &repoDefault, ValidStates: &[]BugzillaBugState{verifiedState}, StateAfterValidation: &preState, StateAfterMerge: &releaseState, AllowedGroups: orgAllowedGroups, StateAfterClose: &newState},
   948  		},
   949  		{
   950  			name:     "branch on configured org and repo gets branch config",
   951  			org:      "my-org",
   952  			repo:     "my-repo",
   953  			branch:   "my-repo-branch",
   954  			expected: BugzillaBranchOptions{ValidateByDefault: &yes, IsOpen: &closed, TargetRelease: &repoBranch, ValidStates: &[]BugzillaBugState{modifiedState, closedErrata}, StateAfterValidation: &preState, StateAfterMerge: &notabugState, AllowedGroups: repoAllowedGroups, StateAfterClose: &resetState},
   955  		},
   956  		{
   957  			name:     "exclude branch on configured org and repo gets branch config",
   958  			org:      "my-org",
   959  			repo:     "my-repo",
   960  			branch:   "my-special-branch",
   961  			expected: BugzillaBranchOptions{ValidateByDefault: &no, ExcludeDefaults: &yes},
   962  		},
   963  		{
   964  			name:     "exclude branch on repo cascades to branch config",
   965  			org:      "my-org",
   966  			repo:     "another-repo",
   967  			branch:   "my-org-branch",
   968  			expected: BugzillaBranchOptions{TargetRelease: &repoBranch, ExcludeDefaults: &yes},
   969  		},
   970  	}
   971  	for _, testCase := range testCases {
   972  		t.Run(testCase.name, func(t *testing.T) {
   973  			if actual, expected := config.OptionsForBranch(testCase.org, testCase.repo, testCase.branch), testCase.expected; !reflect.DeepEqual(actual, expected) {
   974  				t.Errorf("%s: resolved incorrect options for %s/%s#%s: %v", testCase.name, testCase.org, testCase.repo, testCase.branch, diff.ObjectReflectDiff(actual, expected))
   975  			}
   976  		})
   977  	}
   978  
   979  	var repoTestCases = []struct {
   980  		name      string
   981  		org, repo string
   982  		expected  map[string]BugzillaBranchOptions
   983  	}{
   984  		{
   985  			name: "unconfigured repo gets global default",
   986  			org:  "some-org",
   987  			repo: "some-repo",
   988  			expected: map[string]BugzillaBranchOptions{
   989  				"*":             {TargetRelease: &globalDefault},
   990  				"global-branch": {IsOpen: &closed, TargetRelease: &globalBranchDefault},
   991  			},
   992  		},
   993  		{
   994  			name: "repo in configured org gets org default",
   995  			org:  "my-org",
   996  			repo: "some-repo",
   997  			expected: map[string]BugzillaBranchOptions{
   998  				"*":             {IsOpen: &open, TargetRelease: &orgDefault, StateAfterValidation: &preState, AllowedGroups: orgAllowedGroups, StateAfterClose: &newState},
   999  				"my-org-branch": {IsOpen: &open, TargetRelease: &orgBranchDefault, StateAfterValidation: &postState, AllowedGroups: orgAllowedGroups, StateAfterClose: &newState},
  1000  			},
  1001  		},
  1002  		{
  1003  			name: "configured repo gets repo config",
  1004  			org:  "my-org",
  1005  			repo: "my-repo",
  1006  			expected: map[string]BugzillaBranchOptions{
  1007  				"*": {
  1008  					ValidateByDefault:    &no,
  1009  					IsOpen:               &closed,
  1010  					TargetRelease:        &repoDefault,
  1011  					ValidStates:          &[]BugzillaBugState{verifiedState},
  1012  					StateAfterValidation: &preState,
  1013  					StateAfterMerge:      &releaseState,
  1014  					AllowedGroups:        orgAllowedGroups,
  1015  					StateAfterClose:      &newState,
  1016  				},
  1017  				"my-repo-branch": {
  1018  					ValidateByDefault:    &yes,
  1019  					IsOpen:               &closed,
  1020  					TargetRelease:        &repoBranch,
  1021  					ValidStates:          &[]BugzillaBugState{modifiedState, closedErrata},
  1022  					StateAfterValidation: &preState,
  1023  					StateAfterMerge:      &notabugState,
  1024  					AllowedGroups:        repoAllowedGroups,
  1025  					StateAfterClose:      &resetState,
  1026  				},
  1027  				"my-org-branch": {
  1028  					ValidateByDefault:    &no,
  1029  					IsOpen:               &closed,
  1030  					TargetRelease:        &repoDefault,
  1031  					ValidStates:          &[]BugzillaBugState{verifiedState},
  1032  					StateAfterValidation: &postState,
  1033  					StateAfterMerge:      &releaseState,
  1034  					AllowedGroups:        orgAllowedGroups,
  1035  					StateAfterClose:      &newState,
  1036  				},
  1037  				"my-legacy-branch": {
  1038  					ValidateByDefault:    &yes,
  1039  					IsOpen:               &closed,
  1040  					TargetRelease:        &legacyBranch,
  1041  					ValidStates:          &[]BugzillaBugState{modifiedState},
  1042  					DependentBugStates:   &[]BugzillaBugState{verifiedState},
  1043  					StateAfterValidation: &modifiedState,
  1044  					StateAfterMerge:      &notabugState,
  1045  					AllowedGroups:        orgAllowedGroups,
  1046  					StateAfterClose:      &newState,
  1047  				},
  1048  				"my-special-branch": {
  1049  					ValidateByDefault: &no,
  1050  					ExcludeDefaults:   &yes,
  1051  				},
  1052  			},
  1053  		},
  1054  		{
  1055  			name: "excluded repo gets no defaults",
  1056  			org:  "my-org",
  1057  			repo: "another-repo",
  1058  			expected: map[string]BugzillaBranchOptions{
  1059  				"*":             {ExcludeDefaults: &yes},
  1060  				"my-org-branch": {ExcludeDefaults: &yes, TargetRelease: &repoBranch},
  1061  			},
  1062  		},
  1063  	}
  1064  	for _, testCase := range repoTestCases {
  1065  		t.Run(testCase.name, func(t *testing.T) {
  1066  			if actual, expected := config.OptionsForRepo(testCase.org, testCase.repo), testCase.expected; !reflect.DeepEqual(actual, expected) {
  1067  				t.Errorf("%s: resolved incorrect options for %s/%s: %v", testCase.name, testCase.org, testCase.repo, diff.ObjectReflectDiff(actual, expected))
  1068  			}
  1069  		})
  1070  	}
  1071  }
  1072  
  1073  func TestBugzillaBugState_String(t *testing.T) {
  1074  	testCases := []struct {
  1075  		name     string
  1076  		state    *BugzillaBugState
  1077  		expected string
  1078  	}{
  1079  		{
  1080  			name:     "empty struct",
  1081  			state:    &BugzillaBugState{},
  1082  			expected: "",
  1083  		},
  1084  		{
  1085  			name:     "only status",
  1086  			state:    &BugzillaBugState{Status: "CLOSED"},
  1087  			expected: "CLOSED",
  1088  		},
  1089  		{
  1090  			name:     "only resolution",
  1091  			state:    &BugzillaBugState{Resolution: "NOTABUG"},
  1092  			expected: "any status with resolution NOTABUG",
  1093  		},
  1094  		{
  1095  			name:     "status and resolution",
  1096  			state:    &BugzillaBugState{Status: "CLOSED", Resolution: "NOTABUG"},
  1097  			expected: "CLOSED (NOTABUG)",
  1098  		},
  1099  	}
  1100  	for _, tc := range testCases {
  1101  		t.Run(tc.name, func(t *testing.T) {
  1102  			actual := tc.state.String()
  1103  			if actual != tc.expected {
  1104  				t.Errorf("%s: expected %q, got %q", tc.name, tc.expected, actual)
  1105  			}
  1106  		})
  1107  	}
  1108  }
  1109  
  1110  func TestBugzillaBugState_Matches(t *testing.T) {
  1111  	modified, closed, errata, notabug := "MODIFIED", "CLOSED", "ERRATA", "NOTABUG"
  1112  	testCases := []struct {
  1113  		name     string
  1114  		state    *BugzillaBugState
  1115  		bug      *bugzilla.Bug
  1116  		expected bool
  1117  	}{
  1118  		{
  1119  			name: "both pointers are nil -> false",
  1120  		},
  1121  		{
  1122  			name: "state pointer is nil -> false",
  1123  			bug:  &bugzilla.Bug{},
  1124  		},
  1125  		{
  1126  			name:  "bug pointer is nil -> false",
  1127  			state: &BugzillaBugState{},
  1128  		},
  1129  		{
  1130  			name:     "statuses do not match -> false",
  1131  			state:    &BugzillaBugState{Status: modified, Resolution: errata},
  1132  			bug:      &bugzilla.Bug{Status: closed, Resolution: errata},
  1133  			expected: false,
  1134  		},
  1135  		{
  1136  			name:     "resolutions do not match -> false",
  1137  			state:    &BugzillaBugState{Status: closed, Resolution: notabug},
  1138  			bug:      &bugzilla.Bug{Status: closed, Resolution: errata},
  1139  			expected: false,
  1140  		},
  1141  		{
  1142  			name:     "no state enforced -> true",
  1143  			state:    &BugzillaBugState{},
  1144  			bug:      &bugzilla.Bug{Status: closed, Resolution: errata},
  1145  			expected: true,
  1146  		},
  1147  		{
  1148  			name:     "status match, resolution not enforced -> true",
  1149  			state:    &BugzillaBugState{Status: closed},
  1150  			bug:      &bugzilla.Bug{Status: closed, Resolution: errata},
  1151  			expected: true,
  1152  		},
  1153  		{
  1154  			name:     "status not enforced, resolution match -> true",
  1155  			state:    &BugzillaBugState{Resolution: errata},
  1156  			bug:      &bugzilla.Bug{Status: closed, Resolution: errata},
  1157  			expected: true,
  1158  		},
  1159  		{
  1160  			name:     "status and resolution match -> true",
  1161  			state:    &BugzillaBugState{Status: closed, Resolution: errata},
  1162  			bug:      &bugzilla.Bug{Status: closed, Resolution: errata},
  1163  			expected: true,
  1164  		},
  1165  	}
  1166  
  1167  	for _, tc := range testCases {
  1168  		t.Run(tc.name, func(t *testing.T) {
  1169  			actual := tc.state.Matches(tc.bug)
  1170  			if actual != tc.expected {
  1171  				t.Errorf("%s: expected %t, got %t", tc.name, tc.expected, actual)
  1172  			}
  1173  		})
  1174  	}
  1175  }
  1176  
  1177  func TestBugzillaBugState_AsBugUpdate(t *testing.T) {
  1178  	modified, closed, errata, notabug := "MODIFIED", "CLOSED", "ERRATA", "NOTABUG"
  1179  	testCases := []struct {
  1180  		name     string
  1181  		state    *BugzillaBugState
  1182  		bug      *bugzilla.Bug
  1183  		expected *bugzilla.BugUpdate
  1184  	}{
  1185  		{
  1186  			name:     "bug is nil so update contains whole state",
  1187  			state:    &BugzillaBugState{Status: closed, Resolution: errata},
  1188  			expected: &bugzilla.BugUpdate{Status: closed, Resolution: errata},
  1189  		},
  1190  		{
  1191  			name:     "bug is empty so update contains whole state",
  1192  			state:    &BugzillaBugState{Status: closed, Resolution: errata},
  1193  			bug:      &bugzilla.Bug{},
  1194  			expected: &bugzilla.BugUpdate{Status: closed, Resolution: errata},
  1195  		},
  1196  		{
  1197  			name:     "state is empty so update is nil",
  1198  			state:    &BugzillaBugState{},
  1199  			bug:      &bugzilla.Bug{Status: closed, Resolution: errata},
  1200  			expected: nil,
  1201  		},
  1202  		{
  1203  			name:     "status differs so update contains it",
  1204  			state:    &BugzillaBugState{Status: closed},
  1205  			bug:      &bugzilla.Bug{Status: modified, Resolution: errata},
  1206  			expected: &bugzilla.BugUpdate{Status: closed},
  1207  		},
  1208  		{
  1209  			name:     "resolution differs so update contains it",
  1210  			state:    &BugzillaBugState{Status: closed, Resolution: errata},
  1211  			bug:      &bugzilla.Bug{Status: closed, Resolution: notabug},
  1212  			expected: &bugzilla.BugUpdate{Resolution: errata},
  1213  		},
  1214  		{
  1215  			name:     "status and resolution match so update is nil",
  1216  			state:    &BugzillaBugState{Status: closed, Resolution: errata},
  1217  			bug:      &bugzilla.Bug{Status: closed, Resolution: errata},
  1218  			expected: nil,
  1219  		},
  1220  	}
  1221  	for _, tc := range testCases {
  1222  		t.Run(tc.name, func(t *testing.T) {
  1223  			actual := tc.state.AsBugUpdate(tc.bug)
  1224  			if tc.expected != actual {
  1225  				if actual == nil {
  1226  					t.Errorf("%s: unexpected nil", tc.name)
  1227  				}
  1228  				if tc.expected == nil {
  1229  					t.Errorf("%s: expected nil, got %v", tc.name, actual)
  1230  				}
  1231  			}
  1232  
  1233  			if !reflect.DeepEqual(tc.expected, actual) {
  1234  				t.Errorf("%s: BugUpdate differs from expected:\n%s", tc.name, diff.ObjectReflectDiff(*actual, *tc.expected))
  1235  			}
  1236  		})
  1237  	}
  1238  }
  1239  
  1240  func TestBugzillaBugStateSet_Has(t *testing.T) {
  1241  	bugInProgress := BugzillaBugState{Status: "MODIFIED"}
  1242  	bugErrata := BugzillaBugState{Status: "CLOSED", Resolution: "ERRATA"}
  1243  	bugWontfix := BugzillaBugState{Status: "CLOSED", Resolution: "WONTFIX"}
  1244  
  1245  	testCases := []struct {
  1246  		name   string
  1247  		states []BugzillaBugState
  1248  		state  BugzillaBugState
  1249  
  1250  		expectedLength int
  1251  		expectedHas    bool
  1252  	}{
  1253  		{
  1254  			name:           "empty set",
  1255  			state:          bugInProgress,
  1256  			expectedLength: 0,
  1257  			expectedHas:    false,
  1258  		},
  1259  		{
  1260  			name:           "membership",
  1261  			states:         []BugzillaBugState{bugInProgress},
  1262  			state:          bugInProgress,
  1263  			expectedLength: 1,
  1264  			expectedHas:    true,
  1265  		},
  1266  		{
  1267  			name:           "non-membership",
  1268  			states:         []BugzillaBugState{bugInProgress, bugErrata},
  1269  			state:          bugWontfix,
  1270  			expectedLength: 2,
  1271  			expectedHas:    false,
  1272  		},
  1273  		{
  1274  			name:           "actually a set",
  1275  			states:         []BugzillaBugState{bugInProgress, bugInProgress, bugInProgress},
  1276  			state:          bugInProgress,
  1277  			expectedLength: 1,
  1278  			expectedHas:    true,
  1279  		},
  1280  	}
  1281  
  1282  	for _, tc := range testCases {
  1283  		t.Run(tc.name, func(t *testing.T) {
  1284  			set := NewBugzillaBugStateSet(tc.states)
  1285  			if len(set) != tc.expectedLength {
  1286  				t.Errorf("%s: expected set to have %d members, it has %d", tc.name, tc.expectedLength, len(set))
  1287  			}
  1288  			var not string
  1289  			if !tc.expectedHas {
  1290  				not = "not "
  1291  			}
  1292  			has := set.Has(tc.state)
  1293  			if has != tc.expectedHas {
  1294  				t.Errorf("%s: expected set to %scontain %v", tc.name, not, tc.state)
  1295  			}
  1296  		})
  1297  	}
  1298  }
  1299  
  1300  func TestStatesMatch(t *testing.T) {
  1301  	modified := BugzillaBugState{Status: "MODIFIED"}
  1302  	errata := BugzillaBugState{Status: "CLOSED", Resolution: "ERRATA"}
  1303  	wontfix := BugzillaBugState{Status: "CLOSED", Resolution: "WONTFIX"}
  1304  	testCases := []struct {
  1305  		name          string
  1306  		first, second []BugzillaBugState
  1307  		expected      bool
  1308  	}{
  1309  		{
  1310  			name:     "empty slices match",
  1311  			expected: true,
  1312  		},
  1313  		{
  1314  			name:  "one empty, one non-empty do not match",
  1315  			first: []BugzillaBugState{modified},
  1316  		},
  1317  		{
  1318  			name:     "identical slices match",
  1319  			first:    []BugzillaBugState{modified},
  1320  			second:   []BugzillaBugState{modified},
  1321  			expected: true,
  1322  		},
  1323  		{
  1324  			name:     "ordering does not matter",
  1325  			first:    []BugzillaBugState{modified, errata},
  1326  			second:   []BugzillaBugState{errata, modified},
  1327  			expected: true,
  1328  		},
  1329  		{
  1330  			name:     "different slices do not match",
  1331  			first:    []BugzillaBugState{modified, errata},
  1332  			second:   []BugzillaBugState{modified, wontfix},
  1333  			expected: false,
  1334  		},
  1335  		{
  1336  			name:     "suffix in first operand is not ignored",
  1337  			first:    []BugzillaBugState{modified, errata},
  1338  			second:   []BugzillaBugState{modified},
  1339  			expected: false,
  1340  		},
  1341  		{
  1342  			name:     "suffix in second operand is not ignored",
  1343  			first:    []BugzillaBugState{modified},
  1344  			second:   []BugzillaBugState{modified, errata},
  1345  			expected: false,
  1346  		},
  1347  	}
  1348  
  1349  	for _, tc := range testCases {
  1350  		t.Run(tc.name, func(t *testing.T) {
  1351  			actual := statesMatch(tc.first, tc.second)
  1352  			if actual != tc.expected {
  1353  				t.Errorf("%s: expected %t, got %t", tc.name, tc.expected, actual)
  1354  			}
  1355  		})
  1356  	}
  1357  }
  1358  
  1359  func TestValidateConfigUpdater(t *testing.T) {
  1360  	testCases := []struct {
  1361  		name        string
  1362  		cu          *ConfigUpdater
  1363  		expected    error
  1364  		expectedMsg string
  1365  	}{
  1366  		{
  1367  			name: "same key of different cms in different ns",
  1368  			cu: &ConfigUpdater{
  1369  				Maps: map[string]ConfigMapSpec{
  1370  					"core-services/prow/02_config/_plugins.yaml": {
  1371  						Name:     "plugins",
  1372  						Key:      "plugins.yaml",
  1373  						Clusters: map[string][]string{"first": {"some-namespace"}},
  1374  					},
  1375  					"somewhere/else/plugins.yaml": {
  1376  						Name:     "plugins",
  1377  						Key:      "plugins.yaml",
  1378  						Clusters: map[string][]string{"first": {"other-namespace"}},
  1379  					},
  1380  				},
  1381  			},
  1382  			expected: nil,
  1383  		},
  1384  		{
  1385  			name: "same key of a cm in the same ns",
  1386  			cu: &ConfigUpdater{
  1387  				Maps: map[string]ConfigMapSpec{
  1388  					"core-services/prow/02_config/_plugins.yaml": {
  1389  						Name:     "plugins",
  1390  						Key:      "plugins.yaml",
  1391  						Clusters: map[string][]string{"first": {"some-namespace"}},
  1392  					},
  1393  					"somewhere/else/plugins.yaml": {
  1394  						Name:     "plugins",
  1395  						Key:      "plugins.yaml",
  1396  						Clusters: map[string][]string{"first": {"some-namespace"}},
  1397  					},
  1398  				},
  1399  			},
  1400  			expected: fmt.Errorf("key plugins.yaml in configmap plugins updated with more than one file"),
  1401  		},
  1402  		{
  1403  			name: "same key of a cm in the same ns different clusters",
  1404  			cu: &ConfigUpdater{
  1405  				Maps: map[string]ConfigMapSpec{
  1406  					"core-services/prow/02_config/_plugins.yaml": {
  1407  						Name:     "plugins",
  1408  						Key:      "plugins.yaml",
  1409  						Clusters: map[string][]string{"first": {"some-namespace"}},
  1410  					},
  1411  					"somewhere/else/plugins.yaml": {
  1412  						Name:     "plugins",
  1413  						Key:      "plugins.yaml",
  1414  						Clusters: map[string][]string{"other": {"some-namespace"}},
  1415  					},
  1416  				},
  1417  			},
  1418  			expected: nil,
  1419  		},
  1420  	}
  1421  
  1422  	for _, tc := range testCases {
  1423  		t.Run(tc.name, func(t *testing.T) {
  1424  			actual := validateConfigUpdater(tc.cu)
  1425  			if tc.expected == nil && actual != nil {
  1426  				t.Errorf("unexpected error: '%v'", actual)
  1427  			}
  1428  			if tc.expected != nil && actual == nil {
  1429  				t.Errorf("expected error '%v'', but it is nil", tc.expected)
  1430  			}
  1431  			if tc.expected != nil && actual != nil && tc.expected.Error() != actual.Error() {
  1432  				t.Errorf("expected error '%v', but it is '%v'", tc.expected, actual)
  1433  			}
  1434  		})
  1435  	}
  1436  }
  1437  
  1438  func TestConfigUpdaterResolve(t *testing.T) {
  1439  	testCases := []struct {
  1440  		name           string
  1441  		in             ConfigUpdater
  1442  		expectedConfig ConfigUpdater
  1443  		exppectedError string
  1444  	}{
  1445  		{
  1446  			name:           "both cluster and cluster_groups is set, error",
  1447  			in:             ConfigUpdater{Maps: map[string]ConfigMapSpec{"map": {Clusters: map[string][]string{"cluster": nil}, ClusterGroups: []string{"group"}}}},
  1448  			exppectedError: "item maps.map contains both clusters and cluster_groups",
  1449  		},
  1450  		{
  1451  			name:           "inexistent cluster_group is referenced, error",
  1452  			in:             ConfigUpdater{Maps: map[string]ConfigMapSpec{"map": {ClusterGroups: []string{"group"}}}},
  1453  			exppectedError: "item maps.map.cluster_groups.0 references inexistent cluster group named group",
  1454  		},
  1455  		{
  1456  			name: "successful resolving",
  1457  			in: ConfigUpdater{
  1458  				ClusterGroups: map[string]ClusterGroup{
  1459  					"some-group":    {Clusters: []string{"cluster-a"}, Namespaces: []string{"namespace-a"}},
  1460  					"another-group": {Clusters: []string{"cluster-b"}, Namespaces: []string{"namespace-b"}},
  1461  				},
  1462  				Maps: map[string]ConfigMapSpec{"map": {
  1463  					Name:          "name",
  1464  					Key:           "key",
  1465  					GZIP:          utilpointer.Bool(true),
  1466  					ClusterGroups: []string{"some-group", "another-group"}},
  1467  				},
  1468  			},
  1469  			expectedConfig: ConfigUpdater{
  1470  				Maps: map[string]ConfigMapSpec{"map": {
  1471  					Name: "name",
  1472  					Key:  "key",
  1473  					GZIP: utilpointer.Bool(true),
  1474  					Clusters: map[string][]string{
  1475  						"cluster-a": {"namespace-a"},
  1476  						"cluster-b": {"namespace-b"},
  1477  					}}},
  1478  			},
  1479  		},
  1480  	}
  1481  
  1482  	for _, tc := range testCases {
  1483  		t.Run(tc.name, func(t *testing.T) {
  1484  
  1485  			var errMsg string
  1486  			err := tc.in.resolve()
  1487  			if err != nil {
  1488  				errMsg = err.Error()
  1489  			}
  1490  			if errMsg != tc.exppectedError {
  1491  				t.Fatalf("expected error %s, got error %s", tc.exppectedError, errMsg)
  1492  			}
  1493  			if err != nil {
  1494  				return
  1495  			}
  1496  
  1497  			if diff := cmp.Diff(tc.expectedConfig, tc.in); diff != "" {
  1498  				t.Errorf("expected config differs from actual config: %s", diff)
  1499  			}
  1500  		})
  1501  	}
  1502  }
  1503  
  1504  func TestEnabledReposForPlugin(t *testing.T) {
  1505  	pluginsYaml := []byte(`
  1506  orgA:
  1507   excluded_repos:
  1508   - repoB
  1509   plugins:
  1510   - pluginCommon
  1511   - pluginNotForRepoB
  1512  orgA/repoB:
  1513   plugins:
  1514   - pluginCommon
  1515   - pluginOnlyForRepoB
  1516  `)
  1517  	var p Plugins
  1518  	err := yaml.Unmarshal(pluginsYaml, &p)
  1519  	if err != nil {
  1520  		t.Errorf("cannot unmarshal plugins config: %v", err)
  1521  	}
  1522  	cfg := Configuration{
  1523  		Plugins: p,
  1524  	}
  1525  	testCases := []struct {
  1526  		name              string
  1527  		wantOrgs          []string
  1528  		wantRepos         []string
  1529  		wantExcludedRepos map[string]sets.Set[string]
  1530  	}{
  1531  		{
  1532  			name:              "pluginCommon",
  1533  			wantOrgs:          []string{"orgA"},
  1534  			wantRepos:         []string{"orgA/repoB"},
  1535  			wantExcludedRepos: map[string]sets.Set[string]{"orgA": {}},
  1536  		},
  1537  		{
  1538  			name:              "pluginNotForRepoB",
  1539  			wantOrgs:          []string{"orgA"},
  1540  			wantRepos:         nil,
  1541  			wantExcludedRepos: map[string]sets.Set[string]{"orgA": {"orgA/repoB": {}}},
  1542  		},
  1543  		{
  1544  			name:              "pluginOnlyForRepoB",
  1545  			wantOrgs:          nil,
  1546  			wantRepos:         []string{"orgA/repoB"},
  1547  			wantExcludedRepos: map[string]sets.Set[string]{},
  1548  		},
  1549  	}
  1550  	for _, tc := range testCases {
  1551  		t.Run(tc.name, func(t *testing.T) {
  1552  			orgs, repos, excludedRepos := cfg.EnabledReposForPlugin(tc.name)
  1553  			if diff := cmp.Diff(tc.wantOrgs, orgs); diff != "" {
  1554  				t.Errorf("expected wantOrgs differ from actual: %s", diff)
  1555  			}
  1556  			if diff := cmp.Diff(tc.wantRepos, repos); diff != "" {
  1557  				t.Errorf("expected repos differ from actual: %s", diff)
  1558  			}
  1559  			if diff := cmp.Diff(tc.wantExcludedRepos, excludedRepos); diff != "" {
  1560  				t.Errorf("expected excludedRepos differ from actual: %s", diff)
  1561  			}
  1562  		})
  1563  	}
  1564  }
  1565  
  1566  func TestPluginsUnmarshalFailed(t *testing.T) {
  1567  	badPluginsYaml := []byte(`
  1568  orgA:
  1569   excluded_repos = [ repoB ]
  1570   plugins:
  1571   - pluginCommon
  1572   - pluginNotForRepoB
  1573  orgA/repoB:
  1574   plugins:
  1575   - pluginCommon
  1576   - pluginOnlyForRepoB
  1577  `)
  1578  	var p Plugins
  1579  	err := p.UnmarshalJSON(badPluginsYaml)
  1580  	if err == nil {
  1581  		t.Error("expected unmarshal error but didn't get one")
  1582  	}
  1583  }
  1584  
  1585  func TestConfigMergingProperties(t *testing.T) {
  1586  	t.Parallel()
  1587  	testCases := []struct {
  1588  		name          string
  1589  		makeMergeable func(*Configuration)
  1590  	}{
  1591  		{
  1592  			name: "Plugins config",
  1593  			makeMergeable: func(c *Configuration) {
  1594  				*c = Configuration{Plugins: c.Plugins, Bugzilla: c.Bugzilla}
  1595  			},
  1596  		},
  1597  	}
  1598  
  1599  	expectedProperties := []struct {
  1600  		name         string
  1601  		verification func(t *testing.T, fuzzedConfig *Configuration)
  1602  	}{
  1603  		{
  1604  			name: "Merging into empty config always succeeds and makes the empty config equal to the one that was merged in",
  1605  			verification: func(t *testing.T, fuzzedMergeableConfig *Configuration) {
  1606  				newConfig := &Configuration{}
  1607  				if err := newConfig.mergeFrom(fuzzedMergeableConfig); err != nil {
  1608  					t.Fatalf("merging fuzzed mergeable config into empty config failed: %v", err)
  1609  				}
  1610  				if diff := cmp.Diff(newConfig, fuzzedMergeableConfig); diff != "" {
  1611  					t.Errorf("after merging config into an empty config, the config that was merged into differs from the one we merged from:\n%s\n", diff)
  1612  				}
  1613  			},
  1614  		},
  1615  		{
  1616  			name: "Merging empty config in always succeeds",
  1617  			verification: func(t *testing.T, fuzzedMergeableConfig *Configuration) {
  1618  				if err := fuzzedMergeableConfig.mergeFrom(&Configuration{}); err != nil {
  1619  					t.Errorf("merging empty config in failed: %v", err)
  1620  				}
  1621  			},
  1622  		},
  1623  		{
  1624  			name: "Merging a config into itself always fails",
  1625  			verification: func(t *testing.T, fuzzedMergeableConfig *Configuration) {
  1626  
  1627  				// An empty bugzilla org config does nothing, so clean those.
  1628  				for org, val := range fuzzedMergeableConfig.Bugzilla.Orgs {
  1629  					if reflect.DeepEqual(val, BugzillaOrgOptions{}) {
  1630  						delete(fuzzedMergeableConfig.Bugzilla.Orgs, org)
  1631  					}
  1632  				}
  1633  				// An exception to the rule is merging an empty config into itself, that is valid and will just do nothing.
  1634  				if apiequality.Semantic.DeepEqual(fuzzedMergeableConfig, &Configuration{}) {
  1635  					return
  1636  				}
  1637  
  1638  				if err := fuzzedMergeableConfig.mergeFrom(fuzzedMergeableConfig); err == nil {
  1639  					serialized, serializeErr := yaml.Marshal(fuzzedMergeableConfig)
  1640  					if serializeErr != nil {
  1641  						t.Fatalf("merging non-empty config into itself did not yield an error and serializing it afterwards failed: %v. Raw object: %+v", serializeErr, fuzzedMergeableConfig)
  1642  					}
  1643  					t.Errorf("merging a config into itself did not produce an error. Serialized config:\n%s", string(serialized))
  1644  				}
  1645  			},
  1646  		},
  1647  	}
  1648  
  1649  	seed := time.Now().UnixNano()
  1650  	// Print the seed so failures can easily be reproduced
  1651  	t.Logf("Seed: %d", seed)
  1652  	fuzzer := fuzz.NewWithSeed(seed)
  1653  
  1654  	for _, tc := range testCases {
  1655  		tc := tc
  1656  		t.Run(tc.name, func(t *testing.T) {
  1657  			t.Parallel()
  1658  
  1659  			for _, propertyTest := range expectedProperties {
  1660  				propertyTest := propertyTest
  1661  				t.Run(propertyTest.name, func(t *testing.T) {
  1662  					t.Parallel()
  1663  
  1664  					for i := 0; i < 100; i++ {
  1665  						fuzzedConfig := &Configuration{}
  1666  						fuzzer.Fuzz(fuzzedConfig)
  1667  
  1668  						tc.makeMergeable(fuzzedConfig)
  1669  
  1670  						propertyTest.verification(t, fuzzedConfig)
  1671  					}
  1672  				})
  1673  			}
  1674  		})
  1675  	}
  1676  }
  1677  
  1678  func TestPluginsMergeFrom(t *testing.T) {
  1679  	t.Parallel()
  1680  	testCases := []struct {
  1681  		name string
  1682  
  1683  		from *Plugins
  1684  		to   *Plugins
  1685  
  1686  		expected       *Plugins
  1687  		expectedErrMsg string
  1688  	}{
  1689  		{
  1690  			name: "Merging for two different repos succeeds",
  1691  
  1692  			from: &Plugins{"org/repo-1": OrgPlugins{Plugins: []string{"wip"}}},
  1693  			to:   &Plugins{"org/repo-2": OrgPlugins{Plugins: []string{"wip"}}},
  1694  
  1695  			expected: &Plugins{
  1696  				"org/repo-1": OrgPlugins{Plugins: []string{"wip"}},
  1697  				"org/repo-2": OrgPlugins{Plugins: []string{"wip"}},
  1698  			},
  1699  		},
  1700  		{
  1701  			name: "Merging the same repo fails",
  1702  
  1703  			from: &Plugins{"org/repo-1": OrgPlugins{Plugins: []string{"wip"}}},
  1704  			to:   &Plugins{"org/repo-1": OrgPlugins{Plugins: []string{"wip"}}},
  1705  
  1706  			expectedErrMsg: "found duplicate config for plugins.org/repo-1",
  1707  		},
  1708  	}
  1709  
  1710  	for _, tc := range testCases {
  1711  		t.Run(tc.name, func(t *testing.T) {
  1712  			var errMsg string
  1713  			err := tc.to.mergeFrom(tc.from)
  1714  			if err != nil {
  1715  				errMsg = err.Error()
  1716  			}
  1717  			if tc.expectedErrMsg != errMsg {
  1718  				t.Fatalf("expected error message %q, got %s", tc.expectedErrMsg, errMsg)
  1719  			}
  1720  			if err != nil {
  1721  				return
  1722  			}
  1723  
  1724  			if diff := cmp.Diff(tc.expected, tc.to); diff != "" {
  1725  				t.Errorf("expexcted config differs from actual: %s", diff)
  1726  			}
  1727  		})
  1728  	}
  1729  }
  1730  
  1731  func TestBugzillaMergeFrom(t *testing.T) {
  1732  	t.Parallel()
  1733  
  1734  	yes := true
  1735  	targetRelease1 := "target-release-1"
  1736  	targetRelease2 := "target-release-2"
  1737  
  1738  	testCases := []struct {
  1739  		name string
  1740  
  1741  		from *Bugzilla
  1742  		to   *Bugzilla
  1743  
  1744  		expected       *Bugzilla
  1745  		expectedErrMsg string
  1746  	}{
  1747  		{
  1748  			name: "Merging for two different repos",
  1749  
  1750  			from: &Bugzilla{Orgs: map[string]BugzillaOrgOptions{
  1751  				"org": {
  1752  					Repos: map[string]BugzillaRepoOptions{
  1753  						"repo-1": {
  1754  							Branches: map[string]BugzillaBranchOptions{
  1755  								"master": {
  1756  									IsOpen:        &yes,
  1757  									TargetRelease: &targetRelease1,
  1758  								},
  1759  							},
  1760  						},
  1761  					},
  1762  				},
  1763  			}},
  1764  			to: &Bugzilla{Orgs: map[string]BugzillaOrgOptions{
  1765  				"org": {
  1766  					Repos: map[string]BugzillaRepoOptions{
  1767  						"repo-2": {
  1768  							Branches: map[string]BugzillaBranchOptions{
  1769  								"master": {
  1770  									IsOpen:        &yes,
  1771  									TargetRelease: &targetRelease2,
  1772  								},
  1773  							},
  1774  						},
  1775  					},
  1776  				},
  1777  			}},
  1778  
  1779  			expected: &Bugzilla{Orgs: map[string]BugzillaOrgOptions{
  1780  				"org": {
  1781  					Repos: map[string]BugzillaRepoOptions{
  1782  						"repo-1": {
  1783  							Branches: map[string]BugzillaBranchOptions{
  1784  								"master": {
  1785  									IsOpen:        &yes,
  1786  									TargetRelease: &targetRelease1,
  1787  								},
  1788  							},
  1789  						},
  1790  						"repo-2": {
  1791  							Branches: map[string]BugzillaBranchOptions{
  1792  								"master": {
  1793  									IsOpen:        &yes,
  1794  									TargetRelease: &targetRelease2,
  1795  								},
  1796  							},
  1797  						},
  1798  					},
  1799  				},
  1800  			}},
  1801  		},
  1802  		{
  1803  			name: "Merging organization defaults and repo in org",
  1804  
  1805  			from: &Bugzilla{Orgs: map[string]BugzillaOrgOptions{
  1806  				"org": {
  1807  					Repos: map[string]BugzillaRepoOptions{
  1808  						"repo-2": {
  1809  							Branches: map[string]BugzillaBranchOptions{
  1810  								"master": {
  1811  									IsOpen:        &yes,
  1812  									TargetRelease: &targetRelease2,
  1813  								},
  1814  							},
  1815  						},
  1816  					},
  1817  				},
  1818  			}},
  1819  			to: &Bugzilla{Orgs: map[string]BugzillaOrgOptions{
  1820  				"org": {
  1821  					Default: map[string]BugzillaBranchOptions{
  1822  						"master": {
  1823  							IsOpen:        &yes,
  1824  							TargetRelease: &targetRelease1,
  1825  						},
  1826  					},
  1827  				},
  1828  			}},
  1829  
  1830  			expected: &Bugzilla{Orgs: map[string]BugzillaOrgOptions{
  1831  				"org": {
  1832  					Default: map[string]BugzillaBranchOptions{
  1833  						"master": {
  1834  							IsOpen:        &yes,
  1835  							TargetRelease: &targetRelease1,
  1836  						},
  1837  					},
  1838  					Repos: map[string]BugzillaRepoOptions{
  1839  						"repo-2": {
  1840  							Branches: map[string]BugzillaBranchOptions{
  1841  								"master": {
  1842  									IsOpen:        &yes,
  1843  									TargetRelease: &targetRelease2,
  1844  								},
  1845  							},
  1846  						},
  1847  					},
  1848  				},
  1849  			}},
  1850  		},
  1851  		{
  1852  			name: "Merging 2 organizations",
  1853  
  1854  			from: &Bugzilla{Orgs: map[string]BugzillaOrgOptions{
  1855  				"org": {
  1856  					Repos: map[string]BugzillaRepoOptions{
  1857  						"repo-1": {
  1858  							Branches: map[string]BugzillaBranchOptions{
  1859  								"master": {
  1860  									IsOpen:        &yes,
  1861  									TargetRelease: &targetRelease1,
  1862  								},
  1863  							},
  1864  						},
  1865  					},
  1866  				},
  1867  			}},
  1868  			to: &Bugzilla{Orgs: map[string]BugzillaOrgOptions{
  1869  				"org-2": {
  1870  					Repos: map[string]BugzillaRepoOptions{
  1871  						"repo-1": {
  1872  							Branches: map[string]BugzillaBranchOptions{
  1873  								"master": {
  1874  									IsOpen:        &yes,
  1875  									TargetRelease: &targetRelease2,
  1876  								},
  1877  							},
  1878  						},
  1879  					},
  1880  				},
  1881  			}},
  1882  
  1883  			expected: &Bugzilla{Orgs: map[string]BugzillaOrgOptions{
  1884  				"org": {
  1885  					Repos: map[string]BugzillaRepoOptions{
  1886  						"repo-1": {
  1887  							Branches: map[string]BugzillaBranchOptions{
  1888  								"master": {
  1889  									IsOpen:        &yes,
  1890  									TargetRelease: &targetRelease1,
  1891  								},
  1892  							},
  1893  						},
  1894  					}},
  1895  				"org-2": {
  1896  					Repos: map[string]BugzillaRepoOptions{
  1897  						"repo-1": {
  1898  							Branches: map[string]BugzillaBranchOptions{
  1899  								"master": {
  1900  									IsOpen:        &yes,
  1901  									TargetRelease: &targetRelease2,
  1902  								},
  1903  							},
  1904  						},
  1905  					},
  1906  				},
  1907  			}},
  1908  		},
  1909  		{
  1910  			name: "Merging global defaults succeeds",
  1911  
  1912  			from: &Bugzilla{Default: map[string]BugzillaBranchOptions{
  1913  				"master": {
  1914  					IsOpen:        &yes,
  1915  					TargetRelease: &targetRelease1,
  1916  				},
  1917  			}},
  1918  			to: &Bugzilla{Orgs: map[string]BugzillaOrgOptions{
  1919  				"org": {
  1920  					Repos: map[string]BugzillaRepoOptions{
  1921  						"repo-1": {
  1922  							Branches: map[string]BugzillaBranchOptions{
  1923  								"master": {
  1924  									IsOpen:        &yes,
  1925  									TargetRelease: &targetRelease1,
  1926  								},
  1927  							},
  1928  						},
  1929  					},
  1930  				},
  1931  			}},
  1932  			expected: &Bugzilla{Default: map[string]BugzillaBranchOptions{
  1933  				"master": {
  1934  					IsOpen:        &yes,
  1935  					TargetRelease: &targetRelease1,
  1936  				},
  1937  			}, Orgs: map[string]BugzillaOrgOptions{
  1938  				"org": {
  1939  					Repos: map[string]BugzillaRepoOptions{
  1940  						"repo-1": {
  1941  							Branches: map[string]BugzillaBranchOptions{
  1942  								"master": {
  1943  									IsOpen:        &yes,
  1944  									TargetRelease: &targetRelease1,
  1945  								},
  1946  							},
  1947  						},
  1948  					},
  1949  				},
  1950  			}},
  1951  		},
  1952  		{
  1953  			name: "Merging multiple global defaults fails",
  1954  
  1955  			from: &Bugzilla{Default: map[string]BugzillaBranchOptions{
  1956  				"master": {
  1957  					IsOpen:        &yes,
  1958  					TargetRelease: &targetRelease1,
  1959  				},
  1960  			}},
  1961  			to: &Bugzilla{Default: map[string]BugzillaBranchOptions{
  1962  				"master": {
  1963  					IsOpen:        &yes,
  1964  					TargetRelease: &targetRelease2,
  1965  				},
  1966  			}},
  1967  			expectedErrMsg: "configuration of global default defined in multiple places",
  1968  		},
  1969  		{
  1970  			name: "Merging same organization defaults fails",
  1971  
  1972  			from: &Bugzilla{Orgs: map[string]BugzillaOrgOptions{
  1973  				"org": {
  1974  					Default: map[string]BugzillaBranchOptions{
  1975  						"master": {
  1976  							IsOpen:        &yes,
  1977  							TargetRelease: &targetRelease1,
  1978  						},
  1979  					},
  1980  				},
  1981  			}},
  1982  			to: &Bugzilla{Orgs: map[string]BugzillaOrgOptions{
  1983  				"org": {
  1984  					Default: map[string]BugzillaBranchOptions{
  1985  						"master": {
  1986  							IsOpen:        &yes,
  1987  							TargetRelease: &targetRelease2,
  1988  						},
  1989  					},
  1990  				},
  1991  			}},
  1992  
  1993  			expectedErrMsg: "found duplicate organization config for bugzilla.org",
  1994  		},
  1995  		{
  1996  			name: "Merging same repository fails",
  1997  
  1998  			from: &Bugzilla{Orgs: map[string]BugzillaOrgOptions{
  1999  				"org": {
  2000  					Repos: map[string]BugzillaRepoOptions{
  2001  						"repo-1": {
  2002  							Branches: map[string]BugzillaBranchOptions{
  2003  								"master": {
  2004  									IsOpen:        &yes,
  2005  									TargetRelease: &targetRelease1,
  2006  								},
  2007  							},
  2008  						},
  2009  					},
  2010  				},
  2011  			}},
  2012  			to: &Bugzilla{Orgs: map[string]BugzillaOrgOptions{
  2013  				"org": {
  2014  					Repos: map[string]BugzillaRepoOptions{
  2015  						"repo-1": {
  2016  							Branches: map[string]BugzillaBranchOptions{
  2017  								"master": {
  2018  									IsOpen:        &yes,
  2019  									TargetRelease: &targetRelease2,
  2020  								},
  2021  							},
  2022  						},
  2023  					},
  2024  				},
  2025  			}},
  2026  
  2027  			expectedErrMsg: "found duplicate repository config for bugzilla.org/repo-1",
  2028  		},
  2029  	}
  2030  
  2031  	for _, tc := range testCases {
  2032  		t.Run(tc.name, func(t *testing.T) {
  2033  			var errMsg string
  2034  			err := tc.to.mergeFrom(tc.from)
  2035  			if err != nil {
  2036  				errMsg = err.Error()
  2037  			}
  2038  			if tc.expectedErrMsg != errMsg {
  2039  				t.Fatalf("expected error message %q, got %q", tc.expectedErrMsg, errMsg)
  2040  			}
  2041  			if err != nil {
  2042  				return
  2043  			}
  2044  
  2045  			if diff := cmp.Diff(tc.expected, tc.to); diff != "" {
  2046  				t.Errorf("expexcted config differs from actual: %s", diff)
  2047  			}
  2048  		})
  2049  	}
  2050  }
  2051  
  2052  func TestHasConfigFor(t *testing.T) {
  2053  	t.Parallel()
  2054  	testCases := []struct {
  2055  		name            string
  2056  		resultGenerator func(fuzzedConfig *Configuration) (toCheck *Configuration, expectGlobal bool, expectOrgs sets.Set[string], expectRepos sets.Set[string])
  2057  	}{
  2058  		{
  2059  			name: "Any non-empty config with empty Plugins and Bugzilla is considered to be global",
  2060  			resultGenerator: func(fuzzedConfig *Configuration) (toCheck *Configuration, expectGlobal bool, expectOrgs sets.Set[string], expectRepos sets.Set[string]) {
  2061  				fuzzedConfig.Plugins = nil
  2062  				fuzzedConfig.Bugzilla = Bugzilla{}
  2063  				fuzzedConfig.Approve = nil
  2064  				fuzzedConfig.Label.RestrictedLabels = nil
  2065  				fuzzedConfig.Lgtm = nil
  2066  				fuzzedConfig.Triggers = nil
  2067  				fuzzedConfig.Welcome = nil
  2068  				fuzzedConfig.ExternalPlugins = nil
  2069  				return fuzzedConfig, !reflect.DeepEqual(fuzzedConfig, &Configuration{}), nil, nil
  2070  			},
  2071  		},
  2072  		{
  2073  			name: "Any config with plugins is considered to be for the orgs and repos references there",
  2074  			resultGenerator: func(fuzzedConfig *Configuration) (toCheck *Configuration, expectGlobal bool, expectOrgs sets.Set[string], expectRepos sets.Set[string]) {
  2075  				// exclude non-plugins configs to test plugins specifically
  2076  				fuzzedConfig = &Configuration{Plugins: fuzzedConfig.Plugins}
  2077  				expectOrgs, expectRepos = sets.Set[string]{}, sets.Set[string]{}
  2078  				for orgOrRepo := range fuzzedConfig.Plugins {
  2079  					if strings.Contains(orgOrRepo, "/") {
  2080  						expectRepos.Insert(orgOrRepo)
  2081  					} else {
  2082  						expectOrgs.Insert(orgOrRepo)
  2083  					}
  2084  				}
  2085  				return fuzzedConfig, !reflect.DeepEqual(fuzzedConfig, &Configuration{Plugins: fuzzedConfig.Plugins}), expectOrgs, expectRepos
  2086  			},
  2087  		},
  2088  		{
  2089  			name: "Any config with bugzilla is considered to be for the orgs and repos references there",
  2090  			resultGenerator: func(fuzzedConfig *Configuration) (toCheck *Configuration, expectGlobal bool, expectOrgs sets.Set[string], expectRepos sets.Set[string]) {
  2091  				// exclude non-plugins configs to test bugzilla specifically
  2092  				fuzzedConfig = &Configuration{Bugzilla: fuzzedConfig.Bugzilla}
  2093  				expectOrgs, expectRepos = sets.Set[string]{}, sets.Set[string]{}
  2094  				for org, orgConfig := range fuzzedConfig.Bugzilla.Orgs {
  2095  					if orgConfig.Default != nil {
  2096  						expectOrgs.Insert(org)
  2097  					}
  2098  					for repo := range orgConfig.Repos {
  2099  						expectRepos.Insert(org + "/" + repo)
  2100  					}
  2101  				}
  2102  				return fuzzedConfig, len(fuzzedConfig.Bugzilla.Default) > 0, expectOrgs, expectRepos
  2103  			},
  2104  		},
  2105  		{
  2106  			name: "Any config with approve is considered to be for the orgs and repos references there",
  2107  			resultGenerator: func(fuzzedConfig *Configuration) (toCheck *Configuration, expectGlobal bool, expectOrgs sets.Set[string], expectRepos sets.Set[string]) {
  2108  				fuzzedConfig = &Configuration{Approve: fuzzedConfig.Approve}
  2109  				expectOrgs, expectRepos = sets.Set[string]{}, sets.Set[string]{}
  2110  
  2111  				for _, approveConfig := range fuzzedConfig.Approve {
  2112  					for _, orgOrRepo := range approveConfig.Repos {
  2113  						if strings.Contains(orgOrRepo, "/") {
  2114  							expectRepos.Insert(orgOrRepo)
  2115  						} else {
  2116  							expectOrgs.Insert(orgOrRepo)
  2117  						}
  2118  					}
  2119  				}
  2120  
  2121  				return fuzzedConfig, false, expectOrgs, expectRepos
  2122  			},
  2123  		},
  2124  		{
  2125  			name: "Any config with lgtm is considered to be for the orgs and repos references there",
  2126  			resultGenerator: func(fuzzedConfig *Configuration) (toCheck *Configuration, expectGlobal bool, expectOrgs sets.Set[string], expectRepos sets.Set[string]) {
  2127  				fuzzedConfig = &Configuration{Lgtm: fuzzedConfig.Lgtm}
  2128  				expectOrgs, expectRepos = sets.Set[string]{}, sets.Set[string]{}
  2129  
  2130  				for _, lgtm := range fuzzedConfig.Lgtm {
  2131  					for _, orgOrRepo := range lgtm.Repos {
  2132  						if strings.Contains(orgOrRepo, "/") {
  2133  							expectRepos.Insert(orgOrRepo)
  2134  						} else {
  2135  							expectOrgs.Insert(orgOrRepo)
  2136  						}
  2137  					}
  2138  				}
  2139  
  2140  				return fuzzedConfig, false, expectOrgs, expectRepos
  2141  			},
  2142  		},
  2143  		{
  2144  			name: "Any config with triggers is considered to be for the orgs and repos references there",
  2145  			resultGenerator: func(fuzzedConfig *Configuration) (toCheck *Configuration, expectGlobal bool, expectOrgs sets.Set[string], expectRepos sets.Set[string]) {
  2146  				fuzzedConfig = &Configuration{Triggers: fuzzedConfig.Triggers}
  2147  				expectOrgs, expectRepos = sets.Set[string]{}, sets.Set[string]{}
  2148  
  2149  				for _, trigger := range fuzzedConfig.Triggers {
  2150  					for _, orgOrRepo := range trigger.Repos {
  2151  						if strings.Contains(orgOrRepo, "/") {
  2152  							expectRepos.Insert(orgOrRepo)
  2153  						} else {
  2154  							expectOrgs.Insert(orgOrRepo)
  2155  						}
  2156  					}
  2157  				}
  2158  
  2159  				return fuzzedConfig, false, expectOrgs, expectRepos
  2160  			},
  2161  		},
  2162  		{
  2163  			name: "Any config with welcome is considered to be for the orgs and repos references there",
  2164  			resultGenerator: func(fuzzedConfig *Configuration) (toCheck *Configuration, expectGlobal bool, expectOrgs sets.Set[string], expectRepos sets.Set[string]) {
  2165  				fuzzedConfig = &Configuration{Welcome: fuzzedConfig.Welcome}
  2166  				expectOrgs, expectRepos = sets.Set[string]{}, sets.Set[string]{}
  2167  
  2168  				for _, welcome := range fuzzedConfig.Welcome {
  2169  					for _, orgOrRepo := range welcome.Repos {
  2170  						if strings.Contains(orgOrRepo, "/") {
  2171  							expectRepos.Insert(orgOrRepo)
  2172  						} else {
  2173  							expectOrgs.Insert(orgOrRepo)
  2174  						}
  2175  					}
  2176  				}
  2177  
  2178  				return fuzzedConfig, false, expectOrgs, expectRepos
  2179  			},
  2180  		},
  2181  		{
  2182  			name: "Any config with external-plugins is considered to be for the orgs and repos references there",
  2183  			resultGenerator: func(fuzzedConfig *Configuration) (toCheck *Configuration, expectGlobal bool, expectOrgs sets.Set[string], expectRepos sets.Set[string]) {
  2184  				fuzzedConfig = &Configuration{ExternalPlugins: fuzzedConfig.ExternalPlugins}
  2185  				expectOrgs, expectRepos = sets.Set[string]{}, sets.Set[string]{}
  2186  
  2187  				for orgOrRepo := range fuzzedConfig.ExternalPlugins {
  2188  					if strings.Contains(orgOrRepo, "/") {
  2189  						expectRepos.Insert(orgOrRepo)
  2190  					} else {
  2191  						expectOrgs.Insert(orgOrRepo)
  2192  					}
  2193  				}
  2194  				return fuzzedConfig, false, expectOrgs, expectRepos
  2195  			},
  2196  		},
  2197  		{
  2198  			name: "Any config with label.restricted_labels is considered to be for the org and repos references there",
  2199  			resultGenerator: func(fuzzedConfig *Configuration) (toCheck *Configuration, expectGlobal bool, expectOrgs sets.Set[string], expectRepos sets.Set[string]) {
  2200  				fuzzedConfig = &Configuration{Label: fuzzedConfig.Label}
  2201  				if len(fuzzedConfig.Label.AdditionalLabels) > 0 {
  2202  					expectGlobal = true
  2203  				}
  2204  
  2205  				expectOrgs, expectRepos = sets.Set[string]{}, sets.Set[string]{}
  2206  
  2207  				for orgOrRepo := range fuzzedConfig.Label.RestrictedLabels {
  2208  					if orgOrRepo == "*" {
  2209  						expectGlobal = true
  2210  					} else if strings.Contains(orgOrRepo, "/") {
  2211  						expectRepos.Insert(orgOrRepo)
  2212  					} else {
  2213  						expectOrgs.Insert(orgOrRepo)
  2214  					}
  2215  				}
  2216  				return fuzzedConfig, expectGlobal, expectOrgs, expectRepos
  2217  			},
  2218  		},
  2219  	}
  2220  
  2221  	seed := time.Now().UnixNano()
  2222  	// Print the seed so failures can easily be reproduced
  2223  	t.Logf("Seed: %d", seed)
  2224  	fuzzer := fuzz.NewWithSeed(seed)
  2225  
  2226  	for _, tc := range testCases {
  2227  		t.Run(tc.name, func(t *testing.T) {
  2228  			for i := 0; i < 100; i++ {
  2229  				fuzzedConfig := &Configuration{}
  2230  				fuzzer.Fuzz(fuzzedConfig)
  2231  
  2232  				fuzzedAndManipulatedConfig, expectIsGlobal, expectOrgs, expectRepos := tc.resultGenerator(fuzzedConfig)
  2233  				actualIsGlobal, actualOrgs, actualRepos := fuzzedAndManipulatedConfig.HasConfigFor()
  2234  
  2235  				if expectIsGlobal != actualIsGlobal {
  2236  					t.Errorf("exepcted isGlobal: %t, got: %t", expectIsGlobal, actualIsGlobal)
  2237  				}
  2238  
  2239  				if diff := cmp.Diff(expectOrgs, actualOrgs); diff != "" {
  2240  					t.Errorf("expected orgs differ from actual: %s", diff)
  2241  				}
  2242  
  2243  				if diff := cmp.Diff(expectRepos, actualRepos); diff != "" {
  2244  					t.Errorf("expected repos differ from actual: %s", diff)
  2245  				}
  2246  			}
  2247  		})
  2248  	}
  2249  }
  2250  
  2251  func TestMergeFrom(t *testing.T) {
  2252  	t.Parallel()
  2253  	testCases := []struct {
  2254  		name                string
  2255  		in                  Configuration
  2256  		supplementalConfigs []Configuration
  2257  		expected            Configuration
  2258  		errorExpected       bool
  2259  	}{
  2260  		{
  2261  			name:                "Approve config gets merged",
  2262  			in:                  Configuration{Approve: []Approve{{Repos: []string{"foo/bar"}}}},
  2263  			supplementalConfigs: []Configuration{{Approve: []Approve{{Repos: []string{"foo/baz"}}}}},
  2264  			expected: Configuration{Approve: []Approve{
  2265  				{Repos: []string{"foo/bar"}},
  2266  				{Repos: []string{"foo/baz"}},
  2267  			}},
  2268  		},
  2269  		{
  2270  			name:                "LGTM config gets merged",
  2271  			in:                  Configuration{Lgtm: []Lgtm{{Repos: []string{"foo/bar"}}}},
  2272  			supplementalConfigs: []Configuration{{Lgtm: []Lgtm{{Repos: []string{"foo/baz"}}}}},
  2273  			expected: Configuration{Lgtm: []Lgtm{
  2274  				{Repos: []string{"foo/bar"}},
  2275  				{Repos: []string{"foo/baz"}},
  2276  			}},
  2277  		},
  2278  		{
  2279  			name:                "Triggers config gets merged",
  2280  			in:                  Configuration{Triggers: []Trigger{{Repos: []string{"foo/bar"}}}},
  2281  			supplementalConfigs: []Configuration{{Triggers: []Trigger{{Repos: []string{"foo/baz"}}}}},
  2282  			expected: Configuration{Triggers: []Trigger{
  2283  				{Repos: []string{"foo/bar"}},
  2284  				{Repos: []string{"foo/baz"}},
  2285  			}},
  2286  		},
  2287  		{
  2288  			name:                "Welcome config gets merged",
  2289  			in:                  Configuration{Welcome: []Welcome{{Repos: []string{"foo/bar"}}}},
  2290  			supplementalConfigs: []Configuration{{Welcome: []Welcome{{Repos: []string{"foo/baz"}}}}},
  2291  			expected: Configuration{Welcome: []Welcome{
  2292  				{Repos: []string{"foo/bar"}},
  2293  				{Repos: []string{"foo/baz"}},
  2294  			}},
  2295  		},
  2296  		{
  2297  			name: "ExternalPlugins get merged",
  2298  			in: Configuration{
  2299  				ExternalPlugins: map[string][]ExternalPlugin{
  2300  					"foo/bar": {{Name: "refresh", Endpoint: "http://refresh", Events: []string{"issue_comment"}}},
  2301  				},
  2302  			},
  2303  			supplementalConfigs: []Configuration{{ExternalPlugins: map[string][]ExternalPlugin{"foo/baz": {{Name: "refresh", Endpoint: "http://refresh", Events: []string{"issue_comment"}}}}}},
  2304  			expected: Configuration{
  2305  				ExternalPlugins: map[string][]ExternalPlugin{
  2306  					"foo/bar": {{Name: "refresh", Endpoint: "http://refresh", Events: []string{"issue_comment"}}},
  2307  					"foo/baz": {{Name: "refresh", Endpoint: "http://refresh", Events: []string{"issue_comment"}}},
  2308  				},
  2309  			},
  2310  		},
  2311  		{
  2312  			name:                "Labels.restricted_config gets merged",
  2313  			in:                  Configuration{Label: Label{AdditionalLabels: []string{"foo"}}},
  2314  			supplementalConfigs: []Configuration{{Label: Label{RestrictedLabels: map[string][]RestrictedLabel{"org": {{Label: "cherry-pick-approved", AllowedTeams: []string{"patch-managers"}}}}}}},
  2315  			expected: Configuration{
  2316  				Label: Label{
  2317  					AdditionalLabels: []string{"foo"},
  2318  					RestrictedLabels: map[string][]RestrictedLabel{"org": {{Label: "cherry-pick-approved", AllowedTeams: []string{"patch-managers"}}}},
  2319  				},
  2320  			},
  2321  		},
  2322  		{
  2323  			name:                "main config has no ExternalPlugins config, supplemental config has, it gets merged",
  2324  			supplementalConfigs: []Configuration{{ExternalPlugins: map[string][]ExternalPlugin{"foo/bar": {{Name: "refresh", Endpoint: "http://refresh", Events: []string{"issue_comment"}}}}}},
  2325  			expected: Configuration{
  2326  				ExternalPlugins: map[string][]ExternalPlugin{
  2327  					"foo/bar": {{Name: "refresh", Endpoint: "http://refresh", Events: []string{"issue_comment"}}},
  2328  				},
  2329  			},
  2330  		},
  2331  		{
  2332  			name: "ExternalPlugins cant't merge duplicated configs",
  2333  			in: Configuration{
  2334  				ExternalPlugins: map[string][]ExternalPlugin{
  2335  					"foo/bar": {{Name: "refresh", Endpoint: "http://refresh", Events: []string{"issue_comment"}}},
  2336  				},
  2337  			},
  2338  			supplementalConfigs: []Configuration{{ExternalPlugins: map[string][]ExternalPlugin{"foo/bar": {{Name: "refresh", Endpoint: "http://refresh", Events: []string{"issue_comment"}}}}}},
  2339  			errorExpected:       true,
  2340  		},
  2341  	}
  2342  
  2343  	for _, tc := range testCases {
  2344  		for idx, supplementalConfig := range tc.supplementalConfigs {
  2345  			err := tc.in.mergeFrom(&supplementalConfig)
  2346  			if err != nil && !tc.errorExpected {
  2347  				t.Fatalf("failed to merge supplemental config %d: %v", idx, err)
  2348  			}
  2349  			if err == nil && tc.errorExpected {
  2350  				t.Fatal("expected error but got nothing")
  2351  			}
  2352  		}
  2353  
  2354  		if diff := cmp.Diff(tc.expected, tc.in); !tc.errorExpected && diff != "" {
  2355  			t.Errorf("expected config differs from expected: %s", diff)
  2356  		}
  2357  	}
  2358  }