sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/crier/reporters/slack/reporter_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 slack
    18  
    19  import (
    20  	"context"
    21  	"testing"
    22  
    23  	"github.com/sirupsen/logrus"
    24  
    25  	v1 "sigs.k8s.io/prow/pkg/apis/prowjobs/v1"
    26  	"sigs.k8s.io/prow/pkg/config"
    27  )
    28  
    29  func TestShouldReport(t *testing.T) {
    30  	boolPtr := func(b bool) *bool {
    31  		return &b
    32  	}
    33  	testCases := []struct {
    34  		name     string
    35  		config   config.SlackReporter
    36  		pj       *v1.ProwJob
    37  		expected bool
    38  	}{
    39  		{
    40  			name: "Presubmit Job should report",
    41  			config: config.SlackReporter{
    42  				JobTypesToReport: []v1.ProwJobType{v1.PresubmitJob},
    43  				SlackReporterConfig: v1.SlackReporterConfig{
    44  					JobStatesToReport: []v1.ProwJobState{v1.SuccessState},
    45  				},
    46  			},
    47  			pj: &v1.ProwJob{
    48  				Spec: v1.ProwJobSpec{
    49  					Type: v1.PresubmitJob,
    50  				},
    51  				Status: v1.ProwJobStatus{
    52  					State: v1.SuccessState,
    53  				},
    54  			},
    55  			expected: true,
    56  		},
    57  		{
    58  			name: "Wrong job type  should not report",
    59  			config: config.SlackReporter{
    60  				JobTypesToReport: []v1.ProwJobType{v1.PostsubmitJob},
    61  				SlackReporterConfig: v1.SlackReporterConfig{
    62  					JobStatesToReport: []v1.ProwJobState{v1.SuccessState},
    63  				},
    64  			},
    65  			pj: &v1.ProwJob{
    66  				Spec: v1.ProwJobSpec{
    67  					Type: v1.PresubmitJob,
    68  				},
    69  				Status: v1.ProwJobStatus{
    70  					State: v1.SuccessState,
    71  				},
    72  			},
    73  			expected: false,
    74  		},
    75  		{
    76  			name: "Successful Job should report",
    77  			config: config.SlackReporter{
    78  				JobTypesToReport: []v1.ProwJobType{v1.PostsubmitJob},
    79  				SlackReporterConfig: v1.SlackReporterConfig{
    80  					JobStatesToReport: []v1.ProwJobState{v1.SuccessState},
    81  				},
    82  			},
    83  			pj: &v1.ProwJob{
    84  				Spec: v1.ProwJobSpec{
    85  					Type: v1.PostsubmitJob,
    86  				},
    87  				Status: v1.ProwJobStatus{
    88  					State: v1.SuccessState,
    89  				},
    90  			},
    91  			expected: true,
    92  		},
    93  		{
    94  			name: "Successful Job with report:false should not report",
    95  			config: config.SlackReporter{
    96  				JobTypesToReport: []v1.ProwJobType{v1.PostsubmitJob},
    97  				SlackReporterConfig: v1.SlackReporterConfig{
    98  					JobStatesToReport: []v1.ProwJobState{v1.SuccessState},
    99  					Report:            boolPtr(false),
   100  				},
   101  			},
   102  			pj: &v1.ProwJob{
   103  				Spec: v1.ProwJobSpec{
   104  					Type: v1.PostsubmitJob,
   105  				},
   106  				Status: v1.ProwJobStatus{
   107  					State: v1.SuccessState,
   108  				},
   109  			},
   110  			expected: false,
   111  		},
   112  		{
   113  			name: "Successful Job with report:true should report",
   114  			config: config.SlackReporter{
   115  				JobTypesToReport: []v1.ProwJobType{v1.PostsubmitJob},
   116  				SlackReporterConfig: v1.SlackReporterConfig{
   117  					JobStatesToReport: []v1.ProwJobState{v1.SuccessState},
   118  					Report:            boolPtr(true),
   119  				},
   120  			},
   121  			pj: &v1.ProwJob{
   122  				Spec: v1.ProwJobSpec{
   123  					Type: v1.PostsubmitJob,
   124  				},
   125  				Status: v1.ProwJobStatus{
   126  					State: v1.SuccessState,
   127  				},
   128  			},
   129  			expected: true,
   130  		},
   131  		{
   132  			// Note: this is impossible to hit, as roundtrip with `omitempty`
   133  			// would never result in empty slice.
   134  			name: "Empty job config settings negate global",
   135  			config: config.SlackReporter{
   136  				JobTypesToReport: []v1.ProwJobType{v1.PostsubmitJob},
   137  				SlackReporterConfig: v1.SlackReporterConfig{
   138  					JobStatesToReport: []v1.ProwJobState{v1.SuccessState},
   139  				},
   140  			},
   141  			pj: &v1.ProwJob{
   142  				Spec: v1.ProwJobSpec{
   143  					Type: v1.PostsubmitJob,
   144  					ReporterConfig: &v1.ReporterConfig{
   145  						Slack: &v1.SlackReporterConfig{JobStatesToReport: []v1.ProwJobState{}},
   146  					},
   147  				},
   148  				Status: v1.ProwJobStatus{
   149  					State: v1.SuccessState,
   150  				},
   151  			},
   152  			expected: false,
   153  		},
   154  		{
   155  			name: "Nil job config settings does not negate global",
   156  			config: config.SlackReporter{
   157  				JobTypesToReport: []v1.ProwJobType{v1.PostsubmitJob},
   158  				SlackReporterConfig: v1.SlackReporterConfig{
   159  					JobStatesToReport: []v1.ProwJobState{v1.SuccessState},
   160  				},
   161  			},
   162  			pj: &v1.ProwJob{
   163  				Spec: v1.ProwJobSpec{
   164  					Type: v1.PostsubmitJob,
   165  					ReporterConfig: &v1.ReporterConfig{
   166  						Slack: &v1.SlackReporterConfig{JobStatesToReport: nil},
   167  					},
   168  				},
   169  				Status: v1.ProwJobStatus{
   170  					State: v1.SuccessState,
   171  				},
   172  			},
   173  			expected: true,
   174  		},
   175  		{
   176  			name: "Successful Job should not report",
   177  			config: config.SlackReporter{
   178  				JobTypesToReport: []v1.ProwJobType{v1.PostsubmitJob},
   179  				SlackReporterConfig: v1.SlackReporterConfig{
   180  					JobStatesToReport: []v1.ProwJobState{v1.PendingState},
   181  				},
   182  			},
   183  			pj: &v1.ProwJob{
   184  				Spec: v1.ProwJobSpec{
   185  					Type: v1.PostsubmitJob,
   186  				},
   187  				Status: v1.ProwJobStatus{
   188  					State: v1.SuccessState,
   189  				},
   190  			},
   191  			expected: false,
   192  		},
   193  		{
   194  			name: "Job with channel config should ignore the JobTypesToReport config",
   195  			config: config.SlackReporter{
   196  				JobTypesToReport: []v1.ProwJobType{},
   197  				SlackReporterConfig: v1.SlackReporterConfig{
   198  					JobStatesToReport: []v1.ProwJobState{v1.SuccessState},
   199  				},
   200  			},
   201  			pj: &v1.ProwJob{
   202  				Spec: v1.ProwJobSpec{
   203  					Type: v1.PostsubmitJob,
   204  					ReporterConfig: &v1.ReporterConfig{
   205  						Slack: &v1.SlackReporterConfig{Channel: "whatever-channel"},
   206  					},
   207  				},
   208  				Status: v1.ProwJobStatus{
   209  					State: v1.SuccessState,
   210  				},
   211  			},
   212  			expected: true,
   213  		},
   214  		{
   215  			name: "JobStatesToReport in Job config should override the one in Prow config",
   216  			config: config.SlackReporter{
   217  				JobTypesToReport: []v1.ProwJobType{},
   218  				SlackReporterConfig: v1.SlackReporterConfig{
   219  					JobStatesToReport: []v1.ProwJobState{v1.SuccessState},
   220  				},
   221  			},
   222  			pj: &v1.ProwJob{
   223  				Spec: v1.ProwJobSpec{
   224  					Type: v1.PostsubmitJob,
   225  					ReporterConfig: &v1.ReporterConfig{
   226  						Slack: &v1.SlackReporterConfig{
   227  							Channel:           "whatever-channel",
   228  							JobStatesToReport: []v1.ProwJobState{v1.FailureState, v1.PendingState},
   229  						},
   230  					},
   231  				},
   232  				Status: v1.ProwJobStatus{
   233  					State: v1.FailureState,
   234  				},
   235  			},
   236  			expected: true,
   237  		},
   238  		{
   239  			name: "Job with channel config but does not have matched state in Prow config should not report",
   240  			config: config.SlackReporter{
   241  				JobTypesToReport: []v1.ProwJobType{},
   242  				SlackReporterConfig: v1.SlackReporterConfig{
   243  					JobStatesToReport: []v1.ProwJobState{v1.SuccessState},
   244  				},
   245  			},
   246  			pj: &v1.ProwJob{
   247  				Spec: v1.ProwJobSpec{
   248  					Type: v1.PostsubmitJob,
   249  					ReporterConfig: &v1.ReporterConfig{
   250  						Slack: &v1.SlackReporterConfig{Channel: "whatever-channel"},
   251  					},
   252  				},
   253  				Status: v1.ProwJobStatus{
   254  					State: v1.PendingState,
   255  				},
   256  			},
   257  			expected: false,
   258  		},
   259  		{
   260  			name: "Job with channel and state config where the state does not match, should not report",
   261  			config: config.SlackReporter{
   262  				JobTypesToReport: []v1.ProwJobType{},
   263  				SlackReporterConfig: v1.SlackReporterConfig{
   264  					JobStatesToReport: []v1.ProwJobState{v1.SuccessState},
   265  				},
   266  			},
   267  			pj: &v1.ProwJob{
   268  				Spec: v1.ProwJobSpec{
   269  					Type: v1.PostsubmitJob,
   270  					ReporterConfig: &v1.ReporterConfig{
   271  						Slack: &v1.SlackReporterConfig{
   272  							Channel:           "whatever-channel",
   273  							JobStatesToReport: []v1.ProwJobState{v1.FailureState, v1.PendingState},
   274  						},
   275  					},
   276  				},
   277  				Status: v1.ProwJobStatus{
   278  					State: v1.SuccessState,
   279  				},
   280  			},
   281  			expected: false,
   282  		},
   283  		{
   284  			name:   "Empty config should not report",
   285  			config: config.SlackReporter{},
   286  			pj: &v1.ProwJob{
   287  				Spec: v1.ProwJobSpec{
   288  					Type: v1.PostsubmitJob,
   289  				},
   290  				Status: v1.ProwJobStatus{
   291  					State: v1.SuccessState,
   292  				},
   293  			},
   294  			expected: false,
   295  		},
   296  	}
   297  
   298  	for _, tc := range testCases {
   299  		cfgGetter := func(*v1.Refs) config.SlackReporter {
   300  			return tc.config
   301  		}
   302  		t.Run(tc.name, func(t *testing.T) {
   303  			reporter := &slackReporter{
   304  				config: cfgGetter,
   305  			}
   306  
   307  			if result := reporter.ShouldReport(context.Background(), logrus.NewEntry(logrus.StandardLogger()), tc.pj); result != tc.expected {
   308  				t.Errorf("expected result to be %t but was %t", tc.expected, result)
   309  			}
   310  		})
   311  	}
   312  }
   313  
   314  func TestReloadsConfig(t *testing.T) {
   315  	cfg := config.SlackReporter{}
   316  	cfgGetter := func(*v1.Refs) config.SlackReporter {
   317  		return cfg
   318  	}
   319  
   320  	pj := &v1.ProwJob{
   321  		Spec: v1.ProwJobSpec{
   322  			Type: v1.PostsubmitJob,
   323  		},
   324  		Status: v1.ProwJobStatus{
   325  			State: v1.FailureState,
   326  		},
   327  	}
   328  
   329  	reporter := &slackReporter{
   330  		config: cfgGetter,
   331  	}
   332  
   333  	if shouldReport := reporter.ShouldReport(context.Background(), logrus.NewEntry(logrus.StandardLogger()), pj); shouldReport {
   334  		t.Error("Did expect shouldReport to be false")
   335  	}
   336  
   337  	cfg.JobStatesToReport = []v1.ProwJobState{v1.FailureState}
   338  	cfg.JobTypesToReport = []v1.ProwJobType{v1.PostsubmitJob}
   339  
   340  	if shouldReport := reporter.ShouldReport(context.Background(), logrus.NewEntry(logrus.StandardLogger()), pj); !shouldReport {
   341  		t.Error("Did expect shouldReport to be true after config change")
   342  	}
   343  }
   344  
   345  func TestUsesChannelOverrideFromJob(t *testing.T) {
   346  	testCases := []struct {
   347  		name          string
   348  		config        func() config.Config
   349  		pj            *v1.ProwJob
   350  		wantHost      string
   351  		wantChannel   string
   352  		emptyExpected bool
   353  	}{
   354  		{
   355  			name: "No job-level config, use global default",
   356  			config: func() config.Config {
   357  				slackCfg := map[string]config.SlackReporter{
   358  					"*": {
   359  						SlackReporterConfig: v1.SlackReporterConfig{
   360  							Host:    "global-default-host",
   361  							Channel: "global-default",
   362  						},
   363  					},
   364  				}
   365  				return config.Config{
   366  					ProwConfig: config.ProwConfig{
   367  						SlackReporterConfigs: slackCfg,
   368  					},
   369  				}
   370  			},
   371  			pj:          &v1.ProwJob{Spec: v1.ProwJobSpec{}},
   372  			wantHost:    "global-default-host",
   373  			wantChannel: "global-default",
   374  		},
   375  		{
   376  			name: "org/repo for ref exists in config, use it",
   377  			config: func() config.Config {
   378  				slackCfg := map[string]config.SlackReporter{
   379  					"*": {
   380  						SlackReporterConfig: v1.SlackReporterConfig{
   381  							Channel: "global-default",
   382  						},
   383  					},
   384  					"istio/proxy": {
   385  						SlackReporterConfig: v1.SlackReporterConfig{
   386  							Host:    "global-default-host",
   387  							Channel: "org-repo-config",
   388  						},
   389  					},
   390  				}
   391  				return config.Config{
   392  					ProwConfig: config.ProwConfig{
   393  						SlackReporterConfigs: slackCfg,
   394  					},
   395  				}
   396  			},
   397  			pj: &v1.ProwJob{
   398  				Spec: v1.ProwJobSpec{
   399  					Refs: &v1.Refs{
   400  						Org:  "istio",
   401  						Repo: "proxy",
   402  					},
   403  				}},
   404  			wantHost:    "global-default-host",
   405  			wantChannel: "org-repo-config",
   406  		},
   407  		{
   408  			name: "org for ref exists in config, use it",
   409  			config: func() config.Config {
   410  				slackCfg := map[string]config.SlackReporter{
   411  					"*": {
   412  						SlackReporterConfig: v1.SlackReporterConfig{
   413  							Channel: "global-default",
   414  						},
   415  					},
   416  					"istio": {
   417  						SlackReporterConfig: v1.SlackReporterConfig{
   418  							Channel: "org-config",
   419  						},
   420  					},
   421  				}
   422  				return config.Config{
   423  					ProwConfig: config.ProwConfig{
   424  						SlackReporterConfigs: slackCfg,
   425  					},
   426  				}
   427  			},
   428  			pj: &v1.ProwJob{
   429  				Spec: v1.ProwJobSpec{
   430  					Refs: &v1.Refs{
   431  						Org:  "istio",
   432  						Repo: "proxy",
   433  					},
   434  				}},
   435  			wantHost:    "*",
   436  			wantChannel: "org-config",
   437  		},
   438  		{
   439  			name: "org/repo takes precedence over org",
   440  			config: func() config.Config {
   441  				slackCfg := map[string]config.SlackReporter{
   442  					"*": {
   443  						SlackReporterConfig: v1.SlackReporterConfig{
   444  							Channel: "global-default",
   445  						},
   446  					},
   447  					"istio": {
   448  						SlackReporterConfig: v1.SlackReporterConfig{
   449  							Channel: "org-config",
   450  						},
   451  					},
   452  					"istio/proxy": {
   453  						SlackReporterConfig: v1.SlackReporterConfig{
   454  							Channel: "org-repo-config",
   455  						},
   456  					},
   457  				}
   458  				return config.Config{
   459  					ProwConfig: config.ProwConfig{
   460  						SlackReporterConfigs: slackCfg,
   461  					},
   462  				}
   463  			},
   464  			pj: &v1.ProwJob{
   465  				Spec: v1.ProwJobSpec{
   466  					Refs: &v1.Refs{
   467  						Org:  "istio",
   468  						Repo: "proxy",
   469  					},
   470  				}},
   471  			wantHost:    "*",
   472  			wantChannel: "org-repo-config",
   473  		},
   474  		{
   475  			name: "Job-level config present, use it",
   476  			config: func() config.Config {
   477  				slackCfg := map[string]config.SlackReporter{
   478  					"*": {
   479  						SlackReporterConfig: v1.SlackReporterConfig{
   480  							Channel: "global-default",
   481  						},
   482  					},
   483  					"istio": {
   484  						SlackReporterConfig: v1.SlackReporterConfig{
   485  							Channel: "org-config",
   486  						},
   487  					},
   488  					"istio/proxy": {
   489  						SlackReporterConfig: v1.SlackReporterConfig{
   490  							Channel: "org-repo-config",
   491  						},
   492  					},
   493  				}
   494  				return config.Config{
   495  					ProwConfig: config.ProwConfig{
   496  						SlackReporterConfigs: slackCfg,
   497  					},
   498  				}
   499  			},
   500  			pj: &v1.ProwJob{
   501  				Spec: v1.ProwJobSpec{
   502  					ReporterConfig: &v1.ReporterConfig{
   503  						Slack: &v1.SlackReporterConfig{
   504  							Channel: "team-a",
   505  						},
   506  					},
   507  				},
   508  			},
   509  			wantHost:    "*",
   510  			wantChannel: "team-a",
   511  		},
   512  		{
   513  			name: "No matching slack config",
   514  			config: func() config.Config {
   515  				slackCfg := map[string]config.SlackReporter{
   516  					"istio": {
   517  						SlackReporterConfig: v1.SlackReporterConfig{
   518  							Channel: "org-config",
   519  						},
   520  					},
   521  					"istio/proxy": {
   522  						SlackReporterConfig: v1.SlackReporterConfig{
   523  							Channel: "org-repo-config",
   524  						},
   525  					},
   526  				}
   527  				return config.Config{
   528  					ProwConfig: config.ProwConfig{
   529  						SlackReporterConfigs: slackCfg,
   530  					},
   531  				}
   532  			},
   533  			pj: &v1.ProwJob{
   534  				Spec: v1.ProwJobSpec{
   535  					Refs: &v1.Refs{
   536  						Org:  "unknownorg",
   537  						Repo: "unknownrepo",
   538  					},
   539  				}},
   540  			wantHost:      "*",
   541  			emptyExpected: true,
   542  		},
   543  		{
   544  			name: "Refs unset but extra refs exist, use it",
   545  			config: func() config.Config {
   546  				slackCfg := map[string]config.SlackReporter{
   547  					"istio/proxy": {
   548  						SlackReporterConfig: v1.SlackReporterConfig{
   549  							Channel: "org-repo-config",
   550  						},
   551  					},
   552  				}
   553  				return config.Config{
   554  					ProwConfig: config.ProwConfig{
   555  						SlackReporterConfigs: slackCfg,
   556  					},
   557  				}
   558  			},
   559  			pj: &v1.ProwJob{
   560  				Spec: v1.ProwJobSpec{
   561  					ExtraRefs: []v1.Refs{{
   562  						Org:  "istio",
   563  						Repo: "proxy",
   564  					}},
   565  				},
   566  			},
   567  			wantHost:    "*",
   568  			wantChannel: "org-repo-config",
   569  		},
   570  	}
   571  
   572  	for _, tc := range testCases {
   573  		t.Run(tc.name, func(t *testing.T) {
   574  			cfgGetter := func(refs *v1.Refs) config.SlackReporter {
   575  				return tc.config().SlackReporterConfigs.GetSlackReporter(refs)
   576  			}
   577  			sr := slackReporter{
   578  				config: cfgGetter,
   579  			}
   580  
   581  			prowSlackCfg, jobSlackCfg := sr.getConfig(tc.pj)
   582  			jobSlackCfg = jobSlackCfg.ApplyDefault(&prowSlackCfg.SlackReporterConfig)
   583  			gotHost, gotChannel := hostAndChannel(jobSlackCfg)
   584  			if gotHost != tc.wantHost {
   585  				t.Fatalf("Expected host: %q, got: %q", tc.wantHost, gotHost)
   586  			}
   587  			if gotChannel != tc.wantChannel {
   588  				t.Fatalf("Expected channel: %q, got: %q", tc.wantChannel, gotChannel)
   589  			}
   590  		})
   591  	}
   592  }
   593  
   594  func TestShouldReportDefaultsToExtraRefs(t *testing.T) {
   595  	job := &v1.ProwJob{
   596  		Spec: v1.ProwJobSpec{
   597  			Type:      v1.PeriodicJob,
   598  			ExtraRefs: []v1.Refs{{Org: "org"}},
   599  		},
   600  		Status: v1.ProwJobStatus{
   601  			State: v1.SuccessState,
   602  		},
   603  	}
   604  	sr := slackReporter{
   605  		config: func(r *v1.Refs) config.SlackReporter {
   606  			if r.Org == "org" {
   607  				return config.SlackReporter{
   608  					JobTypesToReport: []v1.ProwJobType{v1.PeriodicJob},
   609  					SlackReporterConfig: v1.SlackReporterConfig{
   610  						JobStatesToReport: []v1.ProwJobState{v1.SuccessState},
   611  					},
   612  				}
   613  			}
   614  			return config.SlackReporter{}
   615  		},
   616  	}
   617  
   618  	if !sr.ShouldReport(context.Background(), logrus.NewEntry(logrus.StandardLogger()), job) {
   619  		t.Fatal("expected job to report but did not")
   620  	}
   621  }
   622  
   623  type fakeSlackClient struct {
   624  	messages map[string]string
   625  }
   626  
   627  func (fsc *fakeSlackClient) WriteMessage(text, channel string) error {
   628  	if fsc.messages == nil {
   629  		fsc.messages = map[string]string{}
   630  	}
   631  	fsc.messages[channel] = text
   632  	return nil
   633  }
   634  
   635  var _ slackClient = &fakeSlackClient{}
   636  
   637  func TestReportDefaultsToExtraRefs(t *testing.T) {
   638  	job := &v1.ProwJob{
   639  		Spec: v1.ProwJobSpec{
   640  			Type:      v1.PeriodicJob,
   641  			ExtraRefs: []v1.Refs{{Org: "org"}},
   642  		},
   643  		Status: v1.ProwJobStatus{
   644  			State: v1.SuccessState,
   645  		},
   646  	}
   647  	fsc := &fakeSlackClient{}
   648  	sr := slackReporter{
   649  		config: func(r *v1.Refs) config.SlackReporter {
   650  			if r.Org == "org" {
   651  				return config.SlackReporter{
   652  					JobTypesToReport: []v1.ProwJobType{v1.PeriodicJob},
   653  					SlackReporterConfig: v1.SlackReporterConfig{
   654  						JobStatesToReport: []v1.ProwJobState{v1.SuccessState},
   655  						Channel:           "emercengy",
   656  						ReportTemplate:    "there you go",
   657  					},
   658  				}
   659  			}
   660  			return config.SlackReporter{}
   661  		},
   662  		clients: map[string]slackClient{DefaultHostName: fsc},
   663  	}
   664  
   665  	if _, _, err := sr.Report(context.Background(), logrus.NewEntry(logrus.StandardLogger()), job); err != nil {
   666  		t.Fatalf("reporting failed: %v", err)
   667  	}
   668  	if fsc.messages["emercengy"] != "there you go" {
   669  		t.Errorf("expected the channel 'emergency' to contain message 'there you go' but wasn't the case, all messages: %v", fsc.messages)
   670  	}
   671  }