github.com/argoproj/argo-cd/v3@v3.2.1/reposerver/repository/repository_test.go (about)

     1  package repository
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/json"
     7  	"errors"
     8  	"fmt"
     9  	goio "io"
    10  	"io/fs"
    11  	"net/mail"
    12  	"os"
    13  	"os/exec"
    14  	"path"
    15  	"path/filepath"
    16  	"regexp"
    17  	"slices"
    18  	"sort"
    19  	"strings"
    20  	"sync"
    21  	"testing"
    22  	"time"
    23  
    24  	log "github.com/sirupsen/logrus"
    25  	"k8s.io/apimachinery/pkg/api/resource"
    26  	"k8s.io/apimachinery/pkg/util/intstr"
    27  
    28  	"github.com/argoproj/argo-cd/v3/util/oci"
    29  
    30  	cacheutil "github.com/argoproj/argo-cd/v3/util/cache"
    31  
    32  	"github.com/stretchr/testify/assert"
    33  	"github.com/stretchr/testify/mock"
    34  	"github.com/stretchr/testify/require"
    35  	appsv1 "k8s.io/api/apps/v1"
    36  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    37  	"k8s.io/apimachinery/pkg/runtime"
    38  	"sigs.k8s.io/yaml"
    39  
    40  	"github.com/argoproj/argo-cd/v3/common"
    41  	"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
    42  	"github.com/argoproj/argo-cd/v3/reposerver/apiclient"
    43  	"github.com/argoproj/argo-cd/v3/reposerver/cache"
    44  	repositorymocks "github.com/argoproj/argo-cd/v3/reposerver/cache/mocks"
    45  	"github.com/argoproj/argo-cd/v3/reposerver/metrics"
    46  	fileutil "github.com/argoproj/argo-cd/v3/test/fixture/path"
    47  	"github.com/argoproj/argo-cd/v3/util/argo"
    48  	"github.com/argoproj/argo-cd/v3/util/git"
    49  	gitmocks "github.com/argoproj/argo-cd/v3/util/git/mocks"
    50  	"github.com/argoproj/argo-cd/v3/util/helm"
    51  	helmmocks "github.com/argoproj/argo-cd/v3/util/helm/mocks"
    52  	utilio "github.com/argoproj/argo-cd/v3/util/io"
    53  	iomocks "github.com/argoproj/argo-cd/v3/util/io/mocks"
    54  	ocimocks "github.com/argoproj/argo-cd/v3/util/oci/mocks"
    55  	"github.com/argoproj/argo-cd/v3/util/settings"
    56  )
    57  
    58  const testSignature = `gpg: Signature made Wed Feb 26 23:22:34 2020 CET
    59  gpg:                using RSA key 4AEE18F83AFDEB23
    60  gpg: Good signature from "GitHub (web-flow commit signing) <noreply@github.com>" [ultimate]
    61  `
    62  
    63  type clientFunc func(*gitmocks.Client, *helmmocks.Client, *ocimocks.Client, *iomocks.TempPaths)
    64  
    65  type repoCacheMocks struct {
    66  	mock.Mock
    67  	cacheutilCache *cacheutil.Cache
    68  	cache          *cache.Cache
    69  	mockCache      *repositorymocks.MockRepoCache
    70  }
    71  
    72  type newGitRepoHelmChartOptions struct {
    73  	chartName string
    74  	// valuesFiles is a map of the values file name to the key/value pairs to be written to the file
    75  	valuesFiles map[string]map[string]string
    76  }
    77  
    78  type newGitRepoOptions struct {
    79  	path             string
    80  	createPath       bool
    81  	remote           string
    82  	addEmptyCommit   bool
    83  	helmChartOptions newGitRepoHelmChartOptions
    84  }
    85  
    86  func newCacheMocks() *repoCacheMocks {
    87  	return newCacheMocksWithOpts(1*time.Minute, 1*time.Minute, 10*time.Second)
    88  }
    89  
    90  func newCacheMocksWithOpts(repoCacheExpiration, revisionCacheExpiration, revisionCacheLockTimeout time.Duration) *repoCacheMocks {
    91  	mockRepoCache := repositorymocks.NewMockRepoCache(&repositorymocks.MockCacheOptions{
    92  		RepoCacheExpiration:     1 * time.Minute,
    93  		RevisionCacheExpiration: 1 * time.Minute,
    94  		ReadDelay:               0,
    95  		WriteDelay:              0,
    96  	})
    97  	cacheutilCache := cacheutil.NewCache(mockRepoCache.RedisClient)
    98  	return &repoCacheMocks{
    99  		cacheutilCache: cacheutilCache,
   100  		cache:          cache.NewCache(cacheutilCache, repoCacheExpiration, revisionCacheExpiration, revisionCacheLockTimeout),
   101  		mockCache:      mockRepoCache,
   102  	}
   103  }
   104  
   105  func newServiceWithMocks(t *testing.T, root string, signed bool) (*Service, *gitmocks.Client, *repoCacheMocks) {
   106  	t.Helper()
   107  	root, err := filepath.Abs(root)
   108  	if err != nil {
   109  		panic(err)
   110  	}
   111  	return newServiceWithOpt(t, func(gitClient *gitmocks.Client, helmClient *helmmocks.Client, ociClient *ocimocks.Client, paths *iomocks.TempPaths) {
   112  		gitClient.On("Init").Return(nil)
   113  		gitClient.On("IsRevisionPresent", mock.Anything).Return(false)
   114  		gitClient.On("Fetch", mock.Anything).Return(nil)
   115  		gitClient.On("Checkout", mock.Anything, mock.Anything).Return("", nil)
   116  		gitClient.On("LsRemote", mock.Anything).Return(mock.Anything, nil)
   117  		gitClient.On("CommitSHA").Return(mock.Anything, nil)
   118  		gitClient.On("Root").Return(root)
   119  		gitClient.On("IsAnnotatedTag").Return(false)
   120  		if signed {
   121  			gitClient.On("VerifyCommitSignature", mock.Anything).Return(testSignature, nil)
   122  		} else {
   123  			gitClient.On("VerifyCommitSignature", mock.Anything).Return("", nil)
   124  		}
   125  
   126  		chart := "my-chart"
   127  		oobChart := "out-of-bounds-chart"
   128  		version := "1.1.0"
   129  		helmClient.On("GetIndex", mock.AnythingOfType("bool"), mock.Anything).Return(&helm.Index{Entries: map[string]helm.Entries{
   130  			chart:    {{Version: "1.0.0"}, {Version: version}},
   131  			oobChart: {{Version: "1.0.0"}, {Version: version}},
   132  		}}, nil)
   133  		helmClient.On("GetTags", mock.Anything, mock.Anything).Return(nil, nil)
   134  		helmClient.On("ExtractChart", chart, version, false, int64(0), false).Return("./testdata/my-chart", utilio.NopCloser, nil)
   135  		helmClient.On("ExtractChart", oobChart, version, false, int64(0), false).Return("./testdata2/out-of-bounds-chart", utilio.NopCloser, nil)
   136  		helmClient.On("CleanChartCache", chart, version).Return(nil)
   137  		helmClient.On("CleanChartCache", oobChart, version).Return(nil)
   138  		helmClient.On("DependencyBuild").Return(nil)
   139  
   140  		ociClient.On("GetTags", mock.Anything, mock.Anything).Return(nil)
   141  		ociClient.On("ResolveRevision", mock.Anything, mock.Anything, mock.Anything).Return("", nil)
   142  		ociClient.On("Extract", mock.Anything, mock.Anything).Return("./testdata/my-chart", utilio.NopCloser, nil)
   143  
   144  		paths.On("Add", mock.Anything, mock.Anything).Return(root, nil)
   145  		paths.On("GetPath", mock.Anything).Return(root, nil)
   146  		paths.On("GetPathIfExists", mock.Anything).Return(root, nil)
   147  		paths.On("GetPaths").Return(map[string]string{"fake-nonce": root})
   148  	}, root)
   149  }
   150  
   151  func newServiceWithOpt(t *testing.T, cf clientFunc, root string) (*Service, *gitmocks.Client, *repoCacheMocks) {
   152  	t.Helper()
   153  	helmClient := &helmmocks.Client{}
   154  	gitClient := &gitmocks.Client{}
   155  	ociClient := &ocimocks.Client{}
   156  	paths := &iomocks.TempPaths{}
   157  	cf(gitClient, helmClient, ociClient, paths)
   158  	cacheMocks := newCacheMocks()
   159  	t.Cleanup(cacheMocks.mockCache.StopRedisCallback)
   160  	service := NewService(metrics.NewMetricsServer(), cacheMocks.cache, RepoServerInitConstants{ParallelismLimit: 1}, &git.NoopCredsStore{}, root)
   161  
   162  	service.newGitClient = func(_ string, _ string, _ git.Creds, _ bool, _ bool, _ string, _ string, _ ...git.ClientOpts) (client git.Client, e error) {
   163  		return gitClient, nil
   164  	}
   165  	service.newHelmClient = func(_ string, _ helm.Creds, _ bool, _ string, _ string, _ ...helm.ClientOpts) helm.Client {
   166  		return helmClient
   167  	}
   168  	service.newOCIClient = func(_ string, _ oci.Creds, _ string, _ string, _ []string, _ ...oci.ClientOpts) (oci.Client, error) {
   169  		return ociClient, nil
   170  	}
   171  	service.gitRepoInitializer = func(_ string) goio.Closer {
   172  		return utilio.NopCloser
   173  	}
   174  	service.gitRepoPaths = paths
   175  	return service, gitClient, cacheMocks
   176  }
   177  
   178  func newService(t *testing.T, root string) *Service {
   179  	t.Helper()
   180  	service, _, _ := newServiceWithMocks(t, root, false)
   181  	return service
   182  }
   183  
   184  func newServiceWithSignature(t *testing.T, root string) *Service {
   185  	t.Helper()
   186  	service, _, _ := newServiceWithMocks(t, root, true)
   187  	return service
   188  }
   189  
   190  func newServiceWithCommitSHA(t *testing.T, root, revision string) *Service {
   191  	t.Helper()
   192  	var revisionErr error
   193  
   194  	commitSHARegex := regexp.MustCompile("^[0-9A-Fa-f]{40}$")
   195  	if !commitSHARegex.MatchString(revision) {
   196  		revisionErr = errors.New("not a commit SHA")
   197  	}
   198  
   199  	service, gitClient, _ := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, _ *ocimocks.Client, paths *iomocks.TempPaths) {
   200  		gitClient.On("Init").Return(nil)
   201  		gitClient.On("IsRevisionPresent", mock.Anything).Return(false)
   202  		gitClient.On("Fetch", mock.Anything).Return(nil)
   203  		gitClient.On("Checkout", mock.Anything, mock.Anything).Return("", nil)
   204  		gitClient.On("LsRemote", revision).Return(revision, revisionErr)
   205  		gitClient.On("CommitSHA").Return("632039659e542ed7de0c170a4fcc1c571b288fc0", nil)
   206  		gitClient.On("Root").Return(root)
   207  		paths.On("GetPath", mock.Anything).Return(root, nil)
   208  		paths.On("GetPathIfExists", mock.Anything).Return(root, nil)
   209  	}, root)
   210  
   211  	service.newGitClient = func(_ string, _ string, _ git.Creds, _ bool, _ bool, _ string, _ string, _ ...git.ClientOpts) (client git.Client, e error) {
   212  		return gitClient, nil
   213  	}
   214  
   215  	return service
   216  }
   217  
   218  func TestGenerateYamlManifestInDir(t *testing.T) {
   219  	service := newService(t, "../../manifests/base")
   220  
   221  	src := v1alpha1.ApplicationSource{Path: "."}
   222  	q := apiclient.ManifestRequest{
   223  		Repo:               &v1alpha1.Repository{},
   224  		ApplicationSource:  &src,
   225  		ProjectName:        "something",
   226  		ProjectSourceRepos: []string{"*"},
   227  	}
   228  
   229  	// update this value if we add/remove manifests
   230  	const countOfManifests = 50
   231  
   232  	res1, err := service.GenerateManifest(t.Context(), &q)
   233  
   234  	require.NoError(t, err)
   235  	assert.Len(t, res1.Manifests, countOfManifests)
   236  
   237  	// this will test concatenated manifests to verify we split YAMLs correctly
   238  	res2, err := GenerateManifests(t.Context(), "./testdata/concatenated", "/", "", &q, false, &git.NoopCredsStore{}, resource.MustParse("0"), nil)
   239  	require.NoError(t, err)
   240  	assert.Len(t, res2.Manifests, 3)
   241  }
   242  
   243  func Test_GenerateManifest_KustomizeWithVersionOverride(t *testing.T) {
   244  	t.Parallel()
   245  
   246  	service := newService(t, "../../util/kustomize/testdata/kustomize-with-version-override")
   247  
   248  	src := v1alpha1.ApplicationSource{Path: "."}
   249  	q := apiclient.ManifestRequest{
   250  		Repo:               &v1alpha1.Repository{},
   251  		ApplicationSource:  &src,
   252  		ProjectName:        "something",
   253  		ProjectSourceRepos: []string{"*"},
   254  		KustomizeOptions: &v1alpha1.KustomizeOptions{
   255  			Versions: []v1alpha1.KustomizeVersion{},
   256  		},
   257  	}
   258  
   259  	_, err := service.GenerateManifest(t.Context(), &q)
   260  	require.ErrorAs(t, err, &settings.KustomizeVersionNotRegisteredError{Version: "v1.2.3"})
   261  
   262  	q.KustomizeOptions.Versions = []v1alpha1.KustomizeVersion{
   263  		{
   264  			Name: "v1.2.3",
   265  			Path: "kustomize",
   266  		},
   267  	}
   268  
   269  	res, err := service.GenerateManifest(t.Context(), &q)
   270  	require.NoError(t, err)
   271  	assert.NotNil(t, res)
   272  }
   273  
   274  func Test_GenerateManifests_NoOutOfBoundsAccess(t *testing.T) {
   275  	t.Parallel()
   276  
   277  	testCases := []struct {
   278  		name                    string
   279  		outOfBoundsFilename     string
   280  		outOfBoundsFileContents string
   281  		mustNotContain          string // Optional string that must not appear in error or manifest output. If empty, use outOfBoundsFileContents.
   282  	}{
   283  		{
   284  			name:                    "out of bounds JSON file should not appear in error output",
   285  			outOfBoundsFilename:     "test.json",
   286  			outOfBoundsFileContents: `{"some": "json"}`,
   287  		},
   288  		{
   289  			name:                    "malformed JSON file contents should not appear in error output",
   290  			outOfBoundsFilename:     "test.json",
   291  			outOfBoundsFileContents: "$",
   292  		},
   293  		{
   294  			name:                "out of bounds JSON manifest should not appear in manifest output",
   295  			outOfBoundsFilename: "test.json",
   296  			// JSON marshalling is deterministic. So if there's a leak, exactly this should appear in the manifests.
   297  			outOfBoundsFileContents: `{"apiVersion":"v1","kind":"Secret","metadata":{"name":"test","namespace":"default"},"type":"Opaque"}`,
   298  		},
   299  		{
   300  			name:                    "out of bounds YAML manifest should not appear in manifest output",
   301  			outOfBoundsFilename:     "test.yaml",
   302  			outOfBoundsFileContents: "apiVersion: v1\nkind: Secret\nmetadata:\n  name: test\n  namespace: default\ntype: Opaque",
   303  			mustNotContain:          `{"apiVersion":"v1","kind":"Secret","metadata":{"name":"test","namespace":"default"},"type":"Opaque"}`,
   304  		},
   305  	}
   306  
   307  	for _, testCase := range testCases {
   308  		testCaseCopy := testCase
   309  		t.Run(testCaseCopy.name, func(t *testing.T) {
   310  			t.Parallel()
   311  
   312  			outOfBoundsDir := t.TempDir()
   313  			outOfBoundsFile := path.Join(outOfBoundsDir, testCaseCopy.outOfBoundsFilename)
   314  			err := os.WriteFile(outOfBoundsFile, []byte(testCaseCopy.outOfBoundsFileContents), os.FileMode(0o444))
   315  			require.NoError(t, err)
   316  
   317  			repoDir := t.TempDir()
   318  			err = os.Symlink(outOfBoundsFile, path.Join(repoDir, testCaseCopy.outOfBoundsFilename))
   319  			require.NoError(t, err)
   320  
   321  			mustNotContain := testCaseCopy.outOfBoundsFileContents
   322  			if testCaseCopy.mustNotContain != "" {
   323  				mustNotContain = testCaseCopy.mustNotContain
   324  			}
   325  
   326  			q := apiclient.ManifestRequest{
   327  				Repo: &v1alpha1.Repository{}, ApplicationSource: &v1alpha1.ApplicationSource{}, ProjectName: "something",
   328  				ProjectSourceRepos: []string{"*"},
   329  			}
   330  			res, err := GenerateManifests(t.Context(), repoDir, "", "", &q, false, &git.NoopCredsStore{}, resource.MustParse("0"), nil)
   331  			require.Error(t, err)
   332  			assert.NotContains(t, err.Error(), mustNotContain)
   333  			require.ErrorContains(t, err, "illegal filepath")
   334  			assert.Nil(t, res)
   335  		})
   336  	}
   337  }
   338  
   339  func TestGenerateManifests_MissingSymlinkDestination(t *testing.T) {
   340  	repoDir := t.TempDir()
   341  	err := os.Symlink("/obviously/does/not/exist", path.Join(repoDir, "test.yaml"))
   342  	require.NoError(t, err)
   343  
   344  	q := apiclient.ManifestRequest{
   345  		Repo: &v1alpha1.Repository{}, ApplicationSource: &v1alpha1.ApplicationSource{}, ProjectName: "something",
   346  		ProjectSourceRepos: []string{"*"},
   347  	}
   348  	_, err = GenerateManifests(t.Context(), repoDir, "", "", &q, false, &git.NoopCredsStore{}, resource.MustParse("0"), nil)
   349  	require.NoError(t, err)
   350  }
   351  
   352  func TestGenerateManifests_K8SAPIResetCache(t *testing.T) {
   353  	service := newService(t, "../../manifests/base")
   354  
   355  	src := v1alpha1.ApplicationSource{Path: "."}
   356  	q := apiclient.ManifestRequest{
   357  		KubeVersion:        "v1.16.0",
   358  		Repo:               &v1alpha1.Repository{},
   359  		ApplicationSource:  &src,
   360  		ProjectName:        "something",
   361  		ProjectSourceRepos: []string{"*"},
   362  	}
   363  
   364  	cachedFakeResponse := &apiclient.ManifestResponse{Manifests: []string{"Fake"}, Revision: mock.Anything}
   365  
   366  	err := service.cache.SetManifests(mock.Anything, &src, q.RefSources, &q, "", "", "", "", &cache.CachedManifestResponse{ManifestResponse: cachedFakeResponse}, nil, "")
   367  	require.NoError(t, err)
   368  
   369  	res, err := service.GenerateManifest(t.Context(), &q)
   370  	require.NoError(t, err)
   371  	assert.Equal(t, cachedFakeResponse, res)
   372  
   373  	q.KubeVersion = "v1.17.0"
   374  	res, err = service.GenerateManifest(t.Context(), &q)
   375  	require.NoError(t, err)
   376  	assert.NotEqual(t, cachedFakeResponse, res)
   377  	assert.Greater(t, len(res.Manifests), 1)
   378  }
   379  
   380  func TestGenerateManifests_EmptyCache(t *testing.T) {
   381  	service, gitMocks, mockCache := newServiceWithMocks(t, "../../manifests/base", false)
   382  
   383  	src := v1alpha1.ApplicationSource{Path: "."}
   384  	q := apiclient.ManifestRequest{
   385  		Repo:               &v1alpha1.Repository{},
   386  		ApplicationSource:  &src,
   387  		ProjectName:        "something",
   388  		ProjectSourceRepos: []string{"*"},
   389  	}
   390  
   391  	err := service.cache.SetManifests(mock.Anything, &src, q.RefSources, &q, "", "", "", "", &cache.CachedManifestResponse{ManifestResponse: nil}, nil, "")
   392  	require.NoError(t, err)
   393  
   394  	res, err := service.GenerateManifest(t.Context(), &q)
   395  	require.NoError(t, err)
   396  	assert.NotEmpty(t, res.Manifests)
   397  	mockCache.mockCache.AssertCacheCalledTimes(t, &repositorymocks.CacheCallCounts{
   398  		ExternalSets:    2,
   399  		ExternalGets:    2,
   400  		ExternalDeletes: 1,
   401  	})
   402  	gitMocks.AssertCalled(t, "LsRemote", mock.Anything)
   403  	gitMocks.AssertCalled(t, "Fetch", mock.Anything)
   404  }
   405  
   406  // Test that when Generate manifest is called with a source that is ref only it does not try to generate manifests or hit the manifest cache
   407  // but it does resolve and cache the revision
   408  func TestGenerateManifest_RefOnlyShortCircuit(t *testing.T) {
   409  	lsremoteCalled := false
   410  	dir := t.TempDir()
   411  	repopath := dir + "/tmprepo"
   412  	repoRemote := "file://" + repopath
   413  	cacheMocks := newCacheMocks()
   414  	t.Cleanup(cacheMocks.mockCache.StopRedisCallback)
   415  	service := NewService(metrics.NewMetricsServer(), cacheMocks.cache, RepoServerInitConstants{ParallelismLimit: 1}, &git.NoopCredsStore{}, repopath)
   416  	service.newGitClient = func(rawRepoURL string, root string, creds git.Creds, insecure bool, enableLfs bool, proxy string, noProxy string, opts ...git.ClientOpts) (client git.Client, e error) {
   417  		opts = append(opts, git.WithEventHandlers(git.EventHandlers{
   418  			// Primary check, we want to make sure ls-remote is not called when the item is in cache
   419  			OnLsRemote: func(_ string) func() {
   420  				return func() {
   421  					lsremoteCalled = true
   422  				}
   423  			},
   424  			OnFetch: func(_ string) func() {
   425  				return func() {
   426  					assert.Fail(t, "Fetch should not be called from GenerateManifest when the source is ref only")
   427  				}
   428  			},
   429  		}))
   430  		gitClient, err := git.NewClientExt(rawRepoURL, root, creds, insecure, enableLfs, proxy, noProxy, opts...)
   431  		return gitClient, err
   432  	}
   433  	revision := initGitRepo(t, newGitRepoOptions{
   434  		path:           repopath,
   435  		createPath:     true,
   436  		remote:         repoRemote,
   437  		addEmptyCommit: true,
   438  	})
   439  	src := v1alpha1.ApplicationSource{RepoURL: repoRemote, TargetRevision: "HEAD", Ref: "test-ref"}
   440  	repo := &v1alpha1.Repository{
   441  		Repo: repoRemote,
   442  	}
   443  	q := apiclient.ManifestRequest{
   444  		Repo:               repo,
   445  		Revision:           "HEAD",
   446  		HasMultipleSources: true,
   447  		ApplicationSource:  &src,
   448  		ProjectName:        "default",
   449  		ProjectSourceRepos: []string{"*"},
   450  	}
   451  	_, err := service.GenerateManifest(t.Context(), &q)
   452  	require.NoError(t, err)
   453  	cacheMocks.mockCache.AssertCacheCalledTimes(t, &repositorymocks.CacheCallCounts{
   454  		ExternalSets: 2,
   455  		ExternalGets: 2,
   456  	})
   457  	assert.True(t, lsremoteCalled, "ls-remote should be called when the source is ref only")
   458  	var revisions [][2]string
   459  	require.NoError(t, cacheMocks.cacheutilCache.GetItem("git-refs|"+repoRemote, &revisions))
   460  	assert.ElementsMatch(t, [][2]string{{"refs/heads/main", revision}, {"HEAD", "ref: refs/heads/main"}}, revisions)
   461  }
   462  
   463  // Test that calling manifest generation on source helm reference helm files that when the revision is cached it does not call ls-remote
   464  func TestGenerateManifestsHelmWithRefs_CachedNoLsRemote(t *testing.T) {
   465  	dir := t.TempDir()
   466  	repopath := dir + "/tmprepo"
   467  	cacheMocks := newCacheMocks()
   468  	t.Cleanup(func() {
   469  		cacheMocks.mockCache.StopRedisCallback()
   470  		err := filepath.WalkDir(dir,
   471  			func(path string, _ fs.DirEntry, err error) error {
   472  				if err == nil {
   473  					return os.Chmod(path, 0o777)
   474  				}
   475  				return err
   476  			})
   477  		require.NoError(t, err)
   478  	})
   479  	service := NewService(metrics.NewMetricsServer(), cacheMocks.cache, RepoServerInitConstants{ParallelismLimit: 1}, &git.NoopCredsStore{}, repopath)
   480  	var gitClient git.Client
   481  	var err error
   482  	service.newGitClient = func(rawRepoURL string, root string, creds git.Creds, insecure bool, enableLfs bool, proxy string, noProxy string, opts ...git.ClientOpts) (client git.Client, e error) {
   483  		opts = append(opts, git.WithEventHandlers(git.EventHandlers{
   484  			// Primary check, we want to make sure ls-remote is not called when the item is in cache
   485  			OnLsRemote: func(_ string) func() {
   486  				return func() {
   487  					assert.Fail(t, "LsRemote should not be called when the item is in cache")
   488  				}
   489  			},
   490  		}))
   491  		gitClient, err = git.NewClientExt(rawRepoURL, root, creds, insecure, enableLfs, proxy, noProxy, opts...)
   492  		return gitClient, err
   493  	}
   494  	repoRemote := "file://" + repopath
   495  	revision := initGitRepo(t, newGitRepoOptions{
   496  		path:       repopath,
   497  		createPath: true,
   498  		remote:     repoRemote,
   499  		helmChartOptions: newGitRepoHelmChartOptions{
   500  			chartName:   "my-chart",
   501  			valuesFiles: map[string]map[string]string{"test.yaml": {"testval": "test"}},
   502  		},
   503  	})
   504  	src := v1alpha1.ApplicationSource{RepoURL: repoRemote, Path: ".", TargetRevision: "HEAD", Helm: &v1alpha1.ApplicationSourceHelm{
   505  		ValueFiles: []string{"$ref/test.yaml"},
   506  	}}
   507  	repo := &v1alpha1.Repository{
   508  		Repo: repoRemote,
   509  	}
   510  	q := apiclient.ManifestRequest{
   511  		Repo:               repo,
   512  		Revision:           "HEAD",
   513  		HasMultipleSources: true,
   514  		ApplicationSource:  &src,
   515  		ProjectName:        "default",
   516  		ProjectSourceRepos: []string{"*"},
   517  		RefSources:         map[string]*v1alpha1.RefTarget{"$ref": {TargetRevision: "HEAD", Repo: *repo}},
   518  	}
   519  	err = cacheMocks.cacheutilCache.SetItem("git-refs|"+repoRemote, [][2]string{{"HEAD", revision}}, nil)
   520  	require.NoError(t, err)
   521  	_, err = service.GenerateManifest(t.Context(), &q)
   522  	require.NoError(t, err)
   523  	cacheMocks.mockCache.AssertCacheCalledTimes(t, &repositorymocks.CacheCallCounts{
   524  		ExternalSets: 2,
   525  		ExternalGets: 5,
   526  	})
   527  }
   528  
   529  // ensure we can use a semver constraint range (>= 1.0.0) and get back the correct chart (1.0.0)
   530  func TestHelmManifestFromChartRepo(t *testing.T) {
   531  	root := t.TempDir()
   532  	service, gitMocks, mockCache := newServiceWithMocks(t, root, false)
   533  	source := &v1alpha1.ApplicationSource{Chart: "my-chart", TargetRevision: ">= 1.0.0"}
   534  	request := &apiclient.ManifestRequest{
   535  		Repo: &v1alpha1.Repository{}, ApplicationSource: source, NoCache: true, ProjectName: "something",
   536  		ProjectSourceRepos: []string{"*"},
   537  	}
   538  	response, err := service.GenerateManifest(t.Context(), request)
   539  	require.NoError(t, err)
   540  	assert.NotNil(t, response)
   541  	assert.Equal(t, &apiclient.ManifestResponse{
   542  		Manifests:  []string{"{\"apiVersion\":\"v1\",\"kind\":\"ConfigMap\",\"metadata\":{\"name\":\"my-map\"}}"},
   543  		Namespace:  "",
   544  		Server:     "",
   545  		Revision:   "1.1.0",
   546  		SourceType: "Helm",
   547  		Commands:   []string{`helm template . --name-template "" --include-crds`},
   548  	}, response)
   549  	mockCache.mockCache.AssertCacheCalledTimes(t, &repositorymocks.CacheCallCounts{
   550  		ExternalSets: 1,
   551  		ExternalGets: 0,
   552  	})
   553  	gitMocks.AssertNotCalled(t, "LsRemote", mock.Anything)
   554  }
   555  
   556  func TestHelmChartReferencingExternalValues(t *testing.T) {
   557  	service := newService(t, ".")
   558  	spec := v1alpha1.ApplicationSpec{
   559  		Sources: []v1alpha1.ApplicationSource{
   560  			{RepoURL: "https://helm.example.com", Chart: "my-chart", TargetRevision: ">= 1.0.0", Helm: &v1alpha1.ApplicationSourceHelm{
   561  				ValueFiles: []string{"$ref/testdata/my-chart/my-chart-values.yaml"},
   562  			}},
   563  			{Ref: "ref", RepoURL: "https://git.example.com/test/repo"},
   564  		},
   565  	}
   566  	refSources, err := argo.GetRefSources(t.Context(), spec.Sources, spec.Project, func(_ context.Context, _ string, _ string) (*v1alpha1.Repository, error) {
   567  		return &v1alpha1.Repository{
   568  			Repo: "https://git.example.com/test/repo",
   569  		}, nil
   570  	}, []string{})
   571  	require.NoError(t, err)
   572  	request := &apiclient.ManifestRequest{
   573  		Repo: &v1alpha1.Repository{}, ApplicationSource: &spec.Sources[0], NoCache: true, RefSources: refSources, HasMultipleSources: true, ProjectName: "something",
   574  		ProjectSourceRepos: []string{"*"},
   575  	}
   576  	response, err := service.GenerateManifest(t.Context(), request)
   577  	require.NoError(t, err)
   578  	assert.NotNil(t, response)
   579  	assert.Equal(t, &apiclient.ManifestResponse{
   580  		Manifests:  []string{"{\"apiVersion\":\"v1\",\"kind\":\"ConfigMap\",\"metadata\":{\"name\":\"my-map\"}}"},
   581  		Namespace:  "",
   582  		Server:     "",
   583  		Revision:   "1.1.0",
   584  		SourceType: "Helm",
   585  		Commands:   []string{`helm template . --name-template "" --values ./testdata/my-chart/my-chart-values.yaml --include-crds`},
   586  	}, response)
   587  }
   588  
   589  func TestHelmChartReferencingExternalValues_InvalidRefs(t *testing.T) {
   590  	spec := v1alpha1.ApplicationSpec{
   591  		Sources: []v1alpha1.ApplicationSource{
   592  			{RepoURL: "https://helm.example.com", Chart: "my-chart", TargetRevision: ">= 1.0.0", Helm: &v1alpha1.ApplicationSourceHelm{
   593  				ValueFiles: []string{"$ref/testdata/my-chart/my-chart-values.yaml"},
   594  			}},
   595  			{RepoURL: "https://git.example.com/test/repo"},
   596  		},
   597  	}
   598  
   599  	// Empty refsource
   600  	service := newService(t, ".")
   601  
   602  	getRepository := func(_ context.Context, _ string, _ string) (*v1alpha1.Repository, error) {
   603  		return &v1alpha1.Repository{
   604  			Repo: "https://git.example.com/test/repo",
   605  		}, nil
   606  	}
   607  
   608  	refSources, err := argo.GetRefSources(t.Context(), spec.Sources, spec.Project, getRepository, []string{})
   609  	require.NoError(t, err)
   610  
   611  	request := &apiclient.ManifestRequest{
   612  		Repo: &v1alpha1.Repository{}, ApplicationSource: &spec.Sources[0], NoCache: true, RefSources: refSources, HasMultipleSources: true, ProjectName: "something",
   613  		ProjectSourceRepos: []string{"*"},
   614  	}
   615  	response, err := service.GenerateManifest(t.Context(), request)
   616  	require.Error(t, err)
   617  	assert.Nil(t, response)
   618  
   619  	// Invalid ref
   620  	service = newService(t, ".")
   621  
   622  	spec.Sources[1].Ref = "Invalid"
   623  	refSources, err = argo.GetRefSources(t.Context(), spec.Sources, spec.Project, getRepository, []string{})
   624  	require.NoError(t, err)
   625  
   626  	request = &apiclient.ManifestRequest{
   627  		Repo: &v1alpha1.Repository{}, ApplicationSource: &spec.Sources[0], NoCache: true, RefSources: refSources, HasMultipleSources: true, ProjectName: "something",
   628  		ProjectSourceRepos: []string{"*"},
   629  	}
   630  	response, err = service.GenerateManifest(t.Context(), request)
   631  	require.Error(t, err)
   632  	assert.Nil(t, response)
   633  
   634  	// Helm chart as ref (unsupported)
   635  	service = newService(t, ".")
   636  
   637  	spec.Sources[1].Ref = "ref"
   638  	spec.Sources[1].Chart = "helm-chart"
   639  	refSources, err = argo.GetRefSources(t.Context(), spec.Sources, spec.Project, getRepository, []string{})
   640  	require.NoError(t, err)
   641  
   642  	request = &apiclient.ManifestRequest{
   643  		Repo: &v1alpha1.Repository{}, ApplicationSource: &spec.Sources[0], NoCache: true, RefSources: refSources, HasMultipleSources: true, ProjectName: "something",
   644  		ProjectSourceRepos: []string{"*"},
   645  	}
   646  	response, err = service.GenerateManifest(t.Context(), request)
   647  	require.Error(t, err)
   648  	assert.Nil(t, response)
   649  }
   650  
   651  func TestHelmChartReferencingExternalValues_OutOfBounds_Symlink(t *testing.T) {
   652  	service := newService(t, ".")
   653  	err := os.Mkdir("testdata/oob-symlink", 0o755)
   654  	require.NoError(t, err)
   655  	t.Cleanup(func() {
   656  		err = os.RemoveAll("testdata/oob-symlink")
   657  		require.NoError(t, err)
   658  	})
   659  	// Create a symlink to a file outside the repo
   660  	err = os.Symlink("../../../values.yaml", "./testdata/oob-symlink/oob-symlink.yaml")
   661  	// Create a regular file to reference from another source
   662  	err = os.WriteFile("./testdata/oob-symlink/values.yaml", []byte("foo: bar"), 0o644)
   663  	require.NoError(t, err)
   664  	spec := v1alpha1.ApplicationSpec{
   665  		Project: "default",
   666  		Sources: []v1alpha1.ApplicationSource{
   667  			{RepoURL: "https://helm.example.com", Chart: "my-chart", TargetRevision: ">= 1.0.0", Helm: &v1alpha1.ApplicationSourceHelm{
   668  				// Reference `ref` but do not use the oob symlink. The mere existence of the link should be enough to
   669  				// cause an error.
   670  				ValueFiles: []string{"$ref/testdata/oob-symlink/values.yaml"},
   671  			}},
   672  			{Ref: "ref", RepoURL: "https://git.example.com/test/repo"},
   673  		},
   674  	}
   675  	refSources, err := argo.GetRefSources(t.Context(), spec.Sources, spec.Project, func(_ context.Context, _ string, _ string) (*v1alpha1.Repository, error) {
   676  		return &v1alpha1.Repository{
   677  			Repo: "https://git.example.com/test/repo",
   678  		}, nil
   679  	}, []string{})
   680  	require.NoError(t, err)
   681  	request := &apiclient.ManifestRequest{Repo: &v1alpha1.Repository{}, ApplicationSource: &spec.Sources[0], NoCache: true, RefSources: refSources, HasMultipleSources: true}
   682  	_, err = service.GenerateManifest(t.Context(), request)
   683  	require.Error(t, err)
   684  }
   685  
   686  func TestGenerateManifestsUseExactRevision(t *testing.T) {
   687  	service, gitClient, _ := newServiceWithMocks(t, ".", false)
   688  
   689  	src := v1alpha1.ApplicationSource{Path: "./testdata/recurse", Directory: &v1alpha1.ApplicationSourceDirectory{Recurse: true}}
   690  
   691  	q := apiclient.ManifestRequest{
   692  		Repo: &v1alpha1.Repository{}, ApplicationSource: &src, Revision: "abc", ProjectName: "something",
   693  		ProjectSourceRepos: []string{"*"},
   694  	}
   695  
   696  	res1, err := service.GenerateManifest(t.Context(), &q)
   697  	require.NoError(t, err)
   698  	assert.Len(t, res1.Manifests, 2)
   699  	assert.Equal(t, "abc", gitClient.Calls[0].Arguments[0])
   700  }
   701  
   702  func TestRecurseManifestsInDir(t *testing.T) {
   703  	service := newService(t, ".")
   704  
   705  	src := v1alpha1.ApplicationSource{Path: "./testdata/recurse", Directory: &v1alpha1.ApplicationSourceDirectory{Recurse: true}}
   706  
   707  	q := apiclient.ManifestRequest{
   708  		Repo: &v1alpha1.Repository{}, ApplicationSource: &src, ProjectName: "something",
   709  		ProjectSourceRepos: []string{"*"},
   710  	}
   711  
   712  	res1, err := service.GenerateManifest(t.Context(), &q)
   713  	require.NoError(t, err)
   714  	assert.Len(t, res1.Manifests, 2)
   715  }
   716  
   717  func TestInvalidManifestsInDir(t *testing.T) {
   718  	service := newService(t, ".")
   719  
   720  	src := v1alpha1.ApplicationSource{Path: "./testdata/invalid-manifests", Directory: &v1alpha1.ApplicationSourceDirectory{Recurse: true}}
   721  
   722  	q := apiclient.ManifestRequest{Repo: &v1alpha1.Repository{}, ApplicationSource: &src}
   723  
   724  	_, err := service.GenerateManifest(t.Context(), &q)
   725  	require.Error(t, err)
   726  }
   727  
   728  func TestSkippedInvalidManifestsInDir(t *testing.T) {
   729  	service := newService(t, ".")
   730  
   731  	src := v1alpha1.ApplicationSource{Path: "./testdata/invalid-manifests-skipped", Directory: &v1alpha1.ApplicationSourceDirectory{Recurse: true}}
   732  
   733  	q := apiclient.ManifestRequest{Repo: &v1alpha1.Repository{}, ApplicationSource: &src}
   734  
   735  	_, err := service.GenerateManifest(t.Context(), &q)
   736  	require.NoError(t, err)
   737  }
   738  
   739  func TestInvalidMetadata(t *testing.T) {
   740  	service := newService(t, ".")
   741  
   742  	src := v1alpha1.ApplicationSource{Path: "./testdata/invalid-metadata", Directory: &v1alpha1.ApplicationSourceDirectory{Recurse: true}}
   743  	q := apiclient.ManifestRequest{Repo: &v1alpha1.Repository{}, ApplicationSource: &src, AppLabelKey: "test", AppName: "invalid-metadata", TrackingMethod: "annotation+label"}
   744  	_, err := service.GenerateManifest(t.Context(), &q)
   745  	assert.ErrorContains(t, err, "contains non-string value in the map under key \"invalid\"")
   746  }
   747  
   748  func TestNilMetadataAccessors(t *testing.T) {
   749  	service := newService(t, ".")
   750  	expected := "{\"apiVersion\":\"v1\",\"kind\":\"ConfigMap\",\"metadata\":{\"annotations\":{\"argocd.argoproj.io/tracking-id\":\"nil-metadata-accessors:/ConfigMap:/my-map\"},\"labels\":{\"test\":\"nil-metadata-accessors\"},\"name\":\"my-map\"},\"stringData\":{\"foo\":\"bar\"}}"
   751  
   752  	src := v1alpha1.ApplicationSource{Path: "./testdata/nil-metadata-accessors", Directory: &v1alpha1.ApplicationSourceDirectory{Recurse: true}}
   753  	q := apiclient.ManifestRequest{Repo: &v1alpha1.Repository{}, ApplicationSource: &src, AppLabelKey: "test", AppName: "nil-metadata-accessors", TrackingMethod: "annotation+label"}
   754  	res, err := service.GenerateManifest(t.Context(), &q)
   755  	require.NoError(t, err)
   756  	assert.Len(t, res.Manifests, 1)
   757  	assert.Equal(t, expected, res.Manifests[0])
   758  }
   759  
   760  func TestGenerateJsonnetManifestInDir(t *testing.T) {
   761  	service := newService(t, ".")
   762  
   763  	q := apiclient.ManifestRequest{
   764  		Repo: &v1alpha1.Repository{},
   765  		ApplicationSource: &v1alpha1.ApplicationSource{
   766  			Path: "./testdata/jsonnet",
   767  			Directory: &v1alpha1.ApplicationSourceDirectory{
   768  				Jsonnet: v1alpha1.ApplicationSourceJsonnet{
   769  					ExtVars: []v1alpha1.JsonnetVar{{Name: "extVarString", Value: "extVarString"}, {Name: "extVarCode", Value: "\"extVarCode\"", Code: true}},
   770  					TLAs:    []v1alpha1.JsonnetVar{{Name: "tlaString", Value: "tlaString"}, {Name: "tlaCode", Value: "\"tlaCode\"", Code: true}},
   771  					Libs:    []string{"testdata/jsonnet/vendor"},
   772  				},
   773  			},
   774  		},
   775  		ProjectName:        "something",
   776  		ProjectSourceRepos: []string{"*"},
   777  	}
   778  	res1, err := service.GenerateManifest(t.Context(), &q)
   779  	require.NoError(t, err)
   780  	assert.Len(t, res1.Manifests, 2)
   781  }
   782  
   783  func TestGenerateJsonnetManifestInRootDir(t *testing.T) {
   784  	service := newService(t, "testdata/jsonnet-1")
   785  
   786  	q := apiclient.ManifestRequest{
   787  		Repo: &v1alpha1.Repository{},
   788  		ApplicationSource: &v1alpha1.ApplicationSource{
   789  			Path: ".",
   790  			Directory: &v1alpha1.ApplicationSourceDirectory{
   791  				Jsonnet: v1alpha1.ApplicationSourceJsonnet{
   792  					ExtVars: []v1alpha1.JsonnetVar{{Name: "extVarString", Value: "extVarString"}, {Name: "extVarCode", Value: "\"extVarCode\"", Code: true}},
   793  					TLAs:    []v1alpha1.JsonnetVar{{Name: "tlaString", Value: "tlaString"}, {Name: "tlaCode", Value: "\"tlaCode\"", Code: true}},
   794  					Libs:    []string{"."},
   795  				},
   796  			},
   797  		},
   798  		ProjectName:        "something",
   799  		ProjectSourceRepos: []string{"*"},
   800  	}
   801  	res1, err := service.GenerateManifest(t.Context(), &q)
   802  	require.NoError(t, err)
   803  	assert.Len(t, res1.Manifests, 2)
   804  }
   805  
   806  func TestGenerateJsonnetLibOutside(t *testing.T) {
   807  	service := newService(t, ".")
   808  
   809  	q := apiclient.ManifestRequest{
   810  		Repo: &v1alpha1.Repository{},
   811  		ApplicationSource: &v1alpha1.ApplicationSource{
   812  			Path: "./testdata/jsonnet",
   813  			Directory: &v1alpha1.ApplicationSourceDirectory{
   814  				Jsonnet: v1alpha1.ApplicationSourceJsonnet{
   815  					Libs: []string{"../../../testdata/jsonnet/vendor"},
   816  				},
   817  			},
   818  		},
   819  		ProjectName:        "something",
   820  		ProjectSourceRepos: []string{"*"},
   821  	}
   822  	_, err := service.GenerateManifest(t.Context(), &q)
   823  	require.ErrorContains(t, err, "file '../../../testdata/jsonnet/vendor' resolved to outside repository root")
   824  }
   825  
   826  func TestManifestGenErrorCacheByNumRequests(t *testing.T) {
   827  	// Returns the state of the manifest generation cache, by querying the cache for the previously set result
   828  	getRecentCachedEntry := func(service *Service, manifestRequest *apiclient.ManifestRequest) *cache.CachedManifestResponse {
   829  		assert.NotNil(t, service)
   830  		assert.NotNil(t, manifestRequest)
   831  
   832  		cachedManifestResponse := &cache.CachedManifestResponse{}
   833  		err := service.cache.GetManifests(mock.Anything, manifestRequest.ApplicationSource, manifestRequest.RefSources, manifestRequest, manifestRequest.Namespace, "", manifestRequest.AppLabelKey, manifestRequest.AppName, cachedManifestResponse, nil, "")
   834  		require.NoError(t, err)
   835  		return cachedManifestResponse
   836  	}
   837  
   838  	// Example:
   839  	// With repo server (test) parameters:
   840  	// - PauseGenerationAfterFailedGenerationAttempts: 2
   841  	// - PauseGenerationOnFailureForRequests: 4
   842  	// - TotalCacheInvocations: 10
   843  	//
   844  	// After 2 manifest generation failures in a row, the next 4 manifest generation requests should be cached,
   845  	// with the next 2 after that being uncached. Here's how it looks...
   846  	//
   847  	//  request count) result
   848  	// --------------------------
   849  	// 1) Attempt to generate manifest, fails.
   850  	// 2) Second attempt to generate manifest, fails.
   851  	// 3) Return cached error attempt from #2
   852  	// 4) Return cached error attempt from #2
   853  	// 5) Return cached error attempt from #2
   854  	// 6) Return cached error attempt from #2. Max response limit hit, so reset cache entry.
   855  	// 7) Attempt to generate manifest, fails.
   856  	// 8) Attempt to generate manifest, fails.
   857  	// 9) Return cached error attempt from #8
   858  	// 10) Return cached error attempt from #8
   859  
   860  	// The same pattern PauseGenerationAfterFailedGenerationAttempts generation attempts, followed by
   861  	// PauseGenerationOnFailureForRequests cached responses, should apply for various combinations of
   862  	// both parameters.
   863  
   864  	tests := []struct {
   865  		PauseGenerationAfterFailedGenerationAttempts int
   866  		PauseGenerationOnFailureForRequests          int
   867  		TotalCacheInvocations                        int
   868  	}{
   869  		{2, 4, 10},
   870  		{3, 5, 10},
   871  		{1, 2, 5},
   872  	}
   873  	for _, tt := range tests {
   874  		testName := fmt.Sprintf("gen-attempts-%d-pause-%d-total-%d", tt.PauseGenerationAfterFailedGenerationAttempts, tt.PauseGenerationOnFailureForRequests, tt.TotalCacheInvocations)
   875  		t.Run(testName, func(t *testing.T) {
   876  			service := newService(t, ".")
   877  
   878  			service.initConstants = RepoServerInitConstants{
   879  				ParallelismLimit: 1,
   880  				PauseGenerationAfterFailedGenerationAttempts: tt.PauseGenerationAfterFailedGenerationAttempts,
   881  				PauseGenerationOnFailureForMinutes:           0,
   882  				PauseGenerationOnFailureForRequests:          tt.PauseGenerationOnFailureForRequests,
   883  			}
   884  
   885  			totalAttempts := service.initConstants.PauseGenerationAfterFailedGenerationAttempts + service.initConstants.PauseGenerationOnFailureForRequests
   886  
   887  			for invocationCount := 0; invocationCount < tt.TotalCacheInvocations; invocationCount++ {
   888  				adjustedInvocation := invocationCount % totalAttempts
   889  
   890  				fmt.Printf("%d )-------------------------------------------\n", invocationCount)
   891  
   892  				manifestRequest := &apiclient.ManifestRequest{
   893  					Repo:    &v1alpha1.Repository{},
   894  					AppName: "test",
   895  					ApplicationSource: &v1alpha1.ApplicationSource{
   896  						Path: "./testdata/invalid-helm",
   897  					},
   898  				}
   899  
   900  				res, err := service.GenerateManifest(t.Context(), manifestRequest)
   901  
   902  				// Verify invariant: res != nil xor err != nil
   903  				if err != nil {
   904  					assert.Nil(t, res, "both err and res are non-nil res: %v   err: %v", res, err)
   905  				} else {
   906  					assert.NotNil(t, res, "both err and res are nil")
   907  				}
   908  
   909  				cachedManifestResponse := getRecentCachedEntry(service, manifestRequest)
   910  
   911  				isCachedError := err != nil && strings.HasPrefix(err.Error(), cachedManifestGenerationPrefix)
   912  
   913  				if adjustedInvocation < service.initConstants.PauseGenerationAfterFailedGenerationAttempts {
   914  					// GenerateManifest should not return cached errors for the first X responses, where X is the FailGenAttempts constants
   915  					require.False(t, isCachedError)
   916  
   917  					require.NotNil(t, cachedManifestResponse)
   918  					assert.Nil(t, cachedManifestResponse.ManifestResponse)
   919  					assert.NotEqual(t, 0, cachedManifestResponse.FirstFailureTimestamp)
   920  
   921  					// Internal cache consec failures value should increase with invocations, cached response should stay the same,
   922  					assert.Equal(t, cachedManifestResponse.NumberOfConsecutiveFailures, adjustedInvocation+1)
   923  					assert.Equal(t, 0, cachedManifestResponse.NumberOfCachedResponsesReturned)
   924  				} else {
   925  					// GenerateManifest SHOULD return cached errors for the next X responses, where X is the
   926  					// PauseGenerationOnFailureForRequests constant
   927  					assert.True(t, isCachedError)
   928  					require.NotNil(t, cachedManifestResponse)
   929  					assert.Nil(t, cachedManifestResponse.ManifestResponse)
   930  					assert.NotEqual(t, 0, cachedManifestResponse.FirstFailureTimestamp)
   931  
   932  					// Internal cache values should update correctly based on number of return cache entries, consecutive failures should stay the same
   933  					assert.Equal(t, cachedManifestResponse.NumberOfConsecutiveFailures, service.initConstants.PauseGenerationAfterFailedGenerationAttempts)
   934  					assert.Equal(t, cachedManifestResponse.NumberOfCachedResponsesReturned, (adjustedInvocation - service.initConstants.PauseGenerationAfterFailedGenerationAttempts + 1))
   935  				}
   936  			}
   937  		})
   938  	}
   939  }
   940  
   941  func TestManifestGenErrorCacheFileContentsChange(t *testing.T) {
   942  	tmpDir := t.TempDir()
   943  
   944  	service := newService(t, tmpDir)
   945  
   946  	service.initConstants = RepoServerInitConstants{
   947  		ParallelismLimit: 1,
   948  		PauseGenerationAfterFailedGenerationAttempts: 2,
   949  		PauseGenerationOnFailureForMinutes:           0,
   950  		PauseGenerationOnFailureForRequests:          4,
   951  	}
   952  
   953  	for step := 0; step < 3; step++ {
   954  		// step 1) Attempt to generate manifests against invalid helm chart (should return uncached error)
   955  		// step 2) Attempt to generate manifest against valid helm chart (should succeed and return valid response)
   956  		// step 3) Attempt to generate manifest against invalid helm chart (should return cached value from step 2)
   957  
   958  		errorExpected := step%2 == 0
   959  
   960  		// Ensure that the target directory will succeed or fail, so we can verify the cache correctly handles it
   961  		err := os.RemoveAll(tmpDir)
   962  		require.NoError(t, err)
   963  		err = os.MkdirAll(tmpDir, 0o777)
   964  		require.NoError(t, err)
   965  		if errorExpected {
   966  			// Copy invalid helm chart into temporary directory, ensuring manifest generation will fail
   967  			err = fileutil.CopyDir("./testdata/invalid-helm", tmpDir)
   968  			require.NoError(t, err)
   969  		} else {
   970  			// Copy valid helm chart into temporary directory, ensuring generation will succeed
   971  			err = fileutil.CopyDir("./testdata/my-chart", tmpDir)
   972  			require.NoError(t, err)
   973  		}
   974  
   975  		res, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{
   976  			Repo:    &v1alpha1.Repository{},
   977  			AppName: "test",
   978  			ApplicationSource: &v1alpha1.ApplicationSource{
   979  				Path: ".",
   980  			},
   981  			ProjectName:        "something",
   982  			ProjectSourceRepos: []string{"*"},
   983  		})
   984  
   985  		fmt.Println("-", step, "-", res != nil, err != nil, errorExpected)
   986  		fmt.Println("    err: ", err)
   987  		fmt.Println("    res: ", res)
   988  
   989  		if step < 2 {
   990  			if errorExpected {
   991  				require.Error(t, err, "error return value and error expected did not match")
   992  				assert.Nil(t, res, "GenerateManifest return value and expected value did not match")
   993  			} else {
   994  				require.NoError(t, err, "error return value and error expected did not match")
   995  				assert.NotNil(t, res, "GenerateManifest return value and expected value did not match")
   996  			}
   997  		}
   998  
   999  		if step == 2 {
  1000  			require.NoError(t, err, "error ret val was non-nil on step 3")
  1001  			assert.NotNil(t, res, "GenerateManifest ret val was nil on step 3")
  1002  		}
  1003  	}
  1004  }
  1005  
  1006  func TestManifestGenErrorCacheByMinutesElapsed(t *testing.T) {
  1007  	tests := []struct {
  1008  		// Test with a range of pause expiration thresholds
  1009  		PauseGenerationOnFailureForMinutes int
  1010  	}{
  1011  		{1}, {2}, {10}, {24 * 60},
  1012  	}
  1013  	for _, tt := range tests {
  1014  		testName := fmt.Sprintf("pause-time-%d", tt.PauseGenerationOnFailureForMinutes)
  1015  		t.Run(testName, func(t *testing.T) {
  1016  			service := newService(t, ".")
  1017  
  1018  			// Here we simulate the passage of time by overriding the now() function of Service
  1019  			currentTime := time.Now()
  1020  			service.now = func() time.Time {
  1021  				return currentTime
  1022  			}
  1023  
  1024  			service.initConstants = RepoServerInitConstants{
  1025  				ParallelismLimit: 1,
  1026  				PauseGenerationAfterFailedGenerationAttempts: 1,
  1027  				PauseGenerationOnFailureForMinutes:           tt.PauseGenerationOnFailureForMinutes,
  1028  				PauseGenerationOnFailureForRequests:          0,
  1029  			}
  1030  
  1031  			// 1) Put the cache into the failure state
  1032  			for x := 0; x < 2; x++ {
  1033  				res, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{
  1034  					Repo:    &v1alpha1.Repository{},
  1035  					AppName: "test",
  1036  					ApplicationSource: &v1alpha1.ApplicationSource{
  1037  						Path: "./testdata/invalid-helm",
  1038  					},
  1039  				})
  1040  
  1041  				assert.True(t, err != nil && res == nil)
  1042  
  1043  				// Ensure that the second invocation triggers the cached error state
  1044  				if x == 1 {
  1045  					assert.True(t, strings.HasPrefix(err.Error(), cachedManifestGenerationPrefix))
  1046  				}
  1047  			}
  1048  
  1049  			// 2) Jump forward X-1 minutes in time, where X is the expiration boundary
  1050  			currentTime = currentTime.Add(time.Duration(tt.PauseGenerationOnFailureForMinutes-1) * time.Minute)
  1051  			res, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{
  1052  				Repo:    &v1alpha1.Repository{},
  1053  				AppName: "test",
  1054  				ApplicationSource: &v1alpha1.ApplicationSource{
  1055  					Path: "./testdata/invalid-helm",
  1056  				},
  1057  			})
  1058  
  1059  			// 3) Ensure that the cache still returns a cached copy of the last error
  1060  			assert.True(t, err != nil && res == nil)
  1061  			assert.True(t, strings.HasPrefix(err.Error(), cachedManifestGenerationPrefix))
  1062  
  1063  			// 4) Jump forward 2 minutes in time, such that the pause generation time has elapsed and we should return to normal state
  1064  			currentTime = currentTime.Add(2 * time.Minute)
  1065  
  1066  			res, err = service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{
  1067  				Repo:    &v1alpha1.Repository{},
  1068  				AppName: "test",
  1069  				ApplicationSource: &v1alpha1.ApplicationSource{
  1070  					Path: "./testdata/invalid-helm",
  1071  				},
  1072  			})
  1073  
  1074  			// 5) Ensure that the service no longer returns a cached copy of the last error
  1075  			assert.True(t, err != nil && res == nil)
  1076  			assert.False(t, strings.HasPrefix(err.Error(), cachedManifestGenerationPrefix))
  1077  		})
  1078  	}
  1079  }
  1080  
  1081  func TestManifestGenErrorCacheRespectsNoCache(t *testing.T) {
  1082  	service := newService(t, ".")
  1083  
  1084  	service.initConstants = RepoServerInitConstants{
  1085  		ParallelismLimit: 1,
  1086  		PauseGenerationAfterFailedGenerationAttempts: 1,
  1087  		PauseGenerationOnFailureForMinutes:           0,
  1088  		PauseGenerationOnFailureForRequests:          4,
  1089  	}
  1090  
  1091  	// 1) Put the cache into the failure state
  1092  	for x := 0; x < 2; x++ {
  1093  		res, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{
  1094  			Repo:    &v1alpha1.Repository{},
  1095  			AppName: "test",
  1096  			ApplicationSource: &v1alpha1.ApplicationSource{
  1097  				Path: "./testdata/invalid-helm",
  1098  			},
  1099  		})
  1100  
  1101  		assert.True(t, err != nil && res == nil)
  1102  
  1103  		// Ensure that the second invocation is cached
  1104  		if x == 1 {
  1105  			assert.True(t, strings.HasPrefix(err.Error(), cachedManifestGenerationPrefix))
  1106  		}
  1107  	}
  1108  
  1109  	// 2) Call generateManifest with NoCache enabled
  1110  	res, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{
  1111  		Repo:    &v1alpha1.Repository{},
  1112  		AppName: "test",
  1113  		ApplicationSource: &v1alpha1.ApplicationSource{
  1114  			Path: "./testdata/invalid-helm",
  1115  		},
  1116  		NoCache: true,
  1117  	})
  1118  
  1119  	// 3) Ensure that the cache returns a new generation attempt, rather than a previous cached error
  1120  	assert.True(t, err != nil && res == nil)
  1121  	assert.False(t, strings.HasPrefix(err.Error(), cachedManifestGenerationPrefix))
  1122  
  1123  	// 4) Call generateManifest
  1124  	res, err = service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{
  1125  		Repo:    &v1alpha1.Repository{},
  1126  		AppName: "test",
  1127  		ApplicationSource: &v1alpha1.ApplicationSource{
  1128  			Path: "./testdata/invalid-helm",
  1129  		},
  1130  	})
  1131  
  1132  	// 5) Ensure that the subsequent invocation, after nocache, is cached
  1133  	assert.True(t, err != nil && res == nil)
  1134  	assert.True(t, strings.HasPrefix(err.Error(), cachedManifestGenerationPrefix))
  1135  }
  1136  
  1137  func TestGenerateHelmKubeVersion(t *testing.T) {
  1138  	service := newService(t, "../../util/helm/testdata/redis")
  1139  
  1140  	res, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{
  1141  		Repo:    &v1alpha1.Repository{},
  1142  		AppName: "test",
  1143  		ApplicationSource: &v1alpha1.ApplicationSource{
  1144  			Path: ".",
  1145  			Helm: &v1alpha1.ApplicationSourceHelm{
  1146  				KubeVersion: "1.30.11+IKS",
  1147  			},
  1148  		},
  1149  		ProjectName:        "something",
  1150  		ProjectSourceRepos: []string{"*"},
  1151  	})
  1152  
  1153  	require.NoError(t, err)
  1154  	assert.Len(t, res.Commands, 1)
  1155  	assert.Contains(t, res.Commands[0], "--kube-version 1.30.11")
  1156  }
  1157  
  1158  func TestGenerateHelmWithValues(t *testing.T) {
  1159  	service := newService(t, "../../util/helm/testdata/redis")
  1160  
  1161  	res, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{
  1162  		Repo:    &v1alpha1.Repository{},
  1163  		AppName: "test",
  1164  		ApplicationSource: &v1alpha1.ApplicationSource{
  1165  			Path: ".",
  1166  			Helm: &v1alpha1.ApplicationSourceHelm{
  1167  				ValueFiles:   []string{"values-production.yaml"},
  1168  				ValuesObject: &runtime.RawExtension{Raw: []byte(`cluster: {slaveCount: 2}`)},
  1169  			},
  1170  		},
  1171  		ProjectName:        "something",
  1172  		ProjectSourceRepos: []string{"*"},
  1173  	})
  1174  
  1175  	require.NoError(t, err)
  1176  
  1177  	replicasVerified := false
  1178  	for _, src := range res.Manifests {
  1179  		obj := unstructured.Unstructured{}
  1180  		err = json.Unmarshal([]byte(src), &obj)
  1181  		require.NoError(t, err)
  1182  
  1183  		if obj.GetKind() == "Deployment" && obj.GetName() == "test-redis-slave" {
  1184  			var dep appsv1.Deployment
  1185  			err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &dep)
  1186  			require.NoError(t, err)
  1187  			assert.Equal(t, int32(2), *dep.Spec.Replicas)
  1188  			replicasVerified = true
  1189  		}
  1190  	}
  1191  	assert.True(t, replicasVerified)
  1192  }
  1193  
  1194  func TestHelmWithMissingValueFiles(t *testing.T) {
  1195  	service := newService(t, "../../util/helm/testdata/redis")
  1196  	missingValuesFile := "values-prod-overrides.yaml"
  1197  
  1198  	req := &apiclient.ManifestRequest{
  1199  		Repo:    &v1alpha1.Repository{},
  1200  		AppName: "test",
  1201  		ApplicationSource: &v1alpha1.ApplicationSource{
  1202  			Path: ".",
  1203  			Helm: &v1alpha1.ApplicationSourceHelm{
  1204  				ValueFiles: []string{"values-production.yaml", missingValuesFile},
  1205  			},
  1206  		},
  1207  		ProjectName:        "something",
  1208  		ProjectSourceRepos: []string{"*"},
  1209  	}
  1210  
  1211  	// Should fail since we're passing a non-existent values file, and error should indicate that
  1212  	_, err := service.GenerateManifest(t.Context(), req)
  1213  	require.ErrorContains(t, err, missingValuesFile+": no such file or directory")
  1214  
  1215  	// Should template without error even if defining a non-existent values file
  1216  	req.ApplicationSource.Helm.IgnoreMissingValueFiles = true
  1217  	_, err = service.GenerateManifest(t.Context(), req)
  1218  	require.NoError(t, err)
  1219  }
  1220  
  1221  func TestGenerateHelmWithEnvVars(t *testing.T) {
  1222  	service := newService(t, "../../util/helm/testdata/redis")
  1223  
  1224  	res, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{
  1225  		Repo:    &v1alpha1.Repository{},
  1226  		AppName: "production",
  1227  		ApplicationSource: &v1alpha1.ApplicationSource{
  1228  			Path: ".",
  1229  			Helm: &v1alpha1.ApplicationSourceHelm{
  1230  				ValueFiles: []string{"values-$ARGOCD_APP_NAME.yaml"},
  1231  			},
  1232  		},
  1233  		ProjectName:        "something",
  1234  		ProjectSourceRepos: []string{"*"},
  1235  	})
  1236  
  1237  	require.NoError(t, err)
  1238  
  1239  	replicasVerified := false
  1240  	for _, src := range res.Manifests {
  1241  		obj := unstructured.Unstructured{}
  1242  		err = json.Unmarshal([]byte(src), &obj)
  1243  		require.NoError(t, err)
  1244  
  1245  		if obj.GetKind() == "Deployment" && obj.GetName() == "production-redis-slave" {
  1246  			var dep appsv1.Deployment
  1247  			err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &dep)
  1248  			require.NoError(t, err)
  1249  			assert.Equal(t, int32(3), *dep.Spec.Replicas)
  1250  			replicasVerified = true
  1251  		}
  1252  	}
  1253  	assert.True(t, replicasVerified)
  1254  }
  1255  
  1256  // The requested value file (`../minio/values.yaml`) is outside the app path (`./util/helm/testdata/redis`), however
  1257  // since the requested value is still under the repo directory (`~/go/src/github.com/argoproj/argo-cd`), it is allowed
  1258  func TestGenerateHelmWithValuesDirectoryTraversal(t *testing.T) {
  1259  	service := newService(t, "../../util/helm/testdata")
  1260  	_, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{
  1261  		Repo:    &v1alpha1.Repository{},
  1262  		AppName: "test",
  1263  		ApplicationSource: &v1alpha1.ApplicationSource{
  1264  			Path: "./redis",
  1265  			Helm: &v1alpha1.ApplicationSourceHelm{
  1266  				ValueFiles:   []string{"../minio/values.yaml"},
  1267  				ValuesObject: &runtime.RawExtension{Raw: []byte(`cluster: {slaveCount: 2}`)},
  1268  			},
  1269  		},
  1270  		ProjectName:        "something",
  1271  		ProjectSourceRepos: []string{"*"},
  1272  	})
  1273  	require.NoError(t, err)
  1274  
  1275  	// Test the case where the path is "."
  1276  	service = newService(t, "./testdata")
  1277  	_, err = service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{
  1278  		Repo:    &v1alpha1.Repository{},
  1279  		AppName: "test",
  1280  		ApplicationSource: &v1alpha1.ApplicationSource{
  1281  			Path: "./my-chart",
  1282  		},
  1283  		ProjectName:        "something",
  1284  		ProjectSourceRepos: []string{"*"},
  1285  	})
  1286  	require.NoError(t, err)
  1287  }
  1288  
  1289  func TestChartRepoWithOutOfBoundsSymlink(t *testing.T) {
  1290  	service := newService(t, ".")
  1291  	source := &v1alpha1.ApplicationSource{Chart: "out-of-bounds-chart", TargetRevision: ">= 1.0.0"}
  1292  	request := &apiclient.ManifestRequest{Repo: &v1alpha1.Repository{}, ApplicationSource: source, NoCache: true}
  1293  	_, err := service.GenerateManifest(t.Context(), request)
  1294  	assert.ErrorContains(t, err, "chart contains out-of-bounds symlinks")
  1295  }
  1296  
  1297  // This is a Helm first-class app with a values file inside the repo directory
  1298  // (`~/go/src/github.com/argoproj/argo-cd/reposerver/repository`), so it is allowed
  1299  func TestHelmManifestFromChartRepoWithValueFile(t *testing.T) {
  1300  	service := newService(t, ".")
  1301  	source := &v1alpha1.ApplicationSource{
  1302  		Chart:          "my-chart",
  1303  		TargetRevision: ">= 1.0.0",
  1304  		Helm: &v1alpha1.ApplicationSourceHelm{
  1305  			ValueFiles: []string{"./my-chart-values.yaml"},
  1306  		},
  1307  	}
  1308  	request := &apiclient.ManifestRequest{
  1309  		Repo:               &v1alpha1.Repository{},
  1310  		ApplicationSource:  source,
  1311  		NoCache:            true,
  1312  		ProjectName:        "something",
  1313  		ProjectSourceRepos: []string{"*"},
  1314  	}
  1315  	response, err := service.GenerateManifest(t.Context(), request)
  1316  	require.NoError(t, err)
  1317  	assert.NotNil(t, response)
  1318  	assert.Equal(t, &apiclient.ManifestResponse{
  1319  		Manifests:  []string{"{\"apiVersion\":\"v1\",\"kind\":\"ConfigMap\",\"metadata\":{\"name\":\"my-map\"}}"},
  1320  		Namespace:  "",
  1321  		Server:     "",
  1322  		Revision:   "1.1.0",
  1323  		SourceType: "Helm",
  1324  		Commands:   []string{`helm template . --name-template "" --values ./testdata/my-chart/my-chart-values.yaml --include-crds`},
  1325  	}, response)
  1326  }
  1327  
  1328  // This is a Helm first-class app with a values file outside the repo directory
  1329  // (`~/go/src/github.com/argoproj/argo-cd/reposerver/repository`), so it is not allowed
  1330  func TestHelmManifestFromChartRepoWithValueFileOutsideRepo(t *testing.T) {
  1331  	service := newService(t, ".")
  1332  	source := &v1alpha1.ApplicationSource{
  1333  		Chart:          "my-chart",
  1334  		TargetRevision: ">= 1.0.0",
  1335  		Helm: &v1alpha1.ApplicationSourceHelm{
  1336  			ValueFiles: []string{"../my-chart-2/my-chart-2-values.yaml"},
  1337  		},
  1338  	}
  1339  	request := &apiclient.ManifestRequest{Repo: &v1alpha1.Repository{}, ApplicationSource: source, NoCache: true}
  1340  	_, err := service.GenerateManifest(t.Context(), request)
  1341  	require.Error(t, err)
  1342  }
  1343  
  1344  func TestHelmManifestFromChartRepoWithValueFileLinks(t *testing.T) {
  1345  	t.Run("Valid symlink", func(t *testing.T) {
  1346  		service := newService(t, ".")
  1347  		source := &v1alpha1.ApplicationSource{
  1348  			Chart:          "my-chart",
  1349  			TargetRevision: ">= 1.0.0",
  1350  			Helm: &v1alpha1.ApplicationSourceHelm{
  1351  				ValueFiles: []string{"my-chart-link.yaml"},
  1352  			},
  1353  		}
  1354  		request := &apiclient.ManifestRequest{
  1355  			Repo: &v1alpha1.Repository{}, ApplicationSource: source, NoCache: true, ProjectName: "something",
  1356  			ProjectSourceRepos: []string{"*"},
  1357  		}
  1358  		_, err := service.GenerateManifest(t.Context(), request)
  1359  		require.NoError(t, err)
  1360  	})
  1361  }
  1362  
  1363  func TestGenerateHelmWithURL(t *testing.T) {
  1364  	service := newService(t, "../../util/helm/testdata/redis")
  1365  
  1366  	_, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{
  1367  		Repo:    &v1alpha1.Repository{},
  1368  		AppName: "test",
  1369  		ApplicationSource: &v1alpha1.ApplicationSource{
  1370  			Path: ".",
  1371  			Helm: &v1alpha1.ApplicationSourceHelm{
  1372  				ValueFiles:   []string{"https://raw.githubusercontent.com/argoproj/argocd-example-apps/master/helm-guestbook/values.yaml"},
  1373  				ValuesObject: &runtime.RawExtension{Raw: []byte(`cluster: {slaveCount: 2}`)},
  1374  			},
  1375  		},
  1376  		ProjectName:        "something",
  1377  		ProjectSourceRepos: []string{"*"},
  1378  		HelmOptions:        &v1alpha1.HelmOptions{ValuesFileSchemes: []string{"https"}},
  1379  	})
  1380  	require.NoError(t, err)
  1381  }
  1382  
  1383  // The requested value file (`../minio/values.yaml`) is outside the repo directory
  1384  // (`~/go/src/github.com/argoproj/argo-cd/util/helm/testdata/redis`), so it is blocked
  1385  func TestGenerateHelmWithValuesDirectoryTraversalOutsideRepo(t *testing.T) {
  1386  	t.Run("Values file with relative path pointing outside repo root", func(t *testing.T) {
  1387  		service := newService(t, "../../util/helm/testdata/redis")
  1388  		_, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{
  1389  			Repo:    &v1alpha1.Repository{},
  1390  			AppName: "test",
  1391  			ApplicationSource: &v1alpha1.ApplicationSource{
  1392  				Path: ".",
  1393  				Helm: &v1alpha1.ApplicationSourceHelm{
  1394  					ValueFiles:   []string{"../minio/values.yaml"},
  1395  					ValuesObject: &runtime.RawExtension{Raw: []byte(`cluster: {slaveCount: 2}`)},
  1396  				},
  1397  			},
  1398  			ProjectName:        "something",
  1399  			ProjectSourceRepos: []string{"*"},
  1400  		})
  1401  		assert.ErrorContains(t, err, "outside repository root")
  1402  	})
  1403  
  1404  	t.Run("Values file with relative path pointing inside repo root", func(t *testing.T) {
  1405  		service := newService(t, "./testdata")
  1406  		_, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{
  1407  			Repo:    &v1alpha1.Repository{},
  1408  			AppName: "test",
  1409  			ApplicationSource: &v1alpha1.ApplicationSource{
  1410  				Path: "./my-chart",
  1411  				Helm: &v1alpha1.ApplicationSourceHelm{
  1412  					ValueFiles:   []string{"../my-chart/my-chart-values.yaml"},
  1413  					ValuesObject: &runtime.RawExtension{Raw: []byte(`cluster: {slaveCount: 2}`)},
  1414  				},
  1415  			},
  1416  			ProjectName:        "something",
  1417  			ProjectSourceRepos: []string{"*"},
  1418  		})
  1419  		require.NoError(t, err)
  1420  	})
  1421  
  1422  	t.Run("Values file with absolute path stays within repo root", func(t *testing.T) {
  1423  		service := newService(t, "./testdata")
  1424  		_, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{
  1425  			Repo:    &v1alpha1.Repository{},
  1426  			AppName: "test",
  1427  			ApplicationSource: &v1alpha1.ApplicationSource{
  1428  				Path: "./my-chart",
  1429  				Helm: &v1alpha1.ApplicationSourceHelm{
  1430  					ValueFiles:   []string{"/my-chart/my-chart-values.yaml"},
  1431  					ValuesObject: &runtime.RawExtension{Raw: []byte(`cluster: {slaveCount: 2}`)},
  1432  				},
  1433  			},
  1434  			ProjectName:        "something",
  1435  			ProjectSourceRepos: []string{"*"},
  1436  		})
  1437  		require.NoError(t, err)
  1438  	})
  1439  
  1440  	t.Run("Values file with absolute path using back-references outside repo root", func(t *testing.T) {
  1441  		service := newService(t, "./testdata")
  1442  		_, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{
  1443  			Repo:    &v1alpha1.Repository{},
  1444  			AppName: "test",
  1445  			ApplicationSource: &v1alpha1.ApplicationSource{
  1446  				Path: "./my-chart",
  1447  				Helm: &v1alpha1.ApplicationSourceHelm{
  1448  					ValueFiles:   []string{"/../../../my-chart-values.yaml"},
  1449  					ValuesObject: &runtime.RawExtension{Raw: []byte(`cluster: {slaveCount: 2}`)},
  1450  				},
  1451  			},
  1452  			ProjectName:        "something",
  1453  			ProjectSourceRepos: []string{"*"},
  1454  		})
  1455  		assert.ErrorContains(t, err, "outside repository root")
  1456  	})
  1457  
  1458  	t.Run("Remote values file from forbidden protocol", func(t *testing.T) {
  1459  		service := newService(t, "./testdata")
  1460  		_, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{
  1461  			Repo:    &v1alpha1.Repository{},
  1462  			AppName: "test",
  1463  			ApplicationSource: &v1alpha1.ApplicationSource{
  1464  				Path: "./my-chart",
  1465  				Helm: &v1alpha1.ApplicationSourceHelm{
  1466  					ValueFiles:   []string{"file://../../../../my-chart-values.yaml"},
  1467  					ValuesObject: &runtime.RawExtension{Raw: []byte(`cluster: {slaveCount: 2}`)},
  1468  				},
  1469  			},
  1470  			ProjectName:        "something",
  1471  			ProjectSourceRepos: []string{"*"},
  1472  		})
  1473  		assert.ErrorContains(t, err, "is not allowed")
  1474  	})
  1475  
  1476  	t.Run("Remote values file from custom allowed protocol", func(t *testing.T) {
  1477  		service := newService(t, "./testdata")
  1478  		_, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{
  1479  			Repo:    &v1alpha1.Repository{},
  1480  			AppName: "test",
  1481  			ApplicationSource: &v1alpha1.ApplicationSource{
  1482  				Path: "./my-chart",
  1483  				Helm: &v1alpha1.ApplicationSourceHelm{
  1484  					ValueFiles: []string{"s3://my-bucket/my-chart-values.yaml"},
  1485  				},
  1486  			},
  1487  			HelmOptions:        &v1alpha1.HelmOptions{ValuesFileSchemes: []string{"s3"}},
  1488  			ProjectName:        "something",
  1489  			ProjectSourceRepos: []string{"*"},
  1490  		})
  1491  		assert.ErrorContains(t, err, "s3://my-bucket/my-chart-values.yaml: no such file or directory")
  1492  	})
  1493  }
  1494  
  1495  // File parameter should not allow traversal outside of the repository root
  1496  func TestGenerateHelmWithAbsoluteFileParameter(t *testing.T) {
  1497  	service := newService(t, "../..")
  1498  
  1499  	file, err := os.CreateTemp(t.TempDir(), "external-secret.txt")
  1500  	require.NoError(t, err)
  1501  	externalSecretPath := file.Name()
  1502  	defer func() { _ = os.RemoveAll(externalSecretPath) }()
  1503  	expectedFileContent, err := os.ReadFile("../../util/helm/testdata/external/external-secret.txt")
  1504  	require.NoError(t, err)
  1505  	err = os.WriteFile(externalSecretPath, expectedFileContent, 0o644)
  1506  	require.NoError(t, err)
  1507  	defer func() {
  1508  		if err = file.Close(); err != nil {
  1509  			panic(err)
  1510  		}
  1511  	}()
  1512  
  1513  	_, err = service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{
  1514  		Repo:    &v1alpha1.Repository{},
  1515  		AppName: "test",
  1516  		ApplicationSource: &v1alpha1.ApplicationSource{
  1517  			Path: "./util/helm/testdata/redis",
  1518  			Helm: &v1alpha1.ApplicationSourceHelm{
  1519  				ValueFiles:   []string{"values-production.yaml"},
  1520  				ValuesObject: &runtime.RawExtension{Raw: []byte(`cluster: {slaveCount: 2}`)},
  1521  				FileParameters: []v1alpha1.HelmFileParameter{{
  1522  					Name: "passwordContent",
  1523  					Path: externalSecretPath,
  1524  				}},
  1525  			},
  1526  		},
  1527  		ProjectName:        "something",
  1528  		ProjectSourceRepos: []string{"*"},
  1529  	})
  1530  	require.Error(t, err)
  1531  }
  1532  
  1533  // The requested file parameter (`../external/external-secret.txt`) is outside the app path
  1534  // (`./util/helm/testdata/redis`), however since the requested value is still under the repo
  1535  // directory (`~/go/src/github.com/argoproj/argo-cd`), it is allowed. It is used as a means of
  1536  // providing direct content to a helm chart via a specific key.
  1537  func TestGenerateHelmWithFileParameter(t *testing.T) {
  1538  	service := newService(t, "../../util/helm/testdata")
  1539  
  1540  	res, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{
  1541  		Repo:    &v1alpha1.Repository{},
  1542  		AppName: "test",
  1543  		ApplicationSource: &v1alpha1.ApplicationSource{
  1544  			Path: "./redis",
  1545  			Helm: &v1alpha1.ApplicationSourceHelm{
  1546  				ValueFiles:   []string{"values-production.yaml"},
  1547  				Values:       `cluster: {slaveCount: 10}`,
  1548  				ValuesObject: &runtime.RawExtension{Raw: []byte(`cluster: {slaveCount: 2}`)},
  1549  				FileParameters: []v1alpha1.HelmFileParameter{{
  1550  					Name: "passwordContent",
  1551  					Path: "../external/external-secret.txt",
  1552  				}},
  1553  			},
  1554  		},
  1555  		ProjectName:        "something",
  1556  		ProjectSourceRepos: []string{"*"},
  1557  	})
  1558  	require.NoError(t, err)
  1559  	assert.Contains(t, res.Manifests[6], `"replicas":2`, "ValuesObject should override Values")
  1560  }
  1561  
  1562  func TestGenerateNullList(t *testing.T) {
  1563  	service := newService(t, ".")
  1564  
  1565  	t.Run("null list", func(t *testing.T) {
  1566  		res1, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{
  1567  			Repo:               &v1alpha1.Repository{},
  1568  			ApplicationSource:  &v1alpha1.ApplicationSource{Path: "./testdata/null-list"},
  1569  			NoCache:            true,
  1570  			ProjectName:        "something",
  1571  			ProjectSourceRepos: []string{"*"},
  1572  		})
  1573  		require.NoError(t, err)
  1574  		assert.Len(t, res1.Manifests, 1)
  1575  		assert.Contains(t, res1.Manifests[0], "prometheus-operator-operator")
  1576  	})
  1577  
  1578  	t.Run("empty list", func(t *testing.T) {
  1579  		res1, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{
  1580  			Repo:               &v1alpha1.Repository{},
  1581  			ApplicationSource:  &v1alpha1.ApplicationSource{Path: "./testdata/empty-list"},
  1582  			NoCache:            true,
  1583  			ProjectName:        "something",
  1584  			ProjectSourceRepos: []string{"*"},
  1585  		})
  1586  		require.NoError(t, err)
  1587  		assert.Len(t, res1.Manifests, 1)
  1588  		assert.Contains(t, res1.Manifests[0], "prometheus-operator-operator")
  1589  	})
  1590  
  1591  	t.Run("weird list", func(t *testing.T) {
  1592  		res1, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{
  1593  			Repo:               &v1alpha1.Repository{},
  1594  			ApplicationSource:  &v1alpha1.ApplicationSource{Path: "./testdata/weird-list"},
  1595  			NoCache:            true,
  1596  			ProjectName:        "something",
  1597  			ProjectSourceRepos: []string{"*"},
  1598  		})
  1599  		require.NoError(t, err)
  1600  		assert.Len(t, res1.Manifests, 2)
  1601  	})
  1602  }
  1603  
  1604  func TestIdentifyAppSourceTypeByAppDirWithKustomizations(t *testing.T) {
  1605  	sourceType, err := GetAppSourceType(t.Context(), &v1alpha1.ApplicationSource{}, "./testdata/kustomization_yaml", "./testdata", "testapp", map[string]bool{}, []string{}, []string{})
  1606  	require.NoError(t, err)
  1607  	assert.Equal(t, v1alpha1.ApplicationSourceTypeKustomize, sourceType)
  1608  
  1609  	sourceType, err = GetAppSourceType(t.Context(), &v1alpha1.ApplicationSource{}, "./testdata/kustomization_yml", "./testdata", "testapp", map[string]bool{}, []string{}, []string{})
  1610  	require.NoError(t, err)
  1611  	assert.Equal(t, v1alpha1.ApplicationSourceTypeKustomize, sourceType)
  1612  
  1613  	sourceType, err = GetAppSourceType(t.Context(), &v1alpha1.ApplicationSource{}, "./testdata/Kustomization", "./testdata", "testapp", map[string]bool{}, []string{}, []string{})
  1614  	require.NoError(t, err)
  1615  	assert.Equal(t, v1alpha1.ApplicationSourceTypeKustomize, sourceType)
  1616  }
  1617  
  1618  func TestGenerateFromUTF16(t *testing.T) {
  1619  	q := apiclient.ManifestRequest{
  1620  		Repo:               &v1alpha1.Repository{},
  1621  		ApplicationSource:  &v1alpha1.ApplicationSource{},
  1622  		ProjectName:        "something",
  1623  		ProjectSourceRepos: []string{"*"},
  1624  	}
  1625  	res1, err := GenerateManifests(t.Context(), "./testdata/utf-16", "/", "", &q, false, &git.NoopCredsStore{}, resource.MustParse("0"), nil)
  1626  	require.NoError(t, err)
  1627  	assert.Len(t, res1.Manifests, 2)
  1628  }
  1629  
  1630  func TestListApps(t *testing.T) {
  1631  	service := newService(t, "./testdata")
  1632  
  1633  	res, err := service.ListApps(t.Context(), &apiclient.ListAppsRequest{Repo: &v1alpha1.Repository{}})
  1634  	require.NoError(t, err)
  1635  
  1636  	expectedApps := map[string]string{
  1637  		"Kustomization":                     "Kustomize",
  1638  		"app-parameters/multi":              "Kustomize",
  1639  		"app-parameters/single-app-only":    "Kustomize",
  1640  		"app-parameters/single-global":      "Kustomize",
  1641  		"app-parameters/single-global-helm": "Helm",
  1642  		"in-bounds-values-file-link":        "Helm",
  1643  		"invalid-helm":                      "Helm",
  1644  		"invalid-kustomize":                 "Kustomize",
  1645  		"kustomization_yaml":                "Kustomize",
  1646  		"kustomization_yml":                 "Kustomize",
  1647  		"my-chart":                          "Helm",
  1648  		"my-chart-2":                        "Helm",
  1649  		"oci-dependencies":                  "Helm",
  1650  		"out-of-bounds-values-file-link":    "Helm",
  1651  		"values-files":                      "Helm",
  1652  		"helm-with-dependencies":            "Helm",
  1653  		"helm-with-dependencies-alias":      "Helm",
  1654  		"helm-with-local-dependency":        "Helm",
  1655  		"simple-chart":                      "Helm",
  1656  		"broken-schema-verification":        "Helm",
  1657  	}
  1658  	assert.Equal(t, expectedApps, res.Apps)
  1659  }
  1660  
  1661  func TestGetAppDetailsHelm(t *testing.T) {
  1662  	service := newService(t, "../../util/helm/testdata/dependency")
  1663  
  1664  	res, err := service.GetAppDetails(t.Context(), &apiclient.RepoServerAppDetailsQuery{
  1665  		Repo: &v1alpha1.Repository{},
  1666  		Source: &v1alpha1.ApplicationSource{
  1667  			Path: ".",
  1668  		},
  1669  	})
  1670  
  1671  	require.NoError(t, err)
  1672  	assert.NotNil(t, res.Helm)
  1673  
  1674  	assert.Equal(t, "Helm", res.Type)
  1675  	assert.Equal(t, []string{"values-production.yaml", "values.yaml"}, res.Helm.ValueFiles)
  1676  }
  1677  
  1678  func TestGetAppDetailsHelmUsesCache(t *testing.T) {
  1679  	service := newService(t, "../../util/helm/testdata/dependency")
  1680  
  1681  	res, err := service.GetAppDetails(t.Context(), &apiclient.RepoServerAppDetailsQuery{
  1682  		Repo: &v1alpha1.Repository{},
  1683  		Source: &v1alpha1.ApplicationSource{
  1684  			Path: ".",
  1685  		},
  1686  	})
  1687  
  1688  	require.NoError(t, err)
  1689  	assert.NotNil(t, res.Helm)
  1690  
  1691  	assert.Equal(t, "Helm", res.Type)
  1692  	assert.Equal(t, []string{"values-production.yaml", "values.yaml"}, res.Helm.ValueFiles)
  1693  }
  1694  
  1695  func TestGetAppDetailsHelm_WithNoValuesFile(t *testing.T) {
  1696  	service := newService(t, "../../util/helm/testdata/api-versions")
  1697  
  1698  	res, err := service.GetAppDetails(t.Context(), &apiclient.RepoServerAppDetailsQuery{
  1699  		Repo: &v1alpha1.Repository{},
  1700  		Source: &v1alpha1.ApplicationSource{
  1701  			Path: ".",
  1702  		},
  1703  	})
  1704  
  1705  	require.NoError(t, err)
  1706  	assert.NotNil(t, res.Helm)
  1707  
  1708  	assert.Equal(t, "Helm", res.Type)
  1709  	assert.Empty(t, res.Helm.ValueFiles)
  1710  	assert.Empty(t, res.Helm.Values)
  1711  }
  1712  
  1713  func TestGetAppDetailsKustomize(t *testing.T) {
  1714  	service := newService(t, "../../util/kustomize/testdata/kustomization_yaml")
  1715  
  1716  	res, err := service.GetAppDetails(t.Context(), &apiclient.RepoServerAppDetailsQuery{
  1717  		Repo: &v1alpha1.Repository{},
  1718  		Source: &v1alpha1.ApplicationSource{
  1719  			Path: ".",
  1720  		},
  1721  	})
  1722  
  1723  	require.NoError(t, err)
  1724  
  1725  	assert.Equal(t, "Kustomize", res.Type)
  1726  	assert.NotNil(t, res.Kustomize)
  1727  	assert.Equal(t, []string{"nginx:1.15.4", "registry.k8s.io/nginx-slim:0.8"}, res.Kustomize.Images)
  1728  }
  1729  
  1730  func TestGetAppDetailsKustomize_CustomVersion(t *testing.T) {
  1731  	service := newService(t, "../../util/kustomize/testdata/kustomize-with-version-override")
  1732  
  1733  	q := &apiclient.RepoServerAppDetailsQuery{
  1734  		Repo: &v1alpha1.Repository{},
  1735  		Source: &v1alpha1.ApplicationSource{
  1736  			Path: ".",
  1737  		},
  1738  		KustomizeOptions: &v1alpha1.KustomizeOptions{},
  1739  	}
  1740  
  1741  	_, err := service.GetAppDetails(t.Context(), q)
  1742  	require.ErrorAs(t, err, &settings.KustomizeVersionNotRegisteredError{Version: "v1.2.3"})
  1743  
  1744  	q.KustomizeOptions.Versions = []v1alpha1.KustomizeVersion{
  1745  		{
  1746  			Name: "v1.2.3",
  1747  			Path: "kustomize",
  1748  		},
  1749  	}
  1750  
  1751  	res, err := service.GetAppDetails(t.Context(), q)
  1752  	require.NoError(t, err)
  1753  	assert.Equal(t, "Kustomize", res.Type)
  1754  }
  1755  
  1756  func TestGetHelmCharts(t *testing.T) {
  1757  	service := newService(t, "../..")
  1758  	res, err := service.GetHelmCharts(t.Context(), &apiclient.HelmChartsRequest{Repo: &v1alpha1.Repository{}})
  1759  
  1760  	// fix flakiness
  1761  	sort.Slice(res.Items, func(i, j int) bool {
  1762  		return res.Items[i].Name < res.Items[j].Name
  1763  	})
  1764  
  1765  	require.NoError(t, err)
  1766  	assert.Len(t, res.Items, 2)
  1767  
  1768  	item := res.Items[0]
  1769  	assert.Equal(t, "my-chart", item.Name)
  1770  	assert.Equal(t, []string{"1.0.0", "1.1.0"}, item.Versions)
  1771  
  1772  	item2 := res.Items[1]
  1773  	assert.Equal(t, "out-of-bounds-chart", item2.Name)
  1774  	assert.Equal(t, []string{"1.0.0", "1.1.0"}, item2.Versions)
  1775  }
  1776  
  1777  func TestGetRevisionMetadata(t *testing.T) {
  1778  	service, gitClient, _ := newServiceWithMocks(t, "../..", false)
  1779  	now := time.Now()
  1780  
  1781  	gitClient.On("RevisionMetadata", mock.Anything).Return(&git.RevisionMetadata{
  1782  		Message: "test",
  1783  		Author:  "author",
  1784  		Date:    now,
  1785  		Tags:    []string{"tag1", "tag2"},
  1786  		References: []git.RevisionReference{
  1787  			{
  1788  				Commit: &git.CommitMetadata{
  1789  					Author: mail.Address{
  1790  						Name:    "test-name",
  1791  						Address: "test-email@example.com",
  1792  					},
  1793  					Date:    now.Format(time.RFC3339),
  1794  					Subject: "test-subject",
  1795  					SHA:     "test-sha",
  1796  					RepoURL: "test-repo-url",
  1797  				},
  1798  			},
  1799  		},
  1800  	}, nil)
  1801  
  1802  	res, err := service.GetRevisionMetadata(t.Context(), &apiclient.RepoServerRevisionMetadataRequest{
  1803  		Repo:           &v1alpha1.Repository{},
  1804  		Revision:       "c0b400fc458875d925171398f9ba9eabd5529923",
  1805  		CheckSignature: true,
  1806  	})
  1807  
  1808  	require.NoError(t, err)
  1809  	assert.Equal(t, "test", res.Message)
  1810  	assert.Equal(t, now, res.Date.Time)
  1811  	assert.Equal(t, "author", res.Author)
  1812  	assert.Equal(t, []string{"tag1", "tag2"}, res.Tags)
  1813  	assert.NotEmpty(t, res.SignatureInfo)
  1814  	require.Len(t, res.References, 1)
  1815  	require.NotNil(t, res.References[0].Commit)
  1816  	assert.Equal(t, "test-sha", res.References[0].Commit.SHA)
  1817  
  1818  	// Check for truncated revision value
  1819  	res, err = service.GetRevisionMetadata(t.Context(), &apiclient.RepoServerRevisionMetadataRequest{
  1820  		Repo:           &v1alpha1.Repository{},
  1821  		Revision:       "c0b400f",
  1822  		CheckSignature: true,
  1823  	})
  1824  
  1825  	require.NoError(t, err)
  1826  	assert.Equal(t, "test", res.Message)
  1827  	assert.Equal(t, now, res.Date.Time)
  1828  	assert.Equal(t, "author", res.Author)
  1829  	assert.Equal(t, []string{"tag1", "tag2"}, res.Tags)
  1830  	assert.NotEmpty(t, res.SignatureInfo)
  1831  
  1832  	// Cache hit - signature info should not be in result
  1833  	res, err = service.GetRevisionMetadata(t.Context(), &apiclient.RepoServerRevisionMetadataRequest{
  1834  		Repo:           &v1alpha1.Repository{},
  1835  		Revision:       "c0b400fc458875d925171398f9ba9eabd5529923",
  1836  		CheckSignature: false,
  1837  	})
  1838  	require.NoError(t, err)
  1839  	assert.Empty(t, res.SignatureInfo)
  1840  
  1841  	// Enforce cache miss - signature info should not be in result
  1842  	res, err = service.GetRevisionMetadata(t.Context(), &apiclient.RepoServerRevisionMetadataRequest{
  1843  		Repo:           &v1alpha1.Repository{},
  1844  		Revision:       "da52afd3b2df1ec49470603d8bbb46954dab1091",
  1845  		CheckSignature: false,
  1846  	})
  1847  	require.NoError(t, err)
  1848  	assert.Empty(t, res.SignatureInfo)
  1849  
  1850  	// Cache hit on previous entry that did not have signature info
  1851  	res, err = service.GetRevisionMetadata(t.Context(), &apiclient.RepoServerRevisionMetadataRequest{
  1852  		Repo:           &v1alpha1.Repository{},
  1853  		Revision:       "da52afd3b2df1ec49470603d8bbb46954dab1091",
  1854  		CheckSignature: true,
  1855  	})
  1856  	require.NoError(t, err)
  1857  	assert.NotEmpty(t, res.SignatureInfo)
  1858  }
  1859  
  1860  func TestGetSignatureVerificationResult(t *testing.T) {
  1861  	// Commit with signature and verification requested
  1862  	{
  1863  		service := newServiceWithSignature(t, "../../manifests/base")
  1864  
  1865  		src := v1alpha1.ApplicationSource{Path: "."}
  1866  		q := apiclient.ManifestRequest{
  1867  			Repo:               &v1alpha1.Repository{},
  1868  			ApplicationSource:  &src,
  1869  			VerifySignature:    true,
  1870  			ProjectName:        "something",
  1871  			ProjectSourceRepos: []string{"*"},
  1872  		}
  1873  
  1874  		res, err := service.GenerateManifest(t.Context(), &q)
  1875  		require.NoError(t, err)
  1876  		assert.Equal(t, testSignature, res.VerifyResult)
  1877  	}
  1878  	// Commit with signature and verification not requested
  1879  	{
  1880  		service := newServiceWithSignature(t, "../../manifests/base")
  1881  
  1882  		src := v1alpha1.ApplicationSource{Path: "."}
  1883  		q := apiclient.ManifestRequest{
  1884  			Repo: &v1alpha1.Repository{}, ApplicationSource: &src, ProjectName: "something",
  1885  			ProjectSourceRepos: []string{"*"},
  1886  		}
  1887  
  1888  		res, err := service.GenerateManifest(t.Context(), &q)
  1889  		require.NoError(t, err)
  1890  		assert.Empty(t, res.VerifyResult)
  1891  	}
  1892  	// Commit without signature and verification requested
  1893  	{
  1894  		service := newService(t, "../../manifests/base")
  1895  
  1896  		src := v1alpha1.ApplicationSource{Path: "."}
  1897  		q := apiclient.ManifestRequest{
  1898  			Repo: &v1alpha1.Repository{}, ApplicationSource: &src, VerifySignature: true, ProjectName: "something",
  1899  			ProjectSourceRepos: []string{"*"},
  1900  		}
  1901  
  1902  		res, err := service.GenerateManifest(t.Context(), &q)
  1903  		require.NoError(t, err)
  1904  		assert.Empty(t, res.VerifyResult)
  1905  	}
  1906  	// Commit without signature and verification not requested
  1907  	{
  1908  		service := newService(t, "../../manifests/base")
  1909  
  1910  		src := v1alpha1.ApplicationSource{Path: "."}
  1911  		q := apiclient.ManifestRequest{
  1912  			Repo: &v1alpha1.Repository{}, ApplicationSource: &src, VerifySignature: true, ProjectName: "something",
  1913  			ProjectSourceRepos: []string{"*"},
  1914  		}
  1915  
  1916  		res, err := service.GenerateManifest(t.Context(), &q)
  1917  		require.NoError(t, err)
  1918  		assert.Empty(t, res.VerifyResult)
  1919  	}
  1920  }
  1921  
  1922  func Test_newEnv(t *testing.T) {
  1923  	assert.Equal(t, &v1alpha1.Env{
  1924  		&v1alpha1.EnvEntry{Name: "ARGOCD_APP_NAME", Value: "my-app-name"},
  1925  		&v1alpha1.EnvEntry{Name: "ARGOCD_APP_NAMESPACE", Value: "my-namespace"},
  1926  		&v1alpha1.EnvEntry{Name: "ARGOCD_APP_PROJECT_NAME", Value: "my-project-name"},
  1927  		&v1alpha1.EnvEntry{Name: "ARGOCD_APP_REVISION", Value: "my-revision"},
  1928  		&v1alpha1.EnvEntry{Name: "ARGOCD_APP_REVISION_SHORT", Value: "my-revi"},
  1929  		&v1alpha1.EnvEntry{Name: "ARGOCD_APP_REVISION_SHORT_8", Value: "my-revis"},
  1930  		&v1alpha1.EnvEntry{Name: "ARGOCD_APP_SOURCE_REPO_URL", Value: "https://github.com/my-org/my-repo"},
  1931  		&v1alpha1.EnvEntry{Name: "ARGOCD_APP_SOURCE_PATH", Value: "my-path"},
  1932  		&v1alpha1.EnvEntry{Name: "ARGOCD_APP_SOURCE_TARGET_REVISION", Value: "my-target-revision"},
  1933  	}, newEnv(&apiclient.ManifestRequest{
  1934  		AppName:     "my-app-name",
  1935  		Namespace:   "my-namespace",
  1936  		ProjectName: "my-project-name",
  1937  		Repo:        &v1alpha1.Repository{Repo: "https://github.com/my-org/my-repo"},
  1938  		ApplicationSource: &v1alpha1.ApplicationSource{
  1939  			Path:           "my-path",
  1940  			TargetRevision: "my-target-revision",
  1941  		},
  1942  	}, "my-revision"))
  1943  }
  1944  
  1945  func TestService_newHelmClientResolveRevision(t *testing.T) {
  1946  	service := newService(t, ".")
  1947  
  1948  	t.Run("EmptyRevision", func(t *testing.T) {
  1949  		_, _, err := service.newHelmClientResolveRevision(&v1alpha1.Repository{}, "", "my-chart", true)
  1950  		assert.EqualError(t, err, "invalid revision: failed to determine semver constraint: improper constraint: ")
  1951  	})
  1952  	t.Run("InvalidRevision", func(t *testing.T) {
  1953  		_, _, err := service.newHelmClientResolveRevision(&v1alpha1.Repository{}, "???", "my-chart", true)
  1954  		assert.EqualError(t, err, "invalid revision: failed to determine semver constraint: improper constraint: ???")
  1955  	})
  1956  }
  1957  
  1958  func TestGetAppDetailsWithAppParameterFile(t *testing.T) {
  1959  	t.Run("No app name set and app specific file exists", func(t *testing.T) {
  1960  		service := newService(t, ".")
  1961  		runWithTempTestdata(t, "multi", func(t *testing.T, path string) {
  1962  			t.Helper()
  1963  			details, err := service.GetAppDetails(t.Context(), &apiclient.RepoServerAppDetailsQuery{
  1964  				Repo: &v1alpha1.Repository{},
  1965  				Source: &v1alpha1.ApplicationSource{
  1966  					Path: path,
  1967  				},
  1968  			})
  1969  			require.NoError(t, err)
  1970  			assert.Equal(t, []string{"quay.io/argoprojlabs/argocd-e2e-container:0.2"}, details.Kustomize.Images)
  1971  		})
  1972  	})
  1973  	t.Run("No app specific override", func(t *testing.T) {
  1974  		service := newService(t, ".")
  1975  		runWithTempTestdata(t, "single-global", func(t *testing.T, path string) {
  1976  			t.Helper()
  1977  			details, err := service.GetAppDetails(t.Context(), &apiclient.RepoServerAppDetailsQuery{
  1978  				Repo: &v1alpha1.Repository{},
  1979  				Source: &v1alpha1.ApplicationSource{
  1980  					Path: path,
  1981  				},
  1982  				AppName: "testapp",
  1983  			})
  1984  			require.NoError(t, err)
  1985  			assert.Equal(t, []string{"quay.io/argoprojlabs/argocd-e2e-container:0.2"}, details.Kustomize.Images)
  1986  		})
  1987  	})
  1988  	t.Run("Only app specific override", func(t *testing.T) {
  1989  		service := newService(t, ".")
  1990  		runWithTempTestdata(t, "single-app-only", func(t *testing.T, path string) {
  1991  			t.Helper()
  1992  			details, err := service.GetAppDetails(t.Context(), &apiclient.RepoServerAppDetailsQuery{
  1993  				Repo: &v1alpha1.Repository{},
  1994  				Source: &v1alpha1.ApplicationSource{
  1995  					Path: path,
  1996  				},
  1997  				AppName: "testapp",
  1998  			})
  1999  			require.NoError(t, err)
  2000  			assert.Equal(t, []string{"quay.io/argoprojlabs/argocd-e2e-container:0.3"}, details.Kustomize.Images)
  2001  		})
  2002  	})
  2003  	t.Run("App specific override", func(t *testing.T) {
  2004  		service := newService(t, ".")
  2005  		runWithTempTestdata(t, "multi", func(t *testing.T, path string) {
  2006  			t.Helper()
  2007  			details, err := service.GetAppDetails(t.Context(), &apiclient.RepoServerAppDetailsQuery{
  2008  				Repo: &v1alpha1.Repository{},
  2009  				Source: &v1alpha1.ApplicationSource{
  2010  					Path: path,
  2011  				},
  2012  				AppName: "testapp",
  2013  			})
  2014  			require.NoError(t, err)
  2015  			assert.Equal(t, []string{"quay.io/argoprojlabs/argocd-e2e-container:0.3"}, details.Kustomize.Images)
  2016  		})
  2017  	})
  2018  	t.Run("App specific overrides containing non-mergeable field", func(t *testing.T) {
  2019  		service := newService(t, ".")
  2020  		runWithTempTestdata(t, "multi", func(t *testing.T, path string) {
  2021  			t.Helper()
  2022  			details, err := service.GetAppDetails(t.Context(), &apiclient.RepoServerAppDetailsQuery{
  2023  				Repo: &v1alpha1.Repository{},
  2024  				Source: &v1alpha1.ApplicationSource{
  2025  					Path: path,
  2026  				},
  2027  				AppName: "unmergeable",
  2028  			})
  2029  			require.NoError(t, err)
  2030  			assert.Equal(t, []string{"quay.io/argoprojlabs/argocd-e2e-container:0.3"}, details.Kustomize.Images)
  2031  		})
  2032  	})
  2033  	t.Run("Broken app-specific overrides", func(t *testing.T) {
  2034  		service := newService(t, ".")
  2035  		runWithTempTestdata(t, "multi", func(t *testing.T, path string) {
  2036  			t.Helper()
  2037  			_, err := service.GetAppDetails(t.Context(), &apiclient.RepoServerAppDetailsQuery{
  2038  				Repo: &v1alpha1.Repository{},
  2039  				Source: &v1alpha1.ApplicationSource{
  2040  					Path: path,
  2041  				},
  2042  				AppName: "broken",
  2043  			})
  2044  			require.Error(t, err)
  2045  		})
  2046  	})
  2047  }
  2048  
  2049  // There are unit test that will use kustomize set and by that modify the
  2050  // kustomization.yaml. For proper testing, we need to copy the testdata to a
  2051  // temporary path, run the tests, and then throw the copy away again.
  2052  func mkTempParameters(source string) string {
  2053  	tempDir, err := os.MkdirTemp("./testdata", "app-parameters")
  2054  	if err != nil {
  2055  		panic(err)
  2056  	}
  2057  	cmd := exec.Command("cp", "-R", source, tempDir)
  2058  	err = cmd.Run()
  2059  	if err != nil {
  2060  		os.RemoveAll(tempDir)
  2061  		panic(err)
  2062  	}
  2063  	return tempDir
  2064  }
  2065  
  2066  // Simple wrapper run a test with a temporary copy of the testdata, because
  2067  // the test would modify the data when run.
  2068  func runWithTempTestdata(t *testing.T, path string, runner func(t *testing.T, path string)) {
  2069  	t.Helper()
  2070  	tempDir := mkTempParameters("./testdata/app-parameters")
  2071  	runner(t, filepath.Join(tempDir, "app-parameters", path))
  2072  	os.RemoveAll(tempDir)
  2073  }
  2074  
  2075  func TestGenerateManifestsWithAppParameterFile(t *testing.T) {
  2076  	t.Run("Single global override", func(t *testing.T) {
  2077  		runWithTempTestdata(t, "single-global", func(t *testing.T, path string) {
  2078  			t.Helper()
  2079  			service := newService(t, ".")
  2080  			manifests, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{
  2081  				Repo: &v1alpha1.Repository{},
  2082  				ApplicationSource: &v1alpha1.ApplicationSource{
  2083  					Path: path,
  2084  				},
  2085  				ProjectName:        "something",
  2086  				ProjectSourceRepos: []string{"*"},
  2087  			})
  2088  			require.NoError(t, err)
  2089  			resourceByKindName := make(map[string]*unstructured.Unstructured)
  2090  			for _, manifest := range manifests.Manifests {
  2091  				var un unstructured.Unstructured
  2092  				err := yaml.Unmarshal([]byte(manifest), &un)
  2093  				require.NoError(t, err)
  2094  				resourceByKindName[fmt.Sprintf("%s/%s", un.GetKind(), un.GetName())] = &un
  2095  			}
  2096  			deployment, ok := resourceByKindName["Deployment/guestbook-ui"]
  2097  			require.True(t, ok)
  2098  			containers, ok, _ := unstructured.NestedSlice(deployment.Object, "spec", "template", "spec", "containers")
  2099  			require.True(t, ok)
  2100  			image, ok, _ := unstructured.NestedString(containers[0].(map[string]any), "image")
  2101  			require.True(t, ok)
  2102  			assert.Equal(t, "quay.io/argoprojlabs/argocd-e2e-container:0.2", image)
  2103  		})
  2104  	})
  2105  
  2106  	t.Run("Single global override Helm", func(t *testing.T) {
  2107  		runWithTempTestdata(t, "single-global-helm", func(t *testing.T, path string) {
  2108  			t.Helper()
  2109  			service := newService(t, ".")
  2110  			manifests, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{
  2111  				Repo: &v1alpha1.Repository{},
  2112  				ApplicationSource: &v1alpha1.ApplicationSource{
  2113  					Path: path,
  2114  				},
  2115  				ProjectName:        "something",
  2116  				ProjectSourceRepos: []string{"*"},
  2117  			})
  2118  			require.NoError(t, err)
  2119  			resourceByKindName := make(map[string]*unstructured.Unstructured)
  2120  			for _, manifest := range manifests.Manifests {
  2121  				var un unstructured.Unstructured
  2122  				err := yaml.Unmarshal([]byte(manifest), &un)
  2123  				require.NoError(t, err)
  2124  				resourceByKindName[fmt.Sprintf("%s/%s", un.GetKind(), un.GetName())] = &un
  2125  			}
  2126  			deployment, ok := resourceByKindName["Deployment/guestbook-ui"]
  2127  			require.True(t, ok)
  2128  			containers, ok, _ := unstructured.NestedSlice(deployment.Object, "spec", "template", "spec", "containers")
  2129  			require.True(t, ok)
  2130  			image, ok, _ := unstructured.NestedString(containers[0].(map[string]any), "image")
  2131  			require.True(t, ok)
  2132  			assert.Equal(t, "quay.io/argoprojlabs/argocd-e2e-container:0.2", image)
  2133  		})
  2134  	})
  2135  
  2136  	t.Run("Application specific override", func(t *testing.T) {
  2137  		service := newService(t, ".")
  2138  		runWithTempTestdata(t, "single-app-only", func(t *testing.T, path string) {
  2139  			t.Helper()
  2140  			manifests, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{
  2141  				Repo: &v1alpha1.Repository{},
  2142  				ApplicationSource: &v1alpha1.ApplicationSource{
  2143  					Path: path,
  2144  				},
  2145  				AppName:            "testapp",
  2146  				ProjectName:        "something",
  2147  				ProjectSourceRepos: []string{"*"},
  2148  			})
  2149  			require.NoError(t, err)
  2150  			resourceByKindName := make(map[string]*unstructured.Unstructured)
  2151  			for _, manifest := range manifests.Manifests {
  2152  				var un unstructured.Unstructured
  2153  				err := yaml.Unmarshal([]byte(manifest), &un)
  2154  				require.NoError(t, err)
  2155  				resourceByKindName[fmt.Sprintf("%s/%s", un.GetKind(), un.GetName())] = &un
  2156  			}
  2157  			deployment, ok := resourceByKindName["Deployment/guestbook-ui"]
  2158  			require.True(t, ok)
  2159  			containers, ok, _ := unstructured.NestedSlice(deployment.Object, "spec", "template", "spec", "containers")
  2160  			require.True(t, ok)
  2161  			image, ok, _ := unstructured.NestedString(containers[0].(map[string]any), "image")
  2162  			require.True(t, ok)
  2163  			assert.Equal(t, "quay.io/argoprojlabs/argocd-e2e-container:0.3", image)
  2164  		})
  2165  	})
  2166  
  2167  	t.Run("Multi-source with source as ref only does not generate manifests", func(t *testing.T) {
  2168  		service := newService(t, ".")
  2169  		runWithTempTestdata(t, "single-app-only", func(t *testing.T, _ string) {
  2170  			t.Helper()
  2171  			manifests, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{
  2172  				Repo: &v1alpha1.Repository{},
  2173  				ApplicationSource: &v1alpha1.ApplicationSource{
  2174  					Path:  "",
  2175  					Chart: "",
  2176  					Ref:   "test",
  2177  				},
  2178  				AppName:            "testapp-multi-ref-only",
  2179  				ProjectName:        "something",
  2180  				ProjectSourceRepos: []string{"*"},
  2181  				HasMultipleSources: true,
  2182  			})
  2183  			require.NoError(t, err)
  2184  			assert.Empty(t, manifests.Manifests)
  2185  			assert.NotEmpty(t, manifests.Revision)
  2186  		})
  2187  	})
  2188  
  2189  	t.Run("Application specific override for other app", func(t *testing.T) {
  2190  		service := newService(t, ".")
  2191  		runWithTempTestdata(t, "single-app-only", func(t *testing.T, path string) {
  2192  			t.Helper()
  2193  			manifests, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{
  2194  				Repo: &v1alpha1.Repository{},
  2195  				ApplicationSource: &v1alpha1.ApplicationSource{
  2196  					Path: path,
  2197  				},
  2198  				AppName:            "testapp2",
  2199  				ProjectName:        "something",
  2200  				ProjectSourceRepos: []string{"*"},
  2201  			})
  2202  			require.NoError(t, err)
  2203  			resourceByKindName := make(map[string]*unstructured.Unstructured)
  2204  			for _, manifest := range manifests.Manifests {
  2205  				var un unstructured.Unstructured
  2206  				err := yaml.Unmarshal([]byte(manifest), &un)
  2207  				require.NoError(t, err)
  2208  				resourceByKindName[fmt.Sprintf("%s/%s", un.GetKind(), un.GetName())] = &un
  2209  			}
  2210  			deployment, ok := resourceByKindName["Deployment/guestbook-ui"]
  2211  			require.True(t, ok)
  2212  			containers, ok, _ := unstructured.NestedSlice(deployment.Object, "spec", "template", "spec", "containers")
  2213  			require.True(t, ok)
  2214  			image, ok, _ := unstructured.NestedString(containers[0].(map[string]any), "image")
  2215  			require.True(t, ok)
  2216  			assert.Equal(t, "quay.io/argoprojlabs/argocd-e2e-container:0.1", image)
  2217  		})
  2218  	})
  2219  
  2220  	t.Run("Override info does not appear in cache key", func(t *testing.T) {
  2221  		service := newService(t, ".")
  2222  		runWithTempTestdata(t, "single-global", func(t *testing.T, path string) {
  2223  			t.Helper()
  2224  			source := &v1alpha1.ApplicationSource{
  2225  				Path: path,
  2226  			}
  2227  			sourceCopy := source.DeepCopy() // make a copy in case GenerateManifest mutates it.
  2228  			_, err := service.GenerateManifest(t.Context(), &apiclient.ManifestRequest{
  2229  				Repo:               &v1alpha1.Repository{},
  2230  				ApplicationSource:  sourceCopy,
  2231  				AppName:            "test",
  2232  				ProjectName:        "something",
  2233  				ProjectSourceRepos: []string{"*"},
  2234  			})
  2235  			require.NoError(t, err)
  2236  			res := &cache.CachedManifestResponse{}
  2237  			// Try to pull from the cache with a `source` that does not include any overrides. Overrides should not be
  2238  			// part of the cache key, because you can't get the overrides without a repo operation. And avoiding repo
  2239  			// operations is the point of the cache.
  2240  			err = service.cache.GetManifests(mock.Anything, source, v1alpha1.RefTargetRevisionMapping{}, &v1alpha1.ClusterInfo{}, "", "", "", "test", res, nil, "")
  2241  			require.NoError(t, err)
  2242  		})
  2243  	})
  2244  }
  2245  
  2246  func TestGenerateManifestWithAnnotatedAndRegularGitTagHashes(t *testing.T) {
  2247  	regularGitTagHash := "632039659e542ed7de0c170a4fcc1c571b288fc0"
  2248  	annotatedGitTaghash := "95249be61b028d566c29d47b19e65c5603388a41"
  2249  	invalidGitTaghash := "invalid-tag"
  2250  	actualCommitSHA := "632039659e542ed7de0c170a4fcc1c571b288fc0"
  2251  
  2252  	tests := []struct {
  2253  		name            string
  2254  		ctx             context.Context
  2255  		manifestRequest *apiclient.ManifestRequest
  2256  		wantError       bool
  2257  		service         *Service
  2258  	}{
  2259  		{
  2260  			name: "Case: Git tag hash matches latest commit SHA (regular tag)",
  2261  			ctx:  t.Context(),
  2262  			manifestRequest: &apiclient.ManifestRequest{
  2263  				Repo: &v1alpha1.Repository{},
  2264  				ApplicationSource: &v1alpha1.ApplicationSource{
  2265  					TargetRevision: regularGitTagHash,
  2266  				},
  2267  				NoCache:            true,
  2268  				ProjectName:        "something",
  2269  				ProjectSourceRepos: []string{"*"},
  2270  			},
  2271  			wantError: false,
  2272  			service:   newServiceWithCommitSHA(t, ".", regularGitTagHash),
  2273  		},
  2274  
  2275  		{
  2276  			name: "Case: Git tag hash does not match latest commit SHA (annotated tag)",
  2277  			ctx:  t.Context(),
  2278  			manifestRequest: &apiclient.ManifestRequest{
  2279  				Repo: &v1alpha1.Repository{},
  2280  				ApplicationSource: &v1alpha1.ApplicationSource{
  2281  					TargetRevision: annotatedGitTaghash,
  2282  				},
  2283  				NoCache:            true,
  2284  				ProjectName:        "something",
  2285  				ProjectSourceRepos: []string{"*"},
  2286  			},
  2287  			wantError: false,
  2288  			service:   newServiceWithCommitSHA(t, ".", annotatedGitTaghash),
  2289  		},
  2290  
  2291  		{
  2292  			name: "Case: Git tag hash is invalid",
  2293  			ctx:  t.Context(),
  2294  			manifestRequest: &apiclient.ManifestRequest{
  2295  				Repo: &v1alpha1.Repository{},
  2296  				ApplicationSource: &v1alpha1.ApplicationSource{
  2297  					TargetRevision: invalidGitTaghash,
  2298  				},
  2299  				NoCache:            true,
  2300  				ProjectName:        "something",
  2301  				ProjectSourceRepos: []string{"*"},
  2302  			},
  2303  			wantError: true,
  2304  			service:   newServiceWithCommitSHA(t, ".", invalidGitTaghash),
  2305  		},
  2306  	}
  2307  	for _, tt := range tests {
  2308  		t.Run(tt.name, func(t *testing.T) {
  2309  			manifestResponse, err := tt.service.GenerateManifest(tt.ctx, tt.manifestRequest)
  2310  			if !tt.wantError {
  2311  				require.NoError(t, err)
  2312  				assert.Equal(t, manifestResponse.Revision, actualCommitSHA)
  2313  			} else {
  2314  				assert.Errorf(t, err, "expected an error but did not throw one")
  2315  			}
  2316  		})
  2317  	}
  2318  }
  2319  
  2320  func TestGenerateManifestWithAnnotatedTagsAndMultiSourceApp(t *testing.T) {
  2321  	annotatedGitTaghash := "95249be61b028d566c29d47b19e65c5603388a41"
  2322  
  2323  	service := newServiceWithCommitSHA(t, ".", annotatedGitTaghash)
  2324  
  2325  	refSources := map[string]*v1alpha1.RefTarget{}
  2326  
  2327  	refSources["$global"] = &v1alpha1.RefTarget{
  2328  		TargetRevision: annotatedGitTaghash,
  2329  	}
  2330  
  2331  	refSources["$default"] = &v1alpha1.RefTarget{
  2332  		TargetRevision: annotatedGitTaghash,
  2333  	}
  2334  
  2335  	manifestRequest := &apiclient.ManifestRequest{
  2336  		Repo: &v1alpha1.Repository{},
  2337  		ApplicationSource: &v1alpha1.ApplicationSource{
  2338  			TargetRevision: annotatedGitTaghash,
  2339  			Helm: &v1alpha1.ApplicationSourceHelm{
  2340  				ValueFiles: []string{"$global/values.yaml", "$default/secrets.yaml"},
  2341  			},
  2342  		},
  2343  		HasMultipleSources: true,
  2344  		NoCache:            true,
  2345  		RefSources:         refSources,
  2346  	}
  2347  
  2348  	response, err := service.GenerateManifest(t.Context(), manifestRequest)
  2349  	require.NoError(t, err)
  2350  	assert.Equalf(t, response.Revision, annotatedGitTaghash, "returned SHA %s is different from expected annotated tag %s", response.Revision, annotatedGitTaghash)
  2351  }
  2352  
  2353  func TestGenerateMultiSourceHelmWithFileParameter(t *testing.T) {
  2354  	expectedFileContent, err := os.ReadFile("../../util/helm/testdata/external/external-secret.txt")
  2355  	require.NoError(t, err)
  2356  
  2357  	service := newService(t, "../../util/helm/testdata")
  2358  
  2359  	testCases := []struct {
  2360  		name            string
  2361  		refSources      map[string]*v1alpha1.RefTarget
  2362  		expectedContent string
  2363  		expectedErr     bool
  2364  	}{{
  2365  		name: "Successfully resolve multi-source ref for helm set-file",
  2366  		refSources: map[string]*v1alpha1.RefTarget{
  2367  			"$global": {
  2368  				TargetRevision: "HEAD",
  2369  			},
  2370  		},
  2371  		expectedContent: string(expectedFileContent),
  2372  		expectedErr:     false,
  2373  	}, {
  2374  		name:            "Failed to resolve multi-source ref for helm set-file",
  2375  		refSources:      map[string]*v1alpha1.RefTarget{},
  2376  		expectedContent: "DOES-NOT-EXIST",
  2377  		expectedErr:     true,
  2378  	}}
  2379  
  2380  	for i := range testCases {
  2381  		tc := testCases[i]
  2382  		t.Run(tc.name, func(t *testing.T) {
  2383  			manifestRequest := &apiclient.ManifestRequest{
  2384  				Repo: &v1alpha1.Repository{},
  2385  				ApplicationSource: &v1alpha1.ApplicationSource{
  2386  					Ref:            "$global",
  2387  					Path:           "./redis",
  2388  					TargetRevision: "HEAD",
  2389  					Helm: &v1alpha1.ApplicationSourceHelm{
  2390  						ValueFiles: []string{"$global/redis/values-production.yaml"},
  2391  						FileParameters: []v1alpha1.HelmFileParameter{{
  2392  							Name: "passwordContent",
  2393  							Path: "$global/external/external-secret.txt",
  2394  						}},
  2395  					},
  2396  				},
  2397  				HasMultipleSources: true,
  2398  				NoCache:            true,
  2399  				RefSources:         tc.refSources,
  2400  			}
  2401  
  2402  			res, err := service.GenerateManifest(t.Context(), manifestRequest)
  2403  
  2404  			if !tc.expectedErr {
  2405  				require.NoError(t, err)
  2406  
  2407  				// Check that any of the manifests contains the secret
  2408  				idx := slices.IndexFunc(res.Manifests, func(content string) bool {
  2409  					return strings.Contains(content, tc.expectedContent)
  2410  				})
  2411  				assert.GreaterOrEqual(t, idx, 0, "No manifest contains the value set with the helm fileParameters")
  2412  			} else {
  2413  				assert.Error(t, err)
  2414  			}
  2415  		})
  2416  	}
  2417  }
  2418  
  2419  func TestFindResources(t *testing.T) {
  2420  	testCases := []struct {
  2421  		name          string
  2422  		include       string
  2423  		exclude       string
  2424  		expectedNames []string
  2425  	}{{
  2426  		name:          "Include One Match",
  2427  		include:       "subdir/deploymentSub.yaml",
  2428  		expectedNames: []string{"nginx-deployment-sub"},
  2429  	}, {
  2430  		name:          "Include Everything",
  2431  		include:       "*.yaml",
  2432  		expectedNames: []string{"nginx-deployment", "nginx-deployment-sub"},
  2433  	}, {
  2434  		name:          "Include Subdirectory",
  2435  		include:       "**/*.yaml",
  2436  		expectedNames: []string{"nginx-deployment-sub"},
  2437  	}, {
  2438  		name:          "Include No Matches",
  2439  		include:       "nothing.yaml",
  2440  		expectedNames: []string{},
  2441  	}, {
  2442  		name:          "Exclude - One Match",
  2443  		exclude:       "subdir/deploymentSub.yaml",
  2444  		expectedNames: []string{"nginx-deployment"},
  2445  	}, {
  2446  		name:          "Exclude - Everything",
  2447  		exclude:       "*.yaml",
  2448  		expectedNames: []string{},
  2449  	}}
  2450  	for i := range testCases {
  2451  		tc := testCases[i]
  2452  		t.Run(tc.name, func(t *testing.T) {
  2453  			objs, err := findManifests(&log.Entry{}, "testdata/app-include-exclude", ".", nil, v1alpha1.ApplicationSourceDirectory{
  2454  				Recurse: true,
  2455  				Include: tc.include,
  2456  				Exclude: tc.exclude,
  2457  			}, map[string]bool{}, resource.MustParse("0"))
  2458  			require.NoError(t, err)
  2459  			var names []string
  2460  			for i := range objs {
  2461  				names = append(names, objs[i].GetName())
  2462  			}
  2463  			assert.ElementsMatch(t, tc.expectedNames, names)
  2464  		})
  2465  	}
  2466  }
  2467  
  2468  func TestFindManifests_Exclude(t *testing.T) {
  2469  	objs, err := findManifests(&log.Entry{}, "testdata/app-include-exclude", ".", nil, v1alpha1.ApplicationSourceDirectory{
  2470  		Recurse: true,
  2471  		Exclude: "subdir/deploymentSub.yaml",
  2472  	}, map[string]bool{}, resource.MustParse("0"))
  2473  
  2474  	require.NoError(t, err)
  2475  	require.Len(t, objs, 1)
  2476  
  2477  	assert.Equal(t, "nginx-deployment", objs[0].GetName())
  2478  }
  2479  
  2480  func TestFindManifests_Exclude_NothingMatches(t *testing.T) {
  2481  	objs, err := findManifests(&log.Entry{}, "testdata/app-include-exclude", ".", nil, v1alpha1.ApplicationSourceDirectory{
  2482  		Recurse: true,
  2483  		Exclude: "nothing.yaml",
  2484  	}, map[string]bool{}, resource.MustParse("0"))
  2485  
  2486  	require.NoError(t, err)
  2487  	require.Len(t, objs, 2)
  2488  
  2489  	assert.ElementsMatch(t,
  2490  		[]string{"nginx-deployment", "nginx-deployment-sub"}, []string{objs[0].GetName(), objs[1].GetName()})
  2491  }
  2492  
  2493  func tempDir(t *testing.T) string {
  2494  	t.Helper()
  2495  	dir, err := os.MkdirTemp(".", "")
  2496  	require.NoError(t, err)
  2497  	t.Cleanup(func() {
  2498  		err = os.RemoveAll(dir)
  2499  		if err != nil {
  2500  			panic(err)
  2501  		}
  2502  	})
  2503  	absDir, err := filepath.Abs(dir)
  2504  	require.NoError(t, err)
  2505  	return absDir
  2506  }
  2507  
  2508  func walkFor(t *testing.T, root string, testPath string, run func(info fs.FileInfo)) {
  2509  	t.Helper()
  2510  	hitExpectedPath := false
  2511  	err := filepath.Walk(root, func(path string, info fs.FileInfo, err error) error {
  2512  		if path == testPath {
  2513  			require.NoError(t, err)
  2514  			hitExpectedPath = true
  2515  			run(info)
  2516  		}
  2517  		return nil
  2518  	})
  2519  	require.NoError(t, err)
  2520  	assert.True(t, hitExpectedPath, "did not hit expected path when walking directory")
  2521  }
  2522  
  2523  func Test_getPotentiallyValidManifestFile(t *testing.T) {
  2524  	// These tests use filepath.Walk instead of os.Stat to get file info, because FileInfo from os.Stat does not return
  2525  	// true for IsSymlink like os.Walk does.
  2526  
  2527  	// These tests do not use t.TempDir() because those directories can contain symlinks which cause test to fail
  2528  	// InBound checks.
  2529  
  2530  	t.Run("non-JSON/YAML is skipped with an empty ignore message", func(t *testing.T) {
  2531  		appDir := tempDir(t)
  2532  		filePath := filepath.Join(appDir, "not-json-or-yaml")
  2533  		file, err := os.OpenFile(filePath, os.O_RDONLY|os.O_CREATE, 0o644)
  2534  		require.NoError(t, err)
  2535  		err = file.Close()
  2536  		require.NoError(t, err)
  2537  
  2538  		walkFor(t, appDir, filePath, func(info fs.FileInfo) {
  2539  			realFileInfo, ignoreMessage, err := getPotentiallyValidManifestFile(filePath, info, appDir, appDir, "", "")
  2540  			assert.Nil(t, realFileInfo)
  2541  			assert.Empty(t, ignoreMessage)
  2542  			require.NoError(t, err)
  2543  		})
  2544  	})
  2545  
  2546  	t.Run("circular link should throw an error", func(t *testing.T) {
  2547  		appDir := tempDir(t)
  2548  
  2549  		aPath := filepath.Join(appDir, "a.json")
  2550  		bPath := filepath.Join(appDir, "b.json")
  2551  		err := os.Symlink(bPath, aPath)
  2552  		require.NoError(t, err)
  2553  		err = os.Symlink(aPath, bPath)
  2554  		require.NoError(t, err)
  2555  
  2556  		walkFor(t, appDir, aPath, func(info fs.FileInfo) {
  2557  			realFileInfo, ignoreMessage, err := getPotentiallyValidManifestFile(aPath, info, appDir, appDir, "", "")
  2558  			assert.Nil(t, realFileInfo)
  2559  			assert.Empty(t, ignoreMessage)
  2560  			assert.ErrorContains(t, err, "too many links")
  2561  		})
  2562  	})
  2563  
  2564  	t.Run("symlink with missing destination should throw an error", func(t *testing.T) {
  2565  		appDir := tempDir(t)
  2566  
  2567  		aPath := filepath.Join(appDir, "a.json")
  2568  		bPath := filepath.Join(appDir, "b.json")
  2569  		err := os.Symlink(bPath, aPath)
  2570  		require.NoError(t, err)
  2571  
  2572  		walkFor(t, appDir, aPath, func(info fs.FileInfo) {
  2573  			realFileInfo, ignoreMessage, err := getPotentiallyValidManifestFile(aPath, info, appDir, appDir, "", "")
  2574  			assert.Nil(t, realFileInfo)
  2575  			assert.NotEmpty(t, ignoreMessage)
  2576  			require.NoError(t, err)
  2577  		})
  2578  	})
  2579  
  2580  	t.Run("out-of-bounds symlink should throw an error", func(t *testing.T) {
  2581  		appDir := tempDir(t)
  2582  
  2583  		linkPath := filepath.Join(appDir, "a.json")
  2584  		err := os.Symlink("..", linkPath)
  2585  		require.NoError(t, err)
  2586  
  2587  		walkFor(t, appDir, linkPath, func(info fs.FileInfo) {
  2588  			realFileInfo, ignoreMessage, err := getPotentiallyValidManifestFile(linkPath, info, appDir, appDir, "", "")
  2589  			assert.Nil(t, realFileInfo)
  2590  			assert.Empty(t, ignoreMessage)
  2591  			assert.ErrorContains(t, err, "illegal filepath in symlink")
  2592  		})
  2593  	})
  2594  
  2595  	t.Run("symlink to a non-regular file should be skipped with warning", func(t *testing.T) {
  2596  		appDir := tempDir(t)
  2597  
  2598  		dirPath := filepath.Join(appDir, "test.dir")
  2599  		err := os.MkdirAll(dirPath, 0o644)
  2600  		require.NoError(t, err)
  2601  		linkPath := filepath.Join(appDir, "test.json")
  2602  		err = os.Symlink(dirPath, linkPath)
  2603  		require.NoError(t, err)
  2604  
  2605  		walkFor(t, appDir, linkPath, func(info fs.FileInfo) {
  2606  			realFileInfo, ignoreMessage, err := getPotentiallyValidManifestFile(linkPath, info, appDir, appDir, "", "")
  2607  			assert.Nil(t, realFileInfo)
  2608  			assert.Contains(t, ignoreMessage, "non-regular file")
  2609  			require.NoError(t, err)
  2610  		})
  2611  	})
  2612  
  2613  	t.Run("non-included file should be skipped with no message", func(t *testing.T) {
  2614  		appDir := tempDir(t)
  2615  
  2616  		filePath := filepath.Join(appDir, "not-included.yaml")
  2617  		file, err := os.OpenFile(filePath, os.O_RDONLY|os.O_CREATE, 0o644)
  2618  		require.NoError(t, err)
  2619  		err = file.Close()
  2620  		require.NoError(t, err)
  2621  
  2622  		walkFor(t, appDir, filePath, func(info fs.FileInfo) {
  2623  			realFileInfo, ignoreMessage, err := getPotentiallyValidManifestFile(filePath, info, appDir, appDir, "*.json", "")
  2624  			assert.Nil(t, realFileInfo)
  2625  			assert.Empty(t, ignoreMessage)
  2626  			require.NoError(t, err)
  2627  		})
  2628  	})
  2629  
  2630  	t.Run("excluded file should be skipped with no message", func(t *testing.T) {
  2631  		appDir := tempDir(t)
  2632  
  2633  		filePath := filepath.Join(appDir, "excluded.json")
  2634  		file, err := os.OpenFile(filePath, os.O_RDONLY|os.O_CREATE, 0o644)
  2635  		require.NoError(t, err)
  2636  		err = file.Close()
  2637  		require.NoError(t, err)
  2638  
  2639  		walkFor(t, appDir, filePath, func(info fs.FileInfo) {
  2640  			realFileInfo, ignoreMessage, err := getPotentiallyValidManifestFile(filePath, info, appDir, appDir, "", "excluded.*")
  2641  			assert.Nil(t, realFileInfo)
  2642  			assert.Empty(t, ignoreMessage)
  2643  			require.NoError(t, err)
  2644  		})
  2645  	})
  2646  
  2647  	t.Run("symlink to a regular file is potentially valid", func(t *testing.T) {
  2648  		appDir := tempDir(t)
  2649  
  2650  		filePath := filepath.Join(appDir, "regular-file")
  2651  		file, err := os.OpenFile(filePath, os.O_RDONLY|os.O_CREATE, 0o644)
  2652  		require.NoError(t, err)
  2653  		err = file.Close()
  2654  		require.NoError(t, err)
  2655  
  2656  		linkPath := filepath.Join(appDir, "link.json")
  2657  		err = os.Symlink(filePath, linkPath)
  2658  		require.NoError(t, err)
  2659  
  2660  		walkFor(t, appDir, linkPath, func(info fs.FileInfo) {
  2661  			realFileInfo, ignoreMessage, err := getPotentiallyValidManifestFile(linkPath, info, appDir, appDir, "", "")
  2662  			assert.NotNil(t, realFileInfo)
  2663  			assert.Empty(t, ignoreMessage)
  2664  			require.NoError(t, err)
  2665  		})
  2666  	})
  2667  
  2668  	t.Run("a regular file is potentially valid", func(t *testing.T) {
  2669  		appDir := tempDir(t)
  2670  
  2671  		filePath := filepath.Join(appDir, "regular-file.json")
  2672  		file, err := os.OpenFile(filePath, os.O_RDONLY|os.O_CREATE, 0o644)
  2673  		require.NoError(t, err)
  2674  		err = file.Close()
  2675  		require.NoError(t, err)
  2676  
  2677  		walkFor(t, appDir, filePath, func(info fs.FileInfo) {
  2678  			realFileInfo, ignoreMessage, err := getPotentiallyValidManifestFile(filePath, info, appDir, appDir, "", "")
  2679  			assert.NotNil(t, realFileInfo)
  2680  			assert.Empty(t, ignoreMessage)
  2681  			require.NoError(t, err)
  2682  		})
  2683  	})
  2684  
  2685  	t.Run("realFileInfo is for the destination rather than the symlink", func(t *testing.T) {
  2686  		appDir := tempDir(t)
  2687  
  2688  		filePath := filepath.Join(appDir, "regular-file")
  2689  		file, err := os.OpenFile(filePath, os.O_RDONLY|os.O_CREATE, 0o644)
  2690  		require.NoError(t, err)
  2691  		err = file.Close()
  2692  		require.NoError(t, err)
  2693  
  2694  		linkPath := filepath.Join(appDir, "link.json")
  2695  		err = os.Symlink(filePath, linkPath)
  2696  		require.NoError(t, err)
  2697  
  2698  		walkFor(t, appDir, linkPath, func(info fs.FileInfo) {
  2699  			realFileInfo, ignoreMessage, err := getPotentiallyValidManifestFile(linkPath, info, appDir, appDir, "", "")
  2700  			assert.NotNil(t, realFileInfo)
  2701  			assert.Equal(t, filepath.Base(filePath), realFileInfo.Name())
  2702  			assert.Empty(t, ignoreMessage)
  2703  			require.NoError(t, err)
  2704  		})
  2705  	})
  2706  }
  2707  
  2708  func Test_getPotentiallyValidManifests(t *testing.T) {
  2709  	// Tests which return no manifests and an error check to make sure the directory exists before running. A missing
  2710  	// directory would produce those same results.
  2711  
  2712  	logCtx := log.WithField("test", "test")
  2713  
  2714  	t.Run("unreadable file throws error", func(t *testing.T) {
  2715  		appDir := t.TempDir()
  2716  		unreadablePath := filepath.Join(appDir, "unreadable.json")
  2717  		err := os.WriteFile(unreadablePath, []byte{}, 0o666)
  2718  		require.NoError(t, err)
  2719  		err = os.Chmod(appDir, 0o000)
  2720  		require.NoError(t, err)
  2721  
  2722  		manifests, err := getPotentiallyValidManifests(logCtx, appDir, appDir, false, "", "", resource.MustParse("0"))
  2723  		assert.Empty(t, manifests)
  2724  		require.Error(t, err)
  2725  
  2726  		// allow cleanup
  2727  		err = os.Chmod(appDir, 0o777)
  2728  		if err != nil {
  2729  			panic(err)
  2730  		}
  2731  	})
  2732  
  2733  	t.Run("no recursion when recursion is disabled", func(t *testing.T) {
  2734  		manifests, err := getPotentiallyValidManifests(logCtx, "./testdata/recurse", "./testdata/recurse", false, "", "", resource.MustParse("0"))
  2735  		assert.Len(t, manifests, 1)
  2736  		require.NoError(t, err)
  2737  	})
  2738  
  2739  	t.Run("recursion when recursion is enabled", func(t *testing.T) {
  2740  		manifests, err := getPotentiallyValidManifests(logCtx, "./testdata/recurse", "./testdata/recurse", true, "", "", resource.MustParse("0"))
  2741  		assert.Len(t, manifests, 2)
  2742  		require.NoError(t, err)
  2743  	})
  2744  
  2745  	t.Run("non-JSON/YAML is skipped", func(t *testing.T) {
  2746  		manifests, err := getPotentiallyValidManifests(logCtx, "./testdata/non-manifest-file", "./testdata/non-manifest-file", false, "", "", resource.MustParse("0"))
  2747  		assert.Empty(t, manifests)
  2748  		require.NoError(t, err)
  2749  	})
  2750  
  2751  	t.Run("circular link should throw an error", func(t *testing.T) {
  2752  		const testDir = "./testdata/circular-link"
  2753  		require.DirExists(t, testDir)
  2754  		t.Cleanup(func() {
  2755  			os.Remove(path.Join(testDir, "a.json"))
  2756  			os.Remove(path.Join(testDir, "b.json"))
  2757  		})
  2758  		t.Chdir(testDir)
  2759  		require.NoError(t, fileutil.CreateSymlink(t, "a.json", "b.json"))
  2760  		require.NoError(t, fileutil.CreateSymlink(t, "b.json", "a.json"))
  2761  		manifests, err := getPotentiallyValidManifests(logCtx, "./testdata/circular-link", "./testdata/circular-link", false, "", "", resource.MustParse("0"))
  2762  		assert.Empty(t, manifests)
  2763  		require.Error(t, err)
  2764  	})
  2765  
  2766  	t.Run("out-of-bounds symlink should throw an error", func(t *testing.T) {
  2767  		require.DirExists(t, "./testdata/out-of-bounds-link")
  2768  		manifests, err := getPotentiallyValidManifests(logCtx, "./testdata/out-of-bounds-link", "./testdata/out-of-bounds-link", false, "", "", resource.MustParse("0"))
  2769  		assert.Empty(t, manifests)
  2770  		require.Error(t, err)
  2771  	})
  2772  
  2773  	t.Run("symlink to a regular file works", func(t *testing.T) {
  2774  		repoRoot, err := filepath.Abs("./testdata/in-bounds-link")
  2775  		require.NoError(t, err)
  2776  		appPath, err := filepath.Abs("./testdata/in-bounds-link/app")
  2777  		require.NoError(t, err)
  2778  		manifests, err := getPotentiallyValidManifests(logCtx, appPath, repoRoot, false, "", "", resource.MustParse("0"))
  2779  		assert.Len(t, manifests, 1)
  2780  		require.NoError(t, err)
  2781  	})
  2782  
  2783  	t.Run("symlink to nowhere should be ignored", func(t *testing.T) {
  2784  		manifests, err := getPotentiallyValidManifests(logCtx, "./testdata/link-to-nowhere", "./testdata/link-to-nowhere", false, "", "", resource.MustParse("0"))
  2785  		assert.Empty(t, manifests)
  2786  		require.NoError(t, err)
  2787  	})
  2788  
  2789  	t.Run("link to over-sized manifest fails", func(t *testing.T) {
  2790  		repoRoot, err := filepath.Abs("./testdata/in-bounds-link")
  2791  		require.NoError(t, err)
  2792  		appPath, err := filepath.Abs("./testdata/in-bounds-link/app")
  2793  		require.NoError(t, err)
  2794  		// The file is 35 bytes.
  2795  		manifests, err := getPotentiallyValidManifests(logCtx, appPath, repoRoot, false, "", "", resource.MustParse("34"))
  2796  		assert.Empty(t, manifests)
  2797  		assert.ErrorIs(t, err, ErrExceededMaxCombinedManifestFileSize)
  2798  	})
  2799  
  2800  	t.Run("group of files should be limited at precisely the sum of their size", func(t *testing.T) {
  2801  		// There is a total of 10 files, ech file being 10 bytes.
  2802  		manifests, err := getPotentiallyValidManifests(logCtx, "./testdata/several-files", "./testdata/several-files", false, "", "", resource.MustParse("365"))
  2803  		assert.Len(t, manifests, 10)
  2804  		require.NoError(t, err)
  2805  
  2806  		manifests, err = getPotentiallyValidManifests(logCtx, "./testdata/several-files", "./testdata/several-files", false, "", "", resource.MustParse("100"))
  2807  		assert.Empty(t, manifests)
  2808  		assert.ErrorIs(t, err, ErrExceededMaxCombinedManifestFileSize)
  2809  	})
  2810  }
  2811  
  2812  func Test_findManifests(t *testing.T) {
  2813  	logCtx := log.WithField("test", "test")
  2814  	noRecurse := v1alpha1.ApplicationSourceDirectory{Recurse: false}
  2815  
  2816  	t.Run("unreadable file throws error", func(t *testing.T) {
  2817  		appDir := t.TempDir()
  2818  		unreadablePath := filepath.Join(appDir, "unreadable.json")
  2819  		err := os.WriteFile(unreadablePath, []byte{}, 0o666)
  2820  		require.NoError(t, err)
  2821  		err = os.Chmod(appDir, 0o000)
  2822  		require.NoError(t, err)
  2823  
  2824  		manifests, err := findManifests(logCtx, appDir, appDir, nil, noRecurse, nil, resource.MustParse("0"))
  2825  		assert.Empty(t, manifests)
  2826  		require.Error(t, err)
  2827  
  2828  		// allow cleanup
  2829  		err = os.Chmod(appDir, 0o777)
  2830  		if err != nil {
  2831  			panic(err)
  2832  		}
  2833  	})
  2834  
  2835  	t.Run("no recursion when recursion is disabled", func(t *testing.T) {
  2836  		manifests, err := findManifests(logCtx, "./testdata/recurse", "./testdata/recurse", nil, noRecurse, nil, resource.MustParse("0"))
  2837  		assert.Len(t, manifests, 2)
  2838  		require.NoError(t, err)
  2839  	})
  2840  
  2841  	t.Run("recursion when recursion is enabled", func(t *testing.T) {
  2842  		recurse := v1alpha1.ApplicationSourceDirectory{Recurse: true}
  2843  		manifests, err := findManifests(logCtx, "./testdata/recurse", "./testdata/recurse", nil, recurse, nil, resource.MustParse("0"))
  2844  		assert.Len(t, manifests, 4)
  2845  		require.NoError(t, err)
  2846  	})
  2847  
  2848  	t.Run("non-JSON/YAML is skipped", func(t *testing.T) {
  2849  		manifests, err := findManifests(logCtx, "./testdata/non-manifest-file", "./testdata/non-manifest-file", nil, noRecurse, nil, resource.MustParse("0"))
  2850  		assert.Empty(t, manifests)
  2851  		require.NoError(t, err)
  2852  	})
  2853  
  2854  	t.Run("circular link should throw an error", func(t *testing.T) {
  2855  		const testDir = "./testdata/circular-link"
  2856  		require.DirExists(t, testDir)
  2857  		t.Cleanup(func() {
  2858  			os.Remove(path.Join(testDir, "a.json"))
  2859  			os.Remove(path.Join(testDir, "b.json"))
  2860  		})
  2861  		t.Chdir(testDir)
  2862  		require.NoError(t, fileutil.CreateSymlink(t, "a.json", "b.json"))
  2863  		require.NoError(t, fileutil.CreateSymlink(t, "b.json", "a.json"))
  2864  		manifests, err := findManifests(logCtx, "./testdata/circular-link", "./testdata/circular-link", nil, noRecurse, nil, resource.MustParse("0"))
  2865  		assert.Empty(t, manifests)
  2866  		require.Error(t, err)
  2867  	})
  2868  
  2869  	t.Run("out-of-bounds symlink should throw an error", func(t *testing.T) {
  2870  		require.DirExists(t, "./testdata/out-of-bounds-link")
  2871  		manifests, err := findManifests(logCtx, "./testdata/out-of-bounds-link", "./testdata/out-of-bounds-link", nil, noRecurse, nil, resource.MustParse("0"))
  2872  		assert.Empty(t, manifests)
  2873  		require.Error(t, err)
  2874  	})
  2875  
  2876  	t.Run("symlink to a regular file works", func(t *testing.T) {
  2877  		repoRoot, err := filepath.Abs("./testdata/in-bounds-link")
  2878  		require.NoError(t, err)
  2879  		appPath, err := filepath.Abs("./testdata/in-bounds-link/app")
  2880  		require.NoError(t, err)
  2881  		manifests, err := findManifests(logCtx, appPath, repoRoot, nil, noRecurse, nil, resource.MustParse("0"))
  2882  		assert.Len(t, manifests, 1)
  2883  		require.NoError(t, err)
  2884  	})
  2885  
  2886  	t.Run("symlink to nowhere should be ignored", func(t *testing.T) {
  2887  		manifests, err := findManifests(logCtx, "./testdata/link-to-nowhere", "./testdata/link-to-nowhere", nil, noRecurse, nil, resource.MustParse("0"))
  2888  		assert.Empty(t, manifests)
  2889  		require.NoError(t, err)
  2890  	})
  2891  
  2892  	t.Run("link to over-sized manifest fails", func(t *testing.T) {
  2893  		repoRoot, err := filepath.Abs("./testdata/in-bounds-link")
  2894  		require.NoError(t, err)
  2895  		appPath, err := filepath.Abs("./testdata/in-bounds-link/app")
  2896  		require.NoError(t, err)
  2897  		// The file is 35 bytes.
  2898  		manifests, err := findManifests(logCtx, appPath, repoRoot, nil, noRecurse, nil, resource.MustParse("34"))
  2899  		assert.Empty(t, manifests)
  2900  		assert.ErrorIs(t, err, ErrExceededMaxCombinedManifestFileSize)
  2901  	})
  2902  
  2903  	t.Run("group of files should be limited at precisely the sum of their size", func(t *testing.T) {
  2904  		// There is a total of 10 files, each file being 10 bytes.
  2905  		manifests, err := findManifests(logCtx, "./testdata/several-files", "./testdata/several-files", nil, noRecurse, nil, resource.MustParse("365"))
  2906  		assert.Len(t, manifests, 10)
  2907  		require.NoError(t, err)
  2908  
  2909  		manifests, err = findManifests(logCtx, "./testdata/several-files", "./testdata/several-files", nil, noRecurse, nil, resource.MustParse("364"))
  2910  		assert.Empty(t, manifests)
  2911  		assert.ErrorIs(t, err, ErrExceededMaxCombinedManifestFileSize)
  2912  	})
  2913  
  2914  	t.Run("jsonnet isn't counted against size limit", func(t *testing.T) {
  2915  		// Each file is 36 bytes. Only the 36-byte json file should be counted against the limit.
  2916  		manifests, err := findManifests(logCtx, "./testdata/jsonnet-and-json", "./testdata/jsonnet-and-json", nil, noRecurse, nil, resource.MustParse("36"))
  2917  		assert.Len(t, manifests, 2)
  2918  		require.NoError(t, err)
  2919  
  2920  		manifests, err = findManifests(logCtx, "./testdata/jsonnet-and-json", "./testdata/jsonnet-and-json", nil, noRecurse, nil, resource.MustParse("35"))
  2921  		assert.Empty(t, manifests)
  2922  		assert.ErrorIs(t, err, ErrExceededMaxCombinedManifestFileSize)
  2923  	})
  2924  
  2925  	t.Run("partially valid YAML file throws an error", func(t *testing.T) {
  2926  		require.DirExists(t, "./testdata/partially-valid-yaml")
  2927  		manifests, err := findManifests(logCtx, "./testdata/partially-valid-yaml", "./testdata/partially-valid-yaml", nil, noRecurse, nil, resource.MustParse("0"))
  2928  		assert.Empty(t, manifests)
  2929  		require.Error(t, err)
  2930  	})
  2931  
  2932  	t.Run("invalid manifest throws an error", func(t *testing.T) {
  2933  		require.DirExists(t, "./testdata/invalid-manifests")
  2934  		manifests, err := findManifests(logCtx, "./testdata/invalid-manifests", "./testdata/invalid-manifests", nil, noRecurse, nil, resource.MustParse("0"))
  2935  		assert.Empty(t, manifests)
  2936  		require.Error(t, err)
  2937  	})
  2938  
  2939  	t.Run("invalid manifest containing '+argocd:skip-file-rendering' doesn't throw an error", func(t *testing.T) {
  2940  		require.DirExists(t, "./testdata/invalid-manifests-skipped")
  2941  		manifests, err := findManifests(logCtx, "./testdata/invalid-manifests-skipped", "./testdata/invalid-manifests-skipped", nil, noRecurse, nil, resource.MustParse("0"))
  2942  		assert.Empty(t, manifests)
  2943  		require.NoError(t, err)
  2944  	})
  2945  
  2946  	t.Run("irrelevant YAML gets skipped, relevant YAML gets parsed", func(t *testing.T) {
  2947  		manifests, err := findManifests(logCtx, "./testdata/irrelevant-yaml", "./testdata/irrelevant-yaml", nil, noRecurse, nil, resource.MustParse("0"))
  2948  		assert.Len(t, manifests, 1)
  2949  		require.NoError(t, err)
  2950  	})
  2951  
  2952  	t.Run("multiple JSON objects in one file throws an error", func(t *testing.T) {
  2953  		require.DirExists(t, "./testdata/json-list")
  2954  		manifests, err := findManifests(logCtx, "./testdata/json-list", "./testdata/json-list", nil, noRecurse, nil, resource.MustParse("0"))
  2955  		assert.Empty(t, manifests)
  2956  		require.Error(t, err)
  2957  	})
  2958  
  2959  	t.Run("invalid JSON throws an error", func(t *testing.T) {
  2960  		require.DirExists(t, "./testdata/invalid-json")
  2961  		manifests, err := findManifests(logCtx, "./testdata/invalid-json", "./testdata/invalid-json", nil, noRecurse, nil, resource.MustParse("0"))
  2962  		assert.Empty(t, manifests)
  2963  		require.Error(t, err)
  2964  	})
  2965  
  2966  	t.Run("valid JSON returns manifest and no error", func(t *testing.T) {
  2967  		manifests, err := findManifests(logCtx, "./testdata/valid-json", "./testdata/valid-json", nil, noRecurse, nil, resource.MustParse("0"))
  2968  		assert.Len(t, manifests, 1)
  2969  		require.NoError(t, err)
  2970  	})
  2971  
  2972  	t.Run("YAML with an empty document doesn't throw an error", func(t *testing.T) {
  2973  		manifests, err := findManifests(logCtx, "./testdata/yaml-with-empty-document", "./testdata/yaml-with-empty-document", nil, noRecurse, nil, resource.MustParse("0"))
  2974  		assert.Len(t, manifests, 1)
  2975  		require.NoError(t, err)
  2976  	})
  2977  }
  2978  
  2979  func TestTestRepoHelmOCI(t *testing.T) {
  2980  	service := newService(t, ".")
  2981  	_, err := service.TestRepository(t.Context(), &apiclient.TestRepositoryRequest{
  2982  		Repo: &v1alpha1.Repository{
  2983  			Repo:      "https://demo.goharbor.io",
  2984  			Type:      "helm",
  2985  			EnableOCI: true,
  2986  		},
  2987  	})
  2988  	assert.ErrorContains(t, err, "OCI Helm repository URL should include hostname and port only")
  2989  }
  2990  
  2991  func Test_getHelmDependencyRepos(t *testing.T) {
  2992  	repo1 := "https://charts.bitnami.com/bitnami"
  2993  	repo2 := "https://eventstore.github.io/EventStore.Charts"
  2994  
  2995  	repos, err := getHelmDependencyRepos("../../util/helm/testdata/dependency")
  2996  	require.NoError(t, err)
  2997  	assert.Len(t, repos, 2)
  2998  	assert.Equal(t, repos[0].Repo, repo1)
  2999  	assert.Equal(t, repos[1].Repo, repo2)
  3000  }
  3001  
  3002  func TestResolveRevision(t *testing.T) {
  3003  	service := newService(t, ".")
  3004  	repo := &v1alpha1.Repository{Repo: "https://github.com/argoproj/argo-cd"}
  3005  	app := &v1alpha1.Application{Spec: v1alpha1.ApplicationSpec{Source: &v1alpha1.ApplicationSource{}}}
  3006  	resolveRevisionResponse, err := service.ResolveRevision(t.Context(), &apiclient.ResolveRevisionRequest{
  3007  		Repo:              repo,
  3008  		App:               app,
  3009  		AmbiguousRevision: "v2.2.2",
  3010  	})
  3011  
  3012  	expectedResolveRevisionResponse := &apiclient.ResolveRevisionResponse{
  3013  		Revision:          "03b17e0233e64787ffb5fcf65c740cc2a20822ba",
  3014  		AmbiguousRevision: "v2.2.2 (03b17e0233e64787ffb5fcf65c740cc2a20822ba)",
  3015  	}
  3016  
  3017  	assert.NotNil(t, resolveRevisionResponse.Revision)
  3018  	require.NoError(t, err)
  3019  	assert.Equal(t, expectedResolveRevisionResponse, resolveRevisionResponse)
  3020  }
  3021  
  3022  func TestResolveRevisionNegativeScenarios(t *testing.T) {
  3023  	service := newService(t, ".")
  3024  	repo := &v1alpha1.Repository{Repo: "https://github.com/argoproj/argo-cd"}
  3025  	app := &v1alpha1.Application{Spec: v1alpha1.ApplicationSpec{Source: &v1alpha1.ApplicationSource{}}}
  3026  	resolveRevisionResponse, err := service.ResolveRevision(t.Context(), &apiclient.ResolveRevisionRequest{
  3027  		Repo:              repo,
  3028  		App:               app,
  3029  		AmbiguousRevision: "v2.a.2",
  3030  	})
  3031  
  3032  	expectedResolveRevisionResponse := &apiclient.ResolveRevisionResponse{
  3033  		Revision:          "",
  3034  		AmbiguousRevision: "",
  3035  	}
  3036  
  3037  	assert.NotNil(t, resolveRevisionResponse.Revision)
  3038  	require.Error(t, err)
  3039  	assert.Equal(t, expectedResolveRevisionResponse, resolveRevisionResponse)
  3040  }
  3041  
  3042  func TestDirectoryPermissionInitializer(t *testing.T) {
  3043  	dir := t.TempDir()
  3044  
  3045  	file, err := os.CreateTemp(dir, "")
  3046  	require.NoError(t, err)
  3047  	utilio.Close(file)
  3048  
  3049  	// remove read permissions
  3050  	require.NoError(t, os.Chmod(dir, 0o000))
  3051  
  3052  	// Remember to restore permissions when the test finishes so dir can
  3053  	// be removed properly.
  3054  	t.Cleanup(func() {
  3055  		require.NoError(t, os.Chmod(dir, 0o777))
  3056  	})
  3057  
  3058  	// make sure permission are restored
  3059  	closer := directoryPermissionInitializer(dir)
  3060  	_, err = os.ReadFile(file.Name())
  3061  	require.NoError(t, err)
  3062  
  3063  	// make sure permission are removed by closer
  3064  	utilio.Close(closer)
  3065  	_, err = os.ReadFile(file.Name())
  3066  	require.Error(t, err)
  3067  }
  3068  
  3069  func addHelmToGitRepo(t *testing.T, options newGitRepoOptions) {
  3070  	t.Helper()
  3071  	err := os.WriteFile(filepath.Join(options.path, "Chart.yaml"), []byte("name: test\nversion: v1.0.0"), 0o777)
  3072  	require.NoError(t, err)
  3073  	for valuesFileName, values := range options.helmChartOptions.valuesFiles {
  3074  		valuesFileContents, err := yaml.Marshal(values)
  3075  		require.NoError(t, err)
  3076  		err = os.WriteFile(filepath.Join(options.path, valuesFileName), valuesFileContents, 0o777)
  3077  		require.NoError(t, err)
  3078  	}
  3079  	require.NoError(t, err)
  3080  	cmd := exec.Command("git", "add", "-A")
  3081  	cmd.Dir = options.path
  3082  	require.NoError(t, cmd.Run())
  3083  	cmd = exec.Command("git", "commit", "-m", "Initial commit")
  3084  	cmd.Dir = options.path
  3085  	require.NoError(t, cmd.Run())
  3086  }
  3087  
  3088  func initGitRepo(t *testing.T, options newGitRepoOptions) (revision string) {
  3089  	t.Helper()
  3090  	if options.createPath {
  3091  		require.NoError(t, os.Mkdir(options.path, 0o755))
  3092  	}
  3093  
  3094  	cmd := exec.Command("git", "init", "-b", "main", options.path)
  3095  	cmd.Dir = options.path
  3096  	require.NoError(t, cmd.Run())
  3097  
  3098  	if options.remote != "" {
  3099  		cmd = exec.Command("git", "remote", "add", "origin", options.path)
  3100  		cmd.Dir = options.path
  3101  		require.NoError(t, cmd.Run())
  3102  	}
  3103  
  3104  	commitAdded := options.addEmptyCommit || options.helmChartOptions.chartName != ""
  3105  	if options.addEmptyCommit {
  3106  		cmd = exec.Command("git", "commit", "-m", "Initial commit", "--allow-empty")
  3107  		cmd.Dir = options.path
  3108  		require.NoError(t, cmd.Run())
  3109  	} else if options.helmChartOptions.chartName != "" {
  3110  		addHelmToGitRepo(t, options)
  3111  	}
  3112  
  3113  	if commitAdded {
  3114  		var revB bytes.Buffer
  3115  		cmd = exec.Command("git", "rev-parse", "HEAD", options.path)
  3116  		cmd.Dir = options.path
  3117  		cmd.Stdout = &revB
  3118  		require.NoError(t, cmd.Run())
  3119  		revision = strings.Split(revB.String(), "\n")[0]
  3120  	}
  3121  	return revision
  3122  }
  3123  
  3124  func TestInit(t *testing.T) {
  3125  	dir := t.TempDir()
  3126  
  3127  	// service.Init sets permission to 0300. Restore permissions when the test
  3128  	// finishes so dir can be removed properly.
  3129  	t.Cleanup(func() {
  3130  		require.NoError(t, os.Chmod(dir, 0o777))
  3131  	})
  3132  
  3133  	repoPath := path.Join(dir, "repo1")
  3134  	initGitRepo(t, newGitRepoOptions{path: repoPath, remote: "https://github.com/argo-cd/test-repo1", createPath: true, addEmptyCommit: false})
  3135  
  3136  	service := newService(t, ".")
  3137  	service.rootDir = dir
  3138  
  3139  	require.NoError(t, service.Init())
  3140  
  3141  	_, err := os.ReadDir(dir)
  3142  	require.Error(t, err)
  3143  	initGitRepo(t, newGitRepoOptions{path: path.Join(dir, "repo2"), remote: "https://github.com/argo-cd/test-repo2", createPath: true, addEmptyCommit: false})
  3144  }
  3145  
  3146  // TestCheckoutRevisionCanGetNonstandardRefs shows that we can fetch a revision that points to a non-standard ref. In
  3147  // other words, we haven't regressed and caused this issue again: https://github.com/argoproj/argo-cd/issues/4935
  3148  func TestCheckoutRevisionCanGetNonstandardRefs(t *testing.T) {
  3149  	rootPath := t.TempDir()
  3150  
  3151  	sourceRepoPath, err := os.MkdirTemp(rootPath, "")
  3152  	require.NoError(t, err)
  3153  
  3154  	// Create a repo such that one commit is on a non-standard ref _and nowhere else_. This is meant to simulate, for
  3155  	// example, a GitHub ref for a pull into one repo from a fork of that repo.
  3156  	runGit(t, sourceRepoPath, "init")
  3157  	runGit(t, sourceRepoPath, "checkout", "-b", "main") // make sure there's a main branch to switch back to
  3158  	runGit(t, sourceRepoPath, "commit", "-m", "empty", "--allow-empty")
  3159  	runGit(t, sourceRepoPath, "checkout", "-b", "branch")
  3160  	runGit(t, sourceRepoPath, "commit", "-m", "empty", "--allow-empty")
  3161  	sha := runGit(t, sourceRepoPath, "rev-parse", "HEAD")
  3162  	runGit(t, sourceRepoPath, "update-ref", "refs/pull/123/head", strings.TrimSuffix(sha, "\n"))
  3163  	runGit(t, sourceRepoPath, "checkout", "main")
  3164  	runGit(t, sourceRepoPath, "branch", "-D", "branch")
  3165  
  3166  	destRepoPath, err := os.MkdirTemp(rootPath, "")
  3167  	require.NoError(t, err)
  3168  
  3169  	gitClient, err := git.NewClientExt("file://"+sourceRepoPath, destRepoPath, &git.NopCreds{}, true, false, "", "")
  3170  	require.NoError(t, err)
  3171  
  3172  	pullSha, err := gitClient.LsRemote("refs/pull/123/head")
  3173  	require.NoError(t, err)
  3174  
  3175  	err = checkoutRevision(gitClient, "does-not-exist", false)
  3176  	require.Error(t, err)
  3177  
  3178  	err = checkoutRevision(gitClient, pullSha, false)
  3179  	require.NoError(t, err)
  3180  }
  3181  
  3182  func TestCheckoutRevisionPresentSkipFetch(t *testing.T) {
  3183  	revision := "0123456789012345678901234567890123456789"
  3184  
  3185  	gitClient := &gitmocks.Client{}
  3186  	gitClient.On("Init").Return(nil)
  3187  	gitClient.On("IsRevisionPresent", revision).Return(true)
  3188  	gitClient.On("Checkout", revision, mock.Anything).Return("", nil)
  3189  
  3190  	err := checkoutRevision(gitClient, revision, false)
  3191  	require.NoError(t, err)
  3192  }
  3193  
  3194  func TestCheckoutRevisionNotPresentCallFetch(t *testing.T) {
  3195  	revision := "0123456789012345678901234567890123456789"
  3196  
  3197  	gitClient := &gitmocks.Client{}
  3198  	gitClient.On("Init").Return(nil)
  3199  	gitClient.On("IsRevisionPresent", revision).Return(false)
  3200  	gitClient.On("Fetch", "").Return(nil)
  3201  	gitClient.On("Checkout", revision, mock.Anything).Return("", nil)
  3202  
  3203  	err := checkoutRevision(gitClient, revision, false)
  3204  	require.NoError(t, err)
  3205  }
  3206  
  3207  func TestFetch(t *testing.T) {
  3208  	revision1 := "0123456789012345678901234567890123456789"
  3209  	revision2 := "abcdefabcdefabcdefabcdefabcdefabcdefabcd"
  3210  
  3211  	gitClient := &gitmocks.Client{}
  3212  	gitClient.On("Init").Return(nil)
  3213  	gitClient.On("IsRevisionPresent", revision1).Once().Return(true)
  3214  	gitClient.On("IsRevisionPresent", revision2).Once().Return(false)
  3215  	gitClient.On("Fetch", "").Return(nil)
  3216  	gitClient.On("IsRevisionPresent", revision1).Once().Return(true)
  3217  	gitClient.On("IsRevisionPresent", revision2).Once().Return(true)
  3218  
  3219  	err := fetch(gitClient, []string{revision1, revision2})
  3220  	require.NoError(t, err)
  3221  }
  3222  
  3223  // TestFetchRevisionCanGetNonstandardRefs shows that we can fetch a revision that points to a non-standard ref. In
  3224  func TestFetchRevisionCanGetNonstandardRefs(t *testing.T) {
  3225  	rootPath := t.TempDir()
  3226  
  3227  	sourceRepoPath, err := os.MkdirTemp(rootPath, "")
  3228  	require.NoError(t, err)
  3229  
  3230  	// Create a repo such that one commit is on a non-standard ref _and nowhere else_. This is meant to simulate, for
  3231  	// example, a GitHub ref for a pull into one repo from a fork of that repo.
  3232  	runGit(t, sourceRepoPath, "init")
  3233  	runGit(t, sourceRepoPath, "checkout", "-b", "main") // make sure there's a main branch to switch back to
  3234  	runGit(t, sourceRepoPath, "commit", "-m", "empty", "--allow-empty")
  3235  	runGit(t, sourceRepoPath, "checkout", "-b", "branch")
  3236  	runGit(t, sourceRepoPath, "commit", "-m", "empty", "--allow-empty")
  3237  	sha := runGit(t, sourceRepoPath, "rev-parse", "HEAD")
  3238  	runGit(t, sourceRepoPath, "update-ref", "refs/pull/123/head", strings.TrimSuffix(sha, "\n"))
  3239  	runGit(t, sourceRepoPath, "checkout", "main")
  3240  	runGit(t, sourceRepoPath, "branch", "-D", "branch")
  3241  
  3242  	destRepoPath, err := os.MkdirTemp(rootPath, "")
  3243  	require.NoError(t, err)
  3244  
  3245  	gitClient, err := git.NewClientExt("file://"+sourceRepoPath, destRepoPath, &git.NopCreds{}, true, false, "", "")
  3246  	require.NoError(t, err)
  3247  
  3248  	// We should initialize repository
  3249  	err = gitClient.Init()
  3250  	require.NoError(t, err)
  3251  
  3252  	pullSha, err := gitClient.LsRemote("refs/pull/123/head")
  3253  	require.NoError(t, err)
  3254  
  3255  	err = fetch(gitClient, []string{"does-not-exist"})
  3256  	require.Error(t, err)
  3257  
  3258  	err = fetch(gitClient, []string{pullSha})
  3259  	require.NoError(t, err)
  3260  }
  3261  
  3262  // runGit runs a git command in the given working directory. If the command succeeds, it returns the combined standard
  3263  // and error output. If it fails, it stops the test with a failure message.
  3264  func runGit(t *testing.T, workDir string, args ...string) string {
  3265  	t.Helper()
  3266  	cmd := exec.Command("git", args...)
  3267  	cmd.Dir = workDir
  3268  	out, err := cmd.CombinedOutput()
  3269  	stringOut := string(out)
  3270  	require.NoError(t, err, stringOut)
  3271  	return stringOut
  3272  }
  3273  
  3274  func Test_walkHelmValueFilesInPath(t *testing.T) {
  3275  	t.Run("does not exist", func(t *testing.T) {
  3276  		var files []string
  3277  		root := "/obviously/does/not/exist"
  3278  		err := filepath.Walk(root, walkHelmValueFilesInPath(root, &files))
  3279  		require.Error(t, err)
  3280  		assert.Empty(t, files)
  3281  	})
  3282  	t.Run("values files", func(t *testing.T) {
  3283  		var files []string
  3284  		root := "./testdata/values-files"
  3285  		err := filepath.Walk(root, walkHelmValueFilesInPath(root, &files))
  3286  		require.NoError(t, err)
  3287  		assert.Len(t, files, 5)
  3288  	})
  3289  	t.Run("unrelated root", func(t *testing.T) {
  3290  		var files []string
  3291  		root := "./testdata/values-files"
  3292  		unrelatedRoot := "/different/root/path"
  3293  		err := filepath.Walk(root, walkHelmValueFilesInPath(unrelatedRoot, &files))
  3294  		require.Error(t, err)
  3295  	})
  3296  }
  3297  
  3298  func Test_populateHelmAppDetails(t *testing.T) {
  3299  	emptyTempPaths := utilio.NewRandomizedTempPaths(t.TempDir())
  3300  	res := apiclient.RepoAppDetailsResponse{}
  3301  	q := apiclient.RepoServerAppDetailsQuery{
  3302  		Repo: &v1alpha1.Repository{},
  3303  		Source: &v1alpha1.ApplicationSource{
  3304  			Helm: &v1alpha1.ApplicationSourceHelm{ValueFiles: []string{"exclude.yaml", "has-the-word-values.yaml"}},
  3305  		},
  3306  	}
  3307  	appPath, err := filepath.Abs("./testdata/values-files/")
  3308  	require.NoError(t, err)
  3309  	err = populateHelmAppDetails(&res, appPath, appPath, &q, emptyTempPaths)
  3310  	require.NoError(t, err)
  3311  	assert.Len(t, res.Helm.Parameters, 3)
  3312  	assert.Len(t, res.Helm.ValueFiles, 5)
  3313  }
  3314  
  3315  func Test_populateHelmAppDetails_values_symlinks(t *testing.T) {
  3316  	emptyTempPaths := utilio.NewRandomizedTempPaths(t.TempDir())
  3317  	t.Run("inbound", func(t *testing.T) {
  3318  		res := apiclient.RepoAppDetailsResponse{}
  3319  		q := apiclient.RepoServerAppDetailsQuery{Repo: &v1alpha1.Repository{}, Source: &v1alpha1.ApplicationSource{}}
  3320  		err := populateHelmAppDetails(&res, "./testdata/in-bounds-values-file-link/", "./testdata/in-bounds-values-file-link/", &q, emptyTempPaths)
  3321  		require.NoError(t, err)
  3322  		assert.NotEmpty(t, res.Helm.Values)
  3323  		assert.NotEmpty(t, res.Helm.Parameters)
  3324  	})
  3325  
  3326  	t.Run("out of bounds", func(t *testing.T) {
  3327  		res := apiclient.RepoAppDetailsResponse{}
  3328  		q := apiclient.RepoServerAppDetailsQuery{Repo: &v1alpha1.Repository{}, Source: &v1alpha1.ApplicationSource{}}
  3329  		err := populateHelmAppDetails(&res, "./testdata/out-of-bounds-values-file-link/", "./testdata/out-of-bounds-values-file-link/", &q, emptyTempPaths)
  3330  		require.NoError(t, err)
  3331  		assert.Empty(t, res.Helm.Values)
  3332  		assert.Empty(t, res.Helm.Parameters)
  3333  	})
  3334  }
  3335  
  3336  func TestGetHelmRepos_OCIHelmDependenciesWithHelmRepo(t *testing.T) {
  3337  	q := apiclient.ManifestRequest{Repos: []*v1alpha1.Repository{}, HelmRepoCreds: []*v1alpha1.RepoCreds{
  3338  		{URL: "example.com", Username: "test", Password: "test", EnableOCI: true},
  3339  	}}
  3340  
  3341  	helmRepos, err := getHelmRepos("./testdata/oci-dependencies", q.Repos, q.HelmRepoCreds)
  3342  	require.NoError(t, err)
  3343  
  3344  	assert.Len(t, helmRepos, 1)
  3345  	assert.Equal(t, "test", helmRepos[0].GetUsername())
  3346  	assert.True(t, helmRepos[0].EnableOci)
  3347  	assert.Equal(t, "example.com/myrepo", helmRepos[0].Repo)
  3348  }
  3349  
  3350  func TestGetHelmRepos_OCIHelmDependenciesWithRepo(t *testing.T) {
  3351  	q := apiclient.ManifestRequest{Repos: []*v1alpha1.Repository{{Repo: "example.com", Username: "test", Password: "test", EnableOCI: true}}, HelmRepoCreds: []*v1alpha1.RepoCreds{}}
  3352  
  3353  	helmRepos, err := getHelmRepos("./testdata/oci-dependencies", q.Repos, q.HelmRepoCreds)
  3354  	require.NoError(t, err)
  3355  
  3356  	assert.Len(t, helmRepos, 1)
  3357  	assert.Equal(t, "test", helmRepos[0].GetUsername())
  3358  	assert.True(t, helmRepos[0].EnableOci)
  3359  	assert.Equal(t, "example.com/myrepo", helmRepos[0].Repo)
  3360  }
  3361  
  3362  func TestGetHelmRepos_OCIDependenciesWithHelmRepo(t *testing.T) {
  3363  	q := apiclient.ManifestRequest{Repos: []*v1alpha1.Repository{}, HelmRepoCreds: []*v1alpha1.RepoCreds{
  3364  		{URL: "oci://example.com", Username: "test", Password: "test", Type: "oci"},
  3365  	}}
  3366  
  3367  	helmRepos, err := getHelmRepos("./testdata/oci-dependencies", q.Repos, q.HelmRepoCreds)
  3368  	require.NoError(t, err)
  3369  
  3370  	assert.Len(t, helmRepos, 1)
  3371  	assert.Equal(t, "test", helmRepos[0].GetUsername())
  3372  	assert.True(t, helmRepos[0].EnableOci)
  3373  	assert.Equal(t, "example.com/myrepo", helmRepos[0].Repo)
  3374  }
  3375  
  3376  func TestGetHelmRepos_OCIDependenciesWithRepo(t *testing.T) {
  3377  	q := apiclient.ManifestRequest{Repos: []*v1alpha1.Repository{{Repo: "oci://example.com", Username: "test", Password: "test", Type: "oci"}}, HelmRepoCreds: []*v1alpha1.RepoCreds{}}
  3378  
  3379  	helmRepos, err := getHelmRepos("./testdata/oci-dependencies", q.Repos, q.HelmRepoCreds)
  3380  	require.NoError(t, err)
  3381  
  3382  	assert.Len(t, helmRepos, 1)
  3383  	assert.Equal(t, "test", helmRepos[0].GetUsername())
  3384  	assert.True(t, helmRepos[0].EnableOci)
  3385  	assert.Equal(t, "example.com/myrepo", helmRepos[0].Repo)
  3386  }
  3387  
  3388  func TestGetHelmRepo_NamedRepos(t *testing.T) {
  3389  	q := apiclient.ManifestRequest{
  3390  		Repos: []*v1alpha1.Repository{{
  3391  			Name:     "custom-repo",
  3392  			Repo:     "https://example.com",
  3393  			Username: "test",
  3394  		}},
  3395  	}
  3396  
  3397  	helmRepos, err := getHelmRepos("./testdata/helm-with-dependencies", q.Repos, q.HelmRepoCreds)
  3398  	require.NoError(t, err)
  3399  
  3400  	assert.Len(t, helmRepos, 1)
  3401  	assert.Equal(t, "test", helmRepos[0].GetUsername())
  3402  	assert.Equal(t, "https://example.com", helmRepos[0].Repo)
  3403  }
  3404  
  3405  func TestGetHelmRepo_NamedReposAlias(t *testing.T) {
  3406  	q := apiclient.ManifestRequest{
  3407  		Repos: []*v1alpha1.Repository{{
  3408  			Name:     "custom-repo-alias",
  3409  			Repo:     "https://example.com",
  3410  			Username: "test-alias",
  3411  		}},
  3412  	}
  3413  
  3414  	helmRepos, err := getHelmRepos("./testdata/helm-with-dependencies-alias", q.Repos, q.HelmRepoCreds)
  3415  	require.NoError(t, err)
  3416  
  3417  	assert.Len(t, helmRepos, 1)
  3418  	assert.Equal(t, "test-alias", helmRepos[0].GetUsername())
  3419  	assert.Equal(t, "https://example.com", helmRepos[0].Repo)
  3420  }
  3421  
  3422  func Test_getResolvedValueFiles(t *testing.T) {
  3423  	t.Parallel()
  3424  
  3425  	tempDir := t.TempDir()
  3426  	paths := utilio.NewRandomizedTempPaths(tempDir)
  3427  
  3428  	paths.Add(git.NormalizeGitURL("https://github.com/org/repo1"), path.Join(tempDir, "repo1"))
  3429  
  3430  	testCases := []struct {
  3431  		name         string
  3432  		rawPath      string
  3433  		env          *v1alpha1.Env
  3434  		refSources   map[string]*v1alpha1.RefTarget
  3435  		expectedPath string
  3436  		expectedErr  bool
  3437  	}{
  3438  		{
  3439  			name:         "simple path",
  3440  			rawPath:      "values.yaml",
  3441  			env:          &v1alpha1.Env{},
  3442  			refSources:   map[string]*v1alpha1.RefTarget{},
  3443  			expectedPath: path.Join(tempDir, "main-repo", "values.yaml"),
  3444  		},
  3445  		{
  3446  			name:    "simple ref",
  3447  			rawPath: "$ref/values.yaml",
  3448  			env:     &v1alpha1.Env{},
  3449  			refSources: map[string]*v1alpha1.RefTarget{
  3450  				"$ref": {
  3451  					Repo: v1alpha1.Repository{
  3452  						Repo: "https://github.com/org/repo1",
  3453  					},
  3454  				},
  3455  			},
  3456  			expectedPath: path.Join(tempDir, "repo1", "values.yaml"),
  3457  		},
  3458  		{
  3459  			name:    "only ref",
  3460  			rawPath: "$ref",
  3461  			env:     &v1alpha1.Env{},
  3462  			refSources: map[string]*v1alpha1.RefTarget{
  3463  				"$ref": {
  3464  					Repo: v1alpha1.Repository{
  3465  						Repo: "https://github.com/org/repo1",
  3466  					},
  3467  				},
  3468  			},
  3469  			expectedErr: true,
  3470  		},
  3471  		{
  3472  			name:    "attempted traversal",
  3473  			rawPath: "$ref/../values.yaml",
  3474  			env:     &v1alpha1.Env{},
  3475  			refSources: map[string]*v1alpha1.RefTarget{
  3476  				"$ref": {
  3477  					Repo: v1alpha1.Repository{
  3478  						Repo: "https://github.com/org/repo1",
  3479  					},
  3480  				},
  3481  			},
  3482  			expectedErr: true,
  3483  		},
  3484  		{
  3485  			// Since $ref doesn't resolve to a ref target, we assume it's an env var. Since the env var isn't specified,
  3486  			// it's replaced with an empty string. This is necessary for backwards compatibility with behavior before
  3487  			// ref targets were introduced.
  3488  			name:         "ref doesn't exist",
  3489  			rawPath:      "$ref/values.yaml",
  3490  			env:          &v1alpha1.Env{},
  3491  			refSources:   map[string]*v1alpha1.RefTarget{},
  3492  			expectedPath: path.Join(tempDir, "main-repo", "values.yaml"),
  3493  		},
  3494  		{
  3495  			name:    "repo doesn't exist",
  3496  			rawPath: "$ref/values.yaml",
  3497  			env:     &v1alpha1.Env{},
  3498  			refSources: map[string]*v1alpha1.RefTarget{
  3499  				"$ref": {
  3500  					Repo: v1alpha1.Repository{
  3501  						Repo: "https://github.com/org/repo2",
  3502  					},
  3503  				},
  3504  			},
  3505  			expectedErr: true,
  3506  		},
  3507  		{
  3508  			name:    "env var is resolved",
  3509  			rawPath: "$ref/$APP_PATH/values.yaml",
  3510  			env: &v1alpha1.Env{
  3511  				&v1alpha1.EnvEntry{
  3512  					Name:  "APP_PATH",
  3513  					Value: "app-path",
  3514  				},
  3515  			},
  3516  			refSources: map[string]*v1alpha1.RefTarget{
  3517  				"$ref": {
  3518  					Repo: v1alpha1.Repository{
  3519  						Repo: "https://github.com/org/repo1",
  3520  					},
  3521  				},
  3522  			},
  3523  			expectedPath: path.Join(tempDir, "repo1", "app-path", "values.yaml"),
  3524  		},
  3525  		{
  3526  			name:    "traversal in env var is blocked",
  3527  			rawPath: "$ref/$APP_PATH/values.yaml",
  3528  			env: &v1alpha1.Env{
  3529  				&v1alpha1.EnvEntry{
  3530  					Name:  "APP_PATH",
  3531  					Value: "..",
  3532  				},
  3533  			},
  3534  			refSources: map[string]*v1alpha1.RefTarget{
  3535  				"$ref": {
  3536  					Repo: v1alpha1.Repository{
  3537  						Repo: "https://github.com/org/repo1",
  3538  					},
  3539  				},
  3540  			},
  3541  			expectedErr: true,
  3542  		},
  3543  		{
  3544  			name:    "env var prefix",
  3545  			rawPath: "$APP_PATH/values.yaml",
  3546  			env: &v1alpha1.Env{
  3547  				&v1alpha1.EnvEntry{
  3548  					Name:  "APP_PATH",
  3549  					Value: "app-path",
  3550  				},
  3551  			},
  3552  			refSources:   map[string]*v1alpha1.RefTarget{},
  3553  			expectedPath: path.Join(tempDir, "main-repo", "app-path", "values.yaml"),
  3554  		},
  3555  		{
  3556  			name:         "unresolved env var",
  3557  			rawPath:      "$APP_PATH/values.yaml",
  3558  			env:          &v1alpha1.Env{},
  3559  			refSources:   map[string]*v1alpha1.RefTarget{},
  3560  			expectedPath: path.Join(tempDir, "main-repo", "values.yaml"),
  3561  		},
  3562  	}
  3563  
  3564  	for _, tc := range testCases {
  3565  		tcc := tc
  3566  		t.Run(tcc.name, func(t *testing.T) {
  3567  			t.Parallel()
  3568  			resolvedPaths, err := getResolvedValueFiles(path.Join(tempDir, "main-repo"), path.Join(tempDir, "main-repo"), tcc.env, []string{}, []string{tcc.rawPath}, tcc.refSources, paths, false)
  3569  			if !tcc.expectedErr {
  3570  				require.NoError(t, err)
  3571  				require.Len(t, resolvedPaths, 1)
  3572  				assert.Equal(t, tcc.expectedPath, string(resolvedPaths[0]))
  3573  			} else {
  3574  				require.Error(t, err)
  3575  				assert.Empty(t, resolvedPaths)
  3576  			}
  3577  		})
  3578  	}
  3579  }
  3580  
  3581  func TestErrorGetGitDirectories(t *testing.T) {
  3582  	// test not using the cache
  3583  	root := "./testdata/git-files-dirs"
  3584  
  3585  	type fields struct {
  3586  		service *Service
  3587  	}
  3588  	type args struct {
  3589  		ctx     context.Context
  3590  		request *apiclient.GitDirectoriesRequest
  3591  	}
  3592  	tests := []struct {
  3593  		name    string
  3594  		fields  fields
  3595  		args    args
  3596  		want    *apiclient.GitDirectoriesResponse
  3597  		wantErr assert.ErrorAssertionFunc
  3598  	}{
  3599  		{name: "InvalidRepo", fields: fields{service: newService(t, ".")}, args: args{
  3600  			ctx: t.Context(),
  3601  			request: &apiclient.GitDirectoriesRequest{
  3602  				Repo:             nil,
  3603  				SubmoduleEnabled: false,
  3604  				Revision:         "HEAD",
  3605  			},
  3606  		}, want: nil, wantErr: assert.Error},
  3607  		{name: "InvalidResolveRevision", fields: fields{service: func() *Service {
  3608  			s, _, _ := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, _ *ocimocks.Client, paths *iomocks.TempPaths) {
  3609  				gitClient.On("Checkout", mock.Anything, mock.Anything).Return("", nil)
  3610  				gitClient.On("LsRemote", mock.Anything).Return("", errors.New("ah error"))
  3611  				gitClient.On("Root").Return(root)
  3612  				paths.On("GetPath", mock.Anything).Return(".", nil)
  3613  				paths.On("GetPathIfExists", mock.Anything).Return(".", nil)
  3614  			}, ".")
  3615  			return s
  3616  		}()}, args: args{
  3617  			ctx: t.Context(),
  3618  			request: &apiclient.GitDirectoriesRequest{
  3619  				Repo:             &v1alpha1.Repository{Repo: "not-a-valid-url"},
  3620  				SubmoduleEnabled: false,
  3621  				Revision:         "sadfsadf",
  3622  			},
  3623  		}, want: nil, wantErr: assert.Error},
  3624  		{name: "ErrorVerifyCommit", fields: fields{service: func() *Service {
  3625  			s, _, _ := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, _ *ocimocks.Client, paths *iomocks.TempPaths) {
  3626  				gitClient.On("Checkout", mock.Anything, mock.Anything).Return("", nil)
  3627  				gitClient.On("LsRemote", mock.Anything).Return("", errors.New("ah error"))
  3628  				gitClient.On("VerifyCommitSignature", mock.Anything).Return("", fmt.Errorf("revision %s is not signed", "sadfsadf"))
  3629  				gitClient.On("Root").Return(root)
  3630  				paths.On("GetPath", mock.Anything).Return(".", nil)
  3631  				paths.On("GetPathIfExists", mock.Anything).Return(".", nil)
  3632  			}, ".")
  3633  			return s
  3634  		}()}, args: args{
  3635  			ctx: t.Context(),
  3636  			request: &apiclient.GitDirectoriesRequest{
  3637  				Repo:             &v1alpha1.Repository{Repo: "not-a-valid-url"},
  3638  				SubmoduleEnabled: false,
  3639  				Revision:         "sadfsadf",
  3640  				VerifyCommit:     true,
  3641  			},
  3642  		}, want: nil, wantErr: assert.Error},
  3643  	}
  3644  	for _, tt := range tests {
  3645  		t.Run(tt.name, func(t *testing.T) {
  3646  			s := tt.fields.service
  3647  			got, err := s.GetGitDirectories(tt.args.ctx, tt.args.request)
  3648  			if !tt.wantErr(t, err, fmt.Sprintf("GetGitDirectories(%v, %v)", tt.args.ctx, tt.args.request)) {
  3649  				return
  3650  			}
  3651  			assert.Equalf(t, tt.want, got, "GetGitDirectories(%v, %v)", tt.args.ctx, tt.args.request)
  3652  		})
  3653  	}
  3654  }
  3655  
  3656  func TestGetGitDirectories(t *testing.T) {
  3657  	// test not using the cache
  3658  	root := "./testdata/git-files-dirs"
  3659  	s, _, cacheMocks := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, _ *ocimocks.Client, paths *iomocks.TempPaths) {
  3660  		gitClient.On("Init").Return(nil)
  3661  		gitClient.On("IsRevisionPresent", mock.Anything).Return(false)
  3662  		gitClient.On("Fetch", mock.Anything).Return(nil)
  3663  		gitClient.On("Checkout", mock.Anything, mock.Anything).Once().Return("", nil)
  3664  		gitClient.On("LsRemote", "HEAD").Return("632039659e542ed7de0c170a4fcc1c571b288fc0", nil)
  3665  		gitClient.On("Root").Return(root)
  3666  		paths.On("GetPath", mock.Anything).Return(root, nil)
  3667  		paths.On("GetPathIfExists", mock.Anything).Return(root, nil)
  3668  	}, root)
  3669  	dirRequest := &apiclient.GitDirectoriesRequest{
  3670  		Repo:             &v1alpha1.Repository{Repo: "a-url.com"},
  3671  		SubmoduleEnabled: false,
  3672  		Revision:         "HEAD",
  3673  	}
  3674  	directories, err := s.GetGitDirectories(t.Context(), dirRequest)
  3675  	require.NoError(t, err)
  3676  	assert.ElementsMatch(t, directories.GetPaths(), []string{"app", "app/bar", "app/foo/bar", "somedir", "app/foo"})
  3677  
  3678  	// do the same request again to use the cache
  3679  	// we only allow CheckOut to be called once in the mock
  3680  	directories, err = s.GetGitDirectories(t.Context(), dirRequest)
  3681  	require.NoError(t, err)
  3682  	assert.ElementsMatch(t, []string{"app", "app/bar", "app/foo/bar", "somedir", "app/foo"}, directories.GetPaths())
  3683  	cacheMocks.mockCache.AssertCacheCalledTimes(t, &repositorymocks.CacheCallCounts{
  3684  		ExternalSets: 1,
  3685  		ExternalGets: 2,
  3686  	})
  3687  }
  3688  
  3689  func TestGetGitDirectoriesWithHiddenDirSupported(t *testing.T) {
  3690  	// test not using the cache
  3691  	root := "./testdata/git-files-dirs"
  3692  	s, _, cacheMocks := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, _ *ocimocks.Client, paths *iomocks.TempPaths) {
  3693  		gitClient.On("Init").Return(nil)
  3694  		gitClient.On("IsRevisionPresent", mock.Anything).Return(false)
  3695  		gitClient.On("Fetch", mock.Anything).Return(nil)
  3696  		gitClient.On("Checkout", mock.Anything, mock.Anything).Once().Return("", nil)
  3697  		gitClient.On("LsRemote", "HEAD").Return("632039659e542ed7de0c170a4fcc1c571b288fc0", nil)
  3698  		gitClient.On("Root").Return(root)
  3699  		paths.On("GetPath", mock.Anything).Return(root, nil)
  3700  		paths.On("GetPathIfExists", mock.Anything).Return(root, nil)
  3701  	}, root)
  3702  	s.initConstants.IncludeHiddenDirectories = true
  3703  	dirRequest := &apiclient.GitDirectoriesRequest{
  3704  		Repo:             &v1alpha1.Repository{Repo: "a-url.com"},
  3705  		SubmoduleEnabled: false,
  3706  		Revision:         "HEAD",
  3707  	}
  3708  	directories, err := s.GetGitDirectories(t.Context(), dirRequest)
  3709  	require.NoError(t, err)
  3710  	assert.ElementsMatch(t, directories.GetPaths(), []string{"app", "app/bar", "app/foo/bar", "somedir", "app/foo", "app/bar/.hidden"})
  3711  
  3712  	// do the same request again to use the cache
  3713  	// we only allow CheckOut to be called once in the mock
  3714  	directories, err = s.GetGitDirectories(t.Context(), dirRequest)
  3715  	require.NoError(t, err)
  3716  	assert.ElementsMatch(t, []string{"app", "app/bar", "app/foo/bar", "somedir", "app/foo", "app/bar/.hidden"}, directories.GetPaths())
  3717  	cacheMocks.mockCache.AssertCacheCalledTimes(t, &repositorymocks.CacheCallCounts{
  3718  		ExternalSets: 1,
  3719  		ExternalGets: 2,
  3720  	})
  3721  }
  3722  
  3723  func TestErrorGetGitFiles(t *testing.T) {
  3724  	// test not using the cache
  3725  	root := ""
  3726  
  3727  	type fields struct {
  3728  		service *Service
  3729  	}
  3730  	type args struct {
  3731  		ctx     context.Context
  3732  		request *apiclient.GitFilesRequest
  3733  	}
  3734  	tests := []struct {
  3735  		name    string
  3736  		fields  fields
  3737  		args    args
  3738  		want    *apiclient.GitFilesResponse
  3739  		wantErr assert.ErrorAssertionFunc
  3740  	}{
  3741  		{name: "InvalidRepo", fields: fields{service: newService(t, ".")}, args: args{
  3742  			ctx: t.Context(),
  3743  			request: &apiclient.GitFilesRequest{
  3744  				Repo:             nil,
  3745  				SubmoduleEnabled: false,
  3746  				Revision:         "HEAD",
  3747  			},
  3748  		}, want: nil, wantErr: assert.Error},
  3749  		{name: "InvalidResolveRevision", fields: fields{service: func() *Service {
  3750  			s, _, _ := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, _ *ocimocks.Client, paths *iomocks.TempPaths) {
  3751  				gitClient.On("Checkout", mock.Anything, mock.Anything).Return("", nil)
  3752  				gitClient.On("LsRemote", mock.Anything).Return("", errors.New("ah error"))
  3753  				gitClient.On("Root").Return(root)
  3754  				paths.On("GetPath", mock.Anything).Return(".", nil)
  3755  				paths.On("GetPathIfExists", mock.Anything).Return(".", nil)
  3756  			}, ".")
  3757  			return s
  3758  		}()}, args: args{
  3759  			ctx: t.Context(),
  3760  			request: &apiclient.GitFilesRequest{
  3761  				Repo:             &v1alpha1.Repository{Repo: "not-a-valid-url"},
  3762  				SubmoduleEnabled: false,
  3763  				Revision:         "sadfsadf",
  3764  			},
  3765  		}, want: nil, wantErr: assert.Error},
  3766  	}
  3767  	for _, tt := range tests {
  3768  		t.Run(tt.name, func(t *testing.T) {
  3769  			s := tt.fields.service
  3770  			got, err := s.GetGitFiles(tt.args.ctx, tt.args.request)
  3771  			if !tt.wantErr(t, err, fmt.Sprintf("GetGitFiles(%v, %v)", tt.args.ctx, tt.args.request)) {
  3772  				return
  3773  			}
  3774  			assert.Equalf(t, tt.want, got, "GetGitFiles(%v, %v)", tt.args.ctx, tt.args.request)
  3775  		})
  3776  	}
  3777  }
  3778  
  3779  func TestGetGitFiles(t *testing.T) {
  3780  	// test not using the cache
  3781  	files := []string{
  3782  		"./testdata/git-files-dirs/somedir/config.yaml",
  3783  		"./testdata/git-files-dirs/config.yaml", "./testdata/git-files-dirs/config.yaml", "./testdata/git-files-dirs/app/foo/bar/config.yaml",
  3784  	}
  3785  	root := ""
  3786  	s, _, cacheMocks := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, _ *ocimocks.Client, paths *iomocks.TempPaths) {
  3787  		gitClient.On("Init").Return(nil)
  3788  		gitClient.On("IsRevisionPresent", mock.Anything).Return(false)
  3789  		gitClient.On("Fetch", mock.Anything).Return(nil)
  3790  		gitClient.On("Checkout", mock.Anything, mock.Anything).Once().Return("", nil)
  3791  		gitClient.On("LsRemote", "HEAD").Return("632039659e542ed7de0c170a4fcc1c571b288fc0", nil)
  3792  		gitClient.On("Root").Return(root)
  3793  		gitClient.On("LsFiles", mock.Anything, mock.Anything).Once().Return(files, nil)
  3794  		paths.On("GetPath", mock.Anything).Return(root, nil)
  3795  		paths.On("GetPathIfExists", mock.Anything).Return(root, nil)
  3796  	}, root)
  3797  	filesRequest := &apiclient.GitFilesRequest{
  3798  		Repo:             &v1alpha1.Repository{Repo: "a-url.com"},
  3799  		SubmoduleEnabled: false,
  3800  		Revision:         "HEAD",
  3801  	}
  3802  
  3803  	// expected map
  3804  	expected := make(map[string][]byte)
  3805  	for _, filePath := range files {
  3806  		fileContents, err := os.ReadFile(filePath)
  3807  		require.NoError(t, err)
  3808  		expected[filePath] = fileContents
  3809  	}
  3810  
  3811  	fileResponse, err := s.GetGitFiles(t.Context(), filesRequest)
  3812  	require.NoError(t, err)
  3813  	assert.Equal(t, expected, fileResponse.GetMap())
  3814  
  3815  	// do the same request again to use the cache
  3816  	// we only allow LsFiles to be called once in the mock
  3817  	fileResponse, err = s.GetGitFiles(t.Context(), filesRequest)
  3818  	require.NoError(t, err)
  3819  	assert.Equal(t, expected, fileResponse.GetMap())
  3820  	cacheMocks.mockCache.AssertCacheCalledTimes(t, &repositorymocks.CacheCallCounts{
  3821  		ExternalSets: 1,
  3822  		ExternalGets: 2,
  3823  	})
  3824  }
  3825  
  3826  func TestErrorUpdateRevisionForPaths(t *testing.T) {
  3827  	// test not using the cache
  3828  	root := ""
  3829  
  3830  	type fields struct {
  3831  		service *Service
  3832  	}
  3833  	type args struct {
  3834  		ctx     context.Context
  3835  		request *apiclient.UpdateRevisionForPathsRequest
  3836  	}
  3837  	tests := []struct {
  3838  		name    string
  3839  		fields  fields
  3840  		args    args
  3841  		want    *apiclient.UpdateRevisionForPathsResponse
  3842  		wantErr assert.ErrorAssertionFunc
  3843  	}{
  3844  		{name: "InvalidRepo", fields: fields{service: newService(t, ".")}, args: args{
  3845  			ctx: t.Context(),
  3846  			request: &apiclient.UpdateRevisionForPathsRequest{
  3847  				Repo:           nil,
  3848  				Revision:       "HEAD",
  3849  				SyncedRevision: "sadfsadf",
  3850  			},
  3851  		}, want: nil, wantErr: assert.Error},
  3852  		{name: "InvalidResolveRevision", fields: fields{service: func() *Service {
  3853  			s, _, _ := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, _ *ocimocks.Client, paths *iomocks.TempPaths) {
  3854  				gitClient.On("Checkout", mock.Anything, mock.Anything).Return("", nil)
  3855  				gitClient.On("LsRemote", mock.Anything).Return("", errors.New("ah error"))
  3856  				gitClient.On("Root").Return(root)
  3857  				paths.On("GetPath", mock.Anything).Return(".", nil)
  3858  				paths.On("GetPathIfExists", mock.Anything).Return(".", nil)
  3859  			}, ".")
  3860  			return s
  3861  		}()}, args: args{
  3862  			ctx: t.Context(),
  3863  			request: &apiclient.UpdateRevisionForPathsRequest{
  3864  				Repo:           &v1alpha1.Repository{Repo: "not-a-valid-url"},
  3865  				Revision:       "sadfsadf",
  3866  				SyncedRevision: "HEAD",
  3867  				Paths:          []string{"."},
  3868  			},
  3869  		}, want: nil, wantErr: assert.Error},
  3870  		{name: "InvalidResolveSyncedRevision", fields: fields{service: func() *Service {
  3871  			s, _, _ := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, _ *ocimocks.Client, paths *iomocks.TempPaths) {
  3872  				gitClient.On("Checkout", mock.Anything, mock.Anything).Return("", nil)
  3873  				gitClient.On("LsRemote", "HEAD").Once().Return("632039659e542ed7de0c170a4fcc1c571b288fc0", nil)
  3874  				gitClient.On("LsRemote", mock.Anything).Return("", errors.New("ah error"))
  3875  				gitClient.On("Root").Return(root)
  3876  				paths.On("GetPath", mock.Anything).Return(".", nil)
  3877  				paths.On("GetPathIfExists", mock.Anything).Return(".", nil)
  3878  			}, ".")
  3879  			return s
  3880  		}()}, args: args{
  3881  			ctx: t.Context(),
  3882  			request: &apiclient.UpdateRevisionForPathsRequest{
  3883  				Repo:           &v1alpha1.Repository{Repo: "not-a-valid-url"},
  3884  				Revision:       "HEAD",
  3885  				SyncedRevision: "sadfsadf",
  3886  				Paths:          []string{"."},
  3887  			},
  3888  		}, want: nil, wantErr: assert.Error},
  3889  	}
  3890  	for _, tt := range tests {
  3891  		t.Run(tt.name, func(t *testing.T) {
  3892  			s := tt.fields.service
  3893  			got, err := s.UpdateRevisionForPaths(tt.args.ctx, tt.args.request)
  3894  			if !tt.wantErr(t, err, fmt.Sprintf("UpdateRevisionForPaths(%v, %v)", tt.args.ctx, tt.args.request)) {
  3895  				return
  3896  			}
  3897  			assert.Equalf(t, tt.want, got, "UpdateRevisionForPaths(%v, %v)", tt.args.ctx, tt.args.request)
  3898  		})
  3899  	}
  3900  }
  3901  
  3902  func TestUpdateRevisionForPaths(t *testing.T) {
  3903  	type fields struct {
  3904  		service *Service
  3905  		cache   *repoCacheMocks
  3906  	}
  3907  	type args struct {
  3908  		ctx     context.Context
  3909  		request *apiclient.UpdateRevisionForPathsRequest
  3910  	}
  3911  	type cacheHit struct {
  3912  		revision         string
  3913  		previousRevision string
  3914  	}
  3915  	tests := []struct {
  3916  		name     string
  3917  		fields   fields
  3918  		args     args
  3919  		want     *apiclient.UpdateRevisionForPathsResponse
  3920  		wantErr  assert.ErrorAssertionFunc
  3921  		cacheHit *cacheHit
  3922  	}{
  3923  		{name: "NoPathAbort", fields: func() fields {
  3924  			s, _, c := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, _ *ocimocks.Client, _ *iomocks.TempPaths) {
  3925  				gitClient.On("Checkout", mock.Anything, mock.Anything).Return("", nil)
  3926  			}, ".")
  3927  			return fields{
  3928  				service: s,
  3929  				cache:   c,
  3930  			}
  3931  		}(), args: args{
  3932  			ctx: t.Context(),
  3933  			request: &apiclient.UpdateRevisionForPathsRequest{
  3934  				Repo:  &v1alpha1.Repository{Repo: "a-url.com"},
  3935  				Paths: []string{},
  3936  			},
  3937  		}, want: &apiclient.UpdateRevisionForPathsResponse{}, wantErr: assert.NoError},
  3938  		{name: "SameResolvedRevisionAbort", fields: func() fields {
  3939  			s, _, c := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, _ *ocimocks.Client, paths *iomocks.TempPaths) {
  3940  				gitClient.On("Checkout", mock.Anything, mock.Anything).Return("", nil)
  3941  				gitClient.On("LsRemote", "HEAD").Once().Return("632039659e542ed7de0c170a4fcc1c571b288fc0", nil)
  3942  				gitClient.On("LsRemote", "SYNCEDHEAD").Once().Return("632039659e542ed7de0c170a4fcc1c571b288fc0", nil)
  3943  				paths.On("GetPath", mock.Anything).Return(".", nil)
  3944  				paths.On("GetPathIfExists", mock.Anything).Return(".", nil)
  3945  			}, ".")
  3946  			return fields{
  3947  				service: s,
  3948  				cache:   c,
  3949  			}
  3950  		}(), args: args{
  3951  			ctx: t.Context(),
  3952  			request: &apiclient.UpdateRevisionForPathsRequest{
  3953  				Repo:           &v1alpha1.Repository{Repo: "a-url.com"},
  3954  				Revision:       "HEAD",
  3955  				SyncedRevision: "SYNCEDHEAD",
  3956  				Paths:          []string{"."},
  3957  			},
  3958  		}, want: &apiclient.UpdateRevisionForPathsResponse{
  3959  			Revision: "632039659e542ed7de0c170a4fcc1c571b288fc0",
  3960  		}, wantErr: assert.NoError},
  3961  		{name: "ChangedFilesDoNothing", fields: func() fields {
  3962  			s, _, c := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, _ *ocimocks.Client, paths *iomocks.TempPaths) {
  3963  				gitClient.On("Init").Return(nil)
  3964  				gitClient.On("Fetch", mock.Anything).Once().Return(nil)
  3965  				gitClient.On("IsRevisionPresent", "632039659e542ed7de0c170a4fcc1c571b288fc0").Once().Return(false)
  3966  				gitClient.On("Checkout", "632039659e542ed7de0c170a4fcc1c571b288fc0", mock.Anything).Once().Return("", nil)
  3967  				// fetch
  3968  				gitClient.On("IsRevisionPresent", "1e67a504d03def3a6a1125d934cb511680f72555").Once().Return(false)
  3969  				gitClient.On("Fetch", mock.Anything).Once().Return(nil)
  3970  				gitClient.On("IsRevisionPresent", "1e67a504d03def3a6a1125d934cb511680f72555").Once().Return(true)
  3971  				gitClient.On("LsRemote", "HEAD").Once().Return("632039659e542ed7de0c170a4fcc1c571b288fc0", nil)
  3972  				gitClient.On("LsRemote", "SYNCEDHEAD").Once().Return("1e67a504d03def3a6a1125d934cb511680f72555", nil)
  3973  				paths.On("GetPath", mock.Anything).Return(".", nil)
  3974  				paths.On("GetPathIfExists", mock.Anything).Return(".", nil)
  3975  				gitClient.On("Root").Return("")
  3976  				gitClient.On("ChangedFiles", mock.Anything, mock.Anything).Return([]string{"app.yaml"}, nil)
  3977  			}, ".")
  3978  			return fields{
  3979  				service: s,
  3980  				cache:   c,
  3981  			}
  3982  		}(), args: args{
  3983  			ctx: t.Context(),
  3984  			request: &apiclient.UpdateRevisionForPathsRequest{
  3985  				Repo:           &v1alpha1.Repository{Repo: "a-url.com"},
  3986  				Revision:       "HEAD",
  3987  				SyncedRevision: "SYNCEDHEAD",
  3988  				Paths:          []string{"."},
  3989  			},
  3990  		}, want: &apiclient.UpdateRevisionForPathsResponse{
  3991  			Revision: "632039659e542ed7de0c170a4fcc1c571b288fc0",
  3992  			Changes:  true,
  3993  		}, wantErr: assert.NoError},
  3994  		{name: "NoChangesUpdateCache", fields: func() fields {
  3995  			s, _, c := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, _ *ocimocks.Client, paths *iomocks.TempPaths) {
  3996  				gitClient.On("Init").Return(nil)
  3997  				gitClient.On("Fetch", mock.Anything).Once().Return(nil)
  3998  				gitClient.On("IsRevisionPresent", "632039659e542ed7de0c170a4fcc1c571b288fc0").Once().Return(false)
  3999  				gitClient.On("Checkout", mock.Anything, mock.Anything).Return("", nil)
  4000  				gitClient.On("IsRevisionPresent", "1e67a504d03def3a6a1125d934cb511680f72555").Once().Return(false)
  4001  				// fetch
  4002  				gitClient.On("Fetch", mock.Anything).Once().Return(nil)
  4003  				gitClient.On("IsRevisionPresent", "1e67a504d03def3a6a1125d934cb511680f72555").Once().Return(true)
  4004  				gitClient.On("LsRemote", "HEAD").Once().Return("632039659e542ed7de0c170a4fcc1c571b288fc0", nil)
  4005  				gitClient.On("LsRemote", "SYNCEDHEAD").Once().Return("1e67a504d03def3a6a1125d934cb511680f72555", nil)
  4006  				paths.On("GetPath", mock.Anything).Return(".", nil)
  4007  				paths.On("GetPathIfExists", mock.Anything).Return(".", nil)
  4008  				gitClient.On("Root").Return("")
  4009  				gitClient.On("ChangedFiles", mock.Anything, mock.Anything).Return([]string{}, nil)
  4010  			}, ".")
  4011  			return fields{
  4012  				service: s,
  4013  				cache:   c,
  4014  			}
  4015  		}(), args: args{
  4016  			ctx: t.Context(),
  4017  			request: &apiclient.UpdateRevisionForPathsRequest{
  4018  				Repo:           &v1alpha1.Repository{Repo: "a-url.com"},
  4019  				Revision:       "HEAD",
  4020  				SyncedRevision: "SYNCEDHEAD",
  4021  				Paths:          []string{"."},
  4022  
  4023  				AppLabelKey:       "app.kubernetes.io/name",
  4024  				AppName:           "no-change-update-cache",
  4025  				Namespace:         "default",
  4026  				TrackingMethod:    "annotation+label",
  4027  				ApplicationSource: &v1alpha1.ApplicationSource{Path: "."},
  4028  				KubeVersion:       "v1.16.0",
  4029  			},
  4030  		}, want: &apiclient.UpdateRevisionForPathsResponse{
  4031  			Revision: "632039659e542ed7de0c170a4fcc1c571b288fc0",
  4032  		}, wantErr: assert.NoError, cacheHit: &cacheHit{
  4033  			previousRevision: "1e67a504d03def3a6a1125d934cb511680f72555",
  4034  			revision:         "632039659e542ed7de0c170a4fcc1c571b288fc0",
  4035  		}},
  4036  		{name: "NoChangesHelmMultiSourceUpdateCache", fields: func() fields {
  4037  			s, _, c := newServiceWithOpt(t, func(gitClient *gitmocks.Client, _ *helmmocks.Client, _ *ocimocks.Client, paths *iomocks.TempPaths) {
  4038  				gitClient.On("Init").Return(nil)
  4039  				gitClient.On("IsRevisionPresent", "632039659e542ed7de0c170a4fcc1c571b288fc0").Once().Return(false)
  4040  				gitClient.On("Fetch", mock.Anything).Once().Return(nil)
  4041  				gitClient.On("Checkout", mock.Anything, mock.Anything).Return("", nil)
  4042  				// fetch
  4043  				gitClient.On("IsRevisionPresent", "1e67a504d03def3a6a1125d934cb511680f72555").Once().Return(true)
  4044  				gitClient.On("Fetch", mock.Anything).Once().Return(nil)
  4045  				gitClient.On("LsRemote", "HEAD").Once().Return("632039659e542ed7de0c170a4fcc1c571b288fc0", nil)
  4046  				gitClient.On("LsRemote", "SYNCEDHEAD").Once().Return("1e67a504d03def3a6a1125d934cb511680f72555", nil)
  4047  				paths.On("GetPath", mock.Anything).Return(".", nil)
  4048  				paths.On("GetPathIfExists", mock.Anything).Return(".", nil)
  4049  				gitClient.On("Root").Return("")
  4050  				gitClient.On("ChangedFiles", mock.Anything, mock.Anything).Return([]string{}, nil)
  4051  			}, ".")
  4052  			return fields{
  4053  				service: s,
  4054  				cache:   c,
  4055  			}
  4056  		}(), args: args{
  4057  			ctx: t.Context(),
  4058  			request: &apiclient.UpdateRevisionForPathsRequest{
  4059  				Repo:           &v1alpha1.Repository{Repo: "a-url.com"},
  4060  				Revision:       "HEAD",
  4061  				SyncedRevision: "SYNCEDHEAD",
  4062  				Paths:          []string{"."},
  4063  
  4064  				AppLabelKey:       "app.kubernetes.io/name",
  4065  				AppName:           "no-change-update-cache",
  4066  				Namespace:         "default",
  4067  				TrackingMethod:    "annotation+label",
  4068  				ApplicationSource: &v1alpha1.ApplicationSource{Path: ".", Helm: &v1alpha1.ApplicationSourceHelm{ReleaseName: "test"}},
  4069  				KubeVersion:       "v1.16.0",
  4070  
  4071  				HasMultipleSources: true,
  4072  			},
  4073  		}, want: &apiclient.UpdateRevisionForPathsResponse{
  4074  			Revision: "632039659e542ed7de0c170a4fcc1c571b288fc0",
  4075  		}, wantErr: assert.NoError, cacheHit: &cacheHit{
  4076  			previousRevision: "1e67a504d03def3a6a1125d934cb511680f72555",
  4077  			revision:         "632039659e542ed7de0c170a4fcc1c571b288fc0",
  4078  		}},
  4079  	}
  4080  	for _, tt := range tests {
  4081  		t.Run(tt.name, func(t *testing.T) {
  4082  			s := tt.fields.service
  4083  			cache := tt.fields.cache
  4084  
  4085  			if tt.cacheHit != nil {
  4086  				cache.mockCache.On("Rename", tt.cacheHit.previousRevision, tt.cacheHit.revision, mock.Anything).Return(nil)
  4087  			}
  4088  
  4089  			got, err := s.UpdateRevisionForPaths(tt.args.ctx, tt.args.request)
  4090  			if !tt.wantErr(t, err, fmt.Sprintf("UpdateRevisionForPaths(%v, %v)", tt.args.ctx, tt.args.request)) {
  4091  				return
  4092  			}
  4093  			assert.Equalf(t, tt.want, got, "UpdateRevisionForPaths(%v, %v)", tt.args.ctx, tt.args.request)
  4094  
  4095  			if tt.cacheHit != nil {
  4096  				cache.mockCache.AssertCacheCalledTimes(t, &repositorymocks.CacheCallCounts{
  4097  					ExternalRenames: 1,
  4098  				})
  4099  			} else {
  4100  				cache.mockCache.AssertCacheCalledTimes(t, &repositorymocks.CacheCallCounts{
  4101  					ExternalRenames: 0,
  4102  				})
  4103  			}
  4104  		})
  4105  	}
  4106  }
  4107  
  4108  func Test_getRepoSanitizerRegex(t *testing.T) {
  4109  	r := getRepoSanitizerRegex("/tmp/_argocd-repo")
  4110  	msg := r.ReplaceAllString("error message containing /tmp/_argocd-repo/SENSITIVE and other stuff", "<path to cached source>")
  4111  	assert.Equal(t, "error message containing <path to cached source> and other stuff", msg)
  4112  	msg = r.ReplaceAllString("error message containing /tmp/_argocd-repo/SENSITIVE/with/trailing/path and other stuff", "<path to cached source>")
  4113  	assert.Equal(t, "error message containing <path to cached source>/with/trailing/path and other stuff", msg)
  4114  }
  4115  
  4116  func TestGetRefs_CacheWithLockDisabled(t *testing.T) {
  4117  	// Test that when the lock is disabled the default behavior still works correctly
  4118  	// Also shows the current issue with the git requests due to cache misses
  4119  	dir := t.TempDir()
  4120  	initGitRepo(t, newGitRepoOptions{
  4121  		path:           dir,
  4122  		createPath:     false,
  4123  		remote:         "",
  4124  		addEmptyCommit: true,
  4125  	})
  4126  	// Test in-memory and redis
  4127  	cacheMocks := newCacheMocksWithOpts(1*time.Minute, 1*time.Minute, 0)
  4128  	t.Cleanup(cacheMocks.mockCache.StopRedisCallback)
  4129  	var wg sync.WaitGroup
  4130  	numberOfCallers := 10
  4131  	for i := 0; i < numberOfCallers; i++ {
  4132  		wg.Add(1)
  4133  		go func() {
  4134  			defer wg.Done()
  4135  			client, err := git.NewClient("file://"+dir, git.NopCreds{}, true, false, "", "", git.WithCache(cacheMocks.cache, true))
  4136  			require.NoError(t, err)
  4137  			refs, err := client.LsRefs()
  4138  			require.NoError(t, err)
  4139  			assert.NotNil(t, refs)
  4140  			assert.NotEmpty(t, refs.Branches, "Expected branches to be populated")
  4141  			assert.NotEmpty(t, refs.Branches[0])
  4142  		}()
  4143  	}
  4144  	wg.Wait()
  4145  	// Unlock should not have been called
  4146  	cacheMocks.mockCache.AssertNumberOfCalls(t, "UnlockGitReferences", 0)
  4147  	// Lock should not have been called
  4148  	cacheMocks.mockCache.AssertNumberOfCalls(t, "TryLockGitRefCache", 0)
  4149  }
  4150  
  4151  func TestGetRefs_CacheDisabled(t *testing.T) {
  4152  	// Test that default get refs with cache disabled does not call GetOrLockGitReferences
  4153  	dir := t.TempDir()
  4154  	initGitRepo(t, newGitRepoOptions{
  4155  		path:           dir,
  4156  		createPath:     false,
  4157  		remote:         "",
  4158  		addEmptyCommit: true,
  4159  	})
  4160  	cacheMocks := newCacheMocks()
  4161  	t.Cleanup(cacheMocks.mockCache.StopRedisCallback)
  4162  	client, err := git.NewClient("file://"+dir, git.NopCreds{}, true, false, "", "", git.WithCache(cacheMocks.cache, false))
  4163  	require.NoError(t, err)
  4164  	refs, err := client.LsRefs()
  4165  	require.NoError(t, err)
  4166  	assert.NotNil(t, refs)
  4167  	assert.NotEmpty(t, refs.Branches, "Expected branches to be populated")
  4168  	assert.NotEmpty(t, refs.Branches[0])
  4169  	// Unlock should not have been called
  4170  	cacheMocks.mockCache.AssertNumberOfCalls(t, "UnlockGitReferences", 0)
  4171  	cacheMocks.mockCache.AssertNumberOfCalls(t, "GetOrLockGitReferences", 0)
  4172  }
  4173  
  4174  func TestGetRefs_CacheWithLock(t *testing.T) {
  4175  	// Test that there is only one call to SetGitReferences for the same repo which is done after the ls-remote
  4176  	dir := t.TempDir()
  4177  	initGitRepo(t, newGitRepoOptions{
  4178  		path:           dir,
  4179  		createPath:     false,
  4180  		remote:         "",
  4181  		addEmptyCommit: true,
  4182  	})
  4183  	cacheMocks := newCacheMocks()
  4184  	t.Cleanup(cacheMocks.mockCache.StopRedisCallback)
  4185  	var wg sync.WaitGroup
  4186  	numberOfCallers := 10
  4187  	for i := 0; i < numberOfCallers; i++ {
  4188  		wg.Add(1)
  4189  		go func() {
  4190  			defer wg.Done()
  4191  			client, err := git.NewClient("file://"+dir, git.NopCreds{}, true, false, "", "", git.WithCache(cacheMocks.cache, true))
  4192  			require.NoError(t, err)
  4193  			refs, err := client.LsRefs()
  4194  			require.NoError(t, err)
  4195  			assert.NotNil(t, refs)
  4196  			assert.NotEmpty(t, refs.Branches, "Expected branches to be populated")
  4197  			assert.NotEmpty(t, refs.Branches[0])
  4198  		}()
  4199  	}
  4200  	wg.Wait()
  4201  	// Unlock should not have been called
  4202  	cacheMocks.mockCache.AssertNumberOfCalls(t, "UnlockGitReferences", 0)
  4203  	cacheMocks.mockCache.AssertNumberOfCalls(t, "GetOrLockGitReferences", 0)
  4204  }
  4205  
  4206  func TestGetRefs_CacheUnlockedOnUpdateFailed(t *testing.T) {
  4207  	// Worst case the ttl on the lock expires and the lock is removed
  4208  	// however if the holder of the lock fails to update the cache the caller should remove the lock
  4209  	// to allow other callers to attempt to update the cache as quickly as possible
  4210  	dir := t.TempDir()
  4211  	initGitRepo(t, newGitRepoOptions{
  4212  		path:           dir,
  4213  		createPath:     false,
  4214  		remote:         "",
  4215  		addEmptyCommit: true,
  4216  	})
  4217  	cacheMocks := newCacheMocks()
  4218  	t.Cleanup(cacheMocks.mockCache.StopRedisCallback)
  4219  	repoURL := "file://" + dir
  4220  	client, err := git.NewClient(repoURL, git.NopCreds{}, true, false, "", "", git.WithCache(cacheMocks.cache, true))
  4221  	require.NoError(t, err)
  4222  	refs, err := client.LsRefs()
  4223  	require.NoError(t, err)
  4224  	assert.NotNil(t, refs)
  4225  	assert.NotEmpty(t, refs.Branches, "Expected branches to be populated")
  4226  	assert.NotEmpty(t, refs.Branches[0])
  4227  	var output [][2]string
  4228  	err = cacheMocks.cacheutilCache.GetItem(fmt.Sprintf("git-refs|%s|%s", repoURL, common.CacheVersion), &output)
  4229  	require.Error(t, err, "Should be a cache miss")
  4230  	assert.Empty(t, output, "Expected cache to be empty for key")
  4231  	cacheMocks.mockCache.AssertNumberOfCalls(t, "UnlockGitReferences", 0)
  4232  	cacheMocks.mockCache.AssertNumberOfCalls(t, "GetOrLockGitReferences", 0)
  4233  }
  4234  
  4235  func TestGetRefs_CacheLockTryLockGitRefCacheError(t *testing.T) {
  4236  	// Worst case the ttl on the lock expires and the lock is removed
  4237  	// however if the holder of the lock fails to update the cache the caller should remove the lock
  4238  	// to allow other callers to attempt to update the cache as quickly as possible
  4239  	dir := t.TempDir()
  4240  	initGitRepo(t, newGitRepoOptions{
  4241  		path:           dir,
  4242  		createPath:     false,
  4243  		remote:         "",
  4244  		addEmptyCommit: true,
  4245  	})
  4246  	cacheMocks := newCacheMocks()
  4247  	t.Cleanup(cacheMocks.mockCache.StopRedisCallback)
  4248  	repoURL := "file://" + dir
  4249  	// buf := bytes.Buffer{}
  4250  	// log.SetOutput(&buf)
  4251  	client, err := git.NewClient(repoURL, git.NopCreds{}, true, false, "", "", git.WithCache(cacheMocks.cache, true))
  4252  	require.NoError(t, err)
  4253  	refs, err := client.LsRefs()
  4254  	require.NoError(t, err)
  4255  	assert.NotNil(t, refs)
  4256  }
  4257  
  4258  func TestGetRevisionChartDetails(t *testing.T) {
  4259  	t.Run("Test revision semver", func(t *testing.T) {
  4260  		root := t.TempDir()
  4261  		service := newService(t, root)
  4262  		_, err := service.GetRevisionChartDetails(t.Context(), &apiclient.RepoServerRevisionChartDetailsRequest{
  4263  			Repo: &v1alpha1.Repository{
  4264  				Repo: "file://" + root,
  4265  				Name: "test-repo-name",
  4266  				Type: "helm",
  4267  			},
  4268  			Name:     "test-name",
  4269  			Revision: "test-revision",
  4270  		})
  4271  		assert.ErrorContains(t, err, "invalid revision")
  4272  	})
  4273  
  4274  	t.Run("Test GetRevisionChartDetails", func(t *testing.T) {
  4275  		root := t.TempDir()
  4276  		service := newService(t, root)
  4277  		repoURL := "file://" + root
  4278  		err := service.cache.SetRevisionChartDetails(repoURL, "my-chart", "1.1.0", &v1alpha1.ChartDetails{
  4279  			Description: "test-description",
  4280  			Home:        "test-home",
  4281  			Maintainers: []string{"test-maintainer"},
  4282  		})
  4283  		require.NoError(t, err)
  4284  		chartDetails, err := service.GetRevisionChartDetails(t.Context(), &apiclient.RepoServerRevisionChartDetailsRequest{
  4285  			Repo: &v1alpha1.Repository{
  4286  				Repo: "file://" + root,
  4287  				Name: "test-repo-name",
  4288  				Type: "helm",
  4289  			},
  4290  			Name:     "my-chart",
  4291  			Revision: "1.1.0",
  4292  		})
  4293  		require.NoError(t, err)
  4294  		assert.Equal(t, "test-description", chartDetails.Description)
  4295  		assert.Equal(t, "test-home", chartDetails.Home)
  4296  		assert.Equal(t, []string{"test-maintainer"}, chartDetails.Maintainers)
  4297  	})
  4298  }
  4299  
  4300  func TestVerifyCommitSignature(t *testing.T) {
  4301  	repo := &v1alpha1.Repository{
  4302  		Repo: "https://github.com/example/repo.git",
  4303  	}
  4304  
  4305  	t.Run("VerifyCommitSignature with valid signature", func(t *testing.T) {
  4306  		t.Setenv("ARGOCD_GPG_ENABLED", "true")
  4307  		mockGitClient := &gitmocks.Client{}
  4308  		mockGitClient.On("VerifyCommitSignature", mock.Anything, mock.Anything, mock.Anything, mock.Anything).
  4309  			Return(testSignature, nil)
  4310  		err := verifyCommitSignature(true, mockGitClient, "abcd1234", repo)
  4311  		require.NoError(t, err)
  4312  	})
  4313  
  4314  	t.Run("VerifyCommitSignature with invalid signature", func(t *testing.T) {
  4315  		t.Setenv("ARGOCD_GPG_ENABLED", "true")
  4316  		mockGitClient := &gitmocks.Client{}
  4317  		mockGitClient.On("VerifyCommitSignature", mock.Anything, mock.Anything, mock.Anything, mock.Anything).
  4318  			Return("", nil)
  4319  		err := verifyCommitSignature(true, mockGitClient, "abcd1234", repo)
  4320  		assert.EqualError(t, err, "revision abcd1234 is not signed")
  4321  	})
  4322  
  4323  	t.Run("VerifyCommitSignature with unknown signature", func(t *testing.T) {
  4324  		t.Setenv("ARGOCD_GPG_ENABLED", "true")
  4325  		mockGitClient := &gitmocks.Client{}
  4326  		mockGitClient.On("VerifyCommitSignature", mock.Anything, mock.Anything, mock.Anything, mock.Anything).
  4327  			Return("", errors.New("UNKNOWN signature: gpg: Unknown signature from ABCDEFGH"))
  4328  		err := verifyCommitSignature(true, mockGitClient, "abcd1234", repo)
  4329  		assert.EqualError(t, err, "UNKNOWN signature: gpg: Unknown signature from ABCDEFGH")
  4330  	})
  4331  
  4332  	t.Run("VerifyCommitSignature with error verifying signature", func(t *testing.T) {
  4333  		t.Setenv("ARGOCD_GPG_ENABLED", "true")
  4334  		mockGitClient := &gitmocks.Client{}
  4335  		mockGitClient.On("VerifyCommitSignature", mock.Anything, mock.Anything, mock.Anything, mock.Anything).
  4336  			Return("", errors.New("error verifying signature of commit 'abcd1234' in repo 'https://github.com/example/repo.git': failed to verify signature"))
  4337  		err := verifyCommitSignature(true, mockGitClient, "abcd1234", repo)
  4338  		assert.EqualError(t, err, "error verifying signature of commit 'abcd1234' in repo 'https://github.com/example/repo.git': failed to verify signature")
  4339  	})
  4340  
  4341  	t.Run("VerifyCommitSignature with signature verification disabled", func(t *testing.T) {
  4342  		t.Setenv("ARGOCD_GPG_ENABLED", "false")
  4343  		mockGitClient := &gitmocks.Client{}
  4344  		err := verifyCommitSignature(false, mockGitClient, "abcd1234", repo)
  4345  		require.NoError(t, err)
  4346  	})
  4347  }
  4348  
  4349  func Test_GenerateManifests_Commands(t *testing.T) {
  4350  	t.Run("helm", func(t *testing.T) {
  4351  		service := newService(t, "testdata/my-chart")
  4352  
  4353  		// Fill the manifest request with as many parameters affecting Helm commands as possible.
  4354  		q := apiclient.ManifestRequest{
  4355  			AppName:     "test-app",
  4356  			Namespace:   "test-namespace",
  4357  			KubeVersion: "1.2.3+something",
  4358  			ApiVersions: []string{"v1/Test", "v2/Test"},
  4359  			Repo:        &v1alpha1.Repository{},
  4360  			ApplicationSource: &v1alpha1.ApplicationSource{
  4361  				Path: ".",
  4362  				Helm: &v1alpha1.ApplicationSourceHelm{
  4363  					FileParameters: []v1alpha1.HelmFileParameter{
  4364  						{
  4365  							Name: "test-file-param-name",
  4366  							Path: "test-file-param.yaml",
  4367  						},
  4368  					},
  4369  					Parameters: []v1alpha1.HelmParameter{
  4370  						{
  4371  							Name: "test-param-name",
  4372  							// Use build env var to test substitution.
  4373  							Value:       "test-value-$ARGOCD_APP_NAME",
  4374  							ForceString: true,
  4375  						},
  4376  						{
  4377  							Name: "test-param-bool-name",
  4378  							// Use build env var to test substitution.
  4379  							Value: "false",
  4380  						},
  4381  					},
  4382  					PassCredentials:      true,
  4383  					SkipCrds:             true,
  4384  					SkipSchemaValidation: false,
  4385  					ValueFiles: []string{
  4386  						"my-chart-values.yaml",
  4387  					},
  4388  					Values: "test: values",
  4389  				},
  4390  			},
  4391  			ProjectName:        "something",
  4392  			ProjectSourceRepos: []string{"*"},
  4393  		}
  4394  
  4395  		res, err := service.GenerateManifest(t.Context(), &q)
  4396  
  4397  		require.NoError(t, err)
  4398  		assert.Equal(t, []string{"helm template . --name-template test-app --namespace test-namespace --kube-version 1.2.3 --set test-param-bool-name=false --set-string test-param-name=test-value-test-app --set-file test-file-param-name=./test-file-param.yaml --values ./my-chart-values.yaml --values <temp file with values from source.helm.values/valuesObject> --api-versions v1/Test --api-versions v2/Test"}, res.Commands)
  4399  
  4400  		t.Run("with overrides", func(t *testing.T) {
  4401  			// These can be set explicitly instead of using inferred values. Make sure the overrides apply.
  4402  			q.ApplicationSource.Helm.APIVersions = []string{"v3", "v4"}
  4403  			q.ApplicationSource.Helm.KubeVersion = "5.6.7+something"
  4404  			q.ApplicationSource.Helm.Namespace = "different-namespace"
  4405  			q.ApplicationSource.Helm.ReleaseName = "different-release-name"
  4406  
  4407  			res, err = service.GenerateManifest(t.Context(), &q)
  4408  
  4409  			require.NoError(t, err)
  4410  			assert.Equal(t, []string{"helm template . --name-template different-release-name --namespace different-namespace --kube-version 5.6.7 --set test-param-bool-name=false --set-string test-param-name=test-value-test-app --set-file test-file-param-name=./test-file-param.yaml --values ./my-chart-values.yaml --values <temp file with values from source.helm.values/valuesObject> --api-versions v3 --api-versions v4"}, res.Commands)
  4411  		})
  4412  	})
  4413  
  4414  	t.Run("helm with dependencies", func(t *testing.T) {
  4415  		// This test makes sure we still get commands, even if we hit the code path that has to run "helm dependency build."
  4416  		// We don't actually return the "helm dependency build" command, because we expect that the user is able to read
  4417  		// the "helm template" and figure out how to fix it.
  4418  		t.Cleanup(func() {
  4419  			err := os.Remove("testdata/helm-with-local-dependency/Chart.lock")
  4420  			require.NoError(t, err)
  4421  			err = os.RemoveAll("testdata/helm-with-local-dependency/charts")
  4422  			require.NoError(t, err)
  4423  			err = os.Remove(path.Join("testdata/helm-with-local-dependency", helmDepUpMarkerFile))
  4424  			require.NoError(t, err)
  4425  		})
  4426  
  4427  		service := newService(t, "testdata/helm-with-local-dependency")
  4428  
  4429  		q := apiclient.ManifestRequest{
  4430  			AppName:   "test-app",
  4431  			Namespace: "test-namespace",
  4432  			Repo:      &v1alpha1.Repository{},
  4433  			ApplicationSource: &v1alpha1.ApplicationSource{
  4434  				Path: ".",
  4435  			},
  4436  			ProjectName:        "something",
  4437  			ProjectSourceRepos: []string{"*"},
  4438  		}
  4439  
  4440  		res, err := service.GenerateManifest(t.Context(), &q)
  4441  
  4442  		require.NoError(t, err)
  4443  		assert.Equal(t, []string{"helm template . --name-template test-app --namespace test-namespace --include-crds"}, res.Commands)
  4444  	})
  4445  
  4446  	t.Run("kustomize", func(t *testing.T) {
  4447  		// Write test files to a temp dir, because the test mutates kustomization.yaml in place.
  4448  		tempDir := t.TempDir()
  4449  		err := os.WriteFile(path.Join(tempDir, "kustomization.yaml"), []byte(`
  4450  resources:
  4451  - guestbook.yaml
  4452  `), os.FileMode(0o600))
  4453  		require.NoError(t, err)
  4454  		err = os.WriteFile(path.Join(tempDir, "guestbook.yaml"), []byte(`
  4455  apiVersion: apps/v1
  4456  kind: Deployment
  4457  metadata:
  4458    name: guestbook-ui
  4459  `), os.FileMode(0o400))
  4460  		require.NoError(t, err)
  4461  		err = os.Mkdir(path.Join(tempDir, "component"), os.FileMode(0o700))
  4462  		require.NoError(t, err)
  4463  		err = os.WriteFile(path.Join(tempDir, "component", "kustomization.yaml"), []byte(`
  4464  apiVersion: kustomize.config.k8s.io/v1alpha1
  4465  kind: Component
  4466  images:
  4467  - name: old
  4468    newName: new
  4469  `), os.FileMode(0o400))
  4470  		require.NoError(t, err)
  4471  
  4472  		service := newService(t, tempDir)
  4473  
  4474  		// Fill the manifest request with as many parameters affecting Kustomize commands as possible.
  4475  		q := apiclient.ManifestRequest{
  4476  			AppName:     "test-app",
  4477  			Namespace:   "test-namespace",
  4478  			KubeVersion: "1.2.3+something",
  4479  			ApiVersions: []string{"v1/Test", "v2/Test"},
  4480  			Repo:        &v1alpha1.Repository{},
  4481  			KustomizeOptions: &v1alpha1.KustomizeOptions{
  4482  				BuildOptions: "--enable-helm",
  4483  			},
  4484  			ApplicationSource: &v1alpha1.ApplicationSource{
  4485  				Path: ".",
  4486  				Kustomize: &v1alpha1.ApplicationSourceKustomize{
  4487  					APIVersions: []string{"v1", "v2"},
  4488  					CommonAnnotations: map[string]string{
  4489  						// Use build env var to test substitution.
  4490  						"test": "annotation-$ARGOCD_APP_NAME",
  4491  					},
  4492  					CommonAnnotationsEnvsubst: true,
  4493  					CommonLabels: map[string]string{
  4494  						"test": "label",
  4495  					},
  4496  					Components:             []string{"component"},
  4497  					ForceCommonAnnotations: true,
  4498  					ForceCommonLabels:      true,
  4499  					Images: v1alpha1.KustomizeImages{
  4500  						"image=override",
  4501  					},
  4502  					KubeVersion:           "5.6.7+something",
  4503  					LabelWithoutSelector:  true,
  4504  					LabelIncludeTemplates: true,
  4505  					NamePrefix:            "test-prefix",
  4506  					NameSuffix:            "test-suffix",
  4507  					Namespace:             "override-namespace",
  4508  					Replicas: v1alpha1.KustomizeReplicas{
  4509  						{
  4510  							Name:  "guestbook-ui",
  4511  							Count: intstr.Parse("1337"),
  4512  						},
  4513  					},
  4514  				},
  4515  			},
  4516  			ProjectName:        "something",
  4517  			ProjectSourceRepos: []string{"*"},
  4518  		}
  4519  
  4520  		res, err := service.GenerateManifest(t.Context(), &q)
  4521  		require.NoError(t, err)
  4522  		assert.Equal(t, []string{
  4523  			"kustomize edit set nameprefix -- test-prefix",
  4524  			"kustomize edit set namesuffix -- test-suffix",
  4525  			"kustomize edit set image image=override",
  4526  			"kustomize edit set replicas guestbook-ui=1337",
  4527  			"kustomize edit add label --force --without-selector --include-templates test:label",
  4528  			"kustomize edit add annotation --force test:annotation-test-app",
  4529  			"kustomize edit set namespace -- override-namespace",
  4530  			"kustomize edit add component component",
  4531  			"kustomize build . --enable-helm --helm-kube-version 5.6.7 --helm-api-versions v1 --helm-api-versions v2",
  4532  		}, res.Commands)
  4533  	})
  4534  }
  4535  
  4536  func Test_SkipSchemaValidation(t *testing.T) {
  4537  	t.Run("helm", func(t *testing.T) {
  4538  		service := newService(t, "testdata/broken-schema-verification")
  4539  
  4540  		q := apiclient.ManifestRequest{
  4541  			AppName: "test-app",
  4542  			Repo:    &v1alpha1.Repository{},
  4543  			ApplicationSource: &v1alpha1.ApplicationSource{
  4544  				Path: ".",
  4545  				Helm: &v1alpha1.ApplicationSourceHelm{
  4546  					SkipSchemaValidation: true,
  4547  				},
  4548  			},
  4549  		}
  4550  
  4551  		res, err := service.GenerateManifest(t.Context(), &q)
  4552  
  4553  		require.NoError(t, err)
  4554  		assert.Equal(t, []string{"helm template . --name-template test-app --include-crds --skip-schema-validation"}, res.Commands)
  4555  	})
  4556  	t.Run("helm", func(t *testing.T) {
  4557  		service := newService(t, "testdata/broken-schema-verification")
  4558  
  4559  		q := apiclient.ManifestRequest{
  4560  			AppName: "test-app",
  4561  			Repo:    &v1alpha1.Repository{},
  4562  			ApplicationSource: &v1alpha1.ApplicationSource{
  4563  				Path: ".",
  4564  				Helm: &v1alpha1.ApplicationSourceHelm{
  4565  					SkipSchemaValidation: false,
  4566  				},
  4567  			},
  4568  		}
  4569  
  4570  		_, err := service.GenerateManifest(t.Context(), &q)
  4571  
  4572  		require.ErrorContains(t, err, "values don't meet the specifications of the schema(s)")
  4573  	})
  4574  }
  4575  
  4576  func TestGenerateManifest_OCISourceSkipsGitClient(t *testing.T) {
  4577  	svc := newService(t, t.TempDir())
  4578  
  4579  	gitCalled := false
  4580  	svc.newGitClient = func(_, _ string, _ git.Creds, _, _ bool, _, _ string, _ ...git.ClientOpts) (git.Client, error) {
  4581  		gitCalled = true
  4582  		return nil, errors.New("git should not be called for OCI")
  4583  	}
  4584  
  4585  	req := &apiclient.ManifestRequest{
  4586  		HasMultipleSources: true,
  4587  		Repo: &v1alpha1.Repository{
  4588  			Repo: "oci://example.com/foo",
  4589  		},
  4590  		ApplicationSource: &v1alpha1.ApplicationSource{
  4591  			Path:           "",
  4592  			TargetRevision: "v1",
  4593  			Ref:            "foo",
  4594  			RepoURL:        "oci://example.com/foo",
  4595  		},
  4596  		ProjectName:        "foo-project",
  4597  		ProjectSourceRepos: []string{"*"},
  4598  	}
  4599  
  4600  	_, err := svc.GenerateManifest(t.Context(), req)
  4601  	require.NoError(t, err)
  4602  
  4603  	// verify that newGitClient was never invoked
  4604  	assert.False(t, gitCalled, "GenerateManifest should not invoke Git for OCI sources")
  4605  }