github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/cmd/generic-autobumper/main_test.go (about)

     1  /*
     2  Copyright 2021 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package main
    18  
    19  import (
    20  	"errors"
    21  	"fmt"
    22  	"net/http"
    23  	"net/http/httptest"
    24  	"os"
    25  	"path"
    26  	"reflect"
    27  	"regexp"
    28  	"strings"
    29  	"testing"
    30  
    31  	"github.com/google/go-cmp/cmp/cmpopts"
    32  
    33  	"github.com/google/go-cmp/cmp"
    34  )
    35  
    36  func TestGetAssignment(t *testing.T) {
    37  	cases := []struct {
    38  		description          string
    39  		oncallURL            string
    40  		oncallGroup          string
    41  		oncallServerResponse string
    42  		skipOncallAssignment bool
    43  		selfAssign           bool
    44  		expectResKeyword     string
    45  		expectOncallActive   bool
    46  	}{
    47  		{
    48  			description:          "empty oncall URL will return an empty string",
    49  			oncallURL:            "",
    50  			oncallGroup:          defaultOncallGroup,
    51  			oncallServerResponse: "",
    52  			expectResKeyword:     "",
    53  		},
    54  		{
    55  			description:          "an invalid oncall URL will return an error message",
    56  			oncallURL:            "whatever-url",
    57  			oncallGroup:          defaultOncallGroup,
    58  			oncallServerResponse: "",
    59  			expectResKeyword:     "error",
    60  		},
    61  		{
    62  			description:          "an invalid response will return an error message",
    63  			oncallURL:            "auto",
    64  			oncallGroup:          defaultOncallGroup,
    65  			oncallServerResponse: "whatever-malformed-response",
    66  			expectResKeyword:     "error",
    67  		},
    68  		{
    69  			description:          "a valid response will return the oncaller from default group",
    70  			oncallURL:            "auto",
    71  			oncallGroup:          defaultOncallGroup,
    72  			oncallServerResponse: `{"Oncall":{"testinfra":"fake-oncall-name"},"Active":{"testinfra":false}}`,
    73  			expectResKeyword:     "fake-oncall-name",
    74  		},
    75  		{
    76  			description:          "a valid response will return the oncaller from non-default group",
    77  			oncallURL:            "auto",
    78  			oncallGroup:          "another-group",
    79  			oncallServerResponse: `{"Oncall":{"testinfra":"fake-oncall-name","another-group":"fake-oncall-name2"},"Active":{"another-group":false}}`,
    80  			expectResKeyword:     "fake-oncall-name2",
    81  		},
    82  		{
    83  			description:          "a valid response without expected oncall group",
    84  			oncallURL:            "auto",
    85  			oncallGroup:          "group-not-exist",
    86  			oncallServerResponse: `{"Oncall":{"testinfra":"fake-oncall-name","another-group":"fake-oncall-name2"}}`,
    87  			expectResKeyword:     "error",
    88  		},
    89  		{
    90  			description:          "a valid response with empty oncall will return on oncall message",
    91  			oncallURL:            "auto",
    92  			oncallGroup:          defaultOncallGroup,
    93  			oncallServerResponse: `{"Oncall":{"testinfra":""},"Active":{"testinfra":false}}`,
    94  			expectResKeyword:     "Nobody",
    95  		},
    96  		{
    97  			description:          "oncall active",
    98  			oncallURL:            "auto",
    99  			oncallGroup:          defaultOncallGroup,
   100  			oncallServerResponse: `{"Oncall":{"testinfra":"fake-oncall-name"},"Active":{"testinfra":true}}`,
   101  			expectResKeyword:     "fake-oncall-name",
   102  			expectOncallActive:   true,
   103  		},
   104  		{
   105  			description:          "skip",
   106  			oncallURL:            "auto",
   107  			oncallGroup:          defaultOncallGroup,
   108  			oncallServerResponse: `{"Oncall":{"testinfra":"fake-oncall-name"},"Active":{"testinfra":true}}`,
   109  			skipOncallAssignment: true,
   110  			expectResKeyword:     "",
   111  			expectOncallActive:   true,
   112  		},
   113  		{
   114  			description:          "self-assign-with-oncall",
   115  			oncallURL:            "auto",
   116  			oncallGroup:          defaultOncallGroup,
   117  			oncallServerResponse: `{"Oncall":{"testinfra":"fake-oncall-name"},"Active":{"testinfra":true}}`,
   118  			skipOncallAssignment: true,
   119  			selfAssign:           true,
   120  			expectResKeyword:     "/cc",
   121  			expectOncallActive:   true,
   122  		},
   123  		{
   124  			description:          "self-assign-without-oncall",
   125  			oncallURL:            "auto",
   126  			oncallGroup:          defaultOncallGroup,
   127  			oncallServerResponse: `{"Oncall":{"testinfra":""},"Active":{"testinfra":false}}`,
   128  			skipOncallAssignment: true,
   129  			selfAssign:           true,
   130  			expectResKeyword:     "/cc",
   131  			expectOncallActive:   false,
   132  		},
   133  	}
   134  
   135  	for _, tc := range cases {
   136  		t.Run(tc.description, func(t *testing.T) {
   137  			if tc.oncallURL == "auto" {
   138  				// generate a test server so we can capture and inspect the request
   139  				testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
   140  					res.Write([]byte(tc.oncallServerResponse))
   141  				}))
   142  				defer func() { testServer.Close() }()
   143  				tc.oncallURL = testServer.URL
   144  			}
   145  
   146  			res := getAssignment(tc.oncallURL, tc.oncallGroup, tc.skipOncallAssignment, tc.selfAssign)
   147  			if !strings.Contains(res, tc.expectResKeyword) {
   148  				t.Errorf("Expect the result %q contains keyword %q but it does not", res, tc.expectResKeyword)
   149  			}
   150  			if got, want := isOncallActive(tc.oncallURL, tc.oncallGroup), tc.expectOncallActive; got != want {
   151  				t.Errorf("Expect oncall active. Want: %v, got: %v", want, got)
   152  			}
   153  		})
   154  	}
   155  }
   156  
   157  func TestValidateOptions(t *testing.T) {
   158  	emptyStr := ""
   159  	whateverStr := "whatever"
   160  	emptyArr := make([]string, 0)
   161  	emptyPrefixes := make([]prefix, 0)
   162  	validExceptionPrefixes := []prefix{{
   163  		Name:                      "test",
   164  		Prefix:                    "gcr.io/test/",
   165  		ConsistentImages:          true,
   166  		ConsistentImageExceptions: []string{"gcr.io/test/foo", "gcr.io/test/bar"},
   167  		RefConfigFile:             "",
   168  		StagingRefConfigFile:      "",
   169  	}}
   170  	invalidExceptionPrefixes := []prefix{{
   171  		Name:                      "test",
   172  		Prefix:                    "gcr.io/test/",
   173  		ConsistentImages:          false,
   174  		ConsistentImageExceptions: []string{"gcr.io/test/foo", "gcr.io/test/bar"},
   175  		RefConfigFile:             "",
   176  		StagingRefConfigFile:      "",
   177  	}}
   178  	latestPrefixes := []prefix{{
   179  		Name:                 "test",
   180  		Prefix:               "gcr.io/test/",
   181  		RefConfigFile:        "",
   182  		StagingRefConfigFile: "",
   183  	}}
   184  	upstreamPrefixes := []prefix{{
   185  		Name:                 "test",
   186  		Prefix:               "gcr.io/test/",
   187  		RefConfigFile:        "ref",
   188  		StagingRefConfigFile: "stagingRef",
   189  	}}
   190  	upstreamVersion := "upstream"
   191  	stagingVersion := "upstream-staging"
   192  	cases := []struct {
   193  		name                string
   194  		targetVersion       *string
   195  		includeConfigPaths  *[]string
   196  		prefixes            *[]prefix
   197  		upstreamURLBase     *string
   198  		err                 bool
   199  		upstreamBaseChanged bool
   200  	}{
   201  		{
   202  			name: "Everything correct",
   203  			err:  false,
   204  		},
   205  		{
   206  			name:          "unformatted TargetVersion is also allowed",
   207  			targetVersion: &whateverStr,
   208  			err:           false,
   209  		},
   210  		{
   211  			name:               "must include at least one config path",
   212  			includeConfigPaths: &emptyArr,
   213  			err:                true,
   214  		},
   215  		{
   216  			name:                "must include upstreamURLBase if target version is upstream",
   217  			upstreamURLBase:     &emptyStr,
   218  			targetVersion:       &upstreamVersion,
   219  			prefixes:            &upstreamPrefixes,
   220  			err:                 false,
   221  			upstreamBaseChanged: true,
   222  		},
   223  		{
   224  			name:                "must include upstreamURLBase if target version is upstreamStaging",
   225  			upstreamURLBase:     &emptyStr,
   226  			targetVersion:       &stagingVersion,
   227  			prefixes:            &upstreamPrefixes,
   228  			err:                 false,
   229  			upstreamBaseChanged: true,
   230  		},
   231  		{
   232  			name:     "must include at least one prefix",
   233  			prefixes: &emptyPrefixes,
   234  			err:      true,
   235  		},
   236  		{
   237  			name:     "can enable consistentImageExceptions with consistentImages",
   238  			prefixes: &validExceptionPrefixes,
   239  			err:      false,
   240  		},
   241  		{
   242  			name:     "cannot enable consistentImageExceptions without consistentImages",
   243  			prefixes: &invalidExceptionPrefixes,
   244  			err:      true,
   245  		},
   246  		{
   247  			name:          "must have ref files for upstream version",
   248  			targetVersion: &upstreamVersion,
   249  			prefixes:      &latestPrefixes,
   250  			err:           true,
   251  		},
   252  		{
   253  			name:          "must have stagingRef files for Stagingupstream version",
   254  			targetVersion: &stagingVersion,
   255  			prefixes:      &latestPrefixes,
   256  			err:           true,
   257  		},
   258  		{
   259  			name:                "don't use default upstreamURLbase if not needed for upstream",
   260  			upstreamURLBase:     &whateverStr,
   261  			targetVersion:       &upstreamVersion,
   262  			prefixes:            &upstreamPrefixes,
   263  			err:                 false,
   264  			upstreamBaseChanged: false,
   265  		},
   266  		{
   267  			name:                "don't use default upstreamURLbase if not neededfor upstreamStaging",
   268  			upstreamURLBase:     &whateverStr,
   269  			targetVersion:       &stagingVersion,
   270  			prefixes:            &upstreamPrefixes,
   271  			err:                 false,
   272  			upstreamBaseChanged: false,
   273  		},
   274  	}
   275  	for _, tc := range cases {
   276  		t.Run(tc.name, func(t *testing.T) {
   277  			defaultOption := &options{
   278  				UpstreamURLBase:     "whatever-URLBase",
   279  				Prefixes:            latestPrefixes,
   280  				TargetVersion:       latestVersion,
   281  				IncludedConfigPaths: []string{"whatever-config-path1", "whatever-config-path2"},
   282  			}
   283  
   284  			if tc.targetVersion != nil {
   285  				defaultOption.TargetVersion = *tc.targetVersion
   286  			}
   287  			if tc.includeConfigPaths != nil {
   288  				defaultOption.IncludedConfigPaths = *tc.includeConfigPaths
   289  			}
   290  			if tc.prefixes != nil {
   291  				defaultOption.Prefixes = *tc.prefixes
   292  			}
   293  			if tc.upstreamURLBase != nil {
   294  				defaultOption.UpstreamURLBase = *tc.upstreamURLBase
   295  			}
   296  
   297  			err := validateOptions(defaultOption)
   298  			t.Logf("err is: %v", err)
   299  			if err == nil && tc.err {
   300  				t.Errorf("Expected to get an error for %#v but got nil", defaultOption)
   301  			}
   302  			if err != nil && !tc.err {
   303  				t.Errorf("Expected to not get an error for %#v but got %v", defaultOption, err)
   304  			}
   305  			if tc.upstreamBaseChanged && defaultOption.UpstreamURLBase != defaultUpstreamURLBase {
   306  				t.Errorf("UpstreamURLBase should have been changed to %q, but was %q", defaultOption.UpstreamURLBase, defaultUpstreamURLBase)
   307  			}
   308  			if !tc.upstreamBaseChanged && defaultOption.UpstreamURLBase == defaultUpstreamURLBase {
   309  				t.Errorf("UpstreamURLBase should not have been changed to default, but was")
   310  			}
   311  		})
   312  	}
   313  }
   314  
   315  type fakeImageBumperCli struct {
   316  	replacements map[string]string
   317  	tagCache     map[string]string
   318  }
   319  
   320  func (c *fakeImageBumperCli) FindLatestTag(imageHost, imageName, currentTag string) (string, error) {
   321  	return "fake-latest", nil
   322  }
   323  
   324  func (c *fakeImageBumperCli) UpdateFile(tagPicker func(imageHost, imageName, currentTag string) (string, error),
   325  	path string, imageFilter *regexp.Regexp) error {
   326  	targetTag, _ := tagPicker("", "", "")
   327  	c.replacements[path] = targetTag
   328  	return nil
   329  }
   330  
   331  func (c *fakeImageBumperCli) GetReplacements() map[string]string {
   332  	return c.replacements
   333  }
   334  
   335  func (c *fakeImageBumperCli) AddToCache(image, newTag string) {
   336  	c.tagCache[image] = newTag
   337  }
   338  
   339  func (cli *fakeImageBumperCli) TagExists(imageHost, imageName, currentTag string) (bool, error) {
   340  	if currentTag == "DNE" {
   341  		return false, nil
   342  	}
   343  	return true, nil
   344  }
   345  
   346  func TestUpdateReferences(t *testing.T) {
   347  	tmpDir := t.TempDir()
   348  	for dir, fps := range map[string][]string{
   349  		"testdata/dir/subdir1": {"test1-1.yaml", "test1-2.yaml"},
   350  		"testdata/dir/subdir2": {"test2-1.yaml"},
   351  		"testdata/dir/subdir3": {"test3-1.yaml"},
   352  		"testdata/dir":         {"extra-file"},
   353  	} {
   354  		if err := os.MkdirAll(path.Join(tmpDir, dir), 0755); err != nil {
   355  			t.Fatalf("Faile creating dir %q: %v", dir, err)
   356  		}
   357  		for _, f := range fps {
   358  			if _, err := os.Create(path.Join(tmpDir, dir, f)); err != nil {
   359  				t.Fatalf("Faile creating file %q: %v", f, err)
   360  			}
   361  		}
   362  	}
   363  
   364  	cases := []struct {
   365  		description        string
   366  		targetVersion      string
   367  		includeConfigPaths []string
   368  		excludeConfigPaths []string
   369  		extraFiles         []string
   370  		expectedRes        map[string]string
   371  		expectError        bool
   372  	}{
   373  		{
   374  			description:   "update the images to the latest version",
   375  			targetVersion: latestVersion,
   376  			includeConfigPaths: []string{
   377  				path.Join(tmpDir, "testdata/dir/subdir1"),
   378  				path.Join(tmpDir, "testdata/dir/subdir2"),
   379  			},
   380  			expectedRes: map[string]string{
   381  				path.Join(tmpDir, "testdata/dir/subdir1/test1-1.yaml"): "fake-latest",
   382  				path.Join(tmpDir, "testdata/dir/subdir1/test1-2.yaml"): "fake-latest",
   383  				path.Join(tmpDir, "testdata/dir/subdir2/test2-1.yaml"): "fake-latest",
   384  			},
   385  			expectError: false,
   386  		},
   387  		{
   388  			description:   "update the images to a specific version",
   389  			targetVersion: "v20200101-livebull",
   390  			includeConfigPaths: []string{
   391  				path.Join(tmpDir, "testdata/dir/subdir2"),
   392  			},
   393  			expectedRes: map[string]string{
   394  				path.Join(tmpDir, "testdata/dir/subdir2/test2-1.yaml"): "v20200101-livebull",
   395  			},
   396  			expectError: false,
   397  		},
   398  		{
   399  			description:   "by default only yaml files will be updated",
   400  			targetVersion: latestVersion,
   401  			includeConfigPaths: []string{
   402  				path.Join(tmpDir, "testdata/dir/subdir3"),
   403  			},
   404  			expectedRes: map[string]string{
   405  				path.Join(tmpDir, "testdata/dir/subdir3/test3-1.yaml"): "fake-latest",
   406  			},
   407  			expectError: false,
   408  		},
   409  		{
   410  			description:   "files under the excluded paths will not be updated",
   411  			targetVersion: latestVersion,
   412  			includeConfigPaths: []string{
   413  				path.Join(tmpDir, "testdata/dir"),
   414  			},
   415  			excludeConfigPaths: []string{
   416  				path.Join(tmpDir, "testdata/dir/subdir1"),
   417  				path.Join(tmpDir, "testdata/dir/subdir2"),
   418  			},
   419  			expectedRes: map[string]string{
   420  				path.Join(tmpDir, "testdata/dir/subdir3/test3-1.yaml"): "fake-latest",
   421  			},
   422  			expectError: false,
   423  		},
   424  		{
   425  			description:   "non YAML files could be configured by specifying extraFiles",
   426  			targetVersion: latestVersion,
   427  			includeConfigPaths: []string{
   428  				path.Join(tmpDir, "testdata/dir/subdir3"),
   429  			},
   430  			extraFiles: []string{
   431  				path.Join(tmpDir, "testdata/dir/extra-file"),
   432  				path.Join(tmpDir, "testdata/dir/subdir3/test3-2"),
   433  			},
   434  			expectedRes: map[string]string{
   435  				path.Join(tmpDir, "testdata/dir/subdir3/test3-1.yaml"): "fake-latest",
   436  				path.Join(tmpDir, "testdata/dir/extra-file"):           "fake-latest",
   437  				path.Join(tmpDir, "testdata/dir/subdir3/test3-2"):      "fake-latest",
   438  			},
   439  			expectError: false,
   440  		},
   441  		{
   442  			description:   "updating non-existed files will return an error",
   443  			targetVersion: latestVersion,
   444  			includeConfigPaths: []string{
   445  				path.Join(tmpDir, "testdata/dir/whatever-subdir"),
   446  			},
   447  			expectError: true,
   448  		},
   449  	}
   450  
   451  	for _, tc := range cases {
   452  		t.Run(tc.description, func(t *testing.T) {
   453  			option := &options{
   454  				TargetVersion:       tc.targetVersion,
   455  				IncludedConfigPaths: tc.includeConfigPaths,
   456  				ExtraFiles:          tc.extraFiles,
   457  				ExcludedConfigPaths: tc.excludeConfigPaths,
   458  			}
   459  			cli := &fakeImageBumperCli{replacements: map[string]string{}}
   460  			res, err := updateReferences(cli, nil, option)
   461  			if tc.expectError && err == nil {
   462  				t.Errorf("Expected to get an error but the result is nil")
   463  			}
   464  			if !tc.expectError && err != nil {
   465  				t.Errorf("Expected to not get an error but got one: %v", err)
   466  			}
   467  
   468  			if !reflect.DeepEqual(res, tc.expectedRes) {
   469  				t.Errorf("Expected to get the result map as %v but got %v", tc.expectedRes, res)
   470  			}
   471  		})
   472  	}
   473  }
   474  
   475  func TestParseUpstreamImageVersion(t *testing.T) {
   476  	cases := []struct {
   477  		description            string
   478  		upstreamURL            string
   479  		upstreamServerResponse string
   480  		expectedRes            string
   481  		expectError            bool
   482  		prefix                 string
   483  	}{
   484  		{
   485  			description:            "empty upstream URL will return an error",
   486  			upstreamURL:            "",
   487  			upstreamServerResponse: "",
   488  			expectedRes:            "",
   489  			expectError:            true,
   490  			prefix:                 "gcr.io/k8s-prow/",
   491  		},
   492  		{
   493  			description:            "an invalid upstream URL will return an error",
   494  			upstreamURL:            "whatever-url",
   495  			upstreamServerResponse: "",
   496  			expectedRes:            "",
   497  			expectError:            true,
   498  			prefix:                 "gcr.io/k8s-prow/",
   499  		},
   500  		{
   501  			description:            "an invalid response will return an error",
   502  			upstreamURL:            "auto",
   503  			upstreamServerResponse: "whatever-response",
   504  			expectedRes:            "",
   505  			expectError:            true,
   506  			prefix:                 "gcr.io/k8s-prow/",
   507  		},
   508  		{
   509  			description:            "a valid response will return the correct tag",
   510  			upstreamURL:            "auto",
   511  			upstreamServerResponse: "     image: gcr.io/k8s-prow/deck:v20200717-cf288082e1",
   512  			expectedRes:            "v20200717-cf288082e1",
   513  			expectError:            false,
   514  			prefix:                 "gcr.io/k8s-prow/",
   515  		},
   516  		{
   517  			description:            "a valid response will return the correct tag with other prefixes in the same file",
   518  			upstreamURL:            "auto",
   519  			upstreamServerResponse: "other random garbage\n image: gcr.io/k8s-other/deck:v22222222-cf288082e1\n image: gcr.io/k8s-prow/deck:v20200717-cf288082e1\n     image: gcr.io/k8s-another/deck:v11111111-cf288082e1",
   520  			expectedRes:            "v20200717-cf288082e1",
   521  			expectError:            false,
   522  			prefix:                 "gcr.io/k8s-prow/",
   523  		},
   524  	}
   525  
   526  	for _, tc := range cases {
   527  		t.Run(tc.description, func(t *testing.T) {
   528  			if tc.upstreamURL == "auto" {
   529  				// generate a test server so we can capture and inspect the request
   530  				testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
   531  					res.Write([]byte(tc.upstreamServerResponse))
   532  				}))
   533  				defer func() { testServer.Close() }()
   534  				tc.upstreamURL = testServer.URL
   535  			}
   536  
   537  			res, err := parseUpstreamImageVersion(tc.upstreamURL, tc.prefix)
   538  			if res != tc.expectedRes {
   539  				t.Errorf("The expected result %q != the actual result %q", tc.expectedRes, res)
   540  			}
   541  			if tc.expectError && err == nil {
   542  				t.Errorf("Expected to get an error but the result is nil")
   543  			}
   544  			if !tc.expectError && err != nil {
   545  				t.Errorf("Expected to not get an error but got one: %v", err)
   546  			}
   547  		})
   548  	}
   549  }
   550  
   551  func TestUpstreamImageVersionResolver(t *testing.T) {
   552  	const (
   553  		prowProdFakeVersion        = "v-prow-prod-version"
   554  		prowStagingFakeVersion     = "v-prow-staging-version"
   555  		boskosProdFakeVersion      = "v-boskos-prod-version"
   556  		boskosStagingFakeVersion   = "v-boskos-staging-version"
   557  		prowRefConfigFile          = "prow-prod"
   558  		boskosRefConfigFile        = "boskos-prod"
   559  		prowStagingRefConfigFile   = "prow-staging"
   560  		boskosStagingRefConfigFile = "boskos-staging"
   561  		fakeUpstreamURLBase        = "test.com"
   562  		prowPrefix                 = "gcr.io/k8s-prow/"
   563  		boskosPrefix               = "gcr.io/k8s-boskos/"
   564  		doesNotExistPrefix         = "gcr.io/dne"
   565  		doesNotExist               = "DNE"
   566  	)
   567  	prowPrefixStruct := prefix{
   568  		Prefix:               prowPrefix,
   569  		RefConfigFile:        prowRefConfigFile,
   570  		StagingRefConfigFile: prowStagingRefConfigFile,
   571  	}
   572  	boskosPrefixStruct := prefix{
   573  		Prefix:               boskosPrefix,
   574  		RefConfigFile:        boskosRefConfigFile,
   575  		StagingRefConfigFile: boskosStagingRefConfigFile,
   576  	}
   577  	// prefix used to test when a tag does not exist. This is used to have parser return a tag that will make TagExists return false
   578  	tagDoesNotExistPrefix := prefix{
   579  		Prefix:               doesNotExistPrefix,
   580  		RefConfigFile:        doesNotExist,
   581  		StagingRefConfigFile: doesNotExist,
   582  	}
   583  
   584  	cases := []struct {
   585  		description         string
   586  		parser              func(string, string) (string, error)
   587  		upstreamVersionType string
   588  		imageHost           string
   589  		imageName           string
   590  		currentTag          string
   591  		expectedTargetTag   string
   592  		expectError         bool
   593  		resolverError       bool
   594  		prefixes            []prefix
   595  	}{
   596  		{
   597  			description: "resolve image version with an invalid version type",
   598  			parser: func(upAddr, pref string) (string, error) {
   599  				switch strings.TrimPrefix(upAddr, fakeUpstreamURLBase+"/") {
   600  				case prowRefConfigFile:
   601  					return prowProdFakeVersion, nil
   602  				case boskosRefConfigFile:
   603  					return boskosProdFakeVersion, nil
   604  				default:
   605  					return "", errors.New("not supported")
   606  				}
   607  			},
   608  			upstreamVersionType: "whatever-version-type",
   609  			expectError:         true,
   610  			prefixes:            []prefix{prowPrefixStruct, boskosPrefixStruct},
   611  		},
   612  		{
   613  			description: "resolve image with two prefixes possible and upstreamVersion",
   614  			parser: func(upAddr, pref string) (string, error) {
   615  				switch strings.TrimPrefix(upAddr, fakeUpstreamURLBase+"/") {
   616  				case prowRefConfigFile:
   617  					return prowProdFakeVersion, nil
   618  				case boskosRefConfigFile:
   619  					return boskosProdFakeVersion, nil
   620  				default:
   621  					return "", errors.New("not supported")
   622  				}
   623  			},
   624  			upstreamVersionType: upstreamVersion,
   625  			expectError:         false,
   626  			prefixes:            []prefix{prowPrefixStruct, boskosPrefixStruct},
   627  			imageHost:           prowPrefix,
   628  			currentTag:          "whatever-current-tag",
   629  			expectedTargetTag:   prowProdFakeVersion,
   630  		},
   631  		{
   632  			description: "resolve image with two prefixes possible and staging version",
   633  			parser: func(upAddr, pref string) (string, error) {
   634  				switch strings.TrimPrefix(upAddr, fakeUpstreamURLBase+"/") {
   635  				case prowStagingRefConfigFile:
   636  					return prowStagingFakeVersion, nil
   637  				case boskosStagingRefConfigFile:
   638  					return boskosStagingFakeVersion, nil
   639  				default:
   640  					return "", errors.New("not supported")
   641  				}
   642  			},
   643  			upstreamVersionType: upstreamStagingVersion,
   644  			expectError:         false,
   645  			prefixes:            []prefix{prowPrefixStruct, boskosPrefixStruct},
   646  			imageHost:           boskosPrefix,
   647  			currentTag:          "whatever-current-tag",
   648  			expectedTargetTag:   boskosStagingFakeVersion,
   649  		},
   650  		{
   651  			description: "resolve image when unknown prefix",
   652  			parser: func(upAddr, pref string) (string, error) {
   653  				switch strings.TrimPrefix(upAddr, fakeUpstreamURLBase+"/") {
   654  				case boskosRefConfigFile:
   655  					return boskosProdFakeVersion, nil
   656  				default:
   657  					return "", errors.New("not supported")
   658  				}
   659  			},
   660  			upstreamVersionType: upstreamVersion,
   661  			expectError:         false,
   662  			prefixes:            []prefix{boskosPrefixStruct},
   663  			imageHost:           prowPrefix,
   664  			currentTag:          "whatever-current-tag",
   665  			expectedTargetTag:   "whatever-current-tag",
   666  		},
   667  		{
   668  			description: "tag does not exist",
   669  			parser: func(upAddr, pref string) (string, error) {
   670  				switch strings.TrimPrefix(upAddr, fakeUpstreamURLBase+"/") {
   671  				case doesNotExist:
   672  					return doesNotExist, nil
   673  				default:
   674  					return "", errors.New("not supported")
   675  				}
   676  			},
   677  			upstreamVersionType: upstreamVersion,
   678  			expectError:         false,
   679  			prefixes:            []prefix{tagDoesNotExistPrefix},
   680  			imageHost:           doesNotExistPrefix,
   681  			currentTag:          "doesNotExist",
   682  			expectedTargetTag:   "",
   683  			resolverError:       true,
   684  		},
   685  	}
   686  	for _, tc := range cases {
   687  		t.Run(tc.description, func(t *testing.T) {
   688  			option := &options{
   689  				UpstreamURLBase: fakeUpstreamURLBase,
   690  				Prefixes:        tc.prefixes,
   691  			}
   692  			cli := &fakeImageBumperCli{replacements: map[string]string{}, tagCache: map[string]string{}}
   693  			resolver, err := upstreamImageVersionResolver(option, tc.upstreamVersionType, tc.parser, cli)
   694  			if tc.expectError && err == nil {
   695  				t.Errorf("Expected to get an error but the result is nil")
   696  				return
   697  			}
   698  			if !tc.expectError && err != nil {
   699  				t.Errorf("Expected to not get an error but got one: %v", err)
   700  				return
   701  			}
   702  
   703  			if err == nil && resolver == nil {
   704  				t.Error("Expected to get an image resolver but got nil")
   705  				return
   706  			}
   707  
   708  			if resolver != nil {
   709  				res, resErr := resolver(tc.imageHost, tc.imageName, tc.currentTag)
   710  				if !tc.resolverError && resErr != nil {
   711  					t.Errorf("Expected resolver to return without error, but received error: %v", resErr)
   712  				}
   713  				if tc.resolverError && resErr == nil {
   714  					t.Error("Expected resolver to return with error, but did not receive one")
   715  				}
   716  				if tc.expectedTargetTag != res {
   717  					t.Errorf("Expected to get target tag %q but got %q", tc.expectedTargetTag, res)
   718  				}
   719  
   720  			}
   721  
   722  		})
   723  	}
   724  }
   725  
   726  func TestUpstreamConfigVersions(t *testing.T) {
   727  	prowProdFakeVersion := "v-prow-prod-version"
   728  	prowStagingFakeVersion := "v-prow-staging-version"
   729  	boskosProdFakeVersion := "v-boskos-prod-version"
   730  	boskosStagingFakeVersion := "v-boskos-staging-version"
   731  	prowRefConfigFile := "prow-prod"
   732  	boskosRefConfigFile := "boskos-prod"
   733  	prowStagingRefConfigFile := "prow-staging"
   734  	boskosStagingRefConfigFile := "boskos-staging"
   735  	fakeUpstreamURLBase := "test.com"
   736  	prowPrefix := "gcr.io/k8s-prow/"
   737  	boskosPrefix := "gcr.io/k8s-boskos/"
   738  
   739  	prowPrefixStruct := prefix{
   740  		Prefix:               prowPrefix,
   741  		RefConfigFile:        prowRefConfigFile,
   742  		StagingRefConfigFile: prowStagingRefConfigFile,
   743  	}
   744  	boskosPrefixStruct := prefix{
   745  		Prefix:               boskosPrefix,
   746  		RefConfigFile:        boskosRefConfigFile,
   747  		StagingRefConfigFile: boskosStagingRefConfigFile,
   748  	}
   749  
   750  	fakeImageVersionParser := func(upstreamAddress, prefix string) (string, error) {
   751  		switch upstreamAddress {
   752  		case fakeUpstreamURLBase + "/" + prowRefConfigFile:
   753  			return prowProdFakeVersion, nil
   754  		case fakeUpstreamURLBase + "/" + prowStagingRefConfigFile:
   755  			return prowStagingFakeVersion, nil
   756  		case fakeUpstreamURLBase + "/" + boskosRefConfigFile:
   757  			return boskosProdFakeVersion, nil
   758  		case fakeUpstreamURLBase + "/" + boskosStagingRefConfigFile:
   759  			return boskosStagingFakeVersion, nil
   760  		default:
   761  			return "", fmt.Errorf("unsupported upstream address %q for parsing the image version", upstreamAddress)
   762  		}
   763  	}
   764  	cases := []struct {
   765  		description         string
   766  		upstreamVersionType string
   767  		expectedResult      map[string]string
   768  		expectError         bool
   769  		prefixes            []prefix
   770  	}{
   771  		{
   772  			description:         "resolve image version with an invalid version type",
   773  			upstreamVersionType: "whatever-version-type",
   774  			expectError:         true,
   775  			prefixes:            []prefix{prowPrefixStruct, boskosPrefixStruct},
   776  		},
   777  		{
   778  			description:         "correct versions map for production",
   779  			upstreamVersionType: upstreamVersion,
   780  			expectError:         false,
   781  			prefixes:            []prefix{prowPrefixStruct, boskosPrefixStruct},
   782  			expectedResult:      map[string]string{prowPrefix: prowProdFakeVersion, boskosPrefix: boskosProdFakeVersion},
   783  		},
   784  		{
   785  			description:         "correct versions map for staging",
   786  			upstreamVersionType: upstreamStagingVersion,
   787  			expectError:         false,
   788  			prefixes:            []prefix{prowPrefixStruct, boskosPrefixStruct},
   789  			expectedResult:      map[string]string{prowPrefix: prowStagingFakeVersion, boskosPrefix: boskosStagingFakeVersion},
   790  		},
   791  	}
   792  	for _, tc := range cases {
   793  		t.Run(tc.description, func(t *testing.T) {
   794  			option := &options{
   795  				UpstreamURLBase: fakeUpstreamURLBase,
   796  				Prefixes:        tc.prefixes,
   797  			}
   798  			versions, err := upstreamConfigVersions(tc.upstreamVersionType, option, fakeImageVersionParser)
   799  			if tc.expectError && err == nil {
   800  				t.Errorf("Expected to get an error but the result is nil")
   801  				return
   802  			}
   803  			if !tc.expectError && err != nil {
   804  				t.Errorf("Expected to not get an error but got one: %v", err)
   805  				return
   806  			}
   807  			if err == nil && versions == nil {
   808  				t.Error("Expected to get an versions but did not")
   809  				return
   810  			}
   811  		})
   812  	}
   813  
   814  }
   815  
   816  func TestGetVersionsAndCheckConsistency(t *testing.T) {
   817  	prowPrefix := prefix{Prefix: "gcr.io/k8s-prow/", ConsistentImages: true}
   818  	boskosPrefix := prefix{Prefix: "gcr.io/k8s-boskos/", ConsistentImages: true}
   819  	inconsistentPrefix := prefix{Prefix: "inconsistent/", ConsistentImages: false}
   820  	consistentPrefixWithExceptions := prefix{
   821  		Prefix:                    "consistent/",
   822  		ConsistentImages:          true,
   823  		ConsistentImageExceptions: []string{"consistent/foo", "consistent/bar"},
   824  	}
   825  	testCases := []struct {
   826  		name             string
   827  		images           map[string]string
   828  		prefixes         []prefix
   829  		expectedVersions map[string][]string
   830  		err              bool
   831  	}{
   832  		{
   833  			name:             "two prefixes being bumped with consistent tags",
   834  			prefixes:         []prefix{prowPrefix, boskosPrefix},
   835  			images:           map[string]string{"gcr.io/k8s-prow/test:tag1": "newtag1", "gcr.io/k8s-prow/test2:tag1": "newtag1"},
   836  			err:              false,
   837  			expectedVersions: map[string][]string{"newtag1": {"gcr.io/k8s-prow/test:tag1", "gcr.io/k8s-prow/test2:tag1"}},
   838  		},
   839  		{
   840  			name:     "two prefixes being bumped with inconsistent tags",
   841  			prefixes: []prefix{prowPrefix, boskosPrefix},
   842  			images:   map[string]string{"gcr.io/k8s-prow/test:tag1": "newtag1", "gcr.io/k8s-prow/test2:tag1": "tag1"},
   843  			err:      true,
   844  		},
   845  		{
   846  			name:             "two prefixes being bumped with no bumps",
   847  			prefixes:         []prefix{prowPrefix, boskosPrefix},
   848  			images:           map[string]string{},
   849  			err:              false,
   850  			expectedVersions: map[string][]string{},
   851  		},
   852  		{
   853  			name:             "Prefix being bumped with inconsistent tags",
   854  			prefixes:         []prefix{inconsistentPrefix},
   855  			images:           map[string]string{"inconsistent/test:tag1": "newtag1", "inconsistent/test2:tag2": "newtag2"},
   856  			err:              false,
   857  			expectedVersions: map[string][]string{"newtag1": {"inconsistent/test:tag1"}, "newtag2": {"inconsistent/test2:tag2"}},
   858  		},
   859  		{
   860  			name:             "One of the image types wasn't bumped. Do not include in versions.",
   861  			prefixes:         []prefix{prowPrefix, boskosPrefix},
   862  			images:           map[string]string{"gcr.io/k8s-prow/test:tag1": "newtag1", "gcr.io/k8s-prow/test2:tag1": "newtag1", "gcr.io/k8s-boskos/nobumped:tag1": "tag1"},
   863  			err:              false,
   864  			expectedVersions: map[string][]string{"newtag1": {"gcr.io/k8s-prow/test:tag1", "gcr.io/k8s-prow/test2:tag1"}},
   865  		},
   866  		{
   867  			name:             "Two of the images in one type wasn't bumped. Do not include in versions. Do not error",
   868  			prefixes:         []prefix{prowPrefix, boskosPrefix},
   869  			images:           map[string]string{"gcr.io/k8s-prow/test:tag1": "newtag1", "gcr.io/k8s-prow/test2:tag1": "newtag1", "gcr.io/k8s-boskos/nobumped:tag1": "tag1", "gcr.io/k8s-boskos/nobumped2:tag1": "tag1"},
   870  			err:              false,
   871  			expectedVersions: map[string][]string{"newtag1": {"gcr.io/k8s-prow/test:tag1", "gcr.io/k8s-prow/test2:tag1"}},
   872  		},
   873  		{
   874  			name:             "prefix was not consistent before bump and now is",
   875  			prefixes:         []prefix{prowPrefix},
   876  			images:           map[string]string{"gcr.io/k8s-prow/test:tag1": "newtag1", "gcr.io/k8s-prow/test2:tag2": "newtag1"},
   877  			err:              false,
   878  			expectedVersions: map[string][]string{"newtag1": {"gcr.io/k8s-prow/test:tag1", "gcr.io/k8s-prow/test2:tag2"}},
   879  		},
   880  		{
   881  			name:             "prefix was not consistent before bump one was bumped ahead manually",
   882  			prefixes:         []prefix{prowPrefix},
   883  			images:           map[string]string{"gcr.io/k8s-prow/test:tag1": "newtag1", "gcr.io/k8s-prow/test2:newtag1": "newtag1"},
   884  			err:              false,
   885  			expectedVersions: map[string][]string{"newtag1": {"gcr.io/k8s-prow/test:tag1"}},
   886  		},
   887  		{
   888  			name:             "prefix is not consistent but all images are excepted",
   889  			prefixes:         []prefix{consistentPrefixWithExceptions},
   890  			images:           map[string]string{"consistent/foo:tag1": "newtag1", "consistent/bar:tag2": "newtag2"},
   891  			err:              false,
   892  			expectedVersions: map[string][]string{"newtag1": {"consistent/foo:tag1"}, "newtag2": {"consistent/bar:tag2"}},
   893  		},
   894  		{
   895  			name:             "prefix is not consistent but inconsistent images are excepted",
   896  			prefixes:         []prefix{consistentPrefixWithExceptions},
   897  			images:           map[string]string{"consistent/banana:tag1": "newtag3", "consistent/apple:tag2": "newtag3", "consistent/foo:tag1": "newtag1", "consistent/bar:tag2": "newtag2", "consistent/orange:tag0": "newtag3"},
   898  			err:              false,
   899  			expectedVersions: map[string][]string{"newtag1": {"consistent/foo:tag1"}, "newtag2": {"consistent/bar:tag2"}, "newtag3": {"consistent/banana:tag1", "consistent/apple:tag2", "consistent/orange:tag0"}},
   900  		},
   901  		{
   902  			name:     "prefix is not consistent and not all inconsistent images are excepted",
   903  			prefixes: []prefix{consistentPrefixWithExceptions},
   904  			images:   map[string]string{"consistent/banana:tag1": "newtag4", "consistent/apple:tag2": "newtag4", "consistent/foo:tag1": "newtag1", "consistent/bar:tag2": "newtag2", "consistent/orange:tag0": "newtag3"},
   905  			err:      true,
   906  		},
   907  	}
   908  	for _, tc := range testCases {
   909  		t.Run(tc.name, func(t *testing.T) {
   910  			versions, err := getVersionsAndCheckConsistency(tc.prefixes, tc.images)
   911  			if tc.err && err == nil {
   912  				t.Errorf("expected error but did not get one")
   913  			}
   914  			if !tc.err && err != nil {
   915  				t.Errorf("expected no error, but got one: %v", err)
   916  			}
   917  			if diff := cmp.Diff(tc.expectedVersions, versions, cmpopts.SortSlices(func(x, y string) bool { return strings.Compare(x, y) > 0 })); diff != "" {
   918  				t.Errorf("versions returned unexpected value (-want +got):\n%s", diff)
   919  			}
   920  		})
   921  	}
   922  }
   923  
   924  func TestMakeCommitSummary(t *testing.T) {
   925  	prowPrefix := prefix{Name: "Prow", Prefix: "gcr.io/k8s-prow/", ConsistentImages: true}
   926  	boskosPrefix := prefix{Name: "Boskos", Prefix: "gcr.io/k8s-boskos/", ConsistentImages: true}
   927  	inconsistentPrefix := prefix{Name: "Inconsistent", Prefix: "gcr.io/inconsistent/", ConsistentImages: false}
   928  	testCases := []struct {
   929  		name           string
   930  		prefixes       []prefix
   931  		versions       map[string][]string
   932  		consistency    bool
   933  		expectedResult string
   934  	}{
   935  		{
   936  			name:           "Two prefixes, but only one bumped",
   937  			prefixes:       []prefix{prowPrefix, boskosPrefix},
   938  			versions:       map[string][]string{"tag1": {"gcr.io/k8s-prow/test:tag1"}},
   939  			expectedResult: "Update Prow to tag1",
   940  		},
   941  		{
   942  			name:           "Two prefixes, both bumped",
   943  			prefixes:       []prefix{prowPrefix, boskosPrefix},
   944  			versions:       map[string][]string{"tag1": {"gcr.io/k8s-prow/test:tag1"}, "tag2": {"gcr.io/k8s-boskos/test:tag2"}},
   945  			expectedResult: "Update Prow to tag1, Boskos to tag2",
   946  		},
   947  		{
   948  			name:           "Empty versions",
   949  			prefixes:       []prefix{prowPrefix, boskosPrefix},
   950  			versions:       map[string][]string{},
   951  			expectedResult: "Update Prow, Boskos images as necessary",
   952  		},
   953  		{
   954  			name:           "One bumped inconsistently",
   955  			prefixes:       []prefix{prowPrefix, boskosPrefix, inconsistentPrefix},
   956  			versions:       map[string][]string{"tag1": {"gcr.io/k8s-prow/test:tag1"}, "tag2": {"gcr.io/k8s-boskos/test:tag2"}, "tag3": {"gcr.io/inconsistent/test:tag3"}},
   957  			expectedResult: "Update Prow to tag1, Boskos to tag2 and Inconsistent as needed",
   958  		},
   959  		{
   960  			name:           "inconsistent tag was not bumped, do not include in result",
   961  			prefixes:       []prefix{prowPrefix, boskosPrefix, inconsistentPrefix},
   962  			versions:       map[string][]string{"tag1": {"gcr.io/k8s-prow/test:tag1"}, "tag2": {"gcr.io/k8s-boskos/test:tag2"}},
   963  			expectedResult: "Update Prow to tag1, Boskos to tag2",
   964  		},
   965  		{
   966  			name:           "Two images bumped to same version",
   967  			prefixes:       []prefix{prowPrefix, boskosPrefix, inconsistentPrefix},
   968  			versions:       map[string][]string{"tag1": {"gcr.io/k8s-prow/test:tag1", "gcr.io/inconsistent/test:tag3"}, "tag2": {"gcr.io/k8s-boskos/test:tag2"}},
   969  			expectedResult: "Update Prow to tag1, Boskos to tag2 and Inconsistent as needed",
   970  		},
   971  		{
   972  			name:           "only bump inconsistent",
   973  			prefixes:       []prefix{inconsistentPrefix},
   974  			versions:       map[string][]string{"tag1": {"gcr.io/k8s-prow/test:tag1", "gcr.io/inconsistent/test:tag3"}, "tag2": {"gcr.io/k8s-boskos/test:tag2"}},
   975  			expectedResult: "Update Inconsistent as needed",
   976  		},
   977  	}
   978  	for _, tc := range testCases {
   979  		t.Run(tc.name, func(t *testing.T) {
   980  			res := makeCommitSummary(tc.prefixes, tc.versions)
   981  			if res != tc.expectedResult {
   982  				t.Errorf("expected commit string to be %q, but was %q", tc.expectedResult, res)
   983  			}
   984  		})
   985  	}
   986  }
   987  
   988  func TestGenerateSummary(t *testing.T) {
   989  	beforeCommit := "2b1234567"
   990  	afterCommit := "3a1234567"
   991  	beforeDate := "20210128"
   992  	afterDate := "20210129"
   993  	beforeCommit2 := "1c1234567"
   994  	afterCommit2 := "4f1234567"
   995  	beforeDate2 := "20210125"
   996  	afterDate2 := "20210126"
   997  	unsummarizedOutHeader := `Multiple distinct %s changes:
   998  
   999  Commits | Dates | Images
  1000  --- | --- | ---`
  1001  
  1002  	unsummarizedOutLine := "github.com/test/repo/compare/%s...%s | %s → %s | %s"
  1003  
  1004  	sampleImages := map[string]string{
  1005  		fmt.Sprintf("gcr.io/bumped/bumpName:v%s-%s", beforeDate, beforeCommit):      fmt.Sprintf("v%s-%s", afterDate, afterCommit),
  1006  		fmt.Sprintf("gcr.io/variant/name:v%s-%s-first", beforeDate, beforeCommit):   fmt.Sprintf("v%s-%s", afterDate, afterCommit),
  1007  		fmt.Sprintf("gcr.io/variant/name:v%s-%s-second", beforeDate, beforeCommit):  fmt.Sprintf("v%s-%s", afterDate, afterCommit),
  1008  		fmt.Sprintf("gcr.io/inconsistent/first:v%s-%s", beforeDate2, beforeCommit2): fmt.Sprintf("v%s-%s", afterDate2, afterCommit2),
  1009  		fmt.Sprintf("gcr.io/inconsistent/second:v%s-%s", beforeDate, beforeCommit):  fmt.Sprintf("v%s-%s", afterDate, afterCommit),
  1010  	}
  1011  	testCases := []struct {
  1012  		testName  string
  1013  		name      string
  1014  		repo      string
  1015  		prefix    string
  1016  		summarize bool
  1017  		images    map[string]string
  1018  		expected  string
  1019  	}{
  1020  		{
  1021  			testName:  "Image not bumped unsummarized",
  1022  			name:      "Test",
  1023  			repo:      "repo",
  1024  			prefix:    "gcr.io/none",
  1025  			summarize: true,
  1026  			images:    sampleImages,
  1027  			expected:  "No gcr.io/none changes.",
  1028  		},
  1029  		{
  1030  			testName:  "Image not bumped summarized",
  1031  			name:      "Test",
  1032  			repo:      "repo",
  1033  			prefix:    "gcr.io/none",
  1034  			summarize: true,
  1035  			images:    sampleImages,
  1036  			expected:  "No gcr.io/none changes.",
  1037  		},
  1038  		{
  1039  			testName:  "Image bumped: summarized",
  1040  			name:      "Test",
  1041  			repo:      "github.com/test/repo",
  1042  			prefix:    "gcr.io/bumped",
  1043  			summarize: true,
  1044  			images:    sampleImages,
  1045  			expected:  fmt.Sprintf("gcr.io/bumped changes: github.com/test/repo/compare/%s...%s (%s → %s)", beforeCommit, afterCommit, formatTagDate(beforeDate), formatTagDate(afterDate)),
  1046  		},
  1047  		{
  1048  			testName:  "Image bumped: not summarized",
  1049  			name:      "Test",
  1050  			repo:      "github.com/test/repo",
  1051  			prefix:    "gcr.io/bumped",
  1052  			summarize: false,
  1053  			images:    sampleImages,
  1054  			expected:  fmt.Sprintf("%s\n%s\n", fmt.Sprintf(unsummarizedOutHeader, "gcr.io/bumped"), fmt.Sprintf(unsummarizedOutLine, beforeCommit, afterCommit, formatTagDate(beforeDate), formatTagDate(afterDate), "bumpName")),
  1055  		},
  1056  		{
  1057  			testName:  "Image bumped: not summarized",
  1058  			name:      "Test",
  1059  			repo:      "github.com/test/repo",
  1060  			prefix:    "gcr.io/variant",
  1061  			summarize: false,
  1062  			images:    sampleImages,
  1063  			expected:  fmt.Sprintf("%s\n%s\n", fmt.Sprintf(unsummarizedOutHeader, "gcr.io/variant"), fmt.Sprintf(unsummarizedOutLine, beforeCommit, afterCommit, formatTagDate(beforeDate), formatTagDate(afterDate), "name(first), name(second)")),
  1064  		},
  1065  		{
  1066  			testName:  "Image bumped, inconsistent: not summarized",
  1067  			name:      "Test",
  1068  			repo:      "github.com/test/repo",
  1069  			prefix:    "gcr.io/inconsistent",
  1070  			summarize: false,
  1071  			images:    sampleImages,
  1072  			expected:  fmt.Sprintf("%s\n%s\n%s\n", fmt.Sprintf(unsummarizedOutHeader, "gcr.io/inconsistent"), fmt.Sprintf(unsummarizedOutLine, beforeCommit2, afterCommit2, formatTagDate(beforeDate2), formatTagDate(afterDate2), "first"), fmt.Sprintf(unsummarizedOutLine, beforeCommit, afterCommit, formatTagDate(beforeDate), formatTagDate(afterDate), "second")),
  1073  		},
  1074  	}
  1075  	for _, tc := range testCases {
  1076  		tc := tc
  1077  		t.Run(tc.testName, func(t *testing.T) {
  1078  			want, got := tc.expected, generateSummary(tc.name, tc.repo, tc.prefix, tc.summarize, tc.images)
  1079  			if diff := cmp.Diff(want, got); diff != "" {
  1080  				t.Errorf("generateSummary returned unexpected value (-want +got):\n%s", diff)
  1081  			}
  1082  		})
  1083  
  1084  	}
  1085  }
  1086  
  1087  func TestPRTitleBody(t *testing.T) {
  1088  	prowPrefix := prefix{Name: "Prow", Prefix: "gcr.io/k8s-prow/", ConsistentImages: true}
  1089  	beforeCommit := "2b1234567"
  1090  	afterCommit := "3a1234567"
  1091  	beforeDate := "20210128"
  1092  	afterDate := "20210129"
  1093  	prowImages := map[string]string{
  1094  		fmt.Sprintf("gcr.io/k8s-prow/bumpName:v%s-%s", beforeDate, beforeCommit): fmt.Sprintf("v%s-%s", afterDate, afterCommit),
  1095  	}
  1096  	testCases := []struct {
  1097  		name            string
  1098  		options         options
  1099  		versions        map[string][]string
  1100  		images          map[string]string
  1101  		expectedSummary string
  1102  		expectedBody    string
  1103  	}{
  1104  		{
  1105  			name: "prow bumped",
  1106  			options: options{
  1107  				Prefixes: []prefix{prowPrefix},
  1108  			},
  1109  			versions:        map[string][]string{"tag1": {"gcr.io/k8s-prow/test:tag1"}},
  1110  			images:          prowImages,
  1111  			expectedSummary: "Update Prow to tag1",
  1112  			expectedBody:    "Multiple distinct gcr.io/k8s-prow/ changes:\n\nCommits | Dates | Images\n--- | --- | ---\n/compare/2b1234567...3a1234567 | 2021‑01‑28 → 2021‑01‑29 | bumpName\n\n\n\nNobody is currently oncall, so falling back to Blunderbuss.\n",
  1113  		},
  1114  		{
  1115  			name: "contains additional PR body",
  1116  			options: options{
  1117  				Prefixes:         []prefix{prowPrefix},
  1118  				AdditionalPRBody: "/some-other-command",
  1119  			},
  1120  			versions:        map[string][]string{"tag1": {"gcr.io/k8s-prow/test:tag1"}},
  1121  			images:          prowImages,
  1122  			expectedSummary: "Update Prow to tag1",
  1123  			expectedBody:    "Multiple distinct gcr.io/k8s-prow/ changes:\n\nCommits | Dates | Images\n--- | --- | ---\n/compare/2b1234567...3a1234567 | 2021‑01‑28 → 2021‑01‑29 | bumpName\n\n\n\nNobody is currently oncall, so falling back to Blunderbuss.\n/some-other-command\n",
  1124  		},
  1125  	}
  1126  	for _, tc := range testCases {
  1127  		t.Run(tc.name, func(t *testing.T) {
  1128  			c := client{
  1129  				o:        &tc.options,
  1130  				images:   tc.images,
  1131  				versions: tc.versions,
  1132  			}
  1133  			summary, body := c.PRTitleBody()
  1134  			if diff := cmp.Diff(tc.expectedSummary, summary); diff != "" {
  1135  				t.Fatalf("summary doesn't match expected, diff: %s", diff)
  1136  			}
  1137  			if diff := cmp.Diff(tc.expectedBody, body); diff != "" {
  1138  				t.Fatalf("body doesn't match expected, diff: %s", diff)
  1139  			}
  1140  
  1141  		})
  1142  	}
  1143  }