github.com/freiheit-com/kuberpult@v1.24.2-0.20240328135542-315d5630abe6/services/cd-service/pkg/repository/repository_test.go (about)

     1  /*This file is part of kuberpult.
     2  
     3  Kuberpult is free software: you can redistribute it and/or modify
     4  it under the terms of the Expat(MIT) License as published by
     5  the Free Software Foundation.
     6  
     7  Kuberpult is distributed in the hope that it will be useful,
     8  but WITHOUT ANY WARRANTY; without even the implied warranty of
     9  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    10  MIT License for more details.
    11  
    12  You should have received a copy of the MIT License
    13  along with kuberpult. If not, see <https://directory.fsf.org/wiki/License:Expat>.
    14  
    15  Copyright 2023 freiheit.com*/
    16  
    17  package repository
    18  
    19  import (
    20  	"context"
    21  	"encoding/json"
    22  	"errors"
    23  	"fmt"
    24  	"github.com/freiheit-com/kuberpult/services/cd-service/pkg/config"
    25  	"io"
    26  	"io/fs"
    27  	"net/http"
    28  	"net/http/httptest"
    29  	"os"
    30  	"os/exec"
    31  	"path"
    32  	"path/filepath"
    33  	"strings"
    34  	"testing"
    35  	"time"
    36  
    37  	"github.com/freiheit-com/kuberpult/services/cd-service/pkg/repository/testutil"
    38  	"go.uber.org/zap"
    39  	"golang.org/x/sync/errgroup"
    40  
    41  	"github.com/freiheit-com/kuberpult/pkg/setup"
    42  
    43  	api "github.com/freiheit-com/kuberpult/pkg/api/v1"
    44  	"github.com/freiheit-com/kuberpult/services/cd-service/pkg/repository/testssh"
    45  
    46  	"github.com/cenkalti/backoff/v4"
    47  	"github.com/go-git/go-billy/v5/util"
    48  	"github.com/google/go-cmp/cmp"
    49  	"github.com/google/go-cmp/cmp/cmpopts"
    50  	git "github.com/libgit2/git2go/v34"
    51  	"google.golang.org/grpc/codes"
    52  	"google.golang.org/grpc/status"
    53  )
    54  
    55  // Used to compare two error message strings, needed because errors.Is(fmt.Errorf(text),fmt.Errorf(text)) == false
    56  type errMatcher struct {
    57  	msg string
    58  }
    59  
    60  func (e errMatcher) Error() string {
    61  	return e.msg
    62  }
    63  
    64  func (e errMatcher) Is(err error) bool {
    65  	return e.Error() == err.Error()
    66  }
    67  
    68  func TestNew(t *testing.T) {
    69  	tcs := []struct {
    70  		Name   string
    71  		Branch string
    72  		Setup  func(t *testing.T, remoteDir, localDir string)
    73  		Test   func(t *testing.T, repo Repository, remoteDir string)
    74  	}{
    75  		{
    76  			Name:  "new in empty directory works",
    77  			Setup: func(_ *testing.T, _, _ string) {},
    78  		},
    79  		{
    80  			Name: "new in initialized repository works",
    81  			Setup: func(t *testing.T, remoteDir, localDir string) {
    82  				// run the initialization code once
    83  				_, err := New(
    84  					testutil.MakeTestContext(),
    85  					RepositoryConfig{
    86  						URL:  "file://" + remoteDir,
    87  						Path: localDir,
    88  					},
    89  				)
    90  				if err != nil {
    91  					t.Fatal(err)
    92  				}
    93  			},
    94  			Test: func(t *testing.T, repo Repository, remoteDir string) {
    95  				state := repo.State()
    96  				entries, err := state.Filesystem.ReadDir("")
    97  				if err != nil {
    98  					t.Fatal(err)
    99  				}
   100  				if len(entries) > 0 {
   101  					t.Errorf("repository is not empty but contains %d entries", len(entries))
   102  				}
   103  			},
   104  		},
   105  		{
   106  			Name: "new in initialized repository with data works",
   107  			Setup: func(t *testing.T, remoteDir, localDir string) {
   108  				// run the initialization code once
   109  				repo, err := New(
   110  					testutil.MakeTestContext(),
   111  					RepositoryConfig{
   112  						URL:  remoteDir,
   113  						Path: localDir,
   114  					},
   115  				)
   116  				if err != nil {
   117  					t.Fatal(err)
   118  				}
   119  				err = repo.Apply(testutil.MakeTestContext(), &CreateApplicationVersion{
   120  					Application: "foo",
   121  					Manifests: map[string]string{
   122  						"development": "foo",
   123  					},
   124  				})
   125  				if err != nil {
   126  					t.Fatal(err)
   127  				}
   128  			},
   129  			Test: func(t *testing.T, repo Repository, remoteDir string) {
   130  				state := repo.State()
   131  				entries, err := state.Filesystem.ReadDir("applications/foo/releases")
   132  				if err != nil {
   133  					t.Fatal(err)
   134  				}
   135  				if len(entries) != 1 {
   136  					t.Errorf("applications/foo/releases doesn't contain 1 but %d entries", len(entries))
   137  				}
   138  			},
   139  		},
   140  		{
   141  			Name: "new with empty repository but non-empty remote works",
   142  			Setup: func(t *testing.T, remoteDir, localDir string) {
   143  				// run the initialization code once
   144  				repo, err := New(
   145  					testutil.MakeTestContext(),
   146  					RepositoryConfig{
   147  						URL:  remoteDir,
   148  						Path: t.TempDir(),
   149  					},
   150  				)
   151  				if err != nil {
   152  					t.Fatal(err)
   153  				}
   154  				err = repo.Apply(testutil.MakeTestContext(), &CreateApplicationVersion{
   155  					Application: "foo",
   156  					Manifests: map[string]string{
   157  						"development": "foo",
   158  					},
   159  				})
   160  				if err != nil {
   161  					t.Fatal(err)
   162  				}
   163  			},
   164  			Test: func(t *testing.T, repo Repository, remoteDir string) {
   165  				state := repo.State()
   166  				entries, err := state.Filesystem.ReadDir("applications/foo/releases")
   167  				if err != nil {
   168  					t.Fatal(err)
   169  				}
   170  				if len(entries) != 1 {
   171  					t.Errorf("applications/foo/releases doesn't contain 1 but %d entries", len(entries))
   172  				}
   173  			},
   174  		},
   175  		{
   176  			Name:   "new with changed branch works",
   177  			Branch: "not-master",
   178  			Setup:  func(t *testing.T, remoteDir, localDir string) {},
   179  			Test: func(t *testing.T, repo Repository, remoteDir string) {
   180  				err := repo.Apply(testutil.MakeTestContext(), &CreateApplicationVersion{
   181  					Application: "foo",
   182  					Manifests: map[string]string{
   183  						"development": "foo",
   184  					},
   185  				})
   186  				if err != nil {
   187  					t.Fatal(err)
   188  				}
   189  				cmd := exec.Command("git", "--git-dir="+remoteDir, "rev-parse", "not-master")
   190  				out, err := cmd.Output()
   191  				if err != nil {
   192  					if exitErr, ok := err.(*exec.ExitError); ok {
   193  						t.Logf("stderr: %s\n", exitErr.Stderr)
   194  					}
   195  					t.Fatal(err)
   196  				}
   197  				state := repo.State()
   198  				localRev := state.Commit.Id().String()
   199  				if diff := cmp.Diff(localRev, strings.TrimSpace(string(out))); diff != "" {
   200  					t.Errorf("mismatched revision (-want, +got):\n%s", diff)
   201  				}
   202  			},
   203  		},
   204  		{
   205  			Name:   "old with changed branch works",
   206  			Branch: "master",
   207  			Setup:  func(t *testing.T, remoteDir, localDir string) {},
   208  			Test: func(t *testing.T, repo Repository, remoteDir string) {
   209  				workdir := t.TempDir()
   210  				cmd := exec.Command("git", "clone", remoteDir, workdir) // Clone git dir
   211  				out, err := cmd.Output()
   212  				if err != nil {
   213  					if exitErr, ok := err.(*exec.ExitError); ok {
   214  						t.Logf("stderr: %s\n", exitErr.Stderr)
   215  					}
   216  					t.Fatal(err)
   217  				}
   218  
   219  				if err := os.WriteFile(filepath.Join(workdir, "hello.txt"), []byte("hello"), 0666); err != nil {
   220  					t.Fatal(err)
   221  				}
   222  				cmd = exec.Command("git", "add", "hello.txt") // Add a new file to git
   223  				cmd.Dir = workdir
   224  				out, err = cmd.Output()
   225  				if err != nil {
   226  					if exitErr, ok := err.(*exec.ExitError); ok {
   227  						t.Logf("stderr: %s\n", exitErr.Stderr)
   228  					}
   229  					t.Fatal(err)
   230  				}
   231  				cmd = exec.Command("git", "commit", "-m", "new-file") // commit the new file
   232  				cmd.Dir = workdir
   233  				cmd.Env = []string{
   234  					"GIT_AUTHOR_NAME=kuberpult",
   235  					"GIT_COMMITTER_NAME=kuberpult",
   236  					"EMAIL=test@kuberpult.com",
   237  				}
   238  				out, err = cmd.Output()
   239  				if err != nil {
   240  					if exitErr, ok := err.(*exec.ExitError); ok {
   241  						t.Logf("stderr: %s\n", exitErr.Stderr)
   242  					}
   243  					t.Fatal(err)
   244  				}
   245  				cmd = exec.Command("git", "push", "origin", "HEAD") // push the new commit
   246  				cmd.Dir = workdir
   247  				out, err = cmd.Output()
   248  				if err != nil {
   249  					if exitErr, ok := err.(*exec.ExitError); ok {
   250  						t.Logf("stderr: %s\n", exitErr.Stderr)
   251  					}
   252  					t.Fatal(err)
   253  				}
   254  				err = repo.Apply(testutil.MakeTestContext(), &CreateApplicationVersion{
   255  					Application: "foo",
   256  					Manifests: map[string]string{
   257  						"development": "foo",
   258  					},
   259  				})
   260  				if err != nil {
   261  					t.Fatal(err)
   262  				}
   263  				cmd = exec.Command("git", "--git-dir="+remoteDir, "rev-parse", "master")
   264  				out, err = cmd.Output()
   265  				if err != nil {
   266  					if exitErr, ok := err.(*exec.ExitError); ok {
   267  						t.Logf("stderr: %s\n", exitErr.Stderr)
   268  					}
   269  					t.Fatal(err)
   270  				}
   271  				state := repo.State()
   272  				localRev := state.Commit.Id().String()
   273  				if diff := cmp.Diff(localRev, strings.TrimSpace(string(out))); diff != "" {
   274  					t.Errorf("mismatched revision (-want, +got):\n%s", diff)
   275  				}
   276  
   277  				content, err := util.ReadFile(state.Filesystem, "hello.txt")
   278  				if err != nil {
   279  					t.Fatal(err)
   280  				}
   281  				if diff := cmp.Diff("hello", string(content)); diff != "" {
   282  					t.Errorf("mismatched file content (-want, +got):\n%s", diff)
   283  				}
   284  			},
   285  		},
   286  	}
   287  	for _, tc := range tcs {
   288  		tc := tc
   289  		t.Run(tc.Name, func(t *testing.T) {
   290  			t.Parallel()
   291  			// create a remote
   292  			dir := t.TempDir()
   293  			remoteDir := path.Join(dir, "remote")
   294  			localDir := path.Join(dir, "local")
   295  			cmd := exec.Command("git", "init", "--bare", remoteDir)
   296  			cmd.Start()
   297  			cmd.Wait()
   298  			tc.Setup(t, remoteDir, localDir)
   299  			repo, err := New(
   300  				testutil.MakeTestContext(),
   301  				RepositoryConfig{
   302  					URL:    "file://" + remoteDir,
   303  					Path:   localDir,
   304  					Branch: tc.Branch,
   305  				},
   306  			)
   307  			if err != nil {
   308  				t.Fatalf("new: expected no error, got '%e'", err)
   309  			}
   310  			if tc.Test != nil {
   311  				tc.Test(t, repo, remoteDir)
   312  			}
   313  		})
   314  	}
   315  }
   316  
   317  func TestGetTagsNoTags(t *testing.T) {
   318  	name := "No tags to be returned at all"
   319  
   320  	t.Run(name, func(t *testing.T) {
   321  		t.Parallel()
   322  		dir := t.TempDir()
   323  		remoteDir := path.Join(dir, "remote")
   324  		localDir := path.Join(dir, "local")
   325  		repoConfig := RepositoryConfig{
   326  			StorageBackend: 0,
   327  			URL:            "file://" + remoteDir,
   328  			Path:           localDir,
   329  			Branch:         "master",
   330  		}
   331  		cmd := exec.Command("git", "init", "--bare", remoteDir)
   332  		cmd.Start()
   333  		cmd.Wait()
   334  		_, err := New(
   335  			testutil.MakeTestContext(),
   336  			repoConfig,
   337  		)
   338  
   339  		if err != nil {
   340  			t.Fatal(err)
   341  		}
   342  		tags, err := GetTags(
   343  			repoConfig,
   344  			localDir,
   345  			testutil.MakeTestContext(),
   346  		)
   347  		if err != nil {
   348  			t.Fatalf("new: expected no error, got '%e'", err)
   349  		}
   350  		if len(tags) != 0 {
   351  			t.Fatalf("expected %v tags but got %v", 0, len(tags))
   352  		}
   353  	})
   354  
   355  }
   356  
   357  func TestGetTags(t *testing.T) {
   358  	tcs := []struct {
   359  		Name         string
   360  		expectedTags []api.TagData
   361  		tagsToAdd    []string
   362  	}{
   363  		{
   364  			Name:         "Tags added to be returned",
   365  			tagsToAdd:    []string{"v1.0.0"},
   366  			expectedTags: []api.TagData{{Tag: "refs/tags/v1.0.0", CommitId: ""}},
   367  		},
   368  		{
   369  			Name:         "Tags added in opposite order and are sorted",
   370  			tagsToAdd:    []string{"v1.0.1", "v0.0.1"},
   371  			expectedTags: []api.TagData{{Tag: "refs/tags/v0.0.1", CommitId: ""}, {Tag: "refs/tags/v1.0.1", CommitId: ""}},
   372  		},
   373  	}
   374  	for _, tc := range tcs {
   375  		tc := tc
   376  		t.Run(tc.Name, func(t *testing.T) {
   377  			t.Parallel()
   378  			dir := t.TempDir()
   379  			remoteDir := path.Join(dir, "remote")
   380  			localDir := path.Join(dir, "local")
   381  			repoConfig := RepositoryConfig{
   382  				StorageBackend: 0,
   383  				URL:            "file://" + remoteDir,
   384  				Path:           localDir,
   385  				Branch:         "master",
   386  			}
   387  			cmd := exec.Command("git", "init", "--bare", remoteDir)
   388  			cmd.Start()
   389  			cmd.Wait()
   390  			_, err := New(
   391  				testutil.MakeTestContext(),
   392  				repoConfig,
   393  			)
   394  			if err != nil {
   395  				t.Fatal(err)
   396  			}
   397  			repo, err := git.OpenRepository(localDir)
   398  			if err != nil {
   399  				t.Fatal(err)
   400  			}
   401  			idx, err := repo.Index()
   402  			if err != nil {
   403  				t.Fatal(err)
   404  			}
   405  			treeId, err := idx.WriteTree()
   406  			if err != nil {
   407  				t.Fatal(err)
   408  			}
   409  
   410  			tree, err := repo.LookupTree(treeId)
   411  			if err != nil {
   412  				t.Fatal(err)
   413  			}
   414  			oid, err := repo.CreateCommit("HEAD", &git.Signature{Name: "SRE", Email: "testing@gmail"}, &git.Signature{Name: "SRE", Email: "testing@gmail"}, "testing", tree)
   415  			if err != nil {
   416  				t.Fatal(err)
   417  			}
   418  			commit, err := repo.LookupCommit(oid)
   419  			if err != nil {
   420  				t.Fatal(err)
   421  			}
   422  			var expectedCommits []api.TagData
   423  			for addTag := range tc.tagsToAdd {
   424  				_, err := repo.Tags.Create(tc.tagsToAdd[addTag], commit, &git.Signature{Name: "SRE", Email: "testing@gmail"}, "testing")
   425  				expectedCommits = append(expectedCommits, api.TagData{Tag: tc.tagsToAdd[addTag], CommitId: commit.Id().String()})
   426  				if err != nil {
   427  					t.Fatal(err)
   428  				}
   429  			}
   430  			tags, err := GetTags(
   431  				repoConfig,
   432  				localDir,
   433  				testutil.MakeTestContext(),
   434  			)
   435  			if err != nil {
   436  				t.Fatalf("new: expected no error, got '%e'", err)
   437  			}
   438  			if len(tags) != len(tc.expectedTags) {
   439  				t.Fatalf("expected %v tags but got %v", len(tc.expectedTags), len(tags))
   440  			}
   441  
   442  			iter := 0
   443  			for _, tagData := range tags {
   444  				for commit := range expectedCommits {
   445  					if tagData.Tag != expectedCommits[commit].Tag {
   446  						if tagData.CommitId == expectedCommits[commit].CommitId {
   447  							t.Fatalf("expected [%v] for TagList commit but got [%v]", expectedCommits[commit].CommitId, tagData.CommitId)
   448  						}
   449  					}
   450  				}
   451  				if tagData.Tag != tc.expectedTags[iter].Tag {
   452  					t.Fatalf("expected [%v] for TagList tag but got [%v] with tagList %v", tc.expectedTags[iter].Tag, tagData.Tag, tags)
   453  				}
   454  				iter += 1
   455  			}
   456  		})
   457  	}
   458  }
   459  
   460  func TestBootstrapModeNew(t *testing.T) {
   461  	tcs := []struct {
   462  		Name          string
   463  		PreInitialize bool
   464  	}{
   465  		{
   466  			Name:          "New in empty repo",
   467  			PreInitialize: false,
   468  		},
   469  		{
   470  			Name:          "New in existing repo",
   471  			PreInitialize: true,
   472  		},
   473  	}
   474  	for _, tc := range tcs {
   475  		tc := tc
   476  		t.Run(tc.Name, func(t *testing.T) {
   477  			t.Parallel()
   478  			// create a remote
   479  			dir := t.TempDir()
   480  			remoteDir := path.Join(dir, "remote")
   481  			localDir := path.Join(dir, "local")
   482  
   483  			cmd := exec.Command("git", "init", "--bare", remoteDir)
   484  			cmd.Start()
   485  			cmd.Wait()
   486  
   487  			if tc.PreInitialize {
   488  				_, err := New(
   489  					testutil.MakeTestContext(),
   490  					RepositoryConfig{
   491  						URL:  "file://" + remoteDir,
   492  						Path: localDir,
   493  					},
   494  				)
   495  				if err != nil {
   496  					t.Fatal(err)
   497  				}
   498  			}
   499  
   500  			environmentConfigsPath := filepath.Join(remoteDir, "..", "environment_configs.json")
   501  
   502  			repo, err := New(
   503  				testutil.MakeTestContext(),
   504  				RepositoryConfig{
   505  					URL:                    "file://" + remoteDir,
   506  					Path:                   localDir,
   507  					BootstrapMode:          true,
   508  					EnvironmentConfigsPath: environmentConfigsPath,
   509  				},
   510  			)
   511  			if err != nil {
   512  				t.Fatalf("New: Expected no error, error %e was thrown", err)
   513  			}
   514  
   515  			state := repo.State()
   516  			if !state.BootstrapMode {
   517  				t.Fatalf("Bootstrap mode not preserved")
   518  			}
   519  		})
   520  	}
   521  }
   522  
   523  func TestBootstrapModeReadConfig(t *testing.T) {
   524  	tcs := []struct {
   525  		Name string
   526  	}{
   527  		{
   528  			Name: "Config read correctly",
   529  		},
   530  	}
   531  	for _, tc := range tcs {
   532  		tc := tc
   533  		t.Run(tc.Name, func(t *testing.T) {
   534  			t.Parallel()
   535  			// create a remote
   536  			dir := t.TempDir()
   537  			remoteDir := path.Join(dir, "remote")
   538  			localDir := path.Join(dir, "local")
   539  
   540  			cmd := exec.Command("git", "init", "--bare", remoteDir)
   541  			cmd.Start()
   542  			cmd.Wait()
   543  
   544  			environmentConfigsPath := filepath.Join(remoteDir, "..", "environment_configs.json")
   545  			if err := os.WriteFile(environmentConfigsPath, []byte(`{"uniqueEnv": {"environmentGroup": "testgroup321", "upstream": {"latest": true}}}`), fs.FileMode(0644)); err != nil {
   546  				t.Fatal(err)
   547  			}
   548  
   549  			repo, err := New(
   550  				testutil.MakeTestContext(),
   551  				RepositoryConfig{
   552  					URL:                    "file://" + remoteDir,
   553  					Path:                   localDir,
   554  					BootstrapMode:          true,
   555  					EnvironmentConfigsPath: environmentConfigsPath,
   556  				},
   557  			)
   558  			if err != nil {
   559  				t.Fatalf("New: Expected no error, error %e was thrown", err)
   560  			}
   561  
   562  			state := repo.State()
   563  			if !state.BootstrapMode {
   564  				t.Fatalf("Bootstrap mode not preserved")
   565  			}
   566  			configs, err := state.GetEnvironmentConfigs()
   567  			if err != nil {
   568  				t.Fatal(err)
   569  			}
   570  			if len(configs) != 1 {
   571  				t.Fatal("Configuration not read properly")
   572  			}
   573  			if configs["uniqueEnv"].Upstream.Latest != true {
   574  				t.Fatal("Configuration not read properly")
   575  			}
   576  			if configs["uniqueEnv"].EnvironmentGroup == nil {
   577  				t.Fatalf("EnvironmentGroup not read, found nil")
   578  			}
   579  			if *configs["uniqueEnv"].EnvironmentGroup != "testgroup321" {
   580  				t.Fatalf("EnvironmentGroup not read, found '%s' instead", *configs["uniqueEnv"].EnvironmentGroup)
   581  			}
   582  		})
   583  	}
   584  }
   585  
   586  func TestBootstrapError(t *testing.T) {
   587  	tcs := []struct {
   588  		Name          string
   589  		ConfigContent string
   590  	}{
   591  		{
   592  			Name:          "Invalid json in bootstrap configuration",
   593  			ConfigContent: `{"development": "upstream": {"latest": true}}}`,
   594  		},
   595  	}
   596  
   597  	for _, tc := range tcs {
   598  		tc := tc
   599  		t.Run(tc.Name, func(t *testing.T) {
   600  			t.Parallel()
   601  			// create a remote
   602  			dir := t.TempDir()
   603  			remoteDir := path.Join(dir, "remote")
   604  			localDir := path.Join(dir, "local")
   605  			cmd := exec.Command("git", "init", "--bare", remoteDir)
   606  			cmd.Start()
   607  			cmd.Wait()
   608  
   609  			environmentConfigsPath := filepath.Join(remoteDir, "..", "environment_configs.json")
   610  			if err := os.WriteFile(environmentConfigsPath, []byte(tc.ConfigContent), fs.FileMode(0644)); err != nil {
   611  				t.Fatal(err)
   612  			}
   613  
   614  			_, err := New(
   615  				testutil.MakeTestContext(),
   616  				RepositoryConfig{
   617  					URL:                    "file://" + remoteDir,
   618  					Path:                   localDir,
   619  					BootstrapMode:          true,
   620  					EnvironmentConfigsPath: environmentConfigsPath,
   621  				},
   622  			)
   623  			if err == nil {
   624  				t.Fatalf("New: Expected error but no error was thrown")
   625  			}
   626  		})
   627  	}
   628  }
   629  
   630  func TestConfigReload(t *testing.T) {
   631  	configFiles := []struct {
   632  		ConfigContent string
   633  		ErrorExpected bool
   634  	}{
   635  		{
   636  			ConfigContent: "{\"upstream\": {\"latest\": true }}",
   637  			ErrorExpected: false,
   638  		},
   639  		{
   640  			ConfigContent: "{\"upstream\": \"latest\": true }}",
   641  			ErrorExpected: true,
   642  		},
   643  		{
   644  			ConfigContent: "{\"upstream\": {\"latest\": true }}",
   645  			ErrorExpected: false,
   646  		},
   647  	}
   648  	t.Run("Config file reload on change", func(t *testing.T) {
   649  		t.Parallel()
   650  		// create a remote
   651  		workdir := t.TempDir()
   652  		remoteDir := path.Join(workdir, "remote")
   653  		cmd := exec.Command("git", "init", "--bare", remoteDir)
   654  		cmd.Start()
   655  		cmd.Wait()
   656  
   657  		workdir = t.TempDir()
   658  		cmd = exec.Command("git", "clone", remoteDir, workdir) // Clone git dir
   659  		_, err := cmd.Output()
   660  		if err != nil {
   661  			if exitErr, ok := err.(*exec.ExitError); ok {
   662  				t.Logf("stderr: %s\n", exitErr.Stderr)
   663  			}
   664  			t.Fatal(err)
   665  		}
   666  		cmd = exec.Command("git", "config", "pull.rebase", "false") // Add a new file to git
   667  		cmd.Dir = workdir
   668  		_, err = cmd.Output()
   669  		if err != nil {
   670  			if exitErr, ok := err.(*exec.ExitError); ok {
   671  				t.Logf("stderr: %s\n", exitErr.Stderr)
   672  			}
   673  			t.Fatal(err)
   674  		}
   675  
   676  		if err := os.MkdirAll(path.Join(workdir, "environments", "development"), 0700); err != nil {
   677  			t.Fatal(err)
   678  		}
   679  
   680  		updateConfigFile := func(configFileContent string) error {
   681  			configFilePath := path.Join(workdir, "environments", "development", "config.json")
   682  			if err := os.WriteFile(configFilePath, []byte(configFileContent), 0666); err != nil {
   683  				return err
   684  			}
   685  			cmd = exec.Command("git", "add", configFilePath) // Add a new file to git
   686  			cmd.Dir = workdir
   687  			_, err = cmd.Output()
   688  			if err != nil {
   689  				if exitErr, ok := err.(*exec.ExitError); ok {
   690  					t.Logf("stderr: %s\n", exitErr.Stderr)
   691  				}
   692  				return err
   693  			}
   694  			cmd = exec.Command("git", "commit", "-m", "valid config") // commit the new file
   695  			cmd.Dir = workdir
   696  			cmd.Env = []string{
   697  				"GIT_AUTHOR_NAME=kuberpult",
   698  				"GIT_COMMITTER_NAME=kuberpult",
   699  				"EMAIL=test@kuberpult.com",
   700  			}
   701  			out, err := cmd.Output()
   702  			fmt.Println(string(out))
   703  			if err != nil {
   704  				if exitErr, ok := err.(*exec.ExitError); ok {
   705  					t.Logf("stderr: %s\n", exitErr.Stderr)
   706  					t.Logf("stderr: %s\n", err)
   707  				}
   708  				return err
   709  			}
   710  			cmd = exec.Command("git", "push", "origin", "HEAD") // push the new commit
   711  			cmd.Dir = workdir
   712  			_, err = cmd.Output()
   713  			if err != nil {
   714  				if exitErr, ok := err.(*exec.ExitError); ok {
   715  					t.Logf("stderr: %s\n", exitErr.Stderr)
   716  				}
   717  				return err
   718  			}
   719  			return nil
   720  		}
   721  
   722  		repo, err := New(
   723  			testutil.MakeTestContext(),
   724  			RepositoryConfig{
   725  				URL:  remoteDir,
   726  				Path: t.TempDir(),
   727  			},
   728  		)
   729  
   730  		if err != nil {
   731  			t.Fatal(err)
   732  		}
   733  
   734  		for _, configFile := range configFiles {
   735  			err = updateConfigFile(configFile.ConfigContent)
   736  			if err != nil {
   737  				t.Fatal(err)
   738  			}
   739  			err := repo.Apply(testutil.MakeTestContext(), &CreateApplicationVersion{
   740  				Application: "foo",
   741  				Manifests: map[string]string{
   742  					"development": "foo",
   743  				},
   744  			})
   745  			if configFile.ErrorExpected {
   746  				if err == nil {
   747  					t.Errorf("Apply gave no error even though config.json was incorrect")
   748  				}
   749  			} else {
   750  				if err != nil {
   751  					t.Errorf("Initialization failed with valid config.json: %s", err.Error())
   752  				}
   753  				cmd = exec.Command("git", "pull") // Add a new file to git
   754  				cmd.Dir = workdir
   755  				_, err = cmd.Output()
   756  				if err != nil {
   757  					if exitErr, ok := err.(*exec.ExitError); ok {
   758  						t.Logf("stderr: %s\n", exitErr.Stderr)
   759  					}
   760  					t.Fatal(err)
   761  				}
   762  			}
   763  		}
   764  	})
   765  }
   766  func TestConfigValidity(t *testing.T) {
   767  	tcs := []struct {
   768  		Name          string
   769  		ConfigContent string
   770  		ErrorExpected bool
   771  	}{
   772  		{
   773  			Name:          "Initialization with valid config.json file works",
   774  			ConfigContent: "{\"upstream\": {\"latest\": true }}",
   775  			ErrorExpected: false,
   776  		},
   777  		{
   778  			Name:          "Initialization with invalid config.json file throws error",
   779  			ConfigContent: "{\"upstream\": \"latest\": true }}",
   780  			ErrorExpected: true,
   781  		},
   782  	}
   783  	for _, tc := range tcs {
   784  		tc := tc
   785  		t.Run(tc.Name, func(t *testing.T) {
   786  			t.Parallel()
   787  			// create a remote
   788  			workdir := t.TempDir()
   789  			remoteDir := path.Join(workdir, "remote")
   790  			cmd := exec.Command("git", "init", "--bare", remoteDir)
   791  			cmd.Start()
   792  			cmd.Wait()
   793  
   794  			workdir = t.TempDir()
   795  			cmd = exec.Command("git", "clone", remoteDir, workdir) // Clone git dir
   796  			_, err := cmd.Output()
   797  			if err != nil {
   798  				if exitErr, ok := err.(*exec.ExitError); ok {
   799  					t.Logf("stderr: %s\n", exitErr.Stderr)
   800  				}
   801  				t.Fatal(err)
   802  			}
   803  
   804  			if err := os.MkdirAll(path.Join(workdir, "environments", "development"), 0700); err != nil {
   805  				t.Fatal(err)
   806  			}
   807  
   808  			configFilePath := path.Join(workdir, "environments", "development", "config.json")
   809  			if err := os.WriteFile(configFilePath, []byte(tc.ConfigContent), 0666); err != nil {
   810  				t.Fatal(err)
   811  			}
   812  			cmd = exec.Command("git", "add", configFilePath) // Add a new file to git
   813  			cmd.Dir = workdir
   814  			_, err = cmd.Output()
   815  			if err != nil {
   816  				if exitErr, ok := err.(*exec.ExitError); ok {
   817  					t.Logf("stderr: %s\n", exitErr.Stderr)
   818  				}
   819  				t.Fatal(err)
   820  			}
   821  			cmd = exec.Command("git", "commit", "-m", "valid config") // commit the new file
   822  			cmd.Dir = workdir
   823  			cmd.Env = []string{
   824  				"GIT_AUTHOR_NAME=kuberpult",
   825  				"GIT_COMMITTER_NAME=kuberpult",
   826  				"EMAIL=test@kuberpult.com",
   827  			}
   828  			_, err = cmd.Output()
   829  			if err != nil {
   830  				if exitErr, ok := err.(*exec.ExitError); ok {
   831  					t.Logf("stderr: %s\n", exitErr.Stderr)
   832  				}
   833  				t.Fatal(err)
   834  			}
   835  			cmd = exec.Command("git", "push", "origin", "HEAD") // push the new commit
   836  			cmd.Dir = workdir
   837  			_, err = cmd.Output()
   838  			if err != nil {
   839  				if exitErr, ok := err.(*exec.ExitError); ok {
   840  					t.Logf("stderr: %s\n", exitErr.Stderr)
   841  				}
   842  				t.Fatal(err)
   843  			}
   844  
   845  			_, err = New(
   846  				testutil.MakeTestContext(),
   847  				RepositoryConfig{
   848  					URL:  remoteDir,
   849  					Path: t.TempDir(),
   850  				},
   851  			)
   852  
   853  			if tc.ErrorExpected {
   854  				if err == nil {
   855  					t.Errorf("Initialized even though config.json was incorrect")
   856  				}
   857  			} else {
   858  				if err != nil {
   859  					t.Errorf("Initialization failed with valid config.json")
   860  				}
   861  			}
   862  
   863  		})
   864  	}
   865  }
   866  
   867  func TestGc(t *testing.T) {
   868  	tcs := []struct {
   869  		Name               string
   870  		GcFrequency        uint
   871  		StorageBackend     StorageBackend
   872  		ExpectedGarbageMin uint64
   873  		ExpectedGarbageMax uint64
   874  	}{
   875  		{
   876  			// 0 disables GC entirely
   877  			// we are reasonably expecting some additional files around
   878  			Name:               "gc disabled",
   879  			GcFrequency:        0,
   880  			StorageBackend:     GitBackend,
   881  			ExpectedGarbageMin: 906,
   882  			ExpectedGarbageMax: 1500,
   883  		},
   884  		{
   885  			// we are going to perform 101 requests, that should trigger a gc
   886  			// the number of additional files should be lower than in the case above
   887  			Name:               "gc enabled",
   888  			GcFrequency:        100,
   889  			StorageBackend:     GitBackend,
   890  			ExpectedGarbageMin: 9,
   891  			ExpectedGarbageMax: 10,
   892  		},
   893  		{
   894  			// enabling sqlite should bring the number of loose files down to 0
   895  			Name:               "sqlite",
   896  			GcFrequency:        0, // the actual number here doesn't matter. GC is not run when sqlite is in use
   897  			StorageBackend:     SqliteBackend,
   898  			ExpectedGarbageMin: 0,
   899  			ExpectedGarbageMax: 0,
   900  		},
   901  	}
   902  
   903  	for _, tc := range tcs {
   904  		tc := tc
   905  		t.Run(tc.Name, func(t *testing.T) {
   906  			t.Parallel()
   907  			// create a remote
   908  			dir := t.TempDir()
   909  			remoteDir := path.Join(dir, "remote")
   910  			localDir := path.Join(dir, "local")
   911  			cmd := exec.Command("git", "init", "--bare", remoteDir)
   912  			cmd.Start()
   913  			cmd.Wait()
   914  			ctx := testutil.MakeTestContext()
   915  			repo, err := New(
   916  				ctx,
   917  				RepositoryConfig{
   918  					URL:            "file://" + remoteDir,
   919  					Path:           localDir,
   920  					GcFrequency:    tc.GcFrequency,
   921  					StorageBackend: tc.StorageBackend,
   922  				},
   923  			)
   924  			if err != nil {
   925  				t.Fatalf("new: expected no error, got '%e'", err)
   926  			}
   927  
   928  			err = repo.Apply(ctx, &CreateEnvironment{
   929  				Environment: "test",
   930  			})
   931  			if err != nil {
   932  				t.Fatal(err)
   933  			}
   934  			for i := 0; i < 100; i++ {
   935  				err := repo.Apply(ctx, &CreateApplicationVersion{
   936  					Application: "test",
   937  					Manifests: map[string]string{
   938  						"test": fmt.Sprintf("test%d", i),
   939  					},
   940  				})
   941  				if err != nil {
   942  					t.Fatal(err)
   943  				}
   944  			}
   945  			stats, err := repo.(*repository).countObjects(ctx)
   946  			if err != nil {
   947  				t.Fatal(err)
   948  			}
   949  			if stats.Count > tc.ExpectedGarbageMax {
   950  				t.Errorf("expected object count to be lower than %d, but got %d", tc.ExpectedGarbageMax, stats.Count)
   951  			}
   952  			if stats.Count < tc.ExpectedGarbageMin {
   953  				t.Errorf("expected object count to be higher than %d, but got %d", tc.ExpectedGarbageMin, stats.Count)
   954  			}
   955  		})
   956  	}
   957  }
   958  
   959  func TestRetrySsh(t *testing.T) {
   960  	tcs := []struct {
   961  		Name              string
   962  		NumOfFailures     int
   963  		ExpectedNumOfCall int
   964  		ExpectedResponse  error
   965  		CustomResponse    error
   966  	}{
   967  		{
   968  			Name:              "No retries success from 1st try",
   969  			NumOfFailures:     0,
   970  			ExpectedNumOfCall: 1,
   971  			ExpectedResponse:  nil,
   972  			CustomResponse:    nil,
   973  		}, {
   974  			Name:              "Success after the 4th attempt",
   975  			NumOfFailures:     4,
   976  			ExpectedNumOfCall: 5,
   977  			ExpectedResponse:  nil,
   978  			CustomResponse:    &git.GitError{Message: "mock error"},
   979  		}, {
   980  			Name:              "Fail after the 6th attempt",
   981  			NumOfFailures:     6,
   982  			ExpectedNumOfCall: 6,
   983  			ExpectedResponse:  &git.GitError{Message: "max number of retries exceeded error"},
   984  			CustomResponse:    &git.GitError{Message: "max number of retries exceeded error"},
   985  		}, {
   986  			Name:              "Do not retry after a permanent error",
   987  			NumOfFailures:     1,
   988  			ExpectedNumOfCall: 1,
   989  			ExpectedResponse:  &git.GitError{Message: "permanent error"},
   990  			CustomResponse:    &git.GitError{Message: "permanent error", Code: git.ErrorCodeNonFastForward},
   991  		}, {
   992  			Name:              "Fail after the 6th attempt = Max number of retries ",
   993  			NumOfFailures:     12,
   994  			ExpectedNumOfCall: 6,
   995  			ExpectedResponse:  &git.GitError{Message: "max number of retries exceeded error"},
   996  			CustomResponse:    nil,
   997  		},
   998  	}
   999  	for _, tc := range tcs {
  1000  		tc := tc
  1001  		t.Run(tc.Name, func(t *testing.T) {
  1002  			t.Parallel()
  1003  			repo := &repository{}
  1004  			counter := 0
  1005  			repo.backOffProvider = func() backoff.BackOff {
  1006  				return backoff.WithMaxRetries(&backoff.ZeroBackOff{}, 5)
  1007  			}
  1008  			resp := repo.Push(testutil.MakeTestContext(), func() error {
  1009  				counter++
  1010  				if counter > tc.NumOfFailures {
  1011  					return nil
  1012  				}
  1013  				if counter == tc.NumOfFailures { //  Custom response
  1014  					return tc.CustomResponse
  1015  				}
  1016  				if counter == 6 { // max number of retries
  1017  					return &git.GitError{Message: "max number of retries exceeded error"}
  1018  				}
  1019  				return &git.GitError{Message: fmt.Sprintf("mock error %d", counter)}
  1020  			})
  1021  
  1022  			if resp == nil || tc.ExpectedResponse == nil {
  1023  				if resp != tc.ExpectedResponse {
  1024  					t.Fatalf("new: expected '%v',  got '%v'", tc.ExpectedResponse, resp)
  1025  				}
  1026  			} else if resp.Error() != tc.ExpectedResponse.Error() {
  1027  				t.Fatalf("new: expected '%v',  got '%v'", tc.ExpectedResponse.Error(), resp.Error())
  1028  			}
  1029  			if counter != tc.ExpectedNumOfCall {
  1030  				t.Fatalf("new: expected number of calls  '%d',  got '%d'", tc.ExpectedNumOfCall, counter)
  1031  			}
  1032  
  1033  		})
  1034  	}
  1035  }
  1036  
  1037  type SlowTransformer struct {
  1038  	finished chan struct{}
  1039  	started  chan struct{}
  1040  }
  1041  
  1042  func (s *SlowTransformer) Transform(ctx context.Context, state *State, t TransformerContext) (string, error) {
  1043  	s.started <- struct{}{}
  1044  	<-s.finished
  1045  	return "ok", nil
  1046  }
  1047  
  1048  type EmptyTransformer struct{}
  1049  
  1050  func (p *EmptyTransformer) Transform(ctx context.Context, state *State, t TransformerContext) (string, error) {
  1051  	return "nothing happened", nil
  1052  }
  1053  
  1054  type PanicTransformer struct{}
  1055  
  1056  func (p *PanicTransformer) Transform(ctx context.Context, state *State, t TransformerContext) (string, error) {
  1057  	panic("panic tranformer")
  1058  }
  1059  
  1060  var TransformerError = errors.New("error transformer")
  1061  
  1062  type ErrorTransformer struct{}
  1063  
  1064  func (p *ErrorTransformer) Transform(ctx context.Context, state *State, t TransformerContext) (string, error) {
  1065  	return "error", TransformerError
  1066  }
  1067  
  1068  type InvalidJsonTransformer struct{}
  1069  
  1070  func (p *InvalidJsonTransformer) Transform(ctx context.Context, state *State, t TransformerContext) (string, error) {
  1071  	return "error", InvalidJson
  1072  }
  1073  
  1074  func convertToSet(list []uint64) map[int]bool {
  1075  	set := make(map[int]bool)
  1076  	for _, i := range list {
  1077  		set[int(i)] = true
  1078  	}
  1079  	return set
  1080  }
  1081  
  1082  func TestApplyQueuePanic(t *testing.T) {
  1083  	type action struct {
  1084  		Transformer Transformer
  1085  		// Tests
  1086  		ExpectedError error
  1087  	}
  1088  	tcs := []struct {
  1089  		Name    string
  1090  		Actions []action
  1091  	}{
  1092  		{
  1093  			Name: "panic at the start",
  1094  			Actions: []action{
  1095  				{
  1096  					Transformer:   &PanicTransformer{},
  1097  					ExpectedError: panicError,
  1098  				}, {
  1099  					ExpectedError: panicError,
  1100  				}, {
  1101  					ExpectedError: panicError,
  1102  				},
  1103  			},
  1104  		},
  1105  		{
  1106  			Name: "panic at the middle",
  1107  			Actions: []action{
  1108  				{
  1109  					ExpectedError: panicError,
  1110  				}, {
  1111  					Transformer:   &PanicTransformer{},
  1112  					ExpectedError: panicError,
  1113  				}, {
  1114  					ExpectedError: panicError,
  1115  				},
  1116  			},
  1117  		},
  1118  		{
  1119  			Name: "panic at the end",
  1120  			Actions: []action{
  1121  				{
  1122  					ExpectedError: panicError,
  1123  				}, {
  1124  					ExpectedError: panicError,
  1125  				}, {
  1126  					Transformer:   &PanicTransformer{},
  1127  					ExpectedError: panicError,
  1128  				},
  1129  			},
  1130  		},
  1131  	}
  1132  	for _, tc := range tcs {
  1133  		tc := tc
  1134  		t.Run(tc.Name, func(t *testing.T) {
  1135  			t.Parallel()
  1136  			// create a remote
  1137  			dir := t.TempDir()
  1138  			remoteDir := path.Join(dir, "remote")
  1139  			localDir := path.Join(dir, "local")
  1140  			cmd := exec.Command("git", "init", "--bare", remoteDir)
  1141  			cmd.Start()
  1142  			cmd.Wait()
  1143  			repo, processQueue, err := New2(
  1144  				testutil.MakeTestContext(),
  1145  				RepositoryConfig{
  1146  					URL:                   "file://" + remoteDir,
  1147  					Path:                  localDir,
  1148  					MaximumCommitsPerPush: 3,
  1149  				},
  1150  			)
  1151  			if err != nil {
  1152  				t.Fatalf("new: expected no error, got '%e'", err)
  1153  			}
  1154  			// The worker go routine is not started. We can move some items into the queue now.
  1155  			results := make([]<-chan error, len(tc.Actions))
  1156  			for i, action := range tc.Actions {
  1157  				// We are using the internal interface here as an optimization to avoid spinning up one go-routine per action
  1158  				t := action.Transformer
  1159  				if t == nil {
  1160  					t = &EmptyTransformer{}
  1161  				}
  1162  				results[i] = repo.(*repository).applyDeferred(testutil.MakeTestContext(), t)
  1163  			}
  1164  			defer func() {
  1165  				r := recover()
  1166  				if r == nil {
  1167  					t.Errorf("The code did not panic")
  1168  				} else if r != "panic tranformer" {
  1169  					t.Logf("The code did not panic with the correct string but %#v", r)
  1170  					panic(r)
  1171  				}
  1172  				// Check for the correct errors
  1173  				for i, action := range tc.Actions {
  1174  					if err := <-results[i]; err != action.ExpectedError {
  1175  						t.Errorf("result[%d] error is not \"%v\" but got \"%v\"", i, action.ExpectedError, err)
  1176  					}
  1177  				}
  1178  			}()
  1179  			ctx, cancel := context.WithTimeout(testutil.MakeTestContext(), 10*time.Second)
  1180  			defer cancel()
  1181  			processQueue(ctx, nil)
  1182  		})
  1183  	}
  1184  }
  1185  
  1186  type mockClock struct {
  1187  	t time.Time
  1188  }
  1189  
  1190  func (m *mockClock) now() time.Time {
  1191  	return m.t
  1192  }
  1193  
  1194  func (m *mockClock) sleep(d time.Duration) {
  1195  	m.t = m.t.Add(d)
  1196  }
  1197  
  1198  func TestApplyQueueTtlForHealth(t *testing.T) {
  1199  	// we set the networkTimeout to something low, so that it doesn't interfere with other processes e.g like once per second:
  1200  	networkTimeout := 1 * time.Second
  1201  
  1202  	tcs := []struct {
  1203  		Name string
  1204  	}{
  1205  		{
  1206  			Name: "sleeps way too long, so health should fail",
  1207  		},
  1208  	}
  1209  	for _, tc := range tcs {
  1210  		tc := tc
  1211  		t.Run(tc.Name, func(t *testing.T) {
  1212  			dir := t.TempDir()
  1213  			remoteDir := path.Join(dir, "remote")
  1214  			localDir := path.Join(dir, "local")
  1215  			cmd := exec.Command("git", "init", "--bare", remoteDir)
  1216  			cmd.Start()
  1217  			cmd.Wait()
  1218  			repo, processQueue, err := New2(
  1219  				testutil.MakeTestContext(),
  1220  				RepositoryConfig{
  1221  					URL:            "file://" + remoteDir,
  1222  					Path:           localDir,
  1223  					NetworkTimeout: networkTimeout,
  1224  				},
  1225  			)
  1226  			if err != nil {
  1227  				t.Fatalf("new: expected no error, got '%e'", err)
  1228  			}
  1229  			ctx, cancel := context.WithTimeout(testutil.MakeTestContext(), 10*time.Second)
  1230  
  1231  			mc := mockClock{}
  1232  			hlth := &setup.HealthServer{}
  1233  			hlth.BackOffFactory = func() backoff.BackOff { return backoff.NewConstantBackOff(0) }
  1234  			hlth.Clock = mc.now
  1235  			reporterName := "ClarkKent"
  1236  			reporter := hlth.Reporter(reporterName)
  1237  			isReady := func() bool {
  1238  				return hlth.IsReady(reporterName)
  1239  			}
  1240  			errChan := make(chan error)
  1241  			go func() {
  1242  				err = processQueue(ctx, reporter)
  1243  				errChan <- err
  1244  			}()
  1245  			defer func() {
  1246  				cancel()
  1247  				chanError := <-errChan
  1248  				if chanError != nil {
  1249  					t.Errorf("Expected no error in processQueue but got: %v", chanError)
  1250  				}
  1251  			}()
  1252  
  1253  			finished := make(chan struct{})
  1254  			started := make(chan struct{})
  1255  			var transformer Transformer = &SlowTransformer{
  1256  				finished: finished,
  1257  				started:  started,
  1258  			}
  1259  
  1260  			go repo.Apply(ctx, transformer)
  1261  
  1262  			// first, wait, until the transformer has started:
  1263  			<-started
  1264  			// health should be reporting as ready now
  1265  			if !isReady() {
  1266  				t.Error("Expected health to be ready after transformer was started, but it was not")
  1267  			}
  1268  			// now advance the clock time
  1269  			mc.sleep(4 * networkTimeout)
  1270  
  1271  			// now that the transformer is started, we should get a failed health check immediately, because the networkTimeout is tiny:
  1272  			if isReady() {
  1273  				t.Error("Expected health to be not ready after transformer took too long, but it was")
  1274  			}
  1275  
  1276  			// let the transformer finish:
  1277  			finished <- struct{}{}
  1278  
  1279  		})
  1280  	}
  1281  }
  1282  
  1283  func TestApplyQueue(t *testing.T) {
  1284  	type action struct {
  1285  		CancelBeforeAdd bool
  1286  		CancelAfterAdd  bool
  1287  		Transformer     Transformer
  1288  		// Tests
  1289  		ExpectedError error
  1290  	}
  1291  	tcs := []struct {
  1292  		Name             string
  1293  		Actions          []action
  1294  		ExpectedReleases []uint64
  1295  	}{
  1296  		{
  1297  			Name: "simple",
  1298  			Actions: []action{
  1299  				{}, {}, {},
  1300  			},
  1301  			ExpectedReleases: []uint64{
  1302  				1, 2, 3,
  1303  			},
  1304  		},
  1305  		{
  1306  			Name: "cancellation in the middle (after)",
  1307  			Actions: []action{
  1308  				{}, {
  1309  					CancelAfterAdd: true,
  1310  					ExpectedError:  context.Canceled,
  1311  				}, {},
  1312  			},
  1313  			ExpectedReleases: []uint64{
  1314  				1, 3,
  1315  			},
  1316  		},
  1317  		{
  1318  			Name: "cancellation at the start (after)",
  1319  			Actions: []action{
  1320  				{
  1321  					CancelAfterAdd: true,
  1322  					ExpectedError:  context.Canceled,
  1323  				}, {}, {},
  1324  			},
  1325  			ExpectedReleases: []uint64{
  1326  				2, 3,
  1327  			},
  1328  		},
  1329  		{
  1330  			Name: "cancellation at the end (after)",
  1331  			Actions: []action{
  1332  				{}, {},
  1333  				{
  1334  					CancelAfterAdd: true,
  1335  					ExpectedError:  context.Canceled,
  1336  				},
  1337  			},
  1338  			ExpectedReleases: []uint64{
  1339  				1, 2,
  1340  			},
  1341  		},
  1342  		{
  1343  			Name: "cancellation in the middle (before)",
  1344  			Actions: []action{
  1345  				{}, {
  1346  					CancelBeforeAdd: true,
  1347  					ExpectedError:   context.Canceled,
  1348  				}, {},
  1349  			},
  1350  			ExpectedReleases: []uint64{
  1351  				1, 3,
  1352  			},
  1353  		},
  1354  		{
  1355  			Name: "cancellation at the start (before)",
  1356  			Actions: []action{
  1357  				{
  1358  					CancelBeforeAdd: true,
  1359  					ExpectedError:   context.Canceled,
  1360  				}, {}, {},
  1361  			},
  1362  			ExpectedReleases: []uint64{
  1363  				2, 3,
  1364  			},
  1365  		},
  1366  		{
  1367  			Name: "cancellation at the end (before)",
  1368  			Actions: []action{
  1369  				{}, {},
  1370  				{
  1371  					CancelBeforeAdd: true,
  1372  					ExpectedError:   context.Canceled,
  1373  				},
  1374  			},
  1375  			ExpectedReleases: []uint64{
  1376  				1, 2,
  1377  			},
  1378  		},
  1379  		{
  1380  			Name: "error at the start",
  1381  			Actions: []action{
  1382  				{
  1383  					ExpectedError: &TransformerBatchApplyError{TransformerError: TransformerError, Index: 0},
  1384  					Transformer:   &ErrorTransformer{},
  1385  				}, {}, {},
  1386  			},
  1387  			ExpectedReleases: []uint64{
  1388  				2, 3,
  1389  			},
  1390  		},
  1391  		{
  1392  			Name: "error at the middle",
  1393  			Actions: []action{
  1394  				{},
  1395  				{
  1396  					ExpectedError: &TransformerBatchApplyError{TransformerError: TransformerError, Index: 0},
  1397  					Transformer:   &ErrorTransformer{},
  1398  				}, {},
  1399  			},
  1400  			ExpectedReleases: []uint64{
  1401  				1, 3,
  1402  			},
  1403  		},
  1404  		{
  1405  			Name: "error at the end",
  1406  			Actions: []action{
  1407  				{}, {},
  1408  				{
  1409  					ExpectedError: &TransformerBatchApplyError{TransformerError: TransformerError, Index: 0},
  1410  					Transformer:   &ErrorTransformer{},
  1411  				},
  1412  			},
  1413  			ExpectedReleases: []uint64{
  1414  				1, 2,
  1415  			},
  1416  		},
  1417  		{
  1418  			Name: "Invalid json error at start",
  1419  			Actions: []action{
  1420  				{
  1421  					ExpectedError: &TransformerBatchApplyError{TransformerError: InvalidJson, Index: 0},
  1422  					Transformer:   &InvalidJsonTransformer{},
  1423  				},
  1424  				{}, {},
  1425  			},
  1426  			ExpectedReleases: []uint64{
  1427  				2, 3,
  1428  			},
  1429  		},
  1430  		{
  1431  			Name: "Invalid json error at middle",
  1432  			Actions: []action{
  1433  				{},
  1434  				{
  1435  					ExpectedError: &TransformerBatchApplyError{TransformerError: InvalidJson, Index: 0},
  1436  					Transformer:   &InvalidJsonTransformer{},
  1437  				},
  1438  				{},
  1439  			},
  1440  			ExpectedReleases: []uint64{
  1441  				1, 3,
  1442  			},
  1443  		},
  1444  		{
  1445  			Name: "Invalid json error at end",
  1446  			Actions: []action{
  1447  				{}, {},
  1448  				{
  1449  					ExpectedError: &TransformerBatchApplyError{TransformerError: InvalidJson, Index: 0},
  1450  					Transformer:   &InvalidJsonTransformer{},
  1451  				},
  1452  			},
  1453  			ExpectedReleases: []uint64{
  1454  				1, 2,
  1455  			},
  1456  		},
  1457  	}
  1458  	for _, tc := range tcs {
  1459  		tc := tc
  1460  		t.Run(tc.Name, func(t *testing.T) {
  1461  			t.Parallel()
  1462  			// create a remote
  1463  			dir := t.TempDir()
  1464  			remoteDir := path.Join(dir, "remote")
  1465  			localDir := path.Join(dir, "local")
  1466  			cmd := exec.Command("git", "init", "--bare", remoteDir)
  1467  			cmd.Start()
  1468  			cmd.Wait()
  1469  			repo, err := New(
  1470  				testutil.MakeTestContext(),
  1471  				RepositoryConfig{
  1472  					URL:                   "file://" + remoteDir,
  1473  					Path:                  localDir,
  1474  					MaximumCommitsPerPush: 10,
  1475  				},
  1476  			)
  1477  			if err != nil {
  1478  				t.Fatalf("new: expected no error, got '%e'", err)
  1479  			}
  1480  			repoInternal := repo.(*repository)
  1481  			// Block the worker so that we have multiple items in the queue
  1482  			finished := make(chan struct{})
  1483  			started := make(chan struct{}, 1)
  1484  			go func() {
  1485  				repo.Apply(testutil.MakeTestContext(), &SlowTransformer{finished: finished, started: started})
  1486  			}()
  1487  			<-started
  1488  			// The worker go routine is now blocked. We can move some items into the queue now.
  1489  			results := make([]<-chan error, len(tc.Actions))
  1490  			for i, action := range tc.Actions {
  1491  				ctx, cancel := context.WithCancel(testutil.MakeTestContext())
  1492  				if action.CancelBeforeAdd {
  1493  					cancel()
  1494  				}
  1495  				if action.Transformer != nil {
  1496  					results[i] = repoInternal.applyDeferred(ctx, action.Transformer)
  1497  				} else {
  1498  					tf := &CreateApplicationVersion{
  1499  						Application: "foo",
  1500  						Manifests: map[string]string{
  1501  							"development": fmt.Sprintf("%d", i),
  1502  						},
  1503  						Version: uint64(i + 1),
  1504  					}
  1505  					results[i] = repoInternal.applyDeferred(ctx, tf)
  1506  				}
  1507  				if action.CancelAfterAdd {
  1508  					cancel()
  1509  				}
  1510  			}
  1511  			// Now release the slow transformer
  1512  			finished <- struct{}{}
  1513  			// Check for the correct errors
  1514  			for i, action := range tc.Actions {
  1515  				if err := <-results[i]; err != nil && err.Error() != action.ExpectedError.Error() {
  1516  					t.Errorf("result[%d] error is not \"%v\" but got \"%v\"", i, action.ExpectedError, err)
  1517  				}
  1518  			}
  1519  			releases, _ := repo.State().Releases("foo")
  1520  			if !cmp.Equal(convertToSet(tc.ExpectedReleases), convertToSet(releases)) {
  1521  				t.Fatal("Output mismatch (-want +got):\n", cmp.Diff(tc.ExpectedReleases, releases))
  1522  			}
  1523  
  1524  		})
  1525  	}
  1526  }
  1527  
  1528  func TestMaximumCommitsPerPush(t *testing.T) {
  1529  	tcs := []struct {
  1530  		NumberOfCommits       uint
  1531  		MaximumCommitsPerPush uint
  1532  		ExpectedAtLeastPushes uint
  1533  	}{
  1534  		{
  1535  			NumberOfCommits:       7,
  1536  			MaximumCommitsPerPush: 5,
  1537  			ExpectedAtLeastPushes: 2,
  1538  		},
  1539  		{
  1540  			NumberOfCommits:       5,
  1541  			MaximumCommitsPerPush: 0,
  1542  			ExpectedAtLeastPushes: 5,
  1543  		},
  1544  		{
  1545  			NumberOfCommits:       5,
  1546  			MaximumCommitsPerPush: 10,
  1547  			ExpectedAtLeastPushes: 1,
  1548  		},
  1549  	}
  1550  
  1551  	for _, tc := range tcs {
  1552  		tc := tc
  1553  		t.Run(fmt.Sprintf("with %d commits and %d per push", tc.NumberOfCommits, tc.MaximumCommitsPerPush), func(t *testing.T) {
  1554  			// create a remote
  1555  			dir := t.TempDir()
  1556  			remoteDir := path.Join(dir, "remote")
  1557  			localDir := path.Join(dir, "local")
  1558  			cmd := exec.Command("git", "init", "--bare", remoteDir)
  1559  			cmd.Run()
  1560  			ts := testssh.New(remoteDir)
  1561  			defer ts.Close()
  1562  			repo, processor, err := New2(
  1563  				testutil.MakeTestContext(),
  1564  				RepositoryConfig{
  1565  					URL:  ts.Url,
  1566  					Path: localDir,
  1567  					Certificates: Certificates{
  1568  						KnownHostsFile: ts.KnownHosts,
  1569  					},
  1570  					Credentials: Credentials{
  1571  						SshKey: ts.ClientKey,
  1572  					},
  1573  
  1574  					MaximumCommitsPerPush: tc.MaximumCommitsPerPush,
  1575  				},
  1576  			)
  1577  			if err != nil {
  1578  				t.Fatalf("new: expected no error, got '%e'", err)
  1579  			}
  1580  			var eg errgroup.Group
  1581  			for i := uint(0); i < tc.NumberOfCommits; i++ {
  1582  				eg.Go(func() error {
  1583  					return repo.Apply(testutil.MakeTestContext(), &CreateApplicationVersion{
  1584  						Application: "foo",
  1585  						Manifests:   map[string]string{"development": "foo"},
  1586  					})
  1587  				})
  1588  			}
  1589  			ctx, cancel := context.WithCancel(context.Background())
  1590  			defer cancel()
  1591  			go func() {
  1592  				processor(ctx, nil)
  1593  			}()
  1594  			eg.Wait()
  1595  			if ts.Pushes < tc.ExpectedAtLeastPushes {
  1596  				t.Errorf("expected at least %d pushes, but %d happened", tc.ExpectedAtLeastPushes, ts.Pushes)
  1597  			}
  1598  
  1599  		})
  1600  	}
  1601  }
  1602  
  1603  func getTransformer(i int) (Transformer, error) {
  1604  	transformerType := i % 5
  1605  	switch transformerType {
  1606  	case 0:
  1607  	case 1:
  1608  	case 2:
  1609  		return &CreateApplicationVersion{
  1610  			Application: "foo",
  1611  			Manifests: map[string]string{
  1612  				"development": fmt.Sprintf("%d", i),
  1613  			},
  1614  			Version: uint64(i + 1),
  1615  		}, nil
  1616  	case 3:
  1617  		return &ErrorTransformer{}, TransformerError
  1618  	case 4:
  1619  		return &InvalidJsonTransformer{}, InvalidJson
  1620  	}
  1621  	return &ErrorTransformer{}, TransformerError
  1622  }
  1623  
  1624  func createGitWithCommit(remote string, local string, t *testing.B) {
  1625  	cmd := exec.Command("git", "init", "--bare", remote)
  1626  	cmd.Start()
  1627  	cmd.Wait()
  1628  
  1629  	cmd = exec.Command("git", "clone", remote, local) // Clone git dir
  1630  	_, err := cmd.Output()
  1631  	if err != nil {
  1632  		t.Fatal(err)
  1633  	}
  1634  	cmd = exec.Command("touch", "a") // Add a new file to git
  1635  	cmd.Dir = local
  1636  	_, err = cmd.Output()
  1637  	if err != nil {
  1638  		t.Fatal(err)
  1639  	}
  1640  	cmd = exec.Command("git", "add", "a") // Add a new file to git
  1641  	cmd.Dir = local
  1642  	_, err = cmd.Output()
  1643  	if err != nil {
  1644  		t.Fatal(err)
  1645  	}
  1646  
  1647  	cmd = exec.Command("git", "commit", "-m", "adding") // commit the new file
  1648  	cmd.Dir = local
  1649  	cmd.Env = []string{
  1650  		"GIT_AUTHOR_NAME=kuberpult",
  1651  		"GIT_COMMITTER_NAME=kuberpult",
  1652  		"EMAIL=test@kuberpult.com",
  1653  	}
  1654  	_, err = cmd.Output()
  1655  	if err != nil {
  1656  		if exitErr, ok := err.(*exec.ExitError); ok {
  1657  			t.Logf("stderr: %s\n", exitErr.Stderr)
  1658  			t.Logf("stderr: %s\n", err)
  1659  		}
  1660  		t.Fatal(err)
  1661  	}
  1662  	cmd = exec.Command("git", "push", "origin", "HEAD") // push the new commit
  1663  	cmd.Dir = local
  1664  	_, err = cmd.Output()
  1665  	if err != nil {
  1666  		t.Fatal(err)
  1667  	}
  1668  }
  1669  
  1670  func BenchmarkApplyQueue(t *testing.B) {
  1671  	t.StopTimer()
  1672  	dir := t.TempDir()
  1673  	remoteDir := path.Join(dir, "remote")
  1674  	localDir := path.Join(dir, "local")
  1675  	createGitWithCommit(remoteDir, localDir, t)
  1676  
  1677  	repo, err := New(
  1678  		testutil.MakeTestContext(),
  1679  		RepositoryConfig{
  1680  			URL:  "file://" + remoteDir,
  1681  			Path: localDir,
  1682  		},
  1683  	)
  1684  	if err != nil {
  1685  		t.Fatalf("new: expected no error, got '%e'", err)
  1686  	}
  1687  	repoInternal := repo.(*repository)
  1688  	// The worker go routine is now blocked. We can move some items into the queue now.
  1689  	results := make([]<-chan error, t.N)
  1690  	expectedResults := make([]error, t.N)
  1691  	expectedReleases := make(map[int]bool, t.N)
  1692  	tf, _ := getTransformer(0)
  1693  	repoInternal.Apply(testutil.MakeTestContext(), tf)
  1694  
  1695  	t.StartTimer()
  1696  	for i := 0; i < t.N; i++ {
  1697  		tf, expectedResult := getTransformer(i)
  1698  		results[i] = repoInternal.applyDeferred(testutil.MakeTestContext(), tf)
  1699  		expectedResults[i] = expectedResult
  1700  		if expectedResult == nil {
  1701  			expectedReleases[i+1] = true
  1702  		}
  1703  	}
  1704  
  1705  	for i := 0; i < t.N; i++ {
  1706  		if err := <-results[i]; err != expectedResults[i] {
  1707  			t.Errorf("result[%d] expected error \"%v\" but got \"%v\"", i, expectedResults[i], err)
  1708  		}
  1709  	}
  1710  	releases, _ := repo.State().Releases("foo")
  1711  	if !cmp.Equal(expectedReleases, convertToSet(releases)) {
  1712  		t.Fatal("Output mismatch (-want +got):\n", cmp.Diff(expectedReleases, convertToSet(releases)))
  1713  	}
  1714  }
  1715  
  1716  func TestPushUpdate(t *testing.T) {
  1717  	tcs := []struct {
  1718  		Name            string
  1719  		InputBranch     string
  1720  		InputRefName    string
  1721  		InputStatus     string
  1722  		ExpectedSuccess bool
  1723  	}{
  1724  		{
  1725  			Name:            "Should succeed",
  1726  			InputBranch:     "main",
  1727  			InputRefName:    "refs/heads/main",
  1728  			InputStatus:     "",
  1729  			ExpectedSuccess: true,
  1730  		},
  1731  		{
  1732  			Name:            "Should fail because wrong branch",
  1733  			InputBranch:     "main",
  1734  			InputRefName:    "refs/heads/master",
  1735  			InputStatus:     "",
  1736  			ExpectedSuccess: false,
  1737  		},
  1738  		{
  1739  			Name:            "Should fail because status not empty",
  1740  			InputBranch:     "master",
  1741  			InputRefName:    "refs/heads/master",
  1742  			InputStatus:     "i am the status, stopping this from working",
  1743  			ExpectedSuccess: false,
  1744  		},
  1745  	}
  1746  	for _, tc := range tcs {
  1747  		tc := tc
  1748  		t.Run(tc.Name, func(t *testing.T) {
  1749  			t.Parallel()
  1750  			var success = false
  1751  			actualError := defaultPushUpdate(tc.InputBranch, &success)(tc.InputRefName, tc.InputStatus)
  1752  			if success != tc.ExpectedSuccess {
  1753  				t.Fatal(fmt.Sprintf("expected sucess=%t but got %t", tc.ExpectedSuccess, success))
  1754  			}
  1755  			if actualError != nil {
  1756  				t.Fatal(fmt.Sprintf("expected no error but got %s but got none", actualError))
  1757  			}
  1758  		})
  1759  	}
  1760  }
  1761  
  1762  func TestDeleteDirIfEmpty(t *testing.T) {
  1763  	tcs := []struct {
  1764  		Name           string
  1765  		CreateThisDir  string
  1766  		DeleteThisDir  string
  1767  		ExpectedError  error
  1768  		ExpectedReason SuccessReason
  1769  	}{
  1770  		{
  1771  			Name:           "Should succeed: dir exists and is empty",
  1772  			CreateThisDir:  "foo/bar",
  1773  			DeleteThisDir:  "foo/bar",
  1774  			ExpectedReason: NoReason,
  1775  		},
  1776  		{
  1777  			Name:           "Should succeed: dir does not exist",
  1778  			CreateThisDir:  "foo/bar",
  1779  			DeleteThisDir:  "foo/bar/pow",
  1780  			ExpectedReason: DirDoesNotExist,
  1781  		},
  1782  		{
  1783  			Name:           "Should succeed: dir does not exist",
  1784  			CreateThisDir:  "foo/bar/pow",
  1785  			DeleteThisDir:  "foo/bar",
  1786  			ExpectedReason: DirNotEmpty,
  1787  		},
  1788  	}
  1789  	for _, tc := range tcs {
  1790  		tc := tc
  1791  		t.Run(tc.Name, func(t *testing.T) {
  1792  			t.Parallel()
  1793  			repo := setupRepositoryTest(t)
  1794  			state := repo.State()
  1795  			err := state.Filesystem.MkdirAll(tc.CreateThisDir, 0777)
  1796  			if err != nil {
  1797  				t.Fatalf("error in mkdir: %v", err)
  1798  				return
  1799  			}
  1800  			successReason, err := state.DeleteDirIfEmpty(tc.DeleteThisDir)
  1801  			if diff := cmp.Diff(tc.ExpectedError, err, cmpopts.EquateErrors()); diff != "" {
  1802  				t.Errorf("error mismatch (-want, +got):\n%s", diff)
  1803  			}
  1804  			if successReason != tc.ExpectedReason {
  1805  				t.Fatal("Output mismatch (-want +got):\n", cmp.Diff(tc.ExpectedReason, successReason))
  1806  			}
  1807  		})
  1808  	}
  1809  }
  1810  
  1811  func TestProcessQueueOnce(t *testing.T) {
  1812  	tcs := []struct {
  1813  		Name           string
  1814  		Element        transformerBatch
  1815  		PushUpdateFunc PushUpdateFunc
  1816  		PushActionFunc PushActionCallbackFunc
  1817  		ExpectedError  error
  1818  	}{
  1819  		{
  1820  			Name:           "success",
  1821  			PushUpdateFunc: defaultPushUpdate,
  1822  			PushActionFunc: DefaultPushActionCallback,
  1823  			Element: transformerBatch{
  1824  				ctx: testutil.MakeTestContext(),
  1825  				transformers: []Transformer{
  1826  					&EmptyTransformer{},
  1827  				},
  1828  				result: make(chan error, 1),
  1829  			},
  1830  			ExpectedError: nil,
  1831  		},
  1832  		{
  1833  			Name: "failure because DefaultPushUpdate is wrong (branch protection)",
  1834  			PushUpdateFunc: func(s string, success *bool) git.PushUpdateReferenceCallback {
  1835  				*success = false
  1836  				return nil
  1837  			},
  1838  			PushActionFunc: DefaultPushActionCallback,
  1839  			Element: transformerBatch{
  1840  				ctx: testutil.MakeTestContext(),
  1841  				transformers: []Transformer{
  1842  					&EmptyTransformer{},
  1843  				},
  1844  				result: make(chan error, 1),
  1845  			},
  1846  			ExpectedError: errMatcher{"failed to push - this indicates that branch protection is enabled in 'file://$DIR/remote' on branch 'master'"},
  1847  		},
  1848  		{
  1849  			Name: "failure because error is returned in push (ssh key has read only access)",
  1850  			PushUpdateFunc: func(s string, success *bool) git.PushUpdateReferenceCallback {
  1851  				return nil
  1852  			},
  1853  			PushActionFunc: func(options git.PushOptions, r *repository) PushActionFunc {
  1854  				return func() error {
  1855  					return git.MakeGitError(1)
  1856  				}
  1857  			},
  1858  			Element: transformerBatch{
  1859  				ctx: testutil.MakeTestContext(),
  1860  				transformers: []Transformer{
  1861  					&EmptyTransformer{},
  1862  				},
  1863  				result: make(chan error, 1),
  1864  			},
  1865  			ExpectedError: errMatcher{"rpc error: code = InvalidArgument desc = error: could not push to manifest repository 'file://$DIR/remote' on branch 'master' - this indicates that the ssh key does not have write access"},
  1866  		},
  1867  	}
  1868  	for _, tc := range tcs {
  1869  		tc := tc
  1870  		t.Run(tc.Name, func(t *testing.T) {
  1871  			t.Parallel()
  1872  
  1873  			// create a remote
  1874  			dir := t.TempDir()
  1875  			remoteDir := path.Join(dir, "remote")
  1876  			localDir := path.Join(dir, "local")
  1877  			cmd := exec.Command("git", "init", "--bare", remoteDir)
  1878  			cmd.Start()
  1879  			cmd.Wait()
  1880  			repo, actualError := New(
  1881  				testutil.MakeTestContext(),
  1882  				RepositoryConfig{
  1883  					URL:  "file://" + remoteDir,
  1884  					Path: localDir,
  1885  				},
  1886  			)
  1887  			if actualError != nil {
  1888  				t.Fatalf("new: expected no error, got '%e'", actualError)
  1889  			}
  1890  			repoInternal := repo.(*repository)
  1891  			repoInternal.ProcessQueueOnce(testutil.MakeTestContext(), tc.Element, tc.PushUpdateFunc, tc.PushActionFunc)
  1892  
  1893  			result := tc.Element.result
  1894  			actualError = <-result
  1895  
  1896  			var expectedError error
  1897  			if tc.ExpectedError != nil {
  1898  				expectedError = errMatcher{strings.ReplaceAll(tc.ExpectedError.Error(), "$DIR", dir)}
  1899  			}
  1900  			if diff := cmp.Diff(expectedError, actualError, cmpopts.EquateErrors()); diff != "" {
  1901  				t.Errorf("error mismatch (-want, +got):\n%s", diff)
  1902  			}
  1903  		})
  1904  	}
  1905  }
  1906  
  1907  func TestGitPushDoesntGetStuck(t *testing.T) {
  1908  	tcs := []struct {
  1909  		Name string
  1910  	}{
  1911  		{
  1912  			Name: "it doesnt get stuck",
  1913  		},
  1914  	}
  1915  	for _, tc := range tcs {
  1916  		tc := tc
  1917  		t.Run(tc.Name, func(t *testing.T) {
  1918  			t.Parallel()
  1919  			ctx, cancel := context.WithCancel(context.Background())
  1920  			defer cancel()
  1921  			// create a remote
  1922  			dir := t.TempDir()
  1923  			remoteDir := path.Join(dir, "remote")
  1924  			localDir := path.Join(dir, "local")
  1925  			cmd := exec.Command("git", "init", "--bare", remoteDir)
  1926  			cmd.Run()
  1927  			ts := testssh.New(remoteDir)
  1928  			defer ts.Close()
  1929  			repo, err := New(
  1930  				ctx,
  1931  				RepositoryConfig{
  1932  					URL: ts.Url,
  1933  					Certificates: Certificates{
  1934  						KnownHostsFile: ts.KnownHosts,
  1935  					},
  1936  					Credentials: Credentials{
  1937  						SshKey: ts.ClientKey,
  1938  					},
  1939  					Path:           localDir,
  1940  					NetworkTimeout: time.Second,
  1941  				},
  1942  			)
  1943  			if err != nil {
  1944  				t.Errorf("expected no error, got %q ( %#v )", err, err)
  1945  			}
  1946  			err = repo.Apply(testutil.MakeTestContext(),
  1947  				&CreateEnvironment{Environment: "dev"},
  1948  			)
  1949  			if err != nil {
  1950  				t.Errorf("expected no error, got %q ( %#v )", err, err)
  1951  			}
  1952  			// This will prevent the next push from working
  1953  			ts.DelayExecs(15 * time.Second)
  1954  			err = repo.Apply(testutil.MakeTestContext(),
  1955  				&CreateEnvironment{Environment: "stg"},
  1956  			)
  1957  			if err == nil {
  1958  				t.Errorf("expected an error, but didn't get one")
  1959  			}
  1960  			if status.Code(err) != codes.Canceled {
  1961  				t.Errorf("expected status code cancelled, but got %q", status.Code(err))
  1962  			}
  1963  			// This will make the next push work
  1964  			ts.DelayExecs(0 * time.Second)
  1965  			err = repo.Apply(testutil.MakeTestContext(),
  1966  				&CreateEnvironment{Environment: "stg"},
  1967  			)
  1968  			if err != nil {
  1969  				t.Errorf("expected no error, got %q ( %#v )", err, err)
  1970  			}
  1971  		})
  1972  	}
  1973  }
  1974  
  1975  type TestWebhookResolver struct {
  1976  	t        *testing.T
  1977  	rec      *httptest.ResponseRecorder
  1978  	requests chan *http.Request
  1979  }
  1980  
  1981  func (resolver TestWebhookResolver) Resolve(insecure bool, req *http.Request) (*http.Response, error) {
  1982  	testhandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  1983  		resolver.t.Logf("called with request: %v", *r)
  1984  		resolver.requests <- req
  1985  		close(resolver.requests)
  1986  	})
  1987  	testhandler.ServeHTTP(resolver.rec, req)
  1988  	response := resolver.rec.Result()
  1989  	resolver.t.Logf("responded with: %v", response)
  1990  	return response, nil
  1991  }
  1992  
  1993  func TestSendWebhookToArgoCd(t *testing.T) {
  1994  	tcs := []struct {
  1995  		Name    string
  1996  		Changes TransformerResult
  1997  		webUrl  string
  1998  		branch  string
  1999  	}{
  2000  		{
  2001  			Name: "webhook",
  2002  			Changes: TransformerResult{
  2003  
  2004  				Commits: &CommitIds{
  2005  					Current:  git.NewOidFromBytes([]byte{'C', 'U', 'R', 'R', 'E', 'N', 'T', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}),
  2006  					Previous: git.NewOidFromBytes([]byte{'P', 'R', 'E', 'V', 'I', 'O', 'U', 'S', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}),
  2007  				},
  2008  			},
  2009  			webUrl: "http://example.com",
  2010  			branch: "examplebranch",
  2011  		},
  2012  	}
  2013  	for _, tc := range tcs {
  2014  		tc := tc
  2015  		ctx, cancel := context.WithCancel(context.Background())
  2016  		defer cancel()
  2017  		t.Run(tc.Name, func(t *testing.T) {
  2018  			t.Parallel()
  2019  
  2020  			// given
  2021  			logger, err := zap.NewDevelopment()
  2022  			if err != nil {
  2023  				t.Fatalf("error creating logger: %v", err)
  2024  			}
  2025  			dir := t.TempDir()
  2026  			path := path.Join(dir, "repo")
  2027  			repo, _, err := New2(
  2028  				ctx,
  2029  				RepositoryConfig{
  2030  					URL:  fmt.Sprintf("file://%s", path),
  2031  					Path: path,
  2032  				},
  2033  			)
  2034  			if err != nil {
  2035  				t.Fatalf("new: expected no error, got '%v'", err)
  2036  			}
  2037  			repoInternal := repo.(*repository)
  2038  			repoInternal.config.ArgoWebhookUrl = "http://argo.example.com"
  2039  			rec := httptest.NewRecorder()
  2040  			resolver := TestWebhookResolver{
  2041  				t:        t,
  2042  				rec:      rec,
  2043  				requests: make(chan *http.Request, 1),
  2044  			}
  2045  			repoInternal.config.WebhookResolver = resolver
  2046  
  2047  			// when
  2048  			repoInternal.config.WebURL = tc.webUrl
  2049  			repoInternal.config.Branch = tc.branch
  2050  			repoInternal.sendWebhookToArgoCd(ctx, logger, &tc.Changes)
  2051  
  2052  			// then
  2053  			req := <-resolver.requests
  2054  			buf := make([]byte, req.ContentLength)
  2055  			if _, err = io.ReadFull(req.Body, buf); err != nil {
  2056  				t.Errorf("error reading request body: %v", err)
  2057  			}
  2058  			var jsonRequest map[string]any
  2059  			if err = json.Unmarshal(buf, &jsonRequest); err != nil {
  2060  				t.Errorf("Error parsing request body '%s' as json: %v", string(buf), err)
  2061  			}
  2062  			after := jsonRequest["after"].(string)
  2063  			if after != tc.Changes.Commits.Current.String() {
  2064  				t.Fatalf("after '%s' does not match current '%s'", after, tc.Changes.Commits.Current)
  2065  			}
  2066  			before := jsonRequest["before"].(string)
  2067  			if before != tc.Changes.Commits.Previous.String() {
  2068  				t.Fatalf("before '%s' does not match previous '%s'", before, tc.Changes.Commits.Previous)
  2069  			}
  2070  			ref := jsonRequest["ref"].(string)
  2071  			if ref != fmt.Sprintf("refs/heads/%s", tc.branch) {
  2072  				t.Fatalf("refs '%s' does not match expected for branch given as '%s'", ref, tc.branch)
  2073  			}
  2074  			repository := jsonRequest["repository"].(map[string]any)
  2075  			htmlUrl := repository["html_url"].(string)
  2076  			if htmlUrl != tc.webUrl {
  2077  				t.Fatalf("repository/html_url '%s' does not match expected for webUrl given as '%s'", htmlUrl, tc.webUrl)
  2078  			}
  2079  		})
  2080  	}
  2081  }
  2082  func TestLimit(t *testing.T) {
  2083  	transformers := []Transformer{
  2084  		&CreateEnvironment{
  2085  			Environment: "production",
  2086  			Config:      config.EnvironmentConfig{Upstream: &config.EnvironmentConfigUpstream{Latest: true}},
  2087  		},
  2088  		&CreateApplicationVersion{
  2089  			Application: "test",
  2090  			Manifests: map[string]string{
  2091  				"production": "manifest",
  2092  			},
  2093  		},
  2094  		&CreateApplicationVersion{
  2095  			Application: "test",
  2096  			Manifests: map[string]string{
  2097  				"production": "manifest2",
  2098  			},
  2099  		},
  2100  	}
  2101  	tcs := []struct {
  2102  		Name               string
  2103  		numberBatchActions int
  2104  		ShouldSucceed      bool
  2105  		limit              int
  2106  		Setup              []Transformer
  2107  		ExpectedError      error
  2108  	}{
  2109  		{
  2110  			Name:               "less than maximum number of requests",
  2111  			ShouldSucceed:      true,
  2112  			limit:              5,
  2113  			numberBatchActions: 1,
  2114  			Setup:              transformers,
  2115  			ExpectedError:      nil,
  2116  		},
  2117  		{
  2118  			Name:               "more than the maximum number of requests",
  2119  			numberBatchActions: 10,
  2120  			limit:              5,
  2121  			ShouldSucceed:      false,
  2122  			Setup:              transformers,
  2123  			ExpectedError:      errMatcher{"queue is full. Queue Capacity: 5."},
  2124  		},
  2125  	}
  2126  	for _, tc := range tcs {
  2127  		tc := tc
  2128  		t.Run(tc.Name, func(t *testing.T) {
  2129  
  2130  			repo, err := setupRepositoryTestAux(t, 3)
  2131  			ctx := testutil.MakeTestContext()
  2132  			if err != nil {
  2133  				t.Fatal(err)
  2134  			}
  2135  			for _, tr := range tc.Setup {
  2136  				errCh := repo.(*repository).applyDeferred(ctx, tr)
  2137  				select {
  2138  				case e := <-repo.(*repository).queue.transformerBatches:
  2139  					dummyPushUpdateFunction := func(string, *bool) git.PushUpdateReferenceCallback { return nil }
  2140  					dummyPushActionFunction := func(options git.PushOptions, r *repository) PushActionFunc {
  2141  						return func() error {
  2142  							return nil
  2143  						}
  2144  					}
  2145  					repo.(*repository).ProcessQueueOnce(ctx, e, dummyPushUpdateFunction, dummyPushActionFunction)
  2146  				default:
  2147  				}
  2148  				select {
  2149  				case err := <-errCh:
  2150  					if err != nil {
  2151  						t.Fatal(err)
  2152  					}
  2153  				default:
  2154  				}
  2155  			}
  2156  
  2157  			expectedErrorNumber := tc.numberBatchActions - tc.limit
  2158  			actualErrorNumber := 0
  2159  			for i := 0; i < tc.numberBatchActions; i++ {
  2160  				errCh := repo.(*repository).applyDeferred(ctx, transformers[0])
  2161  				select {
  2162  				case err := <-errCh:
  2163  					if tc.ShouldSucceed {
  2164  						t.Fatalf("Got an error at iteration %d and was not expecting it %v\n", i, err)
  2165  					}
  2166  					//Should get some errors, check if they are the ones we expect
  2167  					if diff := cmp.Diff(tc.ExpectedError, err, cmpopts.EquateErrors()); diff != "" {
  2168  						t.Errorf("error mismatch (-want, +got):\n%s", diff)
  2169  					}
  2170  					actualErrorNumber += 1
  2171  				default:
  2172  					// If there is no error,
  2173  				}
  2174  			}
  2175  			if expectedErrorNumber > 0 && expectedErrorNumber != actualErrorNumber {
  2176  				t.Errorf("error number mismatch expected: %d, got %d", expectedErrorNumber, actualErrorNumber)
  2177  			}
  2178  		})
  2179  	}
  2180  }
  2181  
  2182  func setupRepositoryTestAux(t *testing.T, commits uint) (Repository, error) {
  2183  	t.Parallel()
  2184  	dir := t.TempDir()
  2185  	remoteDir := path.Join(dir, "remote")
  2186  	localDir := path.Join(dir, "local")
  2187  	cmd := exec.Command("git", "init", "--bare", remoteDir)
  2188  	cmd.Start()
  2189  	cmd.Wait()
  2190  	t.Logf("test created dir: %s", localDir)
  2191  	repo, _, err := New2(
  2192  		testutil.MakeTestContext(),
  2193  		RepositoryConfig{
  2194  			URL:                    remoteDir,
  2195  			Path:                   localDir,
  2196  			CommitterEmail:         "kuberpult@freiheit.com",
  2197  			CommitterName:          "kuberpult",
  2198  			EnvironmentConfigsPath: filepath.Join(remoteDir, "..", "environment_configs.json"),
  2199  			MaximumCommitsPerPush:  commits,
  2200  		},
  2201  	)
  2202  	if err != nil {
  2203  		t.Fatal(err)
  2204  	}
  2205  	return repo, nil
  2206  }