github.com/GoogleCloudPlatform/testgrid@v0.0.174/config/config_test.go (about)

     1  /*
     2  Copyright 2019 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package config
    18  
    19  import (
    20  	"errors"
    21  	"reflect"
    22  	"strings"
    23  	"testing"
    24  
    25  	configpb "github.com/GoogleCloudPlatform/testgrid/pb/config"
    26  	multierror "github.com/hashicorp/go-multierror"
    27  )
    28  
    29  func TestNormalize(t *testing.T) {
    30  	tests := []struct {
    31  		input    string
    32  		expected string
    33  	}{
    34  		{
    35  			input:    "normal",
    36  			expected: "normal",
    37  		},
    38  		{
    39  			input:    "UPPER",
    40  			expected: "upper",
    41  		},
    42  		{
    43  			input:    "pun-_*ctuation Y_E_A_H!",
    44  			expected: "punctuationyeah",
    45  		},
    46  	}
    47  
    48  	for _, test := range tests {
    49  		t.Run(test.input, func(t *testing.T) {
    50  			got := Normalize(test.input)
    51  			if got != test.expected {
    52  				t.Fatalf("got %s, want %s", got, test.expected)
    53  			}
    54  		})
    55  	}
    56  }
    57  
    58  func TestValidateUnique(t *testing.T) {
    59  	tests := []struct {
    60  		name         string
    61  		input        []string
    62  		expectedErrs []error
    63  	}{
    64  		{
    65  			name:  "No names",
    66  			input: []string{},
    67  		},
    68  		{
    69  			name:  "Unique names",
    70  			input: []string{"test_group_1", "test_group_2", "test_group_3"},
    71  		},
    72  		{
    73  			name:  "Duplicate name; error",
    74  			input: []string{"test_group_1", "test_group_1"},
    75  			expectedErrs: []error{
    76  				DuplicateNameError{"testgroup1", "TestGroup"},
    77  			},
    78  		},
    79  		{
    80  			name:  "Duplicate name after normalization; error",
    81  			input: []string{"test_group_1", "TEST GROUP 1"},
    82  			expectedErrs: []error{
    83  				DuplicateNameError{"testgroup1", "TestGroup"},
    84  			},
    85  		},
    86  	}
    87  	for _, test := range tests {
    88  		t.Run(test.name, func(t *testing.T) {
    89  			err := validateUnique(test.input, "TestGroup")
    90  			if err == nil {
    91  				if len(test.expectedErrs) > 0 {
    92  					t.Fatalf("Expected %v, but got no error", test.expectedErrs)
    93  				}
    94  			} else {
    95  				if len(test.expectedErrs) == 0 {
    96  					t.Fatalf("Unexpected Error: %v", err)
    97  				}
    98  
    99  				if mErr, ok := err.(*multierror.Error); ok {
   100  					if !reflect.DeepEqual(test.expectedErrs, mErr.Errors) {
   101  						t.Fatalf("Expected %v, but got: %v", test.expectedErrs, mErr.Errors)
   102  					}
   103  				} else {
   104  					t.Fatalf("Expected %v, but got: %v", test.expectedErrs, err)
   105  				}
   106  			}
   107  		})
   108  	}
   109  }
   110  
   111  func TestValidateAllUnique(t *testing.T) {
   112  	cases := []struct {
   113  		name string
   114  		c    *configpb.Configuration
   115  		pass bool
   116  	}{
   117  		{
   118  			name: "reject nil config.Configuration",
   119  			c:    nil,
   120  			pass: false,
   121  		},
   122  		{
   123  			name: "everything works",
   124  			c: &configpb.Configuration{
   125  				TestGroups: []*configpb.TestGroup{
   126  					{
   127  						Name: "test_group_1",
   128  					},
   129  				},
   130  				Dashboards: []*configpb.Dashboard{
   131  					{
   132  						Name: "dash",
   133  						DashboardTab: []*configpb.DashboardTab{
   134  							{
   135  								Name: "tab_1",
   136  							},
   137  						},
   138  					},
   139  				},
   140  				DashboardGroups: []*configpb.DashboardGroup{
   141  					{
   142  						Name: "dash_group_1",
   143  					},
   144  				},
   145  			},
   146  			pass: true,
   147  		},
   148  		{
   149  			name: "reject empty group names",
   150  			c: &configpb.Configuration{
   151  				TestGroups: []*configpb.TestGroup{
   152  					{},
   153  				},
   154  			},
   155  		},
   156  		{
   157  			name: "reject empty dashboard names",
   158  			c: &configpb.Configuration{
   159  				Dashboards: []*configpb.Dashboard{
   160  					{},
   161  				},
   162  			},
   163  		},
   164  		{
   165  			name: "reject empty tab names",
   166  			c: &configpb.Configuration{
   167  				Dashboards: []*configpb.Dashboard{
   168  					{
   169  						Name: "dash_1",
   170  						DashboardTab: []*configpb.DashboardTab{
   171  							{},
   172  						},
   173  					},
   174  				},
   175  			},
   176  		},
   177  		{
   178  			name: "reject empty dashboard group names",
   179  			c: &configpb.Configuration{
   180  				DashboardGroups: []*configpb.DashboardGroup{
   181  					{},
   182  				},
   183  			},
   184  		},
   185  		{
   186  			name: "dashboard group names cannot match a dashboard name",
   187  			c: &configpb.Configuration{
   188  				Dashboards: []*configpb.Dashboard{
   189  					{
   190  						Name: "foo",
   191  					},
   192  				},
   193  				DashboardGroups: []*configpb.DashboardGroup{
   194  					{
   195  						Name: "foo",
   196  					},
   197  				},
   198  			},
   199  		},
   200  	}
   201  
   202  	for _, tc := range cases {
   203  		t.Run(tc.name, func(t *testing.T) {
   204  			err := validateAllUnique(tc.c)
   205  			switch {
   206  			case err != nil:
   207  				if tc.pass {
   208  					t.Errorf("got unexpected error: %v", err)
   209  				}
   210  			case !tc.pass:
   211  				t.Error("failed to get an error")
   212  			}
   213  
   214  		})
   215  	}
   216  }
   217  
   218  func TestValidateReferencesExist(t *testing.T) {
   219  	tests := []struct {
   220  		name         string
   221  		input        *configpb.Configuration
   222  		expectedErrs []error
   223  	}{
   224  		{
   225  			name:         "reject nil config.Configuration",
   226  			input:        nil,
   227  			expectedErrs: []error{errors.New("got an empty config.Configuration")},
   228  		},
   229  		{
   230  			name: "Dashboard Tabs must reference an existing Test Group",
   231  			input: &configpb.Configuration{
   232  				Dashboards: []*configpb.Dashboard{
   233  					{
   234  						Name: "dash_1",
   235  						DashboardTab: []*configpb.DashboardTab{
   236  							{
   237  								Name:          "tab_1",
   238  								TestGroupName: "test_group_1",
   239  							},
   240  							{
   241  								Name:          "tab_2",
   242  								TestGroupName: "test_group_2",
   243  							},
   244  						},
   245  					},
   246  				},
   247  				TestGroups: []*configpb.TestGroup{
   248  					{
   249  						Name: "test_group_1",
   250  					},
   251  				},
   252  			},
   253  			expectedErrs: []error{
   254  				MissingEntityError{"test_group_2", "TestGroup"},
   255  			},
   256  		},
   257  		{
   258  			name: "Test Groups must have an associated Dashboard Tab",
   259  			input: &configpb.Configuration{
   260  				Dashboards: []*configpb.Dashboard{
   261  					{
   262  						Name:         "dash_1",
   263  						DashboardTab: []*configpb.DashboardTab{},
   264  					},
   265  				},
   266  				TestGroups: []*configpb.TestGroup{
   267  					{
   268  						Name: "test_group_1",
   269  					},
   270  				},
   271  			},
   272  			expectedErrs: []error{
   273  				ValidationError{"test_group_1", "TestGroup", "Each Test Group must be referenced by at least 1 Dashboard Tab."},
   274  			},
   275  		},
   276  		{
   277  			name: "Dashboard Groups must reference existing Dashboards",
   278  			input: &configpb.Configuration{
   279  				Dashboards: []*configpb.Dashboard{
   280  					{
   281  						Name: "dash_1",
   282  						DashboardTab: []*configpb.DashboardTab{
   283  							{
   284  								Name:          "tab_1",
   285  								TestGroupName: "test_group_1",
   286  							},
   287  						},
   288  					},
   289  				},
   290  				TestGroups: []*configpb.TestGroup{
   291  					{
   292  						Name: "test_group_1",
   293  					},
   294  				},
   295  				DashboardGroups: []*configpb.DashboardGroup{
   296  					{
   297  						Name:           "dash_group_1",
   298  						DashboardNames: []string{"dash_1", "dash_2", "dash_3"},
   299  					},
   300  				},
   301  			},
   302  			expectedErrs: []error{
   303  				MissingEntityError{"dash_2", "Dashboard"},
   304  				MissingEntityError{"dash_3", "Dashboard"},
   305  			},
   306  		},
   307  		{
   308  			name: "A Dashboard can belong to at most 1 Dashboard Group",
   309  			input: &configpb.Configuration{
   310  				Dashboards: []*configpb.Dashboard{
   311  					{
   312  						Name: "dash_1",
   313  						DashboardTab: []*configpb.DashboardTab{
   314  							{
   315  								Name:          "tab_1",
   316  								TestGroupName: "test_group_1",
   317  							},
   318  						},
   319  					},
   320  				},
   321  				TestGroups: []*configpb.TestGroup{
   322  					{
   323  						Name: "test_group_1",
   324  					},
   325  				},
   326  				DashboardGroups: []*configpb.DashboardGroup{
   327  					{
   328  						Name:           "dash_group_1",
   329  						DashboardNames: []string{"dash_1"},
   330  					},
   331  					{
   332  						Name:           "dash_group_2",
   333  						DashboardNames: []string{"dash_1"},
   334  					},
   335  				},
   336  			},
   337  			expectedErrs: []error{
   338  				ValidationError{"dash_1", "Dashboard", "A Dashboard cannot be in more than 1 Dashboard Group."},
   339  			},
   340  		},
   341  	}
   342  
   343  	for _, test := range tests {
   344  		t.Run(test.name, func(t *testing.T) {
   345  			err := validateReferencesExist(test.input)
   346  			if err != nil && len(test.expectedErrs) == 0 {
   347  				t.Fatalf("Unexpected Error: %v", err)
   348  			}
   349  
   350  			if len(test.expectedErrs) != 0 {
   351  				if err == nil {
   352  					t.Fatalf("Expected %v, but got no error", test.expectedErrs)
   353  				}
   354  
   355  				if mErr, ok := err.(*multierror.Error); ok {
   356  					if !reflect.DeepEqual(test.expectedErrs, mErr.Errors) {
   357  						t.Fatalf("Expected %v, but got: %v", test.expectedErrs, mErr.Errors)
   358  					}
   359  				} else {
   360  					t.Fatalf("Expected %v, but got: %v", test.expectedErrs, err)
   361  				}
   362  			}
   363  		})
   364  	}
   365  }
   366  
   367  func TestValidateName(t *testing.T) {
   368  	stringOfLength := func(length int) string {
   369  		var sb strings.Builder
   370  		for i := 0; i < length; i++ {
   371  			sb.WriteRune('a')
   372  		}
   373  		return sb.String()
   374  	}
   375  
   376  	tests := []struct {
   377  		name  string
   378  		input string
   379  		pass  bool
   380  	}{
   381  		{
   382  			name:  "Names can't be empty",
   383  			input: "",
   384  		},
   385  		{
   386  			name:  "Invalid characters are filtered out",
   387  			input: "___%%%***!!!???'''|||@@@###$$$^^^///\\\\\\",
   388  		},
   389  		{
   390  			name:  "Names can't be too short",
   391  			input: "q",
   392  		},
   393  		{
   394  			name:  "Names must contain 3+ alphanumeric characters",
   395  			input: "?rs=%%",
   396  		},
   397  		{
   398  			name:  "Names can't be too long",
   399  			input: stringOfLength(2049),
   400  		},
   401  		{
   402  			name:  "Names can't start with dashboard",
   403  			input: "dashboard",
   404  		},
   405  		{
   406  			name:  "Names can't start with summary",
   407  			input: "_summary_",
   408  		},
   409  		{
   410  			name:  "Names can't start with alerter",
   411  			input: "ALERTER",
   412  		},
   413  		{
   414  			name:  "Names can't start with bugs",
   415  			input: "bugs-1-2-3",
   416  		},
   417  		{
   418  			name:  "Names may contain forbidden prefixes in the middle",
   419  			input: "file-bugs-for-alerter",
   420  			pass:  true,
   421  		},
   422  		{
   423  			name:  "weird characters",
   424  			input: "[my] dash/tab (this_poem.of-sorts~) <@special1>",
   425  			pass:  true,
   426  		},
   427  		{
   428  			name:  "backslash",
   429  			input: "my\\dash",
   430  		},
   431  		{
   432  			name:  "colon",
   433  			input: "my:dash",
   434  		},
   435  		{
   436  			name:  "question",
   437  			input: "my?dash",
   438  		},
   439  		{
   440  			name:  "semicolon",
   441  			input: "my;dash",
   442  		},
   443  		{
   444  			name:  "Valid name",
   445  			input: "some-test-group",
   446  			pass:  true,
   447  		},
   448  	}
   449  
   450  	for _, test := range tests {
   451  		t.Run(test.name, func(t *testing.T) {
   452  			err := validateName(test.input)
   453  			pass := err == nil
   454  			if pass != test.pass {
   455  				t.Fatalf("name %s got pass = %v, want pass = %v", test.input, pass, test.pass)
   456  			}
   457  		})
   458  	}
   459  }
   460  
   461  func TestValidateResultStoreSource(t *testing.T) {
   462  	tests := []struct {
   463  		name string
   464  		tg   *configpb.TestGroup
   465  		err  bool
   466  	}{
   467  		{
   468  			name: "nil test group",
   469  			tg:   nil,
   470  			err:  false,
   471  		},
   472  		{
   473  			name: "empty test group",
   474  			tg:   &configpb.TestGroup{},
   475  			err:  false,
   476  		},
   477  		{
   478  			name: "empty ResultStore source",
   479  			tg: &configpb.TestGroup{
   480  				ResultSource: &configpb.TestGroup_ResultSource{
   481  					ResultSourceConfig: &configpb.TestGroup_ResultSource_ResultstoreConfig{
   482  						ResultstoreConfig: &configpb.ResultStoreConfig{},
   483  					},
   484  				},
   485  			},
   486  			err: true,
   487  		},
   488  		{
   489  			name: "basically works",
   490  			tg: &configpb.TestGroup{
   491  				ResultSource: &configpb.TestGroup_ResultSource{
   492  					ResultSourceConfig: &configpb.TestGroup_ResultSource_ResultstoreConfig{
   493  						ResultstoreConfig: &configpb.ResultStoreConfig{
   494  							Project: "my-project",
   495  						},
   496  					},
   497  				},
   498  			},
   499  			err: false,
   500  		},
   501  		{
   502  			name: "valid query",
   503  			tg: &configpb.TestGroup{
   504  				ResultSource: &configpb.TestGroup_ResultSource{
   505  					ResultSourceConfig: &configpb.TestGroup_ResultSource_ResultstoreConfig{
   506  						ResultstoreConfig: &configpb.ResultStoreConfig{
   507  							Project: "my-project",
   508  							Query:   `target:"my-job"`,
   509  						},
   510  					},
   511  				},
   512  			},
   513  			err: false,
   514  		},
   515  		{
   516  			name: "invalid query",
   517  			tg: &configpb.TestGroup{
   518  				ResultSource: &configpb.TestGroup_ResultSource{
   519  					ResultSourceConfig: &configpb.TestGroup_ResultSource_ResultstoreConfig{
   520  						ResultstoreConfig: &configpb.ResultStoreConfig{
   521  							Project: "my-project",
   522  							Query:   `label:foo bar`,
   523  						},
   524  					},
   525  				},
   526  			},
   527  			err: true,
   528  		},
   529  		{
   530  			name: "query without project",
   531  			tg: &configpb.TestGroup{
   532  				ResultSource: &configpb.TestGroup_ResultSource{
   533  					ResultSourceConfig: &configpb.TestGroup_ResultSource_ResultstoreConfig{
   534  						ResultstoreConfig: &configpb.ResultStoreConfig{
   535  							Query: `target:"my-job"`,
   536  						},
   537  					},
   538  				},
   539  			},
   540  			err: true,
   541  		},
   542  		{
   543  			name: "gcs_prefix and ResultStore defined",
   544  			tg: &configpb.TestGroup{
   545  				GcsPrefix: "/my-bucket/logs",
   546  				ResultSource: &configpb.TestGroup_ResultSource{
   547  					ResultSourceConfig: &configpb.TestGroup_ResultSource_ResultstoreConfig{
   548  						ResultstoreConfig: &configpb.ResultStoreConfig{
   549  							Project: "my-project",
   550  						},
   551  					},
   552  				},
   553  			},
   554  			err: true,
   555  		},
   556  		{
   557  			name: "use_kubernetes_client and ResultStore defined",
   558  			tg: &configpb.TestGroup{
   559  				UseKubernetesClient: true,
   560  				ResultSource: &configpb.TestGroup_ResultSource{
   561  					ResultSourceConfig: &configpb.TestGroup_ResultSource_ResultstoreConfig{
   562  						ResultstoreConfig: &configpb.ResultStoreConfig{
   563  							Project: "my-project",
   564  						},
   565  					},
   566  				},
   567  			},
   568  			err: true,
   569  		},
   570  		{
   571  			name: "other result source defined",
   572  			tg: &configpb.TestGroup{
   573  				GcsPrefix:           "/my-bucket/logs",
   574  				UseKubernetesClient: true,
   575  			},
   576  			err: false,
   577  		},
   578  	}
   579  	for _, test := range tests {
   580  		t.Run(test.name, func(t *testing.T) {
   581  			err := validateResultStoreSource(test.tg)
   582  			if err != nil && !test.err {
   583  				t.Errorf("validateResultStoreSource(%v) errored unexpectedly: %v", test.tg, err)
   584  			} else if err == nil && test.err {
   585  				t.Errorf("validateResultStoreSource(%v) did not error as expected", test.tg)
   586  			}
   587  		})
   588  	}
   589  }
   590  
   591  func TestValidateGCSSource(t *testing.T) {
   592  	tests := []struct {
   593  		name string
   594  		tg   *configpb.TestGroup
   595  		err  bool
   596  	}{
   597  		{
   598  			name: "nil test group",
   599  			tg:   nil,
   600  			err:  false,
   601  		},
   602  		{
   603  			name: "empty test group",
   604  			tg:   &configpb.TestGroup{},
   605  			err:  false,
   606  		},
   607  		{
   608  			name: "empty GCS source",
   609  			tg: &configpb.TestGroup{
   610  				ResultSource: &configpb.TestGroup_ResultSource{
   611  					ResultSourceConfig: &configpb.TestGroup_ResultSource_GcsConfig{
   612  						GcsConfig: &configpb.GCSConfig{},
   613  					},
   614  				},
   615  			},
   616  			err: true,
   617  		},
   618  		{
   619  			name: "basically works",
   620  			tg: &configpb.TestGroup{
   621  				ResultSource: &configpb.TestGroup_ResultSource{
   622  					ResultSourceConfig: &configpb.TestGroup_ResultSource_GcsConfig{
   623  						GcsConfig: &configpb.GCSConfig{
   624  							GcsPrefix: "/my-bucket/logs",
   625  						},
   626  					},
   627  				},
   628  			},
   629  			err: false,
   630  		},
   631  		{
   632  			name: "gcs_prefix and GCS config defined",
   633  			tg: &configpb.TestGroup{
   634  				GcsPrefix: "/my-bucket/logs",
   635  				ResultSource: &configpb.TestGroup_ResultSource{
   636  					ResultSourceConfig: &configpb.TestGroup_ResultSource_GcsConfig{
   637  						GcsConfig: &configpb.GCSConfig{
   638  							GcsPrefix: "/my-bucket/logs",
   639  						},
   640  					},
   641  				},
   642  			},
   643  			err: true,
   644  		},
   645  		{
   646  			name: "use_kubernetes_client and GCS config defined",
   647  			tg: &configpb.TestGroup{
   648  				UseKubernetesClient: true,
   649  				ResultSource: &configpb.TestGroup_ResultSource{
   650  					ResultSourceConfig: &configpb.TestGroup_ResultSource_GcsConfig{
   651  						GcsConfig: &configpb.GCSConfig{
   652  							GcsPrefix: "/my-bucket/logs",
   653  						},
   654  					},
   655  				},
   656  			},
   657  			err: true,
   658  		},
   659  		{
   660  			name: "other result source defined",
   661  			tg: &configpb.TestGroup{
   662  				GcsPrefix:           "/my-bucket/logs",
   663  				UseKubernetesClient: true,
   664  			},
   665  			err: false,
   666  		},
   667  		{
   668  			name: "GCS config with pubsub",
   669  			tg: &configpb.TestGroup{
   670  				ResultSource: &configpb.TestGroup_ResultSource{
   671  					ResultSourceConfig: &configpb.TestGroup_ResultSource_GcsConfig{
   672  						GcsConfig: &configpb.GCSConfig{
   673  							GcsPrefix:          "/my-bucket/logs",
   674  							PubsubProject:      "my-project",
   675  							PubsubSubscription: "my-gcs-notifications",
   676  						},
   677  					},
   678  				},
   679  			},
   680  			err: false,
   681  		},
   682  		{
   683  			name: "GCS config with partial pubsub",
   684  			tg: &configpb.TestGroup{
   685  				ResultSource: &configpb.TestGroup_ResultSource{
   686  					ResultSourceConfig: &configpb.TestGroup_ResultSource_GcsConfig{
   687  						GcsConfig: &configpb.GCSConfig{
   688  							GcsPrefix:     "/my-bucket/logs",
   689  							PubsubProject: "my-project",
   690  						},
   691  					},
   692  				},
   693  			},
   694  			err: true,
   695  		},
   696  	}
   697  	for _, test := range tests {
   698  		t.Run(test.name, func(t *testing.T) {
   699  			err := validateGCSSource(test.tg)
   700  			if err != nil && !test.err {
   701  				t.Errorf("validateGCSSource(%v) errored unexpectedly: %v", test.tg, err)
   702  			} else if err == nil && test.err {
   703  				t.Errorf("validateGCSSource(%v) did not error as expected", test.tg)
   704  			}
   705  		})
   706  	}
   707  }
   708  
   709  func TestValidateTestGroup(t *testing.T) {
   710  	tests := []struct {
   711  		name      string
   712  		testGroup *configpb.TestGroup
   713  		pass      bool
   714  	}{
   715  		{
   716  			name:      "Nil TestGroup fails",
   717  			pass:      false,
   718  			testGroup: nil,
   719  		},
   720  		{
   721  			name: "Minimal config passes",
   722  			pass: true,
   723  			testGroup: &configpb.TestGroup{
   724  				Name:             "test_group",
   725  				DaysOfResults:    1,
   726  				GcsPrefix:        "fake path",
   727  				NumColumnsRecent: 1,
   728  			},
   729  		},
   730  		{
   731  			name: "Must have days_of_results",
   732  			testGroup: &configpb.TestGroup{
   733  				Name:             "test_group",
   734  				GcsPrefix:        "fake path",
   735  				NumColumnsRecent: 1,
   736  			},
   737  		},
   738  		{
   739  			name: "days_of_results must be positive",
   740  			testGroup: &configpb.TestGroup{
   741  				Name:             "test_group",
   742  				DaysOfResults:    -1,
   743  				GcsPrefix:        "fake path",
   744  				NumColumnsRecent: 1,
   745  			},
   746  		},
   747  		{
   748  			name: "Must have gcs_prefix",
   749  			testGroup: &configpb.TestGroup{
   750  				Name:             "test_group",
   751  				DaysOfResults:    1,
   752  				NumColumnsRecent: 1,
   753  			},
   754  		},
   755  		{
   756  			name: "Must have num_columns_recent",
   757  			testGroup: &configpb.TestGroup{
   758  				Name:          "test_group",
   759  				DaysOfResults: 1,
   760  				GcsPrefix:     "fake path",
   761  			},
   762  		},
   763  		{
   764  			name: "num_columns_recent must be positive",
   765  			testGroup: &configpb.TestGroup{
   766  				Name:             "test_group",
   767  				DaysOfResults:    1,
   768  				GcsPrefix:        "fake path",
   769  				NumColumnsRecent: -1,
   770  			},
   771  		},
   772  		{
   773  			name: "test_method_match_regex must compile",
   774  			testGroup: &configpb.TestGroup{
   775  				Name:                 "test_group",
   776  				DaysOfResults:        1,
   777  				GcsPrefix:            "fake path",
   778  				NumColumnsRecent:     1,
   779  				TestMethodMatchRegex: "[.*",
   780  			},
   781  		},
   782  		{
   783  			name: "Notifications must have a summary",
   784  			testGroup: &configpb.TestGroup{
   785  				Name:             "test_group",
   786  				DaysOfResults:    1,
   787  				GcsPrefix:        "fake path",
   788  				NumColumnsRecent: 1,
   789  				Notifications: []*configpb.Notification{
   790  					{},
   791  				},
   792  			},
   793  		},
   794  		{
   795  			name: "Test Annotations must have property_name",
   796  			testGroup: &configpb.TestGroup{
   797  				Name:             "test_group",
   798  				DaysOfResults:    1,
   799  				GcsPrefix:        "fake path",
   800  				NumColumnsRecent: 1,
   801  				TestAnnotations: []*configpb.TestGroup_TestAnnotation{
   802  					{
   803  						ShortText: "a",
   804  					},
   805  				},
   806  			},
   807  		},
   808  		{
   809  			name: "Test Annotation short_text has to be at least 1 character",
   810  			testGroup: &configpb.TestGroup{
   811  				Name:             "test_group",
   812  				DaysOfResults:    1,
   813  				GcsPrefix:        "fake path",
   814  				NumColumnsRecent: 1,
   815  				TestAnnotations: []*configpb.TestGroup_TestAnnotation{
   816  					{
   817  						ShortTextMessageSource: &configpb.TestGroup_TestAnnotation_PropertyName{
   818  							PropertyName: "something",
   819  						},
   820  						ShortText: "",
   821  					},
   822  				},
   823  			},
   824  		},
   825  		{
   826  			name: "Test Annotation short_text has to be at most 5 characters",
   827  			testGroup: &configpb.TestGroup{
   828  				Name:             "test_group",
   829  				DaysOfResults:    1,
   830  				GcsPrefix:        "fake path",
   831  				NumColumnsRecent: 1,
   832  				TestAnnotations: []*configpb.TestGroup_TestAnnotation{
   833  					{
   834  						ShortTextMessageSource: &configpb.TestGroup_TestAnnotation_PropertyName{
   835  							PropertyName: "something",
   836  						},
   837  						ShortText: "abcdef",
   838  					},
   839  				},
   840  			},
   841  		},
   842  		{
   843  			name: "fallback_grouping_configuration_value requires fallback_group = configuration_value",
   844  			testGroup: &configpb.TestGroup{
   845  				Name:                               "test_group",
   846  				DaysOfResults:                      1,
   847  				GcsPrefix:                          "fake path",
   848  				NumColumnsRecent:                   1,
   849  				FallbackGroupingConfigurationValue: "something",
   850  			},
   851  		},
   852  		{
   853  			name: "fallback_grouping = configuration_value requires fallback_grouping_configuration_value",
   854  			testGroup: &configpb.TestGroup{
   855  				Name:             "test_group",
   856  				DaysOfResults:    1,
   857  				GcsPrefix:        "fake path",
   858  				NumColumnsRecent: 1,
   859  				FallbackGrouping: configpb.TestGroup_FALLBACK_GROUPING_CONFIGURATION_VALUE,
   860  			},
   861  		},
   862  		{
   863  			name: "Complex config passes",
   864  			pass: true,
   865  			testGroup: &configpb.TestGroup{
   866  				// Basic config
   867  				Name:             "test_group",
   868  				DaysOfResults:    1,
   869  				GcsPrefix:        "fake path",
   870  				NumColumnsRecent: 1,
   871  				// Regexes compile
   872  				TestMethodMatchRegex: "test.*",
   873  				// Simple notification
   874  				Notifications: []*configpb.Notification{
   875  					{
   876  						Summary: "I'm a notification!",
   877  					},
   878  				},
   879  				// Fallback grouping based on a configuration value
   880  				FallbackGrouping:                   configpb.TestGroup_FALLBACK_GROUPING_CONFIGURATION_VALUE,
   881  				FallbackGroupingConfigurationValue: "something",
   882  				// Simple test annotation based on a property
   883  				TestAnnotations: []*configpb.TestGroup_TestAnnotation{
   884  					{
   885  						ShortTextMessageSource: &configpb.TestGroup_TestAnnotation_PropertyName{
   886  							PropertyName: "something",
   887  						},
   888  						ShortText: "abc",
   889  					},
   890  				},
   891  			},
   892  		},
   893  		{
   894  			name: "accept filled column headers",
   895  			pass: true,
   896  			testGroup: &configpb.TestGroup{
   897  				Name:             "test_group",
   898  				DaysOfResults:    1,
   899  				GcsPrefix:        "fake path",
   900  				NumColumnsRecent: 1,
   901  				ColumnHeader: []*configpb.TestGroup_ColumnHeader{
   902  					{
   903  						Label: "lab",
   904  					},
   905  					{
   906  						Property: "prop",
   907  					},
   908  					{
   909  						ConfigurationValue: "yay",
   910  					},
   911  				},
   912  			},
   913  		},
   914  		{
   915  			name: "reject column headers with label and configuration_value",
   916  			testGroup: &configpb.TestGroup{
   917  				Name:             "test_group",
   918  				DaysOfResults:    1,
   919  				GcsPrefix:        "fake path",
   920  				NumColumnsRecent: 1,
   921  				ColumnHeader: []*configpb.TestGroup_ColumnHeader{
   922  					{
   923  						Label:              "labtoo",
   924  						ConfigurationValue: "foo",
   925  					},
   926  				},
   927  			},
   928  		},
   929  		{
   930  			name: "reject column headers with configuration_value and property",
   931  			testGroup: &configpb.TestGroup{
   932  				Name:             "test_group",
   933  				DaysOfResults:    1,
   934  				GcsPrefix:        "fake path",
   935  				NumColumnsRecent: 1,
   936  				ColumnHeader: []*configpb.TestGroup_ColumnHeader{
   937  					{
   938  						ConfigurationValue: "bar",
   939  						Property:           "proptoo",
   940  					},
   941  				},
   942  			},
   943  		},
   944  		{
   945  			name: "reject column headers with label and property",
   946  			testGroup: &configpb.TestGroup{
   947  				Name:             "test_group",
   948  				DaysOfResults:    1,
   949  				GcsPrefix:        "fake path",
   950  				NumColumnsRecent: 1,
   951  				ColumnHeader: []*configpb.TestGroup_ColumnHeader{
   952  					{
   953  						Label:    "labtoo",
   954  						Property: "proptoo",
   955  					},
   956  				},
   957  			},
   958  		},
   959  		{
   960  			name: "reject empty column headers",
   961  			testGroup: &configpb.TestGroup{
   962  				Name:             "test_group",
   963  				DaysOfResults:    1,
   964  				GcsPrefix:        "fake path",
   965  				NumColumnsRecent: 1,
   966  				ColumnHeader: []*configpb.TestGroup_ColumnHeader{
   967  					{},
   968  				},
   969  			},
   970  		},
   971  		{
   972  			name: "reject unformatted name format",
   973  			testGroup: &configpb.TestGroup{
   974  				Name:             "simple",
   975  				DaysOfResults:    1,
   976  				GcsPrefix:        "fake path",
   977  				NumColumnsRecent: 1,
   978  				TestNameConfig: &configpb.TestNameConfig{
   979  					NameFormat: "hello world",
   980  				},
   981  			},
   982  		},
   983  		{
   984  			name: "accept complex and balanced name formats",
   985  			pass: true,
   986  			testGroup: &configpb.TestGroup{
   987  				Name:             "complex",
   988  				DaysOfResults:    1,
   989  				GcsPrefix:        "fake path",
   990  				NumColumnsRecent: 1,
   991  				TestNameConfig: &configpb.TestNameConfig{
   992  					NameFormat: "hello %s you are %s",
   993  					NameElements: []*configpb.TestNameConfig_NameElement{
   994  						{
   995  							Labels: "world",
   996  						},
   997  						{
   998  							Labels: "great",
   999  						},
  1000  					},
  1001  				},
  1002  			},
  1003  		},
  1004  		{
  1005  			name: "reject unbalanced name formats",
  1006  			testGroup: &configpb.TestGroup{
  1007  				Name:             "bad",
  1008  				DaysOfResults:    1,
  1009  				GcsPrefix:        "fake path",
  1010  				NumColumnsRecent: 1,
  1011  				TestNameConfig: &configpb.TestNameConfig{
  1012  					NameFormat: "sorry %s but this is just too %s to tell you",
  1013  					NameElements: []*configpb.TestNameConfig_NameElement{
  1014  						{
  1015  							Labels: "charlie",
  1016  						},
  1017  					},
  1018  				},
  1019  			},
  1020  		},
  1021  		{
  1022  			name: "basic test metadata options",
  1023  			pass: true,
  1024  			testGroup: &configpb.TestGroup{
  1025  				Name:             "bad",
  1026  				DaysOfResults:    1,
  1027  				GcsPrefix:        "fake path",
  1028  				NumColumnsRecent: 1,
  1029  				TestMetadataOptions: []*configpb.TestMetadataOptions{
  1030  					{
  1031  						BugComponent:  1234,
  1032  						TestNameRegex: ".*stuff",
  1033  					},
  1034  				},
  1035  			},
  1036  		},
  1037  		{
  1038  			name: "test metadata options zero component allowed",
  1039  			pass: true,
  1040  			testGroup: &configpb.TestGroup{
  1041  				Name:             "bad",
  1042  				DaysOfResults:    1,
  1043  				GcsPrefix:        "fake path",
  1044  				NumColumnsRecent: 1,
  1045  				TestMetadataOptions: []*configpb.TestMetadataOptions{
  1046  					{
  1047  						BugComponent:  0,
  1048  						TestNameRegex: ".*stuff",
  1049  					},
  1050  				},
  1051  			},
  1052  		},
  1053  		{
  1054  			name: "test metadata options negative component allowed",
  1055  			pass: true,
  1056  			testGroup: &configpb.TestGroup{
  1057  				Name:             "bad",
  1058  				DaysOfResults:    1,
  1059  				GcsPrefix:        "fake path",
  1060  				NumColumnsRecent: 1,
  1061  				TestMetadataOptions: []*configpb.TestMetadataOptions{
  1062  					{
  1063  						BugComponent:  -1,
  1064  						TestNameRegex: ".*stuff",
  1065  					},
  1066  				},
  1067  			},
  1068  		},
  1069  		{
  1070  			name: "invalid empty test metadata options",
  1071  			testGroup: &configpb.TestGroup{
  1072  				Name:             "bad",
  1073  				DaysOfResults:    1,
  1074  				GcsPrefix:        "fake path",
  1075  				NumColumnsRecent: 1,
  1076  				TestMetadataOptions: []*configpb.TestMetadataOptions{
  1077  					{
  1078  						BugComponent: 1234,
  1079  					},
  1080  				},
  1081  			},
  1082  		},
  1083  		{
  1084  			name: "invalid test name regex",
  1085  			testGroup: &configpb.TestGroup{
  1086  				Name:             "bad",
  1087  				DaysOfResults:    1,
  1088  				GcsPrefix:        "fake path",
  1089  				NumColumnsRecent: 1,
  1090  				TestMetadataOptions: []*configpb.TestMetadataOptions{
  1091  					{
  1092  						BugComponent:  1234,
  1093  						TestNameRegex: "?bad",
  1094  					},
  1095  				},
  1096  			},
  1097  		},
  1098  		{
  1099  			name: "invalid message regex",
  1100  			testGroup: &configpb.TestGroup{
  1101  				Name:             "bad",
  1102  				DaysOfResults:    1,
  1103  				GcsPrefix:        "fake path",
  1104  				NumColumnsRecent: 1,
  1105  				TestMetadataOptions: []*configpb.TestMetadataOptions{
  1106  					{
  1107  						BugComponent: 1234,
  1108  						MessageRegex: "?bad",
  1109  					},
  1110  				},
  1111  			},
  1112  		},
  1113  	}
  1114  	for _, test := range tests {
  1115  		t.Run(test.name, func(t *testing.T) {
  1116  			err := validateTestGroup(test.testGroup)
  1117  			pass := err == nil
  1118  			if test.pass != pass {
  1119  				t.Fatalf("test group config got pass = %v, want pass = %v: %v", pass, test.pass, err)
  1120  			}
  1121  		})
  1122  	}
  1123  }
  1124  
  1125  func TestInvalidEmails(t *testing.T) {
  1126  	tests := []struct {
  1127  		name      string
  1128  		addresses string
  1129  		pass      bool
  1130  	}{
  1131  		{
  1132  			name:      "Addresses can't be blank",
  1133  			addresses: "",
  1134  		},
  1135  		{
  1136  			name:      "Comma-separated addresses can't be blank",
  1137  			addresses: ",",
  1138  		},
  1139  		{
  1140  			name:      "Comma-separated addresses still can't be blank",
  1141  			addresses: ",thing@email.com",
  1142  		},
  1143  		{
  1144  			name:      "no username",
  1145  			addresses: "@email.com",
  1146  		},
  1147  		{
  1148  			name:      "no domain name",
  1149  			addresses: "username",
  1150  		},
  1151  		{
  1152  			name:      "@ but no domain name",
  1153  			addresses: "username@",
  1154  		},
  1155  		{
  1156  			name:      "too many @'s",
  1157  			addresses: "hey@hello@greetings.com",
  1158  		},
  1159  		{
  1160  			name:      "Valid Address",
  1161  			addresses: "hey@greetings.com",
  1162  			pass:      true,
  1163  		},
  1164  		{
  1165  			name:      "Multiple Valid Addresses",
  1166  			addresses: "hey@greetings.com,something@mail.com",
  1167  			pass:      true,
  1168  		},
  1169  	}
  1170  	for _, test := range tests {
  1171  		t.Run(test.name, func(t *testing.T) {
  1172  			err := validateEmails(test.addresses)
  1173  			pass := err == nil
  1174  			if test.pass != pass {
  1175  				t.Fatalf("addresses (%s) got pass = %v, want pass = %v: %v", test.addresses, pass, test.pass, err)
  1176  			}
  1177  		})
  1178  	}
  1179  }
  1180  
  1181  func TestValidateDashboardTab(t *testing.T) {
  1182  	tests := []struct {
  1183  		name string
  1184  		tab  *configpb.DashboardTab
  1185  		err  bool
  1186  	}{
  1187  		{
  1188  			name: "nil DashboardTab fails",
  1189  			tab:  nil,
  1190  			err:  true,
  1191  		},
  1192  		{
  1193  			name: "tab, missing test group",
  1194  			tab: &configpb.DashboardTab{
  1195  				Name: "tabby",
  1196  			},
  1197  			err: true,
  1198  		},
  1199  		{
  1200  			name: "tab, has test group",
  1201  			tab: &configpb.DashboardTab{
  1202  				Name:          "tabby",
  1203  				TestGroupName: "test_group_1",
  1204  			},
  1205  		},
  1206  		{
  1207  			name: "tabular names basically works",
  1208  			tab: &configpb.DashboardTab{
  1209  				Name:              "tabby",
  1210  				TestGroupName:     "test_group_1",
  1211  				TabularNamesRegex: `(?P<hello>\d+).*(?P<hi>\d+)`,
  1212  			},
  1213  		},
  1214  		{
  1215  			name: "tabular names, invalid compile",
  1216  			tab: &configpb.DashboardTab{
  1217  				Name:              "tabby",
  1218  				TestGroupName:     "test_group_1",
  1219  				TabularNamesRegex: `([1!]`,
  1220  			},
  1221  			err: true,
  1222  		},
  1223  		{
  1224  			name: "tabular names, 0 capture groups",
  1225  			tab: &configpb.DashboardTab{
  1226  				Name:              "tabby",
  1227  				TestGroupName:     "test_group_1",
  1228  				TabularNamesRegex: `.*`,
  1229  			},
  1230  			err: true,
  1231  		},
  1232  		{
  1233  			name: "tabular names, unnamed capture groups",
  1234  			tab: &configpb.DashboardTab{
  1235  				Name:              "tabby",
  1236  				TestGroupName:     "test_group_1",
  1237  				TabularNamesRegex: `(\d+).*(\d+)`,
  1238  			},
  1239  			err: true,
  1240  		},
  1241  		{
  1242  			name: "invalid max acceptable flakiness parameter",
  1243  			tab: &configpb.DashboardTab{
  1244  				Name:          "pug",
  1245  				TestGroupName: "test_group_2",
  1246  				StatusCustomizationOptions: &configpb.DashboardTabStatusCustomizationOptions{
  1247  					MaxAcceptableFlakiness: 101.5,
  1248  				},
  1249  			},
  1250  			err: true,
  1251  		},
  1252  		{
  1253  			name: "tab, has testgroup, valid max acceptable flakiness parameter",
  1254  			tab: &configpb.DashboardTab{
  1255  				Name:          "pug",
  1256  				TestGroupName: "test_group_2",
  1257  				StatusCustomizationOptions: &configpb.DashboardTabStatusCustomizationOptions{
  1258  					MaxAcceptableFlakiness: 25.0,
  1259  				},
  1260  			},
  1261  		},
  1262  		{
  1263  			name: "tab, has testgroup, valid max acceptable flakiness parameter, lower boundary",
  1264  			tab: &configpb.DashboardTab{
  1265  				Name:          "pug",
  1266  				TestGroupName: "test_group_2",
  1267  				StatusCustomizationOptions: &configpb.DashboardTabStatusCustomizationOptions{
  1268  					MaxAcceptableFlakiness: 0.0,
  1269  				},
  1270  			},
  1271  		},
  1272  		{
  1273  			name: "tab, has testgroup, valid max acceptable flakiness parameter, upper boundary",
  1274  			tab: &configpb.DashboardTab{
  1275  				Name:          "pug",
  1276  				TestGroupName: "test_group_2",
  1277  				StatusCustomizationOptions: &configpb.DashboardTabStatusCustomizationOptions{
  1278  					MaxAcceptableFlakiness: 100.0,
  1279  				},
  1280  			},
  1281  		},
  1282  	}
  1283  	for _, test := range tests {
  1284  		t.Run(test.name, func(t *testing.T) {
  1285  			err := validateDashboardTab(test.tab)
  1286  			if err == nil && test.err {
  1287  				t.Fatalf("Did not get expected error")
  1288  			}
  1289  			if err != nil && !test.err {
  1290  				t.Fatalf("Got unexpected error: %v", err)
  1291  			}
  1292  		})
  1293  	}
  1294  }
  1295  
  1296  func TestUpdate_Validate(t *testing.T) {
  1297  	tests := []struct {
  1298  		name         string
  1299  		input        *configpb.Configuration
  1300  		expectedErrs []error
  1301  	}{
  1302  		{
  1303  			name:         "Nil input; returns error",
  1304  			input:        nil,
  1305  			expectedErrs: []error{errors.New("got an empty config.Configuration")},
  1306  		},
  1307  		{
  1308  			name: "Dashboard Only; returns error",
  1309  			input: &configpb.Configuration{
  1310  				Dashboards: []*configpb.Dashboard{
  1311  					{
  1312  						Name: "dash_1",
  1313  					},
  1314  				},
  1315  			},
  1316  			expectedErrs: []error{
  1317  				MissingFieldError{"TestGroups"},
  1318  			},
  1319  		},
  1320  		{
  1321  			name: "Test Group Only; returns error",
  1322  			input: &configpb.Configuration{
  1323  				TestGroups: []*configpb.TestGroup{
  1324  					{
  1325  						Name: "test_group_1",
  1326  					},
  1327  				},
  1328  			},
  1329  			expectedErrs: []error{
  1330  				MissingFieldError{"Dashboards"},
  1331  			},
  1332  		},
  1333  		{
  1334  			name: "Complete Minimal Config",
  1335  			input: &configpb.Configuration{
  1336  				Dashboards: []*configpb.Dashboard{
  1337  					{
  1338  						Name: "dash_1",
  1339  						DashboardTab: []*configpb.DashboardTab{
  1340  							{
  1341  								Name:          "tab_1",
  1342  								TestGroupName: "test_group_1",
  1343  							},
  1344  						},
  1345  					},
  1346  				},
  1347  				TestGroups: []*configpb.TestGroup{
  1348  					{
  1349  						Name:             "test_group_1",
  1350  						GcsPrefix:        "fake GcsPrefix",
  1351  						DaysOfResults:    1,
  1352  						NumColumnsRecent: 1,
  1353  					},
  1354  				},
  1355  			},
  1356  		},
  1357  		{
  1358  			name: "Empty Dashboard; returns error",
  1359  			input: &configpb.Configuration{
  1360  				Dashboards: []*configpb.Dashboard{
  1361  					{
  1362  						Name: "dash_1",
  1363  						DashboardTab: []*configpb.DashboardTab{
  1364  							{
  1365  								Name:          "tab_1",
  1366  								TestGroupName: "test_group_1",
  1367  							},
  1368  						},
  1369  					},
  1370  					{
  1371  						Name: "dash_2",
  1372  					},
  1373  				},
  1374  				TestGroups: []*configpb.TestGroup{
  1375  					{
  1376  						Name:             "test_group_1",
  1377  						GcsPrefix:        "fake GcsPrefix",
  1378  						DaysOfResults:    1,
  1379  						NumColumnsRecent: 1,
  1380  					},
  1381  				},
  1382  			},
  1383  			expectedErrs: []error{
  1384  				ValidationError{"dash_2", "Dashboard", "contains no tabs"},
  1385  			},
  1386  		},
  1387  		{
  1388  			name: "Dashboards and Dashboard Groups cannot share names.",
  1389  			input: &configpb.Configuration{
  1390  				Dashboards: []*configpb.Dashboard{
  1391  					{
  1392  						Name: "name_1",
  1393  						DashboardTab: []*configpb.DashboardTab{
  1394  							{
  1395  								Name:          "tab_1",
  1396  								TestGroupName: "test_group_1",
  1397  							},
  1398  						},
  1399  					},
  1400  				},
  1401  				DashboardGroups: []*configpb.DashboardGroup{
  1402  					{
  1403  						Name: "name_1",
  1404  					},
  1405  				},
  1406  				TestGroups: []*configpb.TestGroup{
  1407  					{
  1408  						Name:             "test_group_1",
  1409  						GcsPrefix:        "fake GcsPrefix",
  1410  						DaysOfResults:    1,
  1411  						NumColumnsRecent: 1,
  1412  					},
  1413  				},
  1414  			},
  1415  			expectedErrs: []error{
  1416  				DuplicateNameError{"name1", "Dashboard/DashboardGroup"},
  1417  			},
  1418  		},
  1419  		{
  1420  			name: "Dashboard Tabs must reference an existing Test Group",
  1421  			input: &configpb.Configuration{
  1422  				Dashboards: []*configpb.Dashboard{
  1423  					{
  1424  						Name: "dash_1",
  1425  						DashboardTab: []*configpb.DashboardTab{
  1426  							{
  1427  								Name:          "tab_1",
  1428  								TestGroupName: "test_group_1",
  1429  							},
  1430  							{
  1431  								Name:          "tab_2",
  1432  								TestGroupName: "test_group_2",
  1433  							},
  1434  						},
  1435  					},
  1436  				},
  1437  				TestGroups: []*configpb.TestGroup{
  1438  					{
  1439  						Name:             "test_group_1",
  1440  						GcsPrefix:        "fake GcsPrefix",
  1441  						DaysOfResults:    1,
  1442  						NumColumnsRecent: 1,
  1443  					},
  1444  				},
  1445  			},
  1446  			expectedErrs: []error{
  1447  				MissingEntityError{"test_group_2", "TestGroup"},
  1448  			},
  1449  		},
  1450  		{
  1451  			name: "Test Groups must have an associated Dashboard Tab",
  1452  			input: &configpb.Configuration{
  1453  				Dashboards: []*configpb.Dashboard{
  1454  					{
  1455  						Name: "dash_1",
  1456  						DashboardTab: []*configpb.DashboardTab{
  1457  							{
  1458  								Name:          "tab_1",
  1459  								TestGroupName: "test_group_1",
  1460  							},
  1461  						},
  1462  					},
  1463  				},
  1464  				TestGroups: []*configpb.TestGroup{
  1465  					{
  1466  						Name:             "test_group_1",
  1467  						GcsPrefix:        "fake GcsPrefix",
  1468  						DaysOfResults:    1,
  1469  						NumColumnsRecent: 1,
  1470  					},
  1471  					{
  1472  						Name:             "test_group_2",
  1473  						GcsPrefix:        "fake GcsPrefix",
  1474  						DaysOfResults:    1,
  1475  						NumColumnsRecent: 1,
  1476  					},
  1477  				},
  1478  			},
  1479  			expectedErrs: []error{
  1480  				ValidationError{"test_group_2", "TestGroup", "Each Test Group must be referenced by at least 1 Dashboard Tab."},
  1481  			},
  1482  		},
  1483  		{
  1484  			name: "Dashboard Groups must reference existing Dashboards",
  1485  			input: &configpb.Configuration{
  1486  				Dashboards: []*configpb.Dashboard{
  1487  					{
  1488  						Name: "dash_1",
  1489  						DashboardTab: []*configpb.DashboardTab{
  1490  							{
  1491  								Name:          "tab_1",
  1492  								TestGroupName: "test_group_1",
  1493  							},
  1494  						},
  1495  					},
  1496  				},
  1497  				TestGroups: []*configpb.TestGroup{
  1498  					{
  1499  						Name:             "test_group_1",
  1500  						GcsPrefix:        "fake GcsPrefix",
  1501  						DaysOfResults:    1,
  1502  						NumColumnsRecent: 1,
  1503  					},
  1504  				},
  1505  				DashboardGroups: []*configpb.DashboardGroup{
  1506  					{
  1507  						Name:           "dash_group_1",
  1508  						DashboardNames: []string{"dash_1", "dash_2", "dash_3"},
  1509  					},
  1510  				},
  1511  			},
  1512  			expectedErrs: []error{
  1513  				MissingEntityError{"dash_2", "Dashboard"},
  1514  				MissingEntityError{"dash_3", "Dashboard"},
  1515  			},
  1516  		},
  1517  		{
  1518  			name: "A Dashboard can belong to at most 1 Dashboard Group",
  1519  			input: &configpb.Configuration{
  1520  				Dashboards: []*configpb.Dashboard{
  1521  					{
  1522  						Name: "dash_1",
  1523  						DashboardTab: []*configpb.DashboardTab{
  1524  							{
  1525  								Name:          "tab_1",
  1526  								TestGroupName: "test_group_1",
  1527  							},
  1528  						},
  1529  					},
  1530  				},
  1531  				TestGroups: []*configpb.TestGroup{
  1532  					{
  1533  						Name:             "test_group_1",
  1534  						GcsPrefix:        "fake GcsPrefix",
  1535  						DaysOfResults:    1,
  1536  						NumColumnsRecent: 1,
  1537  					},
  1538  				},
  1539  				DashboardGroups: []*configpb.DashboardGroup{
  1540  					{
  1541  						Name:           "dash_group_1",
  1542  						DashboardNames: []string{"dash_1"},
  1543  					},
  1544  					{
  1545  						Name:           "dash_group_2",
  1546  						DashboardNames: []string{"dash_1"},
  1547  					},
  1548  				},
  1549  			},
  1550  			expectedErrs: []error{
  1551  				ValidationError{"dash_1", "Dashboard", "A Dashboard cannot be in more than 1 Dashboard Group."},
  1552  			},
  1553  		},
  1554  	}
  1555  
  1556  	for _, test := range tests {
  1557  		t.Run(test.name, func(t *testing.T) {
  1558  			err := Validate(test.input)
  1559  			if err != nil && len(test.expectedErrs) == 0 {
  1560  				t.Fatalf("Unexpected Error: %v", err)
  1561  			}
  1562  
  1563  			if len(test.expectedErrs) != 0 {
  1564  				if err == nil {
  1565  					t.Fatalf("Expected %v, but got no error", test.expectedErrs)
  1566  				}
  1567  
  1568  				if mErr, ok := err.(*multierror.Error); ok {
  1569  					if !reflect.DeepEqual(test.expectedErrs, mErr.Errors) {
  1570  						t.Fatalf("Expected %v, but got: %v", test.expectedErrs, mErr.Errors)
  1571  					}
  1572  				} else {
  1573  					t.Fatalf("Expected %v, but got: %v", test.expectedErrs, err)
  1574  				}
  1575  			}
  1576  		})
  1577  	}
  1578  }