github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/pkg/git/v2/interactor_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 git
    18  
    19  import (
    20  	"errors"
    21  	"fmt"
    22  	"reflect"
    23  	"testing"
    24  
    25  	"github.com/sirupsen/logrus"
    26  
    27  	"k8s.io/apimachinery/pkg/util/diff"
    28  )
    29  
    30  func TestInteractor_Clone(t *testing.T) {
    31  	var testCases = []struct {
    32  		name          string
    33  		dir           string
    34  		from          string
    35  		remote        RemoteResolver
    36  		responses     map[string]execResponse
    37  		expectedCalls [][]string
    38  		expectedErr   bool
    39  	}{
    40  		{
    41  			name: "happy case",
    42  			dir:  "/secondaryclone",
    43  			from: "/mirrorclone",
    44  			responses: map[string]execResponse{
    45  				"clone /mirrorclone /secondaryclone": {
    46  					out: []byte(`ok`),
    47  				},
    48  			},
    49  			expectedCalls: [][]string{
    50  				{"clone", "/mirrorclone", "/secondaryclone"},
    51  			},
    52  			expectedErr: false,
    53  		},
    54  		{
    55  			name: "clone fails",
    56  			dir:  "/secondaryclone",
    57  			from: "/mirrorclone",
    58  			responses: map[string]execResponse{
    59  				"clone /mirrorclone /secondaryclone": {
    60  					err: errors.New("oops"),
    61  				},
    62  			},
    63  			expectedCalls: [][]string{
    64  				{"clone", "/mirrorclone", "/secondaryclone"},
    65  			},
    66  			expectedErr: true,
    67  		},
    68  	}
    69  
    70  	for _, testCase := range testCases {
    71  		t.Run(testCase.name, func(t *testing.T) {
    72  			e := fakeExecutor{
    73  				records:   [][]string{},
    74  				responses: testCase.responses,
    75  			}
    76  			i := interactor{
    77  				executor: &e,
    78  				remote:   testCase.remote,
    79  				dir:      testCase.dir,
    80  				logger:   logrus.WithField("test", testCase.name),
    81  			}
    82  			actualErr := i.Clone(testCase.from)
    83  			if testCase.expectedErr && actualErr == nil {
    84  				t.Errorf("%s: expected an error but got none", testCase.name)
    85  			}
    86  			if !testCase.expectedErr && actualErr != nil {
    87  				t.Errorf("%s: expected no error but got one: %v", testCase.name, actualErr)
    88  			}
    89  			if actual, expected := e.records, testCase.expectedCalls; !reflect.DeepEqual(actual, expected) {
    90  				t.Errorf("%s: got incorrect git calls: %v", testCase.name, diff.ObjectReflectDiff(actual, expected))
    91  			}
    92  		})
    93  	}
    94  }
    95  
    96  func TestInteractor_CloneWithRepoOpts(t *testing.T) {
    97  	var testCases = []struct {
    98  		name          string
    99  		dir           string
   100  		from          string
   101  		remote        RemoteResolver
   102  		repoOpts      RepoOpts
   103  		responses     map[string]execResponse
   104  		expectedCalls [][]string
   105  		expectedErr   bool
   106  	}{
   107  		{
   108  			name:     "blank RepoOpts",
   109  			dir:      "/secondaryclone",
   110  			from:     "/mirrorclone",
   111  			repoOpts: RepoOpts{},
   112  			responses: map[string]execResponse{
   113  				"clone /mirrorclone /secondaryclone": {
   114  					out: []byte(`ok`),
   115  				},
   116  			},
   117  			expectedCalls: [][]string{
   118  				{"clone", "/mirrorclone", "/secondaryclone"},
   119  			},
   120  			expectedErr: false,
   121  		},
   122  		{
   123  			name: "shared git objects",
   124  			dir:  "/secondaryclone",
   125  			from: "/mirrorclone",
   126  			repoOpts: RepoOpts{
   127  				SparseCheckoutDirs:           nil,
   128  				ShareObjectsWithPrimaryClone: true,
   129  			},
   130  			responses: map[string]execResponse{
   131  				"clone --shared /mirrorclone /secondaryclone": {
   132  					out: []byte(`ok`),
   133  				},
   134  			},
   135  			expectedCalls: [][]string{
   136  				{"clone", "--shared", "/mirrorclone", "/secondaryclone"},
   137  			},
   138  			expectedErr: false,
   139  		},
   140  		{
   141  			name: "shared git objects and sparse checkout (toplevel only)",
   142  			dir:  "/secondaryclone",
   143  			from: "/mirrorclone",
   144  			repoOpts: RepoOpts{
   145  				SparseCheckoutDirs:           []string{},
   146  				ShareObjectsWithPrimaryClone: true,
   147  			},
   148  			responses: map[string]execResponse{
   149  				"clone --shared --sparse /mirrorclone /secondaryclone": {
   150  					out: []byte(`ok`),
   151  				},
   152  			},
   153  			expectedCalls: [][]string{
   154  				{"clone", "--shared", "--sparse", "/mirrorclone", "/secondaryclone"},
   155  			},
   156  			expectedErr: false,
   157  		},
   158  		{
   159  			name: "shared git objects and sparse checkout (toplevel+subdirs)",
   160  			dir:  "/secondaryclone",
   161  			from: "/mirrorclone",
   162  			repoOpts: RepoOpts{
   163  				SparseCheckoutDirs:           []string{"a", "b"},
   164  				ShareObjectsWithPrimaryClone: true,
   165  			},
   166  			responses: map[string]execResponse{
   167  				"clone --shared --sparse /mirrorclone /secondaryclone": {
   168  					out: []byte(`ok`),
   169  				},
   170  				"-C /secondaryclone sparse-checkout set a b": {
   171  					out: []byte(`ok`),
   172  				},
   173  			},
   174  			expectedCalls: [][]string{
   175  				{"clone", "--shared", "--sparse", "/mirrorclone", "/secondaryclone"},
   176  				{"-C", "/secondaryclone", "sparse-checkout", "set", "a", "b"},
   177  			},
   178  			expectedErr: false,
   179  		},
   180  	}
   181  
   182  	for _, testCase := range testCases {
   183  		t.Run(testCase.name, func(t *testing.T) {
   184  			e := fakeExecutor{
   185  				records:   [][]string{},
   186  				responses: testCase.responses,
   187  			}
   188  			i := interactor{
   189  				executor: &e,
   190  				remote:   testCase.remote,
   191  				dir:      testCase.dir,
   192  				logger:   logrus.WithField("test", testCase.name),
   193  			}
   194  			actualErr := i.CloneWithRepoOpts(testCase.from, testCase.repoOpts)
   195  			if testCase.expectedErr && actualErr == nil {
   196  				t.Errorf("%s: expected an error but got none", testCase.name)
   197  			}
   198  			if !testCase.expectedErr && actualErr != nil {
   199  				t.Errorf("%s: expected no error but got one: %v", testCase.name, actualErr)
   200  			}
   201  			if actual, expected := e.records, testCase.expectedCalls; !reflect.DeepEqual(actual, expected) {
   202  				t.Errorf("%s: got incorrect git calls: %v", testCase.name, diff.ObjectReflectDiff(actual, expected))
   203  			}
   204  		})
   205  	}
   206  }
   207  
   208  func TestInteractor_MirrorClone(t *testing.T) {
   209  	var testCases = []struct {
   210  		name          string
   211  		dir           string
   212  		remote        RemoteResolver
   213  		responses     map[string]execResponse
   214  		expectedCalls [][]string
   215  		expectedErr   bool
   216  	}{
   217  		{
   218  			name: "happy case",
   219  			dir:  "/else",
   220  			remote: func() (string, error) {
   221  				return "someone.com", nil
   222  			},
   223  			responses: map[string]execResponse{
   224  				"clone --mirror someone.com /else": {
   225  					out: []byte(`ok`),
   226  				},
   227  			},
   228  			expectedCalls: [][]string{
   229  				{"clone", "--mirror", "someone.com", "/else"},
   230  			},
   231  			expectedErr: false,
   232  		},
   233  		{
   234  			name: "remote resolution fails",
   235  			dir:  "/else",
   236  			remote: func() (string, error) {
   237  				return "", errors.New("oops")
   238  			},
   239  			responses:     map[string]execResponse{},
   240  			expectedCalls: [][]string{},
   241  			expectedErr:   true,
   242  		},
   243  		{
   244  			name: "clone fails",
   245  			dir:  "/else",
   246  			remote: func() (string, error) {
   247  				return "someone.com", nil
   248  			},
   249  			responses: map[string]execResponse{
   250  				"clone --mirror someone.com /else": {
   251  					err: errors.New("oops"),
   252  				},
   253  			},
   254  			expectedCalls: [][]string{
   255  				{"clone", "--mirror", "someone.com", "/else"},
   256  			},
   257  			expectedErr: true,
   258  		},
   259  	}
   260  
   261  	for _, testCase := range testCases {
   262  		t.Run(testCase.name, func(t *testing.T) {
   263  			e := fakeExecutor{
   264  				records:   [][]string{},
   265  				responses: testCase.responses,
   266  			}
   267  			i := interactor{
   268  				executor: &e,
   269  				remote:   testCase.remote,
   270  				dir:      testCase.dir,
   271  				logger:   logrus.WithField("test", testCase.name),
   272  			}
   273  			actualErr := i.MirrorClone()
   274  			if testCase.expectedErr && actualErr == nil {
   275  				t.Errorf("%s: expected an error but got none", testCase.name)
   276  			}
   277  			if !testCase.expectedErr && actualErr != nil {
   278  				t.Errorf("%s: expected no error but got one: %v", testCase.name, actualErr)
   279  			}
   280  			if actual, expected := e.records, testCase.expectedCalls; !reflect.DeepEqual(actual, expected) {
   281  				t.Errorf("%s: got incorrect git calls: %v", testCase.name, diff.ObjectReflectDiff(actual, expected))
   282  			}
   283  		})
   284  	}
   285  }
   286  
   287  func TestInteractor_Checkout(t *testing.T) {
   288  	var testCases = []struct {
   289  		name          string
   290  		commitlike    string
   291  		remote        RemoteResolver
   292  		responses     map[string]execResponse
   293  		expectedCalls [][]string
   294  		expectedErr   bool
   295  	}{
   296  		{
   297  			name:       "happy case",
   298  			commitlike: "shasum",
   299  			responses: map[string]execResponse{
   300  				"checkout shasum": {
   301  					out: []byte(`ok`),
   302  				},
   303  			},
   304  			expectedCalls: [][]string{
   305  				{"checkout", "shasum"},
   306  			},
   307  			expectedErr: false,
   308  		},
   309  		{
   310  			name:       "checkout fails",
   311  			commitlike: "shasum",
   312  			responses: map[string]execResponse{
   313  				"checkout shasum": {
   314  					err: errors.New("oops"),
   315  				},
   316  			},
   317  			expectedCalls: [][]string{
   318  				{"checkout", "shasum"},
   319  			},
   320  			expectedErr: true,
   321  		},
   322  	}
   323  
   324  	for _, testCase := range testCases {
   325  		t.Run(testCase.name, func(t *testing.T) {
   326  			e := fakeExecutor{
   327  				records:   [][]string{},
   328  				responses: testCase.responses,
   329  			}
   330  			i := interactor{
   331  				executor: &e,
   332  				remote:   testCase.remote,
   333  				logger:   logrus.WithField("test", testCase.name),
   334  			}
   335  			actualErr := i.Checkout(testCase.commitlike)
   336  			if testCase.expectedErr && actualErr == nil {
   337  				t.Errorf("%s: expected an error but got none", testCase.name)
   338  			}
   339  			if !testCase.expectedErr && actualErr != nil {
   340  				t.Errorf("%s: expected no error but got one: %v", testCase.name, actualErr)
   341  			}
   342  			if actual, expected := e.records, testCase.expectedCalls; !reflect.DeepEqual(actual, expected) {
   343  				t.Errorf("%s: got incorrect git calls: %v", testCase.name, diff.ObjectReflectDiff(actual, expected))
   344  			}
   345  		})
   346  	}
   347  }
   348  
   349  func TestInteractor_RevParse(t *testing.T) {
   350  	var testCases = []struct {
   351  		name          string
   352  		commitlike    string
   353  		remote        RemoteResolver
   354  		responses     map[string]execResponse
   355  		expectedCalls [][]string
   356  		expectedOut   string
   357  		expectedErr   bool
   358  	}{
   359  		{
   360  			name:       "happy case",
   361  			commitlike: "shasum",
   362  			responses: map[string]execResponse{
   363  				"rev-parse shasum": {
   364  					out: []byte(`ok`),
   365  				},
   366  			},
   367  			expectedCalls: [][]string{
   368  				{"rev-parse", "shasum"},
   369  			},
   370  			expectedOut: "ok",
   371  			expectedErr: false,
   372  		},
   373  		{
   374  			name:       "rev-parse fails",
   375  			commitlike: "shasum",
   376  			responses: map[string]execResponse{
   377  				"rev-parse shasum": {
   378  					err: errors.New("oops"),
   379  				},
   380  			},
   381  			expectedCalls: [][]string{
   382  				{"rev-parse", "shasum"},
   383  			},
   384  			expectedErr: true,
   385  		},
   386  	}
   387  
   388  	for _, testCase := range testCases {
   389  		t.Run(testCase.name, func(t *testing.T) {
   390  			e := fakeExecutor{
   391  				records:   [][]string{},
   392  				responses: testCase.responses,
   393  			}
   394  			i := interactor{
   395  				executor: &e,
   396  				remote:   testCase.remote,
   397  				logger:   logrus.WithField("test", testCase.name),
   398  			}
   399  			actualOut, actualErr := i.RevParse(testCase.commitlike)
   400  			if testCase.expectedErr && actualErr == nil {
   401  				t.Errorf("%s: expected an error but got none", testCase.name)
   402  			}
   403  			if !testCase.expectedErr && actualErr != nil {
   404  				t.Errorf("%s: expected no error but got one: %v", testCase.name, actualErr)
   405  			}
   406  			if actual, expected := e.records, testCase.expectedCalls; !reflect.DeepEqual(actual, expected) {
   407  				t.Errorf("%s: got incorrect git calls: %v", testCase.name, diff.ObjectReflectDiff(actual, expected))
   408  			}
   409  			if actualOut != testCase.expectedOut {
   410  				t.Errorf("%s: got incorrect output: expected %v, got %v", testCase.name, testCase.expectedOut, actualOut)
   411  			}
   412  		})
   413  	}
   414  }
   415  
   416  func TestInteractor_BranchExists(t *testing.T) {
   417  	var testCases = []struct {
   418  		name          string
   419  		branch        string
   420  		remote        RemoteResolver
   421  		responses     map[string]execResponse
   422  		expectedCalls [][]string
   423  		expectedOut   bool
   424  	}{
   425  		{
   426  			name:   "happy case",
   427  			branch: "branch",
   428  			responses: map[string]execResponse{
   429  				"ls-remote --exit-code --heads origin branch": {
   430  					out: []byte(`c165713776618ff3162643ea4d0382ca039adfeb	refs/heads/branch`),
   431  				},
   432  			},
   433  			expectedCalls: [][]string{
   434  				{"ls-remote", "--exit-code", "--heads", "origin", "branch"},
   435  			},
   436  			expectedOut: true,
   437  		},
   438  		{
   439  			name:   "ls-remote fails",
   440  			branch: "branch",
   441  			responses: map[string]execResponse{
   442  				"ls-remote --exit-code --heads origin branch": {
   443  					err: errors.New("oops"),
   444  				},
   445  			},
   446  			expectedCalls: [][]string{
   447  				{"ls-remote", "--exit-code", "--heads", "origin", "branch"},
   448  			},
   449  			expectedOut: false,
   450  		},
   451  	}
   452  
   453  	for _, testCase := range testCases {
   454  		t.Run(testCase.name, func(t *testing.T) {
   455  			e := fakeExecutor{
   456  				records:   [][]string{},
   457  				responses: testCase.responses,
   458  			}
   459  			i := interactor{
   460  				executor: &e,
   461  				remote:   testCase.remote,
   462  				logger:   logrus.WithField("test", testCase.name),
   463  			}
   464  			actualOut := i.BranchExists(testCase.branch)
   465  			if testCase.expectedOut != actualOut {
   466  				t.Errorf("%s: got incorrect output: expected %v, got %v", testCase.name, testCase.expectedOut, actualOut)
   467  			}
   468  			if actual, expected := e.records, testCase.expectedCalls; !reflect.DeepEqual(actual, expected) {
   469  				t.Errorf("%s: got incorrect git calls: %v", testCase.name, diff.ObjectReflectDiff(actual, expected))
   470  			}
   471  		})
   472  	}
   473  }
   474  
   475  func TestInteractor_ObjectExists(t *testing.T) {
   476  	var testCases = []struct {
   477  		name          string
   478  		object        string
   479  		responses     map[string]execResponse
   480  		expectedCalls [][]string
   481  		expectedOut   bool
   482  		// ObjectExists always returns a nil error, so we don't test it.
   483  	}{
   484  		{
   485  			name:   "happy case",
   486  			object: "abc123",
   487  			responses: map[string]execResponse{
   488  				"cat-file -e abc123": {out: []byte("")},
   489  			},
   490  			expectedCalls: [][]string{
   491  				{"cat-file", "-e", "abc123"},
   492  			},
   493  			expectedOut: true,
   494  		},
   495  		{
   496  			name:   "Does not exist",
   497  			object: "000000",
   498  			responses: map[string]execResponse{
   499  				"cat-file -e 000000": {out: []byte(""), err: errors.New("")},
   500  			},
   501  			expectedCalls: [][]string{
   502  				{"cat-file", "-e", "000000"},
   503  			},
   504  			expectedOut: false,
   505  		},
   506  	}
   507  
   508  	for _, testCase := range testCases {
   509  		t.Run(testCase.name, func(t *testing.T) {
   510  			e := fakeExecutor{
   511  				records:   [][]string{},
   512  				responses: testCase.responses,
   513  			}
   514  			i := interactor{
   515  				executor: &e,
   516  				logger:   logrus.WithField("test", testCase.name),
   517  			}
   518  			actualOut, _ := i.ObjectExists(testCase.object)
   519  			if testCase.expectedOut != actualOut {
   520  				t.Errorf("%s: got incorrect output: expected %v, got %v", testCase.name, testCase.expectedOut, actualOut)
   521  			}
   522  			if actual, expected := e.records, testCase.expectedCalls; !reflect.DeepEqual(actual, expected) {
   523  				t.Errorf("%s: got incorrect git calls: %v", testCase.name, diff.ObjectReflectDiff(actual, expected))
   524  			}
   525  		})
   526  	}
   527  }
   528  
   529  func TestInteractor_CheckoutNewBranch(t *testing.T) {
   530  	var testCases = []struct {
   531  		name          string
   532  		branch        string
   533  		remote        RemoteResolver
   534  		responses     map[string]execResponse
   535  		expectedCalls [][]string
   536  		expectedErr   bool
   537  	}{
   538  		{
   539  			name:   "happy case",
   540  			branch: "new-branch",
   541  			responses: map[string]execResponse{
   542  				"checkout -b new-branch": {
   543  					out: []byte(`ok`),
   544  				},
   545  			},
   546  			expectedCalls: [][]string{
   547  				{"checkout", "-b", "new-branch"},
   548  			},
   549  			expectedErr: false,
   550  		},
   551  		{
   552  			name:   "checkout fails",
   553  			branch: "new-branch",
   554  			responses: map[string]execResponse{
   555  				"checkout -b new-branch": {
   556  					err: errors.New("oops"),
   557  				},
   558  			},
   559  			expectedCalls: [][]string{
   560  				{"checkout", "-b", "new-branch"},
   561  			},
   562  			expectedErr: true,
   563  		},
   564  	}
   565  
   566  	for _, testCase := range testCases {
   567  		t.Run(testCase.name, func(t *testing.T) {
   568  			e := fakeExecutor{
   569  				records:   [][]string{},
   570  				responses: testCase.responses,
   571  			}
   572  			i := interactor{
   573  				executor: &e,
   574  				remote:   testCase.remote,
   575  				logger:   logrus.WithField("test", testCase.name),
   576  			}
   577  			actualErr := i.CheckoutNewBranch(testCase.branch)
   578  			if testCase.expectedErr && actualErr == nil {
   579  				t.Errorf("%s: expected an error but got none", testCase.name)
   580  			}
   581  			if !testCase.expectedErr && actualErr != nil {
   582  				t.Errorf("%s: expected no error but got one: %v", testCase.name, actualErr)
   583  			}
   584  			if actual, expected := e.records, testCase.expectedCalls; !reflect.DeepEqual(actual, expected) {
   585  				t.Errorf("%s: got incorrect git calls: %v", testCase.name, diff.ObjectReflectDiff(actual, expected))
   586  			}
   587  		})
   588  	}
   589  }
   590  
   591  func TestInteractor_Merge(t *testing.T) {
   592  	var testCases = []struct {
   593  		name          string
   594  		commitlike    string
   595  		remote        RemoteResolver
   596  		responses     map[string]execResponse
   597  		expectedCalls [][]string
   598  		expectedMerge bool
   599  		expectedErr   bool
   600  	}{
   601  		{
   602  			name:       "happy case",
   603  			commitlike: "shasum",
   604  			responses: map[string]execResponse{
   605  				"merge --no-ff --no-stat -m merge shasum": {
   606  					out: []byte(`ok`),
   607  				},
   608  			},
   609  			expectedCalls: [][]string{
   610  				{"merge", "--no-ff", "--no-stat", "-m", "merge", "shasum"},
   611  			},
   612  			expectedMerge: true,
   613  			expectedErr:   false,
   614  		},
   615  		{
   616  			name:       "merge fails but abort succeeds",
   617  			commitlike: "shasum",
   618  			responses: map[string]execResponse{
   619  				"merge --no-ff --no-stat -m merge shasum": {
   620  					err: errors.New("oops"),
   621  				},
   622  				"merge --abort": {
   623  					out: []byte(`ok`),
   624  				},
   625  			},
   626  			expectedCalls: [][]string{
   627  				{"merge", "--no-ff", "--no-stat", "-m", "merge", "shasum"},
   628  				{"merge", "--abort"},
   629  			},
   630  			expectedMerge: false,
   631  			expectedErr:   false,
   632  		},
   633  		{
   634  			name:       "merge fails and abort fails",
   635  			commitlike: "shasum",
   636  			responses: map[string]execResponse{
   637  				"merge --no-ff --no-stat -m merge shasum": {
   638  					err: errors.New("oops"),
   639  				},
   640  				"merge --abort": {
   641  					err: errors.New("oops"),
   642  				},
   643  			},
   644  			expectedCalls: [][]string{
   645  				{"merge", "--no-ff", "--no-stat", "-m", "merge", "shasum"},
   646  				{"merge", "--abort"},
   647  			},
   648  			expectedMerge: false,
   649  			expectedErr:   true,
   650  		},
   651  	}
   652  
   653  	for _, testCase := range testCases {
   654  		t.Run(testCase.name, func(t *testing.T) {
   655  			e := fakeExecutor{
   656  				records:   [][]string{},
   657  				responses: testCase.responses,
   658  			}
   659  			i := interactor{
   660  				executor: &e,
   661  				remote:   testCase.remote,
   662  				logger:   logrus.WithField("test", testCase.name),
   663  			}
   664  			actualMerge, actualErr := i.Merge(testCase.commitlike)
   665  			if testCase.expectedMerge != actualMerge {
   666  				t.Errorf("%s: got incorrect output: expected %v, got %v", testCase.name, testCase.expectedMerge, actualMerge)
   667  			}
   668  			if testCase.expectedErr && actualErr == nil {
   669  				t.Errorf("%s: expected an error but got none", testCase.name)
   670  			}
   671  			if !testCase.expectedErr && actualErr != nil {
   672  				t.Errorf("%s: expected no error but got one: %v", testCase.name, actualErr)
   673  			}
   674  			if actual, expected := e.records, testCase.expectedCalls; !reflect.DeepEqual(actual, expected) {
   675  				t.Errorf("%s: got incorrect git calls: %v", testCase.name, diff.ObjectReflectDiff(actual, expected))
   676  			}
   677  		})
   678  	}
   679  }
   680  
   681  func TestInteractor_MergeWithStrategy(t *testing.T) {
   682  	var testCases = []struct {
   683  		name          string
   684  		commitlike    string
   685  		strategy      string
   686  		opts          []MergeOpt
   687  		remote        RemoteResolver
   688  		responses     map[string]execResponse
   689  		expectedCalls [][]string
   690  		expectedMerge bool
   691  		expectedErr   bool
   692  	}{
   693  		{
   694  			name:       "happy merge case",
   695  			commitlike: "shasum",
   696  			strategy:   "merge",
   697  			responses: map[string]execResponse{
   698  				"merge --no-ff --no-stat -m merge shasum": {
   699  					out: []byte(`ok`),
   700  				},
   701  			},
   702  			expectedCalls: [][]string{
   703  				{"merge", "--no-ff", "--no-stat", "-m", "merge", "shasum"},
   704  			},
   705  			expectedMerge: true,
   706  			expectedErr:   false,
   707  		},
   708  		{
   709  			name:       "happy merge case with options",
   710  			commitlike: "shasum",
   711  			strategy:   "merge",
   712  			opts:       []MergeOpt{{CommitMessage: "message"}},
   713  			responses: map[string]execResponse{
   714  				"merge --no-ff --no-stat -m message shasum": {
   715  					out: []byte(`ok`),
   716  				},
   717  			},
   718  			expectedCalls: [][]string{
   719  				{"merge", "--no-ff", "--no-stat", "-m", "message", "shasum"},
   720  			},
   721  			expectedMerge: true,
   722  			expectedErr:   false,
   723  		},
   724  		{
   725  			name:       "happy merge case with multi words message",
   726  			commitlike: "shasum",
   727  			strategy:   "merge",
   728  			opts:       []MergeOpt{{CommitMessage: "my happy merge message"}},
   729  			responses: map[string]execResponse{
   730  				"merge --no-ff --no-stat -m my happy merge message shasum": {
   731  					out: []byte(`ok`),
   732  				},
   733  			},
   734  			expectedCalls: [][]string{
   735  				{"merge", "--no-ff", "--no-stat", "-m", "my happy merge message", "shasum"},
   736  			},
   737  			expectedMerge: true,
   738  			expectedErr:   false,
   739  		},
   740  		{
   741  			name:       "happy merge case with multiple options with single/multi words message",
   742  			commitlike: "shasum",
   743  			strategy:   "merge",
   744  			opts: []MergeOpt{
   745  				{CommitMessage: "my"},
   746  				{CommitMessage: "happy merge"},
   747  				{CommitMessage: "message"},
   748  			},
   749  			responses: map[string]execResponse{
   750  				"merge --no-ff --no-stat -m my -m happy merge -m message shasum": {
   751  					out: []byte(`ok`),
   752  				},
   753  			},
   754  			expectedCalls: [][]string{
   755  				{"merge", "--no-ff", "--no-stat", "-m", "my", "-m", "happy merge", "-m", "message", "shasum"},
   756  			},
   757  			expectedMerge: true,
   758  			expectedErr:   false,
   759  		},
   760  		{
   761  			name:       "happy squash case",
   762  			commitlike: "shasum",
   763  			strategy:   "squash",
   764  			responses: map[string]execResponse{
   765  				"merge --squash --no-stat shasum": {
   766  					out: []byte(`ok`),
   767  				},
   768  				"commit --no-stat -m merge": {
   769  					out: []byte(`ok`),
   770  				},
   771  			},
   772  			expectedCalls: [][]string{
   773  				{"merge", "--squash", "--no-stat", "shasum"},
   774  				{"commit", "--no-stat", "-m", "merge"},
   775  			},
   776  			expectedMerge: true,
   777  			expectedErr:   false,
   778  		},
   779  		{
   780  			name:          "invalid strategy",
   781  			commitlike:    "shasum",
   782  			strategy:      "whatever",
   783  			responses:     map[string]execResponse{},
   784  			expectedCalls: [][]string{},
   785  			expectedMerge: false,
   786  			expectedErr:   true,
   787  		},
   788  		{
   789  			name:       "merge fails but abort succeeds",
   790  			commitlike: "shasum",
   791  			strategy:   "merge",
   792  			responses: map[string]execResponse{
   793  				"merge --no-ff --no-stat -m merge shasum": {
   794  					err: errors.New("oops"),
   795  				},
   796  				"merge --abort": {
   797  					out: []byte(`ok`),
   798  				},
   799  			},
   800  			expectedCalls: [][]string{
   801  				{"merge", "--no-ff", "--no-stat", "-m", "merge", "shasum"},
   802  				{"merge", "--abort"},
   803  			},
   804  			expectedMerge: false,
   805  			expectedErr:   false,
   806  		},
   807  		{
   808  			name:       "merge fails and abort fails",
   809  			commitlike: "shasum",
   810  			strategy:   "merge",
   811  			responses: map[string]execResponse{
   812  				"merge --no-ff --no-stat -m merge shasum": {
   813  					err: errors.New("oops"),
   814  				},
   815  				"merge --abort": {
   816  					err: errors.New("oops"),
   817  				},
   818  			},
   819  			expectedCalls: [][]string{
   820  				{"merge", "--no-ff", "--no-stat", "-m", "merge", "shasum"},
   821  				{"merge", "--abort"},
   822  			},
   823  			expectedMerge: false,
   824  			expectedErr:   true,
   825  		},
   826  		{
   827  			name:       "squash merge fails but abort succeeds",
   828  			commitlike: "shasum",
   829  			strategy:   "squash",
   830  			responses: map[string]execResponse{
   831  				"merge --squash --no-stat shasum": {
   832  					err: errors.New("oops"),
   833  				},
   834  				"reset --hard HEAD": {
   835  					out: []byte(`ok`),
   836  				},
   837  			},
   838  			expectedCalls: [][]string{
   839  				{"merge", "--squash", "--no-stat", "shasum"},
   840  				{"reset", "--hard", "HEAD"},
   841  			},
   842  			expectedMerge: false,
   843  			expectedErr:   false,
   844  		},
   845  		{
   846  			name:       "squash merge fails and abort fails",
   847  			commitlike: "shasum",
   848  			strategy:   "squash",
   849  			responses: map[string]execResponse{
   850  				"merge --squash --no-stat shasum": {
   851  					err: errors.New("oops"),
   852  				},
   853  				"reset --hard HEAD": {
   854  					err: errors.New("oops"),
   855  				},
   856  			},
   857  			expectedCalls: [][]string{
   858  				{"merge", "--squash", "--no-stat", "shasum"},
   859  				{"reset", "--hard", "HEAD"},
   860  			},
   861  			expectedMerge: false,
   862  			expectedErr:   true,
   863  		},
   864  		{
   865  			name:       "squash merge staging succeeds, commit fails and abort succeeds",
   866  			commitlike: "shasum",
   867  			strategy:   "squash",
   868  			responses: map[string]execResponse{
   869  				"merge --squash --no-stat shasum": {
   870  					out: []byte(`ok`),
   871  				},
   872  				"commit --no-stat -m merge": {
   873  					err: errors.New("oops"),
   874  				},
   875  				"reset --hard HEAD": {
   876  					out: []byte(`ok`),
   877  				},
   878  			},
   879  			expectedCalls: [][]string{
   880  				{"merge", "--squash", "--no-stat", "shasum"},
   881  				{"commit", "--no-stat", "-m", "merge"},
   882  				{"reset", "--hard", "HEAD"},
   883  			},
   884  			expectedMerge: false,
   885  			expectedErr:   false,
   886  		},
   887  		{
   888  			name:       "squash merge staging succeeds, commit fails and abort fails",
   889  			commitlike: "shasum",
   890  			strategy:   "squash",
   891  			responses: map[string]execResponse{
   892  				"merge --squash --no-stat shasum": {
   893  					out: []byte(`ok`),
   894  				},
   895  				"commit --no-stat -m merge": {
   896  					err: errors.New("oops"),
   897  				},
   898  				"reset --hard HEAD": {
   899  					err: errors.New("oops"),
   900  				},
   901  			},
   902  			expectedCalls: [][]string{
   903  				{"merge", "--squash", "--no-stat", "shasum"},
   904  				{"commit", "--no-stat", "-m", "merge"},
   905  				{"reset", "--hard", "HEAD"},
   906  			},
   907  			expectedMerge: false,
   908  			expectedErr:   true,
   909  		},
   910  	}
   911  
   912  	for _, testCase := range testCases {
   913  		t.Run(testCase.name, func(t *testing.T) {
   914  			e := fakeExecutor{
   915  				records:   [][]string{},
   916  				responses: testCase.responses,
   917  			}
   918  			i := interactor{
   919  				executor: &e,
   920  				remote:   testCase.remote,
   921  				logger:   logrus.WithField("test", testCase.name),
   922  			}
   923  			actualMerge, actualErr := i.MergeWithStrategy(testCase.commitlike, testCase.strategy, testCase.opts...)
   924  			if testCase.expectedMerge != actualMerge {
   925  				t.Errorf("%s: got incorrect output: expected %v, got %v", testCase.name, testCase.expectedMerge, actualMerge)
   926  			}
   927  			if testCase.expectedErr && actualErr == nil {
   928  				t.Errorf("%s: expected an error but got none", testCase.name)
   929  			}
   930  			if !testCase.expectedErr && actualErr != nil {
   931  				t.Errorf("%s: expected no error but got one: %v", testCase.name, actualErr)
   932  			}
   933  			if actual, expected := e.records, testCase.expectedCalls; !reflect.DeepEqual(actual, expected) {
   934  				t.Errorf("%s: got incorrect git calls: %v", testCase.name, diff.ObjectReflectDiff(actual, expected))
   935  			}
   936  		})
   937  	}
   938  }
   939  
   940  func TestInteractor_MergeAndCheckout(t *testing.T) {
   941  	var testCases = []struct {
   942  		name          string
   943  		baseSHA       string
   944  		commitlikes   []string
   945  		strategy      string
   946  		remote        RemoteResolver
   947  		responses     map[string]execResponse
   948  		expectedCalls [][]string
   949  		expectedErr   bool
   950  	}{
   951  		{
   952  			name:        "happy do nothing case",
   953  			baseSHA:     "base",
   954  			commitlikes: []string{},
   955  			strategy:    "merge",
   956  			responses: map[string]execResponse{
   957  				"checkout base": {
   958  					out: []byte(`ok`),
   959  				},
   960  			},
   961  			expectedCalls: [][]string{
   962  				{"checkout", "base"},
   963  			},
   964  			expectedErr: false,
   965  		},
   966  		{
   967  			name:        "happy merge case",
   968  			baseSHA:     "base",
   969  			commitlikes: []string{"first", "second"},
   970  			strategy:    "merge",
   971  			responses: map[string]execResponse{
   972  				"checkout base": {
   973  					out: []byte(`ok`),
   974  				},
   975  				"merge --no-ff --no-stat -m merge first": {
   976  					out: []byte(`ok`),
   977  				},
   978  				"merge --no-ff --no-stat -m merge second": {
   979  					out: []byte(`ok`),
   980  				},
   981  			},
   982  			expectedCalls: [][]string{
   983  				{"checkout", "base"},
   984  				{"merge", "--no-ff", "--no-stat", "-m", "merge", "first"},
   985  				{"merge", "--no-ff", "--no-stat", "-m", "merge", "second"},
   986  			},
   987  			expectedErr: false,
   988  		},
   989  		{
   990  			name:        "happy squash case",
   991  			baseSHA:     "base",
   992  			commitlikes: []string{"first", "second"},
   993  			strategy:    "squash",
   994  			responses: map[string]execResponse{
   995  				"checkout base": {
   996  					out: []byte(`ok`),
   997  				},
   998  				"merge --squash --no-stat first": {
   999  					out: []byte(`ok`),
  1000  				},
  1001  				"commit --no-stat -m merge": {
  1002  					out: []byte(`ok`),
  1003  				},
  1004  				"merge --squash --no-stat second": {
  1005  					out: []byte(`ok`),
  1006  				},
  1007  			},
  1008  			expectedCalls: [][]string{
  1009  				{"checkout", "base"},
  1010  				{"merge", "--squash", "--no-stat", "first"},
  1011  				{"commit", "--no-stat", "-m", "merge"},
  1012  				{"merge", "--squash", "--no-stat", "second"},
  1013  				{"commit", "--no-stat", "-m", "merge"},
  1014  			},
  1015  			expectedErr: false,
  1016  		},
  1017  		{
  1018  			name:          "invalid strategy",
  1019  			commitlikes:   []string{"shasum"},
  1020  			strategy:      "whatever",
  1021  			responses:     map[string]execResponse{},
  1022  			expectedCalls: [][]string{},
  1023  			expectedErr:   true,
  1024  		},
  1025  		{
  1026  			name:        "checkout fails",
  1027  			baseSHA:     "base",
  1028  			commitlikes: []string{"first", "second"},
  1029  			strategy:    "squash",
  1030  			responses: map[string]execResponse{
  1031  				"checkout base": {
  1032  					err: errors.New("oops"),
  1033  				},
  1034  			},
  1035  			expectedCalls: [][]string{
  1036  				{"checkout", "base"},
  1037  			},
  1038  			expectedErr: true,
  1039  		},
  1040  		{
  1041  			name:        "merge fails but abort succeeds",
  1042  			baseSHA:     "base",
  1043  			commitlikes: []string{"first", "second"},
  1044  			strategy:    "merge",
  1045  			responses: map[string]execResponse{
  1046  				"checkout base": {
  1047  					out: []byte(`ok`),
  1048  				},
  1049  				"merge --no-ff --no-stat -m merge first": {
  1050  					err: errors.New("oops"),
  1051  				},
  1052  				"merge --abort": {
  1053  					out: []byte(`ok`),
  1054  				},
  1055  			},
  1056  			expectedCalls: [][]string{
  1057  				{"checkout", "base"},
  1058  				{"merge", "--no-ff", "--no-stat", "-m", "merge", "first"},
  1059  				{"merge", "--abort"},
  1060  			},
  1061  			expectedErr: true,
  1062  		},
  1063  		{
  1064  			name:        "merge fails and abort fails",
  1065  			baseSHA:     "base",
  1066  			commitlikes: []string{"first", "second"},
  1067  			strategy:    "merge",
  1068  			responses: map[string]execResponse{
  1069  				"checkout base": {
  1070  					out: []byte(`ok`),
  1071  				},
  1072  				"merge --no-ff --no-stat -m merge first": {
  1073  					err: errors.New("oops"),
  1074  				},
  1075  				"merge --abort": {
  1076  					err: errors.New("oops"),
  1077  				},
  1078  			},
  1079  			expectedCalls: [][]string{
  1080  				{"checkout", "base"},
  1081  				{"merge", "--no-ff", "--no-stat", "-m", "merge", "first"},
  1082  				{"merge", "--abort"},
  1083  			},
  1084  			expectedErr: true,
  1085  		},
  1086  	}
  1087  
  1088  	for _, testCase := range testCases {
  1089  		t.Run(testCase.name, func(t *testing.T) {
  1090  			e := fakeExecutor{
  1091  				records:   [][]string{},
  1092  				responses: testCase.responses,
  1093  			}
  1094  			i := interactor{
  1095  				executor: &e,
  1096  				remote:   testCase.remote,
  1097  				logger:   logrus.WithField("test", testCase.name),
  1098  			}
  1099  			actualErr := i.MergeAndCheckout(testCase.baseSHA, testCase.strategy, testCase.commitlikes...)
  1100  			if testCase.expectedErr && actualErr == nil {
  1101  				t.Errorf("%s: expected an error but got none", testCase.name)
  1102  			}
  1103  			if !testCase.expectedErr && actualErr != nil {
  1104  				t.Errorf("%s: expected no error but got one: %v", testCase.name, actualErr)
  1105  			}
  1106  			if actual, expected := e.records, testCase.expectedCalls; !reflect.DeepEqual(actual, expected) {
  1107  				t.Errorf("%s: got incorrect git calls: %v", testCase.name, diff.ObjectReflectDiff(actual, expected))
  1108  			}
  1109  		})
  1110  	}
  1111  }
  1112  
  1113  func TestInteractor_Am(t *testing.T) {
  1114  	var testCases = []struct {
  1115  		name          string
  1116  		path          string
  1117  		remote        RemoteResolver
  1118  		responses     map[string]execResponse
  1119  		expectedCalls [][]string
  1120  		expectedErr   bool
  1121  	}{
  1122  		{
  1123  			name: "happy case",
  1124  			path: "my/changes.patch",
  1125  			responses: map[string]execResponse{
  1126  				"am --3way my/changes.patch": {
  1127  					out: []byte(`ok`),
  1128  				},
  1129  			},
  1130  			expectedCalls: [][]string{
  1131  				{"am", "--3way", "my/changes.patch"},
  1132  			},
  1133  			expectedErr: false,
  1134  		},
  1135  		{
  1136  			name: "am fails but abort succeeds",
  1137  			path: "my/changes.patch",
  1138  			responses: map[string]execResponse{
  1139  				"am --3way my/changes.patch": {
  1140  					err: errors.New("oops"),
  1141  				},
  1142  				"am --abort": {
  1143  					out: []byte(`ok`),
  1144  				},
  1145  			},
  1146  			expectedCalls: [][]string{
  1147  				{"am", "--3way", "my/changes.patch"},
  1148  				{"am", "--abort"},
  1149  			},
  1150  			expectedErr: true,
  1151  		},
  1152  		{
  1153  			name: "am fails and abort fails",
  1154  			path: "my/changes.patch",
  1155  			responses: map[string]execResponse{
  1156  				"am --3way my/changes.patch": {
  1157  					err: errors.New("oops"),
  1158  				},
  1159  				"am --abort": {
  1160  					err: errors.New("oops"),
  1161  				},
  1162  			},
  1163  			expectedCalls: [][]string{
  1164  				{"am", "--3way", "my/changes.patch"},
  1165  				{"am", "--abort"},
  1166  			},
  1167  			expectedErr: true,
  1168  		},
  1169  	}
  1170  
  1171  	for _, testCase := range testCases {
  1172  		t.Run(testCase.name, func(t *testing.T) {
  1173  			e := fakeExecutor{
  1174  				records:   [][]string{},
  1175  				responses: testCase.responses,
  1176  			}
  1177  			i := interactor{
  1178  				executor: &e,
  1179  				remote:   testCase.remote,
  1180  				logger:   logrus.WithField("test", testCase.name),
  1181  			}
  1182  			actualErr := i.Am(testCase.path)
  1183  			if testCase.expectedErr && actualErr == nil {
  1184  				t.Errorf("%s: expected an error but got none", testCase.name)
  1185  			}
  1186  			if !testCase.expectedErr && actualErr != nil {
  1187  				t.Errorf("%s: expected no error but got one: %v", testCase.name, actualErr)
  1188  			}
  1189  			if actual, expected := e.records, testCase.expectedCalls; !reflect.DeepEqual(actual, expected) {
  1190  				t.Errorf("%s: got incorrect git calls: %v", testCase.name, diff.ObjectReflectDiff(actual, expected))
  1191  			}
  1192  		})
  1193  	}
  1194  }
  1195  
  1196  func TestInteractor_RemoteUpdate(t *testing.T) {
  1197  	var testCases = []struct {
  1198  		name          string
  1199  		remote        RemoteResolver
  1200  		responses     map[string]execResponse
  1201  		expectedCalls [][]string
  1202  		expectedErr   bool
  1203  	}{
  1204  		{
  1205  			name: "happy case",
  1206  			remote: func() (string, error) {
  1207  				return "someone.com", nil
  1208  			},
  1209  			responses: map[string]execResponse{
  1210  				"remote set-url origin someone.com": {
  1211  					out: []byte(`ok`),
  1212  				},
  1213  				"remote update --prune": {
  1214  					out: []byte(`ok`),
  1215  				},
  1216  			},
  1217  			expectedCalls: [][]string{
  1218  				{"remote", "set-url", "origin", "someone.com"},
  1219  				{"remote", "update", "--prune"},
  1220  			},
  1221  			expectedErr: false,
  1222  		},
  1223  		{
  1224  			name: "remote resolution fails",
  1225  			remote: func() (string, error) {
  1226  				return "", errors.New("oops")
  1227  			},
  1228  			responses:     map[string]execResponse{},
  1229  			expectedCalls: [][]string{},
  1230  			expectedErr:   true,
  1231  		},
  1232  		{
  1233  			name: "setting remote URL fails",
  1234  			remote: func() (string, error) {
  1235  				return "someone.com", nil
  1236  			},
  1237  			responses: map[string]execResponse{
  1238  				"remote set-url origin someone.com": {
  1239  					err: errors.New("oops"),
  1240  				},
  1241  			},
  1242  			expectedCalls: [][]string{
  1243  				{"remote", "set-url", "origin", "someone.com"},
  1244  			},
  1245  			expectedErr: true,
  1246  		},
  1247  		{
  1248  			name: "update fails",
  1249  			remote: func() (string, error) {
  1250  				return "someone.com", nil
  1251  			},
  1252  			responses: map[string]execResponse{
  1253  				"remote set-url origin someone.com": {
  1254  					out: []byte(`ok`),
  1255  				},
  1256  				"remote update --prune": {
  1257  					err: errors.New("oops"),
  1258  				},
  1259  			},
  1260  			expectedCalls: [][]string{
  1261  				{"remote", "set-url", "origin", "someone.com"},
  1262  				{"remote", "update", "--prune"},
  1263  			},
  1264  			expectedErr: true,
  1265  		},
  1266  	}
  1267  
  1268  	for _, testCase := range testCases {
  1269  		t.Run(testCase.name, func(t *testing.T) {
  1270  			e := fakeExecutor{
  1271  				records:   [][]string{},
  1272  				responses: testCase.responses,
  1273  			}
  1274  			i := interactor{
  1275  				executor: &e,
  1276  				remote:   testCase.remote,
  1277  				logger:   logrus.WithField("test", testCase.name),
  1278  			}
  1279  			actualErr := i.RemoteUpdate()
  1280  			if testCase.expectedErr && actualErr == nil {
  1281  				t.Errorf("%s: expected an error but got none", testCase.name)
  1282  			}
  1283  			if !testCase.expectedErr && actualErr != nil {
  1284  				t.Errorf("%s: expected no error but got one: %v", testCase.name, actualErr)
  1285  			}
  1286  			if actual, expected := e.records, testCase.expectedCalls; !reflect.DeepEqual(actual, expected) {
  1287  				t.Errorf("%s: got incorrect git calls: %v", testCase.name, diff.ObjectReflectDiff(actual, expected))
  1288  			}
  1289  		})
  1290  	}
  1291  }
  1292  
  1293  func TestInteractor_Fetch(t *testing.T) {
  1294  	var testCases = []struct {
  1295  		name          string
  1296  		remote        RemoteResolver
  1297  		responses     map[string]execResponse
  1298  		extraArgs     []string
  1299  		expectedCalls [][]string
  1300  		expectedErr   bool
  1301  	}{
  1302  		{
  1303  			name: "happy case",
  1304  			remote: func() (string, error) {
  1305  				return "someone.com", nil
  1306  			},
  1307  			responses: map[string]execResponse{
  1308  				"fetch someone.com": {
  1309  					out: []byte(`ok`),
  1310  				},
  1311  			},
  1312  			expectedCalls: [][]string{
  1313  				{"fetch", "someone.com"},
  1314  			},
  1315  			expectedErr: false,
  1316  		},
  1317  		{
  1318  			name: "with arg",
  1319  			remote: func() (string, error) {
  1320  				return "someone.com", nil
  1321  			},
  1322  			responses: map[string]execResponse{
  1323  				"fetch someone.com --prune": {
  1324  					out: []byte(`ok`),
  1325  				},
  1326  			},
  1327  			extraArgs: []string{"--prune"},
  1328  			expectedCalls: [][]string{
  1329  				{"fetch", "someone.com", "--prune"},
  1330  			},
  1331  			expectedErr: false,
  1332  		},
  1333  		{
  1334  			name: "remote resolution fails",
  1335  			remote: func() (string, error) {
  1336  				return "", errors.New("oops")
  1337  			},
  1338  			responses:     map[string]execResponse{},
  1339  			expectedCalls: [][]string{},
  1340  			expectedErr:   true,
  1341  		},
  1342  		{
  1343  			name: "fetch fails",
  1344  			remote: func() (string, error) {
  1345  				return "someone.com", nil
  1346  			},
  1347  			responses: map[string]execResponse{
  1348  				"fetch someone.com": {
  1349  					err: errors.New("oops"),
  1350  				},
  1351  			},
  1352  			expectedCalls: [][]string{
  1353  				{"fetch", "someone.com"},
  1354  			},
  1355  			expectedErr: true,
  1356  		},
  1357  	}
  1358  
  1359  	for _, testCase := range testCases {
  1360  		t.Run(testCase.name, func(t *testing.T) {
  1361  			e := fakeExecutor{
  1362  				records:   [][]string{},
  1363  				responses: testCase.responses,
  1364  			}
  1365  			i := interactor{
  1366  				executor: &e,
  1367  				remote:   testCase.remote,
  1368  				logger:   logrus.WithField("test", testCase.name),
  1369  			}
  1370  			actualErr := i.Fetch(testCase.extraArgs...)
  1371  			if testCase.expectedErr && actualErr == nil {
  1372  				t.Errorf("%s: expected an error but got none", testCase.name)
  1373  			}
  1374  			if !testCase.expectedErr && actualErr != nil {
  1375  				t.Errorf("%s: expected no error but got one: %v", testCase.name, actualErr)
  1376  			}
  1377  			if actual, expected := e.records, testCase.expectedCalls; !reflect.DeepEqual(actual, expected) {
  1378  				t.Errorf("%s: got incorrect git calls: %v", testCase.name, diff.ObjectReflectDiff(actual, expected))
  1379  			}
  1380  		})
  1381  	}
  1382  }
  1383  
  1384  func TestInteractor_FetchRef(t *testing.T) {
  1385  	var testCases = []struct {
  1386  		name          string
  1387  		refspec       string
  1388  		remote        RemoteResolver
  1389  		responses     map[string]execResponse
  1390  		expectedCalls [][]string
  1391  		expectedErr   bool
  1392  	}{
  1393  		{
  1394  			name:    "happy case",
  1395  			refspec: "shasum",
  1396  			remote: func() (string, error) {
  1397  				return "someone.com", nil
  1398  			},
  1399  			responses: map[string]execResponse{
  1400  				"fetch someone.com shasum": {
  1401  					out: []byte(`ok`),
  1402  				},
  1403  			},
  1404  			expectedCalls: [][]string{
  1405  				{"fetch", "someone.com", "shasum"},
  1406  			},
  1407  			expectedErr: false,
  1408  		},
  1409  		{
  1410  			name:    "remote resolution fails",
  1411  			refspec: "shasum",
  1412  			remote: func() (string, error) {
  1413  				return "", errors.New("oops")
  1414  			},
  1415  			responses:     map[string]execResponse{},
  1416  			expectedCalls: [][]string{},
  1417  			expectedErr:   true,
  1418  		},
  1419  		{
  1420  			name:    "fetch fails",
  1421  			refspec: "shasum",
  1422  			remote: func() (string, error) {
  1423  				return "someone.com", nil
  1424  			},
  1425  			responses: map[string]execResponse{
  1426  				"fetch someone.com shasum": {
  1427  					err: errors.New("oops"),
  1428  				},
  1429  			},
  1430  			expectedCalls: [][]string{
  1431  				{"fetch", "someone.com", "shasum"},
  1432  			},
  1433  			expectedErr: true,
  1434  		},
  1435  	}
  1436  
  1437  	for _, testCase := range testCases {
  1438  		t.Run(testCase.name, func(t *testing.T) {
  1439  			e := fakeExecutor{
  1440  				records:   [][]string{},
  1441  				responses: testCase.responses,
  1442  			}
  1443  			i := interactor{
  1444  				executor: &e,
  1445  				remote:   testCase.remote,
  1446  				logger:   logrus.WithField("test", testCase.name),
  1447  			}
  1448  			actualErr := i.FetchRef(testCase.refspec)
  1449  			if testCase.expectedErr && actualErr == nil {
  1450  				t.Errorf("%s: expected an error but got none", testCase.name)
  1451  			}
  1452  			if !testCase.expectedErr && actualErr != nil {
  1453  				t.Errorf("%s: expected no error but got one: %v", testCase.name, actualErr)
  1454  			}
  1455  			if actual, expected := e.records, testCase.expectedCalls; !reflect.DeepEqual(actual, expected) {
  1456  				t.Errorf("%s: got incorrect git calls: %v", testCase.name, diff.ObjectReflectDiff(actual, expected))
  1457  			}
  1458  		})
  1459  	}
  1460  }
  1461  
  1462  func TestInteractor_FetchFromRemote(t *testing.T) {
  1463  	var testCases = []struct {
  1464  		name          string
  1465  		remote        RemoteResolver
  1466  		toRemote      RemoteResolver
  1467  		branch        string
  1468  		responses     map[string]execResponse
  1469  		expectedCalls [][]string
  1470  		expectedErr   bool
  1471  	}{
  1472  		{
  1473  			name: "fetch from different remote without token",
  1474  			remote: func() (string, error) {
  1475  				return "someone.com", nil
  1476  			},
  1477  			toRemote: func() (string, error) {
  1478  				return "https://github.com/kubernetes/test-infra-fork", nil
  1479  			},
  1480  			branch: "test-branch",
  1481  			responses: map[string]execResponse{
  1482  				"fetch https://github.com/kubernetes/test-infra-fork test-branch": {
  1483  					out: []byte(`ok`),
  1484  				},
  1485  			},
  1486  			expectedCalls: [][]string{
  1487  				{"fetch", "https://github.com/kubernetes/test-infra-fork", "test-branch"},
  1488  			},
  1489  			expectedErr: false,
  1490  		},
  1491  		{
  1492  			name: "fetch from different remote with token",
  1493  			remote: func() (string, error) {
  1494  				return "someone.com", nil
  1495  			},
  1496  			toRemote: func() (string, error) {
  1497  				return "https://user:pass@github.com/kubernetes/test-infra-fork", nil
  1498  			},
  1499  			branch: "test-branch",
  1500  			responses: map[string]execResponse{
  1501  				"fetch https://user:pass@github.com/kubernetes/test-infra-fork test-branch": {
  1502  					out: []byte(`ok`),
  1503  				},
  1504  			},
  1505  			expectedCalls: [][]string{
  1506  				{"fetch", "https://user:pass@github.com/kubernetes/test-infra-fork", "test-branch"},
  1507  			},
  1508  			expectedErr: false,
  1509  		},
  1510  		{
  1511  			name: "passing non-valid remote",
  1512  			remote: func() (string, error) {
  1513  				return "someone.com", nil
  1514  			},
  1515  			toRemote: func() (string, error) {
  1516  				return "", fmt.Errorf("non-valid URL")
  1517  			},
  1518  			branch:        "test-branch",
  1519  			expectedCalls: [][]string{},
  1520  			expectedErr:   true,
  1521  		},
  1522  	}
  1523  
  1524  	for _, testCase := range testCases {
  1525  		t.Run(testCase.name, func(t *testing.T) {
  1526  			e := fakeExecutor{
  1527  				records:   [][]string{},
  1528  				responses: testCase.responses,
  1529  			}
  1530  			i := interactor{
  1531  				executor: &e,
  1532  				remote:   testCase.remote,
  1533  				logger:   logrus.WithField("test", testCase.name),
  1534  			}
  1535  
  1536  			actualErr := i.FetchFromRemote(testCase.toRemote, testCase.branch)
  1537  			if testCase.expectedErr && actualErr == nil {
  1538  				t.Errorf("%s: expected an error but got none", testCase.name)
  1539  			}
  1540  			if !testCase.expectedErr && actualErr != nil {
  1541  				t.Errorf("%s: expected no error but got one: %v", testCase.name, actualErr)
  1542  			}
  1543  			if actual, expected := e.records, testCase.expectedCalls; !reflect.DeepEqual(actual, expected) {
  1544  				t.Errorf("%s: got incorrect git calls: %v", testCase.name, diff.ObjectReflectDiff(actual, expected))
  1545  			}
  1546  		})
  1547  	}
  1548  }
  1549  
  1550  func TestInteractor_CheckoutPullRequest(t *testing.T) {
  1551  	var testCases = []struct {
  1552  		name          string
  1553  		number        int
  1554  		remote        RemoteResolver
  1555  		responses     map[string]execResponse
  1556  		expectedCalls [][]string
  1557  		expectedErr   bool
  1558  	}{
  1559  		{
  1560  			name:   "happy case",
  1561  			number: 1,
  1562  			remote: func() (string, error) {
  1563  				return "someone.com", nil
  1564  			},
  1565  			responses: map[string]execResponse{
  1566  				"fetch someone.com pull/1/head": {
  1567  					out: []byte(`ok`),
  1568  				},
  1569  				"checkout FETCH_HEAD": {
  1570  					out: []byte(`ok`),
  1571  				},
  1572  				"checkout -b pull1": {
  1573  					out: []byte(`ok`),
  1574  				},
  1575  			},
  1576  			expectedCalls: [][]string{
  1577  				{"fetch", "someone.com", "pull/1/head"},
  1578  				{"checkout", "FETCH_HEAD"},
  1579  				{"checkout", "-b", "pull1"},
  1580  			},
  1581  			expectedErr: false,
  1582  		},
  1583  		{
  1584  			name:   "remote resolution fails",
  1585  			number: 1,
  1586  			remote: func() (string, error) {
  1587  				return "", errors.New("oops")
  1588  			},
  1589  			responses:     map[string]execResponse{},
  1590  			expectedCalls: [][]string{},
  1591  			expectedErr:   true,
  1592  		},
  1593  		{
  1594  			name:   "fetch fails",
  1595  			number: 1,
  1596  			remote: func() (string, error) {
  1597  				return "someone.com", nil
  1598  			},
  1599  			responses: map[string]execResponse{
  1600  				"fetch someone.com pull/1/head": {
  1601  					err: errors.New("oops"),
  1602  				},
  1603  			},
  1604  			expectedCalls: [][]string{
  1605  				{"fetch", "someone.com", "pull/1/head"},
  1606  			},
  1607  			expectedErr: true,
  1608  		},
  1609  		{
  1610  			name:   "checkout fails",
  1611  			number: 1,
  1612  			remote: func() (string, error) {
  1613  				return "someone.com", nil
  1614  			},
  1615  			responses: map[string]execResponse{
  1616  				"fetch someone.com pull/1/head": {
  1617  					out: []byte(`ok`),
  1618  				},
  1619  				"checkout FETCH_HEAD": {
  1620  					err: errors.New("oops"),
  1621  				},
  1622  			},
  1623  			expectedCalls: [][]string{
  1624  				{"fetch", "someone.com", "pull/1/head"},
  1625  				{"checkout", "FETCH_HEAD"},
  1626  			},
  1627  			expectedErr: true,
  1628  		},
  1629  		{
  1630  			name:   "branch fails",
  1631  			number: 1,
  1632  			remote: func() (string, error) {
  1633  				return "someone.com", nil
  1634  			},
  1635  			responses: map[string]execResponse{
  1636  				"fetch someone.com pull/1/head": {
  1637  					out: []byte(`ok`),
  1638  				},
  1639  				"checkout FETCH_HEAD": {
  1640  					out: []byte(`ok`),
  1641  				},
  1642  				"checkout -b pull1": {
  1643  					err: errors.New("oops"),
  1644  				},
  1645  			},
  1646  			expectedCalls: [][]string{
  1647  				{"fetch", "someone.com", "pull/1/head"},
  1648  				{"checkout", "FETCH_HEAD"},
  1649  				{"checkout", "-b", "pull1"},
  1650  			},
  1651  			expectedErr: true,
  1652  		},
  1653  	}
  1654  
  1655  	for _, testCase := range testCases {
  1656  		t.Run(testCase.name, func(t *testing.T) {
  1657  			e := fakeExecutor{
  1658  				records:   [][]string{},
  1659  				responses: testCase.responses,
  1660  			}
  1661  			i := interactor{
  1662  				executor: &e,
  1663  				remote:   testCase.remote,
  1664  				logger:   logrus.WithField("test", testCase.name),
  1665  			}
  1666  			actualErr := i.CheckoutPullRequest(testCase.number)
  1667  			if testCase.expectedErr && actualErr == nil {
  1668  				t.Errorf("%s: expected an error but got none", testCase.name)
  1669  			}
  1670  			if !testCase.expectedErr && actualErr != nil {
  1671  				t.Errorf("%s: expected no error but got one: %v", testCase.name, actualErr)
  1672  			}
  1673  			if actual, expected := e.records, testCase.expectedCalls; !reflect.DeepEqual(actual, expected) {
  1674  				t.Errorf("%s: got incorrect git calls: %v", testCase.name, diff.ObjectReflectDiff(actual, expected))
  1675  			}
  1676  		})
  1677  	}
  1678  }
  1679  
  1680  func TestInteractor_Config(t *testing.T) {
  1681  	var testCases = []struct {
  1682  		name          string
  1683  		key, value    string
  1684  		remote        RemoteResolver
  1685  		responses     map[string]execResponse
  1686  		expectedCalls [][]string
  1687  		expectedErr   bool
  1688  	}{
  1689  		{
  1690  			name:  "happy case",
  1691  			key:   "key",
  1692  			value: "value",
  1693  			responses: map[string]execResponse{
  1694  				"config key value": {
  1695  					out: []byte(`ok`),
  1696  				},
  1697  			},
  1698  			expectedCalls: [][]string{
  1699  				{"config", "key", "value"},
  1700  			},
  1701  			expectedErr: false,
  1702  		},
  1703  		{
  1704  			name:  "config fails",
  1705  			key:   "key",
  1706  			value: "value",
  1707  			responses: map[string]execResponse{
  1708  				"config key value": {
  1709  					err: errors.New("oops"),
  1710  				},
  1711  			},
  1712  			expectedCalls: [][]string{
  1713  				{"config", "key", "value"},
  1714  			},
  1715  			expectedErr: true,
  1716  		},
  1717  	}
  1718  
  1719  	for _, testCase := range testCases {
  1720  		t.Run(testCase.name, func(t *testing.T) {
  1721  			e := fakeExecutor{
  1722  				records:   [][]string{},
  1723  				responses: testCase.responses,
  1724  			}
  1725  			i := interactor{
  1726  				executor: &e,
  1727  				remote:   testCase.remote,
  1728  				logger:   logrus.WithField("test", testCase.name),
  1729  			}
  1730  			actualErr := i.Config(testCase.key, testCase.value)
  1731  			if testCase.expectedErr && actualErr == nil {
  1732  				t.Errorf("%s: expected an error but got none", testCase.name)
  1733  			}
  1734  			if !testCase.expectedErr && actualErr != nil {
  1735  				t.Errorf("%s: expected no error but got one: %v", testCase.name, actualErr)
  1736  			}
  1737  			if actual, expected := e.records, testCase.expectedCalls; !reflect.DeepEqual(actual, expected) {
  1738  				t.Errorf("%s: got incorrect git calls: %v", testCase.name, diff.ObjectReflectDiff(actual, expected))
  1739  			}
  1740  		})
  1741  	}
  1742  }
  1743  
  1744  func TestInteractor_Diff(t *testing.T) {
  1745  	var testCases = []struct {
  1746  		name          string
  1747  		head, sha     string
  1748  		remote        RemoteResolver
  1749  		responses     map[string]execResponse
  1750  		expectedCalls [][]string
  1751  		expectedOut   []string
  1752  		expectedErr   bool
  1753  	}{
  1754  		{
  1755  			name: "happy case",
  1756  			head: "head",
  1757  			sha:  "sha",
  1758  			responses: map[string]execResponse{
  1759  				"diff head sha --name-only": {
  1760  					out: []byte(`prow/git/v2/client_factory.go
  1761  prow/git/v2/executor.go
  1762  prow/git/v2/executor_test.go
  1763  prow/git/v2/fakes.go
  1764  prow/git/v2/interactor.go
  1765  prow/git/v2/publisher.go
  1766  prow/git/v2/publisher_test.go
  1767  prow/git/v2/remote.go
  1768  prow/git/v2/remote_test.go`),
  1769  				},
  1770  			},
  1771  			expectedCalls: [][]string{
  1772  				{"diff", "head", "sha", "--name-only"},
  1773  			},
  1774  			expectedOut: []string{
  1775  				"prow/git/v2/client_factory.go",
  1776  				"prow/git/v2/executor.go",
  1777  				"prow/git/v2/executor_test.go",
  1778  				"prow/git/v2/fakes.go",
  1779  				"prow/git/v2/interactor.go",
  1780  				"prow/git/v2/publisher.go",
  1781  				"prow/git/v2/publisher_test.go",
  1782  				"prow/git/v2/remote.go",
  1783  				"prow/git/v2/remote_test.go",
  1784  			},
  1785  			expectedErr: false,
  1786  		},
  1787  		{
  1788  			name: "config fails",
  1789  			head: "head",
  1790  			sha:  "sha",
  1791  			responses: map[string]execResponse{
  1792  				"diff head sha --name-only": {
  1793  					err: errors.New("oops"),
  1794  				},
  1795  			},
  1796  			expectedCalls: [][]string{
  1797  				{"diff", "head", "sha", "--name-only"},
  1798  			},
  1799  			expectedErr: true,
  1800  		},
  1801  	}
  1802  
  1803  	for _, testCase := range testCases {
  1804  		t.Run(testCase.name, func(t *testing.T) {
  1805  			e := fakeExecutor{
  1806  				records:   [][]string{},
  1807  				responses: testCase.responses,
  1808  			}
  1809  			i := interactor{
  1810  				executor: &e,
  1811  				remote:   testCase.remote,
  1812  				logger:   logrus.WithField("test", testCase.name),
  1813  			}
  1814  			actualOut, actualErr := i.Diff(testCase.head, testCase.sha)
  1815  			if !reflect.DeepEqual(actualOut, testCase.expectedOut) {
  1816  				t.Errorf("%s: got incorrect output: %v", testCase.name, diff.ObjectReflectDiff(actualOut, testCase.expectedOut))
  1817  			}
  1818  			if testCase.expectedErr && actualErr == nil {
  1819  				t.Errorf("%s: expected an error but got none", testCase.name)
  1820  			}
  1821  			if !testCase.expectedErr && actualErr != nil {
  1822  				t.Errorf("%s: expected no error but got one: %v", testCase.name, actualErr)
  1823  			}
  1824  			if actual, expected := e.records, testCase.expectedCalls; !reflect.DeepEqual(actual, expected) {
  1825  				t.Errorf("%s: got incorrect git calls: %v", testCase.name, diff.ObjectReflectDiff(actual, expected))
  1826  			}
  1827  		})
  1828  	}
  1829  }
  1830  
  1831  func TestInteractor_MergeCommitsExistBetween(t *testing.T) {
  1832  	var testCases = []struct {
  1833  		name          string
  1834  		target, head  string
  1835  		responses     map[string]execResponse
  1836  		expectedCalls [][]string
  1837  		expectedOut   bool
  1838  		expectedErr   bool
  1839  	}{
  1840  		{
  1841  			name:   "happy case and merges exist",
  1842  			target: "target",
  1843  			head:   "head",
  1844  			responses: map[string]execResponse{
  1845  				"log target..head --oneline --merges": {
  1846  					out: []byte(`8df5654e6 Merge pull request #14911 from mborsz/etcd
  1847  96cbeee23 Merge pull request #14755 from justinsb/the_life_changing_magic_of_tidying_up`),
  1848  				},
  1849  			},
  1850  			expectedCalls: [][]string{
  1851  				{"log", "target..head", "--oneline", "--merges"},
  1852  			},
  1853  			expectedOut: true,
  1854  			expectedErr: false,
  1855  		},
  1856  		{
  1857  			name:   "happy case and merges don't exist",
  1858  			target: "target",
  1859  			head:   "head",
  1860  			responses: map[string]execResponse{
  1861  				"log target..head --oneline --merges": {
  1862  					out: []byte(``),
  1863  				},
  1864  			},
  1865  			expectedCalls: [][]string{
  1866  				{"log", "target..head", "--oneline", "--merges"},
  1867  			},
  1868  			expectedOut: false,
  1869  			expectedErr: false,
  1870  		},
  1871  		{
  1872  			name:   "log fails",
  1873  			target: "target",
  1874  			head:   "head",
  1875  			responses: map[string]execResponse{
  1876  				"log target..head --oneline --merges": {
  1877  					err: errors.New("oops"),
  1878  				},
  1879  			},
  1880  			expectedCalls: [][]string{
  1881  				{"log", "target..head", "--oneline", "--merges"},
  1882  			},
  1883  			expectedOut: false,
  1884  			expectedErr: true,
  1885  		},
  1886  	}
  1887  
  1888  	for _, testCase := range testCases {
  1889  		t.Run(testCase.name, func(t *testing.T) {
  1890  			e := fakeExecutor{
  1891  				records:   [][]string{},
  1892  				responses: testCase.responses,
  1893  			}
  1894  			i := interactor{
  1895  				executor: &e,
  1896  				logger:   logrus.WithField("test", testCase.name),
  1897  			}
  1898  			actualOut, actualErr := i.MergeCommitsExistBetween(testCase.target, testCase.head)
  1899  			if testCase.expectedOut != actualOut {
  1900  				t.Errorf("%s: got incorrect output: expected %v, got %v", testCase.name, testCase.expectedOut, actualOut)
  1901  			}
  1902  			if testCase.expectedErr && actualErr == nil {
  1903  				t.Errorf("%s: expected an error but got none", testCase.name)
  1904  			}
  1905  			if !testCase.expectedErr && actualErr != nil {
  1906  				t.Errorf("%s: expected no error but got one: %v", testCase.name, actualErr)
  1907  			}
  1908  			if actual, expected := e.records, testCase.expectedCalls; !reflect.DeepEqual(actual, expected) {
  1909  				t.Errorf("%s: got incorrect git calls: %v", testCase.name, diff.ObjectReflectDiff(actual, expected))
  1910  			}
  1911  		})
  1912  	}
  1913  }
  1914  
  1915  func TestInteractor_ShowRef(t *testing.T) {
  1916  	const target = "some-branch"
  1917  	var testCases = []struct {
  1918  		name          string
  1919  		responses     map[string]execResponse
  1920  		expectedCalls [][]string
  1921  		expectedErr   bool
  1922  	}{
  1923  		{
  1924  			name: "happy case",
  1925  			responses: map[string]execResponse{
  1926  				"show-ref -s some-branch": {out: []byte("32d3f5a6826109c625527f18a59f2e7144a330b6\n")},
  1927  			},
  1928  			expectedCalls: [][]string{
  1929  				{"show-ref", "-s", target},
  1930  			},
  1931  			expectedErr: false,
  1932  		},
  1933  		{
  1934  			name: "unhappy case",
  1935  			responses: map[string]execResponse{
  1936  				"git show-ref -s some-undef-branch": {err: errors.New("some-err")},
  1937  			},
  1938  			expectedCalls: [][]string{
  1939  				{"show-ref", "-s", target},
  1940  			},
  1941  			expectedErr: true,
  1942  		},
  1943  	}
  1944  	for _, testCase := range testCases {
  1945  		t.Run(testCase.name, func(t *testing.T) {
  1946  			e := fakeExecutor{
  1947  				records:   [][]string{},
  1948  				responses: testCase.responses,
  1949  			}
  1950  			i := interactor{
  1951  				executor: &e,
  1952  				logger:   logrus.WithField("test", testCase.name),
  1953  			}
  1954  			_, actualErr := i.ShowRef(target)
  1955  			if testCase.expectedErr && actualErr == nil {
  1956  				t.Errorf("%s: expected an error but got none", testCase.name)
  1957  			}
  1958  			if !testCase.expectedErr && actualErr != nil {
  1959  				t.Errorf("%s: expected no error but got one: %v", testCase.name, actualErr)
  1960  			}
  1961  			if actual, expected := e.records, testCase.expectedCalls; !reflect.DeepEqual(actual, expected) {
  1962  				t.Errorf("%s: got incorrect git calls: %v", testCase.name, diff.ObjectReflectDiff(actual, expected))
  1963  			}
  1964  		})
  1965  	}
  1966  }