github.com/argoproj/argo-cd/v2@v2.10.9/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  	"os"
    12  	"os/exec"
    13  	"path"
    14  	"path/filepath"
    15  	"regexp"
    16  	"sort"
    17  	"strings"
    18  	"testing"
    19  	"time"
    20  
    21  	cacheutil "github.com/argoproj/argo-cd/v2/util/cache"
    22  	log "github.com/sirupsen/logrus"
    23  	"k8s.io/apimachinery/pkg/api/resource"
    24  
    25  	"github.com/stretchr/testify/assert"
    26  	"github.com/stretchr/testify/mock"
    27  	"github.com/stretchr/testify/require"
    28  	v1 "k8s.io/api/apps/v1"
    29  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    30  	"k8s.io/apimachinery/pkg/runtime"
    31  	"sigs.k8s.io/yaml"
    32  
    33  	"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
    34  	argoappv1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
    35  	"github.com/argoproj/argo-cd/v2/reposerver/apiclient"
    36  	"github.com/argoproj/argo-cd/v2/reposerver/cache"
    37  	repositorymocks "github.com/argoproj/argo-cd/v2/reposerver/cache/mocks"
    38  	"github.com/argoproj/argo-cd/v2/reposerver/metrics"
    39  	fileutil "github.com/argoproj/argo-cd/v2/test/fixture/path"
    40  	"github.com/argoproj/argo-cd/v2/util/argo"
    41  	dbmocks "github.com/argoproj/argo-cd/v2/util/db/mocks"
    42  	"github.com/argoproj/argo-cd/v2/util/git"
    43  	gitmocks "github.com/argoproj/argo-cd/v2/util/git/mocks"
    44  	"github.com/argoproj/argo-cd/v2/util/helm"
    45  	helmmocks "github.com/argoproj/argo-cd/v2/util/helm/mocks"
    46  	"github.com/argoproj/argo-cd/v2/util/io"
    47  	iomocks "github.com/argoproj/argo-cd/v2/util/io/mocks"
    48  )
    49  
    50  const testSignature = `gpg: Signature made Wed Feb 26 23:22:34 2020 CET
    51  gpg:                using RSA key 4AEE18F83AFDEB23
    52  gpg: Good signature from "GitHub (web-flow commit signing) <noreply@github.com>" [ultimate]
    53  `
    54  
    55  type clientFunc func(*gitmocks.Client, *helmmocks.Client, *iomocks.TempPaths)
    56  
    57  type repoCacheMocks struct {
    58  	mock.Mock
    59  	cacheutilCache *cacheutil.Cache
    60  	cache          *cache.Cache
    61  	mockCache      *repositorymocks.MockRepoCache
    62  }
    63  
    64  type newGitRepoHelmChartOptions struct {
    65  	chartName    string
    66  	chartVersion string
    67  	// valuesFiles is a map of the values file name to the key/value pairs to be written to the file
    68  	valuesFiles map[string]map[string]string
    69  }
    70  
    71  type newGitRepoOptions struct {
    72  	path             string
    73  	createPath       bool
    74  	remote           string
    75  	addEmptyCommit   bool
    76  	helmChartOptions newGitRepoHelmChartOptions
    77  }
    78  
    79  func newCacheMocks() *repoCacheMocks {
    80  	mockRepoCache := repositorymocks.NewMockRepoCache(&repositorymocks.MockCacheOptions{
    81  		RepoCacheExpiration:     1 * time.Minute,
    82  		RevisionCacheExpiration: 1 * time.Minute,
    83  		ReadDelay:               0,
    84  		WriteDelay:              0,
    85  	})
    86  	cacheutilCache := cacheutil.NewCache(mockRepoCache.RedisClient)
    87  	return &repoCacheMocks{
    88  		cacheutilCache: cacheutilCache,
    89  		cache:          cache.NewCache(cacheutilCache, 1*time.Minute, 1*time.Minute),
    90  		mockCache:      mockRepoCache,
    91  	}
    92  }
    93  
    94  func newServiceWithMocks(t *testing.T, root string, signed bool) (*Service, *gitmocks.Client, *repoCacheMocks) {
    95  	root, err := filepath.Abs(root)
    96  	if err != nil {
    97  		panic(err)
    98  	}
    99  	return newServiceWithOpt(t, func(gitClient *gitmocks.Client, helmClient *helmmocks.Client, paths *iomocks.TempPaths) {
   100  		gitClient.On("Init").Return(nil)
   101  		gitClient.On("Fetch", mock.Anything).Return(nil)
   102  		gitClient.On("Checkout", mock.Anything, mock.Anything).Return(nil)
   103  		gitClient.On("LsRemote", mock.Anything).Return(mock.Anything, nil)
   104  		gitClient.On("CommitSHA").Return(mock.Anything, nil)
   105  		gitClient.On("Root").Return(root)
   106  		gitClient.On("IsAnnotatedTag").Return(false)
   107  		if signed {
   108  			gitClient.On("VerifyCommitSignature", mock.Anything).Return(testSignature, nil)
   109  		} else {
   110  			gitClient.On("VerifyCommitSignature", mock.Anything).Return("", nil)
   111  		}
   112  
   113  		chart := "my-chart"
   114  		oobChart := "out-of-bounds-chart"
   115  		version := "1.1.0"
   116  		helmClient.On("GetIndex", mock.AnythingOfType("bool"), mock.Anything).Return(&helm.Index{Entries: map[string]helm.Entries{
   117  			chart:    {{Version: "1.0.0"}, {Version: version}},
   118  			oobChart: {{Version: "1.0.0"}, {Version: version}},
   119  		}}, nil)
   120  		helmClient.On("ExtractChart", chart, version).Return("./testdata/my-chart", io.NopCloser, nil)
   121  		helmClient.On("ExtractChart", oobChart, version).Return("./testdata2/out-of-bounds-chart", io.NopCloser, nil)
   122  		helmClient.On("CleanChartCache", chart, version).Return(nil)
   123  		helmClient.On("CleanChartCache", oobChart, version).Return(nil)
   124  		helmClient.On("DependencyBuild").Return(nil)
   125  
   126  		paths.On("Add", mock.Anything, mock.Anything).Return(root, nil)
   127  		paths.On("GetPath", mock.Anything).Return(root, nil)
   128  		paths.On("GetPathIfExists", mock.Anything).Return(root, nil)
   129  	}, root)
   130  }
   131  
   132  func newServiceWithOpt(t *testing.T, cf clientFunc, root string) (*Service, *gitmocks.Client, *repoCacheMocks) {
   133  	helmClient := &helmmocks.Client{}
   134  	gitClient := &gitmocks.Client{}
   135  	paths := &iomocks.TempPaths{}
   136  	cf(gitClient, helmClient, paths)
   137  	cacheMocks := newCacheMocks()
   138  	t.Cleanup(cacheMocks.mockCache.StopRedisCallback)
   139  	service := NewService(metrics.NewMetricsServer(), cacheMocks.cache, RepoServerInitConstants{ParallelismLimit: 1}, argo.NewResourceTracking(), &git.NoopCredsStore{}, root)
   140  
   141  	service.newGitClient = func(rawRepoURL string, root string, creds git.Creds, insecure bool, enableLfs bool, proxy string, opts ...git.ClientOpts) (client git.Client, e error) {
   142  		return gitClient, nil
   143  	}
   144  	service.newHelmClient = func(repoURL string, creds helm.Creds, enableOci bool, proxy string, opts ...helm.ClientOpts) helm.Client {
   145  		return helmClient
   146  	}
   147  	service.gitRepoInitializer = func(rootPath string) goio.Closer {
   148  		return io.NopCloser
   149  	}
   150  	service.gitRepoPaths = paths
   151  	return service, gitClient, cacheMocks
   152  }
   153  
   154  func newService(t *testing.T, root string) *Service {
   155  	service, _, _ := newServiceWithMocks(t, root, false)
   156  	return service
   157  }
   158  
   159  func newServiceWithSignature(t *testing.T, root string) *Service {
   160  	service, _, _ := newServiceWithMocks(t, root, true)
   161  	return service
   162  }
   163  
   164  func newServiceWithCommitSHA(t *testing.T, root, revision string) *Service {
   165  	var revisionErr error
   166  
   167  	commitSHARegex := regexp.MustCompile("^[0-9A-Fa-f]{40}$")
   168  	if !commitSHARegex.MatchString(revision) {
   169  		revisionErr = errors.New("not a commit SHA")
   170  	}
   171  
   172  	service, gitClient, _ := newServiceWithOpt(t, func(gitClient *gitmocks.Client, helmClient *helmmocks.Client, paths *iomocks.TempPaths) {
   173  		gitClient.On("Init").Return(nil)
   174  		gitClient.On("Fetch", mock.Anything).Return(nil)
   175  		gitClient.On("Checkout", mock.Anything, mock.Anything).Return(nil)
   176  		gitClient.On("LsRemote", revision).Return(revision, revisionErr)
   177  		gitClient.On("CommitSHA").Return("632039659e542ed7de0c170a4fcc1c571b288fc0", nil)
   178  		gitClient.On("Root").Return(root)
   179  		paths.On("GetPath", mock.Anything).Return(root, nil)
   180  		paths.On("GetPathIfExists", mock.Anything).Return(root, nil)
   181  	}, root)
   182  
   183  	service.newGitClient = func(rawRepoURL string, root string, creds git.Creds, insecure bool, enableLfs bool, proxy string, opts ...git.ClientOpts) (client git.Client, e error) {
   184  		return gitClient, nil
   185  	}
   186  
   187  	return service
   188  }
   189  
   190  func TestGenerateYamlManifestInDir(t *testing.T) {
   191  	service := newService(t, "../../manifests/base")
   192  
   193  	src := argoappv1.ApplicationSource{Path: "."}
   194  	q := apiclient.ManifestRequest{
   195  		Repo:               &argoappv1.Repository{},
   196  		ApplicationSource:  &src,
   197  		ProjectName:        "something",
   198  		ProjectSourceRepos: []string{"*"},
   199  	}
   200  
   201  	// update this value if we add/remove manifests
   202  	const countOfManifests = 48
   203  
   204  	res1, err := service.GenerateManifest(context.Background(), &q)
   205  
   206  	assert.NoError(t, err)
   207  	assert.Equal(t, countOfManifests, len(res1.Manifests))
   208  
   209  	// this will test concatenated manifests to verify we split YAMLs correctly
   210  	res2, err := GenerateManifests(context.Background(), "./testdata/concatenated", "/", "", &q, false, &git.NoopCredsStore{}, resource.MustParse("0"), nil)
   211  	assert.NoError(t, err)
   212  	assert.Equal(t, 3, len(res2.Manifests))
   213  }
   214  
   215  func Test_GenerateManifests_NoOutOfBoundsAccess(t *testing.T) {
   216  	testCases := []struct {
   217  		name                    string
   218  		outOfBoundsFilename     string
   219  		outOfBoundsFileContents string
   220  		mustNotContain          string // Optional string that must not appear in error or manifest output. If empty, use outOfBoundsFileContents.
   221  	}{
   222  		{
   223  			name:                    "out of bounds JSON file should not appear in error output",
   224  			outOfBoundsFilename:     "test.json",
   225  			outOfBoundsFileContents: `{"some": "json"}`,
   226  		},
   227  		{
   228  			name:                    "malformed JSON file contents should not appear in error output",
   229  			outOfBoundsFilename:     "test.json",
   230  			outOfBoundsFileContents: "$",
   231  		},
   232  		{
   233  			name:                "out of bounds JSON manifest should not appear in manifest output",
   234  			outOfBoundsFilename: "test.json",
   235  			// JSON marshalling is deterministic. So if there's a leak, exactly this should appear in the manifests.
   236  			outOfBoundsFileContents: `{"apiVersion":"v1","kind":"Secret","metadata":{"name":"test","namespace":"default"},"type":"Opaque"}`,
   237  		},
   238  		{
   239  			name:                    "out of bounds YAML manifest should not appear in manifest output",
   240  			outOfBoundsFilename:     "test.yaml",
   241  			outOfBoundsFileContents: "apiVersion: v1\nkind: Secret\nmetadata:\n  name: test\n  namespace: default\ntype: Opaque",
   242  			mustNotContain:          `{"apiVersion":"v1","kind":"Secret","metadata":{"name":"test","namespace":"default"},"type":"Opaque"}`,
   243  		},
   244  	}
   245  
   246  	for _, testCase := range testCases {
   247  		testCaseCopy := testCase
   248  		t.Run(testCaseCopy.name, func(t *testing.T) {
   249  			t.Parallel()
   250  
   251  			outOfBoundsDir := t.TempDir()
   252  			outOfBoundsFile := path.Join(outOfBoundsDir, testCaseCopy.outOfBoundsFilename)
   253  			err := os.WriteFile(outOfBoundsFile, []byte(testCaseCopy.outOfBoundsFileContents), os.FileMode(0444))
   254  			require.NoError(t, err)
   255  
   256  			repoDir := t.TempDir()
   257  			err = os.Symlink(outOfBoundsFile, path.Join(repoDir, testCaseCopy.outOfBoundsFilename))
   258  			require.NoError(t, err)
   259  
   260  			var mustNotContain = testCaseCopy.outOfBoundsFileContents
   261  			if testCaseCopy.mustNotContain != "" {
   262  				mustNotContain = testCaseCopy.mustNotContain
   263  			}
   264  
   265  			q := apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: &argoappv1.ApplicationSource{}, ProjectName: "something",
   266  				ProjectSourceRepos: []string{"*"}}
   267  			res, err := GenerateManifests(context.Background(), repoDir, "", "", &q, false, &git.NoopCredsStore{}, resource.MustParse("0"), nil)
   268  			require.Error(t, err)
   269  			assert.NotContains(t, err.Error(), mustNotContain)
   270  			assert.Contains(t, err.Error(), "illegal filepath")
   271  			assert.Nil(t, res)
   272  		})
   273  	}
   274  }
   275  
   276  func TestGenerateManifests_MissingSymlinkDestination(t *testing.T) {
   277  	repoDir := t.TempDir()
   278  	err := os.Symlink("/obviously/does/not/exist", path.Join(repoDir, "test.yaml"))
   279  	require.NoError(t, err)
   280  
   281  	q := apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: &argoappv1.ApplicationSource{}, ProjectName: "something",
   282  		ProjectSourceRepos: []string{"*"}}
   283  	_, err = GenerateManifests(context.Background(), repoDir, "", "", &q, false, &git.NoopCredsStore{}, resource.MustParse("0"), nil)
   284  	require.NoError(t, err)
   285  }
   286  
   287  func TestGenerateManifests_K8SAPIResetCache(t *testing.T) {
   288  	service := newService(t, "../../manifests/base")
   289  
   290  	src := argoappv1.ApplicationSource{Path: "."}
   291  	q := apiclient.ManifestRequest{
   292  		KubeVersion:        "v1.16.0",
   293  		Repo:               &argoappv1.Repository{},
   294  		ApplicationSource:  &src,
   295  		ProjectName:        "something",
   296  		ProjectSourceRepos: []string{"*"},
   297  	}
   298  
   299  	cachedFakeResponse := &apiclient.ManifestResponse{Manifests: []string{"Fake"}}
   300  
   301  	err := service.cache.SetManifests(mock.Anything, &src, q.RefSources, &q, "", "", "", "", &cache.CachedManifestResponse{ManifestResponse: cachedFakeResponse}, nil)
   302  	assert.NoError(t, err)
   303  
   304  	res, err := service.GenerateManifest(context.Background(), &q)
   305  	assert.NoError(t, err)
   306  	assert.Equal(t, cachedFakeResponse, res)
   307  
   308  	q.KubeVersion = "v1.17.0"
   309  	res, err = service.GenerateManifest(context.Background(), &q)
   310  	assert.NoError(t, err)
   311  	assert.NotEqual(t, cachedFakeResponse, res)
   312  	assert.True(t, len(res.Manifests) > 1)
   313  }
   314  
   315  func TestGenerateManifests_EmptyCache(t *testing.T) {
   316  	service, gitMocks, mockCache := newServiceWithMocks(t, "../../manifests/base", false)
   317  
   318  	src := argoappv1.ApplicationSource{Path: "."}
   319  	q := apiclient.ManifestRequest{
   320  		Repo:               &argoappv1.Repository{},
   321  		ApplicationSource:  &src,
   322  		ProjectName:        "something",
   323  		ProjectSourceRepos: []string{"*"},
   324  	}
   325  
   326  	err := service.cache.SetManifests(mock.Anything, &src, q.RefSources, &q, "", "", "", "", &cache.CachedManifestResponse{ManifestResponse: nil}, nil)
   327  	assert.NoError(t, err)
   328  
   329  	res, err := service.GenerateManifest(context.Background(), &q)
   330  	assert.NoError(t, err)
   331  	assert.True(t, len(res.Manifests) > 0)
   332  	mockCache.mockCache.AssertCacheCalledTimes(t, &repositorymocks.CacheCallCounts{
   333  		ExternalSets:    2,
   334  		ExternalGets:    2,
   335  		ExternalDeletes: 1})
   336  	gitMocks.AssertCalled(t, "LsRemote", mock.Anything)
   337  	gitMocks.AssertCalled(t, "Fetch", mock.Anything)
   338  }
   339  
   340  // 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
   341  // but it does resolve and cache the revision
   342  func TestGenerateManifest_RefOnlyShortCircuit(t *testing.T) {
   343  	lsremoteCalled := false
   344  	dir := t.TempDir()
   345  	repopath := fmt.Sprintf("%s/tmprepo", dir)
   346  	repoRemote := fmt.Sprintf("file://%s", repopath)
   347  	cacheMocks := newCacheMocks()
   348  	t.Cleanup(cacheMocks.mockCache.StopRedisCallback)
   349  	service := NewService(metrics.NewMetricsServer(), cacheMocks.cache, RepoServerInitConstants{ParallelismLimit: 1}, argo.NewResourceTracking(), &git.NoopCredsStore{}, repopath)
   350  	service.newGitClient = func(rawRepoURL string, root string, creds git.Creds, insecure bool, enableLfs bool, proxy string, opts ...git.ClientOpts) (client git.Client, e error) {
   351  		opts = append(opts, git.WithEventHandlers(git.EventHandlers{
   352  			// Primary check, we want to make sure ls-remote is not called when the item is in cache
   353  			OnLsRemote: func(repo string) func() {
   354  				return func() {
   355  					lsremoteCalled = true
   356  				}
   357  			},
   358  			OnFetch: func(repo string) func() {
   359  				return func() {
   360  					assert.Fail(t, "Fetch should not be called from GenerateManifest when the source is ref only")
   361  				}
   362  			},
   363  		}))
   364  		gitClient, err := git.NewClientExt(rawRepoURL, root, creds, insecure, enableLfs, proxy, opts...)
   365  		return gitClient, err
   366  	}
   367  	revision := initGitRepo(t, newGitRepoOptions{
   368  		path:           repopath,
   369  		createPath:     true,
   370  		remote:         repoRemote,
   371  		addEmptyCommit: true,
   372  	})
   373  	src := argoappv1.ApplicationSource{RepoURL: repoRemote, TargetRevision: "HEAD", Ref: "test-ref"}
   374  	repo := &argoappv1.Repository{
   375  		Repo: repoRemote,
   376  	}
   377  	q := apiclient.ManifestRequest{
   378  		Repo:               repo,
   379  		Revision:           "HEAD",
   380  		HasMultipleSources: true,
   381  		ApplicationSource:  &src,
   382  		ProjectName:        "default",
   383  		ProjectSourceRepos: []string{"*"},
   384  	}
   385  	_, err := service.GenerateManifest(context.Background(), &q)
   386  	assert.NoError(t, err)
   387  	cacheMocks.mockCache.AssertCacheCalledTimes(t, &repositorymocks.CacheCallCounts{
   388  		ExternalSets: 1,
   389  		ExternalGets: 1})
   390  	assert.True(t, lsremoteCalled, "ls-remote should be called when the source is ref only")
   391  	var revisions [][2]string
   392  	assert.NoError(t, cacheMocks.cacheutilCache.GetItem(fmt.Sprintf("git-refs|%s", repoRemote), &revisions))
   393  	assert.ElementsMatch(t, [][2]string{{"refs/heads/main", revision}, {"HEAD", "ref: refs/heads/main"}}, revisions)
   394  }
   395  
   396  // Test that calling manifest generation on source helm reference helm files that when the revision is cached it does not call ls-remote
   397  func TestGenerateManifestsHelmWithRefs_CachedNoLsRemote(t *testing.T) {
   398  	dir := t.TempDir()
   399  	repopath := fmt.Sprintf("%s/tmprepo", dir)
   400  	cacheMocks := newCacheMocks()
   401  	t.Cleanup(func() {
   402  		cacheMocks.mockCache.StopRedisCallback()
   403  		err := filepath.WalkDir(dir,
   404  			func(path string, di fs.DirEntry, err error) error {
   405  				if err == nil {
   406  					return os.Chmod(path, 0777)
   407  				}
   408  				return err
   409  			})
   410  		if err != nil {
   411  			t.Fatal(err)
   412  		}
   413  	})
   414  	service := NewService(metrics.NewMetricsServer(), cacheMocks.cache, RepoServerInitConstants{ParallelismLimit: 1}, argo.NewResourceTracking(), &git.NoopCredsStore{}, repopath)
   415  	var gitClient git.Client
   416  	var err error
   417  	service.newGitClient = func(rawRepoURL string, root string, creds git.Creds, insecure bool, enableLfs bool, proxy string, opts ...git.ClientOpts) (client git.Client, e error) {
   418  		opts = append(opts, git.WithEventHandlers(git.EventHandlers{
   419  			// Primary check, we want to make sure ls-remote is not called when the item is in cache
   420  			OnLsRemote: func(repo string) func() {
   421  				return func() {
   422  					assert.Fail(t, "LsRemote should not be called when the item is in cache")
   423  				}
   424  			},
   425  		}))
   426  		gitClient, err = git.NewClientExt(rawRepoURL, root, creds, insecure, enableLfs, proxy, opts...)
   427  		return gitClient, err
   428  	}
   429  	repoRemote := fmt.Sprintf("file://%s", repopath)
   430  	revision := initGitRepo(t, newGitRepoOptions{
   431  		path:       repopath,
   432  		createPath: true,
   433  		remote:     repoRemote,
   434  		helmChartOptions: newGitRepoHelmChartOptions{
   435  			chartName:    "my-chart",
   436  			chartVersion: "v1.0.0",
   437  			valuesFiles:  map[string]map[string]string{"test.yaml": {"testval": "test"}}},
   438  	})
   439  	src := argoappv1.ApplicationSource{RepoURL: repoRemote, Path: ".", TargetRevision: "HEAD", Helm: &argoappv1.ApplicationSourceHelm{
   440  		ValueFiles: []string{"$ref/test.yaml"},
   441  	}}
   442  	repo := &argoappv1.Repository{
   443  		Repo: repoRemote,
   444  	}
   445  	q := apiclient.ManifestRequest{
   446  		Repo:               repo,
   447  		Revision:           "HEAD",
   448  		HasMultipleSources: true,
   449  		ApplicationSource:  &src,
   450  		ProjectName:        "default",
   451  		ProjectSourceRepos: []string{"*"},
   452  		RefSources:         map[string]*argoappv1.RefTarget{"$ref": {TargetRevision: "HEAD", Repo: *repo}},
   453  	}
   454  	err = cacheMocks.cacheutilCache.SetItem(fmt.Sprintf("git-refs|%s", repoRemote), [][2]string{{"HEAD", revision}}, 30*time.Second, false)
   455  	assert.NoError(t, err)
   456  	_, err = service.GenerateManifest(context.Background(), &q)
   457  	assert.NoError(t, err)
   458  	cacheMocks.mockCache.AssertCacheCalledTimes(t, &repositorymocks.CacheCallCounts{
   459  		ExternalSets: 2,
   460  		ExternalGets: 5})
   461  }
   462  
   463  // ensure we can use a semver constraint range (>= 1.0.0) and get back the correct chart (1.0.0)
   464  func TestHelmManifestFromChartRepo(t *testing.T) {
   465  	root := t.TempDir()
   466  	service, gitMocks, mockCache := newServiceWithMocks(t, root, false)
   467  	source := &argoappv1.ApplicationSource{Chart: "my-chart", TargetRevision: ">= 1.0.0"}
   468  	request := &apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: source, NoCache: true, ProjectName: "something",
   469  		ProjectSourceRepos: []string{"*"}}
   470  	response, err := service.GenerateManifest(context.Background(), request)
   471  	assert.NoError(t, err)
   472  	assert.NotNil(t, response)
   473  	assert.Equal(t, &apiclient.ManifestResponse{
   474  		Manifests:  []string{"{\"apiVersion\":\"v1\",\"kind\":\"ConfigMap\",\"metadata\":{\"name\":\"my-map\"}}"},
   475  		Namespace:  "",
   476  		Server:     "",
   477  		Revision:   "1.1.0",
   478  		SourceType: "Helm",
   479  	}, response)
   480  	mockCache.mockCache.AssertCacheCalledTimes(t, &repositorymocks.CacheCallCounts{
   481  		ExternalSets: 1,
   482  		ExternalGets: 0})
   483  	gitMocks.AssertNotCalled(t, "LsRemote", mock.Anything)
   484  }
   485  
   486  func TestHelmChartReferencingExternalValues(t *testing.T) {
   487  	service := newService(t, ".")
   488  	spec := argoappv1.ApplicationSpec{
   489  		Sources: []argoappv1.ApplicationSource{
   490  			{RepoURL: "https://helm.example.com", Chart: "my-chart", TargetRevision: ">= 1.0.0", Helm: &argoappv1.ApplicationSourceHelm{
   491  				ValueFiles: []string{"$ref/testdata/my-chart/my-chart-values.yaml"},
   492  			}},
   493  			{Ref: "ref", RepoURL: "https://git.example.com/test/repo"},
   494  		},
   495  	}
   496  	repoDB := &dbmocks.ArgoDB{}
   497  	repoDB.On("GetRepository", context.Background(), "https://git.example.com/test/repo").Return(&argoappv1.Repository{
   498  		Repo: "https://git.example.com/test/repo",
   499  	}, nil)
   500  	refSources, err := argo.GetRefSources(context.Background(), spec, repoDB)
   501  	require.NoError(t, err)
   502  	request := &apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: &spec.Sources[0], NoCache: true, RefSources: refSources, HasMultipleSources: true, ProjectName: "something",
   503  		ProjectSourceRepos: []string{"*"}}
   504  	response, err := service.GenerateManifest(context.Background(), request)
   505  	assert.NoError(t, err)
   506  	assert.NotNil(t, response)
   507  	assert.Equal(t, &apiclient.ManifestResponse{
   508  		Manifests:  []string{"{\"apiVersion\":\"v1\",\"kind\":\"ConfigMap\",\"metadata\":{\"name\":\"my-map\"}}"},
   509  		Namespace:  "",
   510  		Server:     "",
   511  		Revision:   "1.1.0",
   512  		SourceType: "Helm",
   513  	}, response)
   514  }
   515  
   516  func TestHelmChartReferencingExternalValues_OutOfBounds_Symlink(t *testing.T) {
   517  	service := newService(t, ".")
   518  	err := os.Mkdir("testdata/oob-symlink", 0755)
   519  	require.NoError(t, err)
   520  	t.Cleanup(func() {
   521  		err = os.RemoveAll("testdata/oob-symlink")
   522  		require.NoError(t, err)
   523  	})
   524  	// Create a symlink to a file outside of the repo
   525  	err = os.Symlink("../../../values.yaml", "./testdata/oob-symlink/oob-symlink.yaml")
   526  	// Create a regular file to reference from another source
   527  	err = os.WriteFile("./testdata/oob-symlink/values.yaml", []byte("foo: bar"), 0644)
   528  	require.NoError(t, err)
   529  	spec := argoappv1.ApplicationSpec{
   530  		Sources: []argoappv1.ApplicationSource{
   531  			{RepoURL: "https://helm.example.com", Chart: "my-chart", TargetRevision: ">= 1.0.0", Helm: &argoappv1.ApplicationSourceHelm{
   532  				// Reference `ref` but do not use the oob symlink. The mere existence of the link should be enough to
   533  				// cause an error.
   534  				ValueFiles: []string{"$ref/testdata/oob-symlink/values.yaml"},
   535  			}},
   536  			{Ref: "ref", RepoURL: "https://git.example.com/test/repo"},
   537  		},
   538  	}
   539  	repoDB := &dbmocks.ArgoDB{}
   540  	repoDB.On("GetRepository", context.Background(), "https://git.example.com/test/repo").Return(&argoappv1.Repository{
   541  		Repo: "https://git.example.com/test/repo",
   542  	}, nil)
   543  	refSources, err := argo.GetRefSources(context.Background(), spec, repoDB)
   544  	require.NoError(t, err)
   545  	request := &apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: &spec.Sources[0], NoCache: true, RefSources: refSources, HasMultipleSources: true}
   546  	_, err = service.GenerateManifest(context.Background(), request)
   547  	assert.Error(t, err)
   548  }
   549  
   550  func TestGenerateManifestsUseExactRevision(t *testing.T) {
   551  	service, gitClient, _ := newServiceWithMocks(t, ".", false)
   552  
   553  	src := argoappv1.ApplicationSource{Path: "./testdata/recurse", Directory: &argoappv1.ApplicationSourceDirectory{Recurse: true}}
   554  
   555  	q := apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: &src, Revision: "abc", ProjectName: "something",
   556  		ProjectSourceRepos: []string{"*"}}
   557  
   558  	res1, err := service.GenerateManifest(context.Background(), &q)
   559  	assert.Nil(t, err)
   560  	assert.Equal(t, 2, len(res1.Manifests))
   561  	assert.Equal(t, gitClient.Calls[0].Arguments[0], "abc")
   562  }
   563  
   564  func TestRecurseManifestsInDir(t *testing.T) {
   565  	service := newService(t, ".")
   566  
   567  	src := argoappv1.ApplicationSource{Path: "./testdata/recurse", Directory: &argoappv1.ApplicationSourceDirectory{Recurse: true}}
   568  
   569  	q := apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: &src, ProjectName: "something",
   570  		ProjectSourceRepos: []string{"*"}}
   571  
   572  	res1, err := service.GenerateManifest(context.Background(), &q)
   573  	assert.Nil(t, err)
   574  	assert.Equal(t, 2, len(res1.Manifests))
   575  }
   576  
   577  func TestInvalidManifestsInDir(t *testing.T) {
   578  	service := newService(t, ".")
   579  
   580  	src := argoappv1.ApplicationSource{Path: "./testdata/invalid-manifests", Directory: &argoappv1.ApplicationSourceDirectory{Recurse: true}}
   581  
   582  	q := apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: &src}
   583  
   584  	_, err := service.GenerateManifest(context.Background(), &q)
   585  	assert.NotNil(t, err)
   586  }
   587  
   588  func TestInvalidMetadata(t *testing.T) {
   589  	service := newService(t, ".")
   590  
   591  	src := argoappv1.ApplicationSource{Path: "./testdata/invalid-metadata", Directory: &argoappv1.ApplicationSourceDirectory{Recurse: true}}
   592  	q := apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: &src, AppLabelKey: "test", AppName: "invalid-metadata", TrackingMethod: "annotation+label"}
   593  	_, err := service.GenerateManifest(context.Background(), &q)
   594  	assert.Error(t, err)
   595  	assert.Contains(t, err.Error(), "contains non-string key in the map")
   596  }
   597  
   598  func TestNilMetadataAccessors(t *testing.T) {
   599  	service := newService(t, ".")
   600  	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\"}}"
   601  
   602  	src := argoappv1.ApplicationSource{Path: "./testdata/nil-metadata-accessors", Directory: &argoappv1.ApplicationSourceDirectory{Recurse: true}}
   603  	q := apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: &src, AppLabelKey: "test", AppName: "nil-metadata-accessors", TrackingMethod: "annotation+label"}
   604  	res, err := service.GenerateManifest(context.Background(), &q)
   605  	assert.NoError(t, err)
   606  	assert.Equal(t, len(res.Manifests), 1)
   607  	assert.Equal(t, expected, res.Manifests[0])
   608  }
   609  
   610  func TestGenerateJsonnetManifestInDir(t *testing.T) {
   611  	service := newService(t, ".")
   612  
   613  	q := apiclient.ManifestRequest{
   614  		Repo: &argoappv1.Repository{},
   615  		ApplicationSource: &argoappv1.ApplicationSource{
   616  			Path: "./testdata/jsonnet",
   617  			Directory: &argoappv1.ApplicationSourceDirectory{
   618  				Jsonnet: argoappv1.ApplicationSourceJsonnet{
   619  					ExtVars: []argoappv1.JsonnetVar{{Name: "extVarString", Value: "extVarString"}, {Name: "extVarCode", Value: "\"extVarCode\"", Code: true}},
   620  					TLAs:    []argoappv1.JsonnetVar{{Name: "tlaString", Value: "tlaString"}, {Name: "tlaCode", Value: "\"tlaCode\"", Code: true}},
   621  					Libs:    []string{"testdata/jsonnet/vendor"},
   622  				},
   623  			},
   624  		},
   625  		ProjectName:        "something",
   626  		ProjectSourceRepos: []string{"*"},
   627  	}
   628  	res1, err := service.GenerateManifest(context.Background(), &q)
   629  	assert.Nil(t, err)
   630  	assert.Equal(t, 2, len(res1.Manifests))
   631  }
   632  
   633  func TestGenerateJsonnetManifestInRootDir(t *testing.T) {
   634  	service := newService(t, "testdata/jsonnet-1")
   635  
   636  	q := apiclient.ManifestRequest{
   637  		Repo: &argoappv1.Repository{},
   638  		ApplicationSource: &argoappv1.ApplicationSource{
   639  			Path: ".",
   640  			Directory: &argoappv1.ApplicationSourceDirectory{
   641  				Jsonnet: argoappv1.ApplicationSourceJsonnet{
   642  					ExtVars: []argoappv1.JsonnetVar{{Name: "extVarString", Value: "extVarString"}, {Name: "extVarCode", Value: "\"extVarCode\"", Code: true}},
   643  					TLAs:    []argoappv1.JsonnetVar{{Name: "tlaString", Value: "tlaString"}, {Name: "tlaCode", Value: "\"tlaCode\"", Code: true}},
   644  					Libs:    []string{"."},
   645  				},
   646  			},
   647  		},
   648  		ProjectName:        "something",
   649  		ProjectSourceRepos: []string{"*"},
   650  	}
   651  	res1, err := service.GenerateManifest(context.Background(), &q)
   652  	assert.Nil(t, err)
   653  	assert.Equal(t, 2, len(res1.Manifests))
   654  }
   655  
   656  func TestGenerateJsonnetLibOutside(t *testing.T) {
   657  	service := newService(t, ".")
   658  
   659  	q := apiclient.ManifestRequest{
   660  		Repo: &argoappv1.Repository{},
   661  		ApplicationSource: &argoappv1.ApplicationSource{
   662  			Path: "./testdata/jsonnet",
   663  			Directory: &argoappv1.ApplicationSourceDirectory{
   664  				Jsonnet: argoappv1.ApplicationSourceJsonnet{
   665  					Libs: []string{"../../../testdata/jsonnet/vendor"},
   666  				},
   667  			},
   668  		},
   669  		ProjectName:        "something",
   670  		ProjectSourceRepos: []string{"*"},
   671  	}
   672  	_, err := service.GenerateManifest(context.Background(), &q)
   673  	require.Error(t, err)
   674  	require.Contains(t, err.Error(), "file '../../../testdata/jsonnet/vendor' resolved to outside repository root")
   675  }
   676  
   677  func TestManifestGenErrorCacheByNumRequests(t *testing.T) {
   678  
   679  	// Returns the state of the manifest generation cache, by querying the cache for the previously set result
   680  	getRecentCachedEntry := func(service *Service, manifestRequest *apiclient.ManifestRequest) *cache.CachedManifestResponse {
   681  		assert.NotNil(t, service)
   682  		assert.NotNil(t, manifestRequest)
   683  
   684  		cachedManifestResponse := &cache.CachedManifestResponse{}
   685  		err := service.cache.GetManifests(mock.Anything, manifestRequest.ApplicationSource, manifestRequest.RefSources, manifestRequest, manifestRequest.Namespace, "", manifestRequest.AppLabelKey, manifestRequest.AppName, cachedManifestResponse, nil)
   686  		assert.Nil(t, err)
   687  		return cachedManifestResponse
   688  	}
   689  
   690  	// Example:
   691  	// With repo server (test) parameters:
   692  	// - PauseGenerationAfterFailedGenerationAttempts: 2
   693  	// - PauseGenerationOnFailureForRequests: 4
   694  	// - TotalCacheInvocations: 10
   695  	//
   696  	// After 2 manifest generation failures in a row, the next 4 manifest generation requests should be cached,
   697  	// with the next 2 after that being uncached. Here's how it looks...
   698  	//
   699  	//  request count) result
   700  	// --------------------------
   701  	// 1) Attempt to generate manifest, fails.
   702  	// 2) Second attempt to generate manifest, fails.
   703  	// 3) Return cached error attempt from #2
   704  	// 4) Return cached error attempt from #2
   705  	// 5) Return cached error attempt from #2
   706  	// 6) Return cached error attempt from #2. Max response limit hit, so reset cache entry.
   707  	// 7) Attempt to generate manifest, fails.
   708  	// 8) Attempt to generate manifest, fails.
   709  	// 9) Return cached error attempt from #8
   710  	// 10) Return cached error attempt from #8
   711  
   712  	// The same pattern PauseGenerationAfterFailedGenerationAttempts generation attempts, followed by
   713  	// PauseGenerationOnFailureForRequests cached responses, should apply for various combinations of
   714  	// both parameters.
   715  
   716  	tests := []struct {
   717  		PauseGenerationAfterFailedGenerationAttempts int
   718  		PauseGenerationOnFailureForRequests          int
   719  		TotalCacheInvocations                        int
   720  	}{
   721  		{2, 4, 10},
   722  		{3, 5, 10},
   723  		{1, 2, 5},
   724  	}
   725  	for _, tt := range tests {
   726  		testName := fmt.Sprintf("gen-attempts-%d-pause-%d-total-%d", tt.PauseGenerationAfterFailedGenerationAttempts, tt.PauseGenerationOnFailureForRequests, tt.TotalCacheInvocations)
   727  		t.Run(testName, func(t *testing.T) {
   728  			service := newService(t, ".")
   729  
   730  			service.initConstants = RepoServerInitConstants{
   731  				ParallelismLimit: 1,
   732  				PauseGenerationAfterFailedGenerationAttempts: tt.PauseGenerationAfterFailedGenerationAttempts,
   733  				PauseGenerationOnFailureForMinutes:           0,
   734  				PauseGenerationOnFailureForRequests:          tt.PauseGenerationOnFailureForRequests,
   735  			}
   736  
   737  			totalAttempts := service.initConstants.PauseGenerationAfterFailedGenerationAttempts + service.initConstants.PauseGenerationOnFailureForRequests
   738  
   739  			for invocationCount := 0; invocationCount < tt.TotalCacheInvocations; invocationCount++ {
   740  				adjustedInvocation := invocationCount % totalAttempts
   741  
   742  				fmt.Printf("%d )-------------------------------------------\n", invocationCount)
   743  
   744  				manifestRequest := &apiclient.ManifestRequest{
   745  					Repo:    &argoappv1.Repository{},
   746  					AppName: "test",
   747  					ApplicationSource: &argoappv1.ApplicationSource{
   748  						Path: "./testdata/invalid-helm",
   749  					},
   750  				}
   751  
   752  				res, err := service.GenerateManifest(context.Background(), manifestRequest)
   753  
   754  				// Verify invariant: res != nil xor err != nil
   755  				if err != nil {
   756  					assert.True(t, res == nil, "both err and res are non-nil res: %v   err: %v", res, err)
   757  				} else {
   758  					assert.True(t, res != nil, "both err and res are nil")
   759  				}
   760  
   761  				cachedManifestResponse := getRecentCachedEntry(service, manifestRequest)
   762  
   763  				isCachedError := err != nil && strings.HasPrefix(err.Error(), cachedManifestGenerationPrefix)
   764  
   765  				if adjustedInvocation < service.initConstants.PauseGenerationAfterFailedGenerationAttempts {
   766  					// GenerateManifest should not return cached errors for the first X responses, where X is the FailGenAttempts constants
   767  					require.False(t, isCachedError)
   768  
   769  					require.NotNil(t, cachedManifestResponse)
   770  					// nolint:staticcheck
   771  					assert.Nil(t, cachedManifestResponse.ManifestResponse)
   772  					// nolint:staticcheck
   773  					assert.True(t, cachedManifestResponse.FirstFailureTimestamp != 0)
   774  
   775  					// Internal cache consec failures value should increase with invocations, cached response should stay the same,
   776  					// nolint:staticcheck
   777  					assert.True(t, cachedManifestResponse.NumberOfConsecutiveFailures == adjustedInvocation+1)
   778  					// nolint:staticcheck
   779  					assert.True(t, cachedManifestResponse.NumberOfCachedResponsesReturned == 0)
   780  
   781  				} else {
   782  					// GenerateManifest SHOULD return cached errors for the next X responses, where X is the
   783  					// PauseGenerationOnFailureForRequests constant
   784  					assert.True(t, isCachedError)
   785  					require.NotNil(t, cachedManifestResponse)
   786  					// nolint:staticcheck
   787  					assert.Nil(t, cachedManifestResponse.ManifestResponse)
   788  					// nolint:staticcheck
   789  					assert.True(t, cachedManifestResponse.FirstFailureTimestamp != 0)
   790  
   791  					// Internal cache values should update correctly based on number of return cache entries, consecutive failures should stay the same
   792  					// nolint:staticcheck
   793  					assert.True(t, cachedManifestResponse.NumberOfConsecutiveFailures == service.initConstants.PauseGenerationAfterFailedGenerationAttempts)
   794  					// nolint:staticcheck
   795  					assert.True(t, cachedManifestResponse.NumberOfCachedResponsesReturned == (adjustedInvocation-service.initConstants.PauseGenerationAfterFailedGenerationAttempts+1))
   796  				}
   797  			}
   798  		})
   799  	}
   800  }
   801  
   802  func TestManifestGenErrorCacheFileContentsChange(t *testing.T) {
   803  
   804  	tmpDir := t.TempDir()
   805  
   806  	service := newService(t, tmpDir)
   807  
   808  	service.initConstants = RepoServerInitConstants{
   809  		ParallelismLimit: 1,
   810  		PauseGenerationAfterFailedGenerationAttempts: 2,
   811  		PauseGenerationOnFailureForMinutes:           0,
   812  		PauseGenerationOnFailureForRequests:          4,
   813  	}
   814  
   815  	for step := 0; step < 3; step++ {
   816  
   817  		// step 1) Attempt to generate manifests against invalid helm chart (should return uncached error)
   818  		// step 2) Attempt to generate manifest against valid helm chart (should succeed and return valid response)
   819  		// step 3) Attempt to generate manifest against invalid helm chart (should return cached value from step 2)
   820  
   821  		errorExpected := step%2 == 0
   822  
   823  		// Ensure that the target directory will succeed or fail, so we can verify the cache correctly handles it
   824  		err := os.RemoveAll(tmpDir)
   825  		assert.NoError(t, err)
   826  		err = os.MkdirAll(tmpDir, 0777)
   827  		assert.NoError(t, err)
   828  		if errorExpected {
   829  			// Copy invalid helm chart into temporary directory, ensuring manifest generation will fail
   830  			err = fileutil.CopyDir("./testdata/invalid-helm", tmpDir)
   831  			assert.NoError(t, err)
   832  
   833  		} else {
   834  			// Copy valid helm chart into temporary directory, ensuring generation will succeed
   835  			err = fileutil.CopyDir("./testdata/my-chart", tmpDir)
   836  			assert.NoError(t, err)
   837  		}
   838  
   839  		res, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
   840  			Repo:    &argoappv1.Repository{},
   841  			AppName: "test",
   842  			ApplicationSource: &argoappv1.ApplicationSource{
   843  				Path: ".",
   844  			},
   845  			ProjectName:        "something",
   846  			ProjectSourceRepos: []string{"*"},
   847  		})
   848  
   849  		fmt.Println("-", step, "-", res != nil, err != nil, errorExpected)
   850  		fmt.Println("    err: ", err)
   851  		fmt.Println("    res: ", res)
   852  
   853  		if step < 2 {
   854  			assert.True(t, (err != nil) == errorExpected, "error return value and error expected did not match")
   855  			assert.True(t, (res != nil) == !errorExpected, "GenerateManifest return value and expected value did not match")
   856  		}
   857  
   858  		if step == 2 {
   859  			assert.NoError(t, err, "error ret val was non-nil on step 3")
   860  			assert.NotNil(t, res, "GenerateManifest ret val was nil on step 3")
   861  		}
   862  	}
   863  }
   864  
   865  func TestManifestGenErrorCacheByMinutesElapsed(t *testing.T) {
   866  
   867  	tests := []struct {
   868  		// Test with a range of pause expiration thresholds
   869  		PauseGenerationOnFailureForMinutes int
   870  	}{
   871  		{1}, {2}, {10}, {24 * 60},
   872  	}
   873  	for _, tt := range tests {
   874  		testName := fmt.Sprintf("pause-time-%d", tt.PauseGenerationOnFailureForMinutes)
   875  		t.Run(testName, func(t *testing.T) {
   876  			service := newService(t, ".")
   877  
   878  			// Here we simulate the passage of time by overriding the now() function of Service
   879  			currentTime := time.Now()
   880  			service.now = func() time.Time {
   881  				return currentTime
   882  			}
   883  
   884  			service.initConstants = RepoServerInitConstants{
   885  				ParallelismLimit: 1,
   886  				PauseGenerationAfterFailedGenerationAttempts: 1,
   887  				PauseGenerationOnFailureForMinutes:           tt.PauseGenerationOnFailureForMinutes,
   888  				PauseGenerationOnFailureForRequests:          0,
   889  			}
   890  
   891  			// 1) Put the cache into the failure state
   892  			for x := 0; x < 2; x++ {
   893  				res, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
   894  					Repo:    &argoappv1.Repository{},
   895  					AppName: "test",
   896  					ApplicationSource: &argoappv1.ApplicationSource{
   897  						Path: "./testdata/invalid-helm",
   898  					},
   899  				})
   900  
   901  				assert.True(t, err != nil && res == nil)
   902  
   903  				// Ensure that the second invocation triggers the cached error state
   904  				if x == 1 {
   905  					assert.True(t, strings.HasPrefix(err.Error(), cachedManifestGenerationPrefix))
   906  				}
   907  
   908  			}
   909  
   910  			// 2) Jump forward X-1 minutes in time, where X is the expiration boundary
   911  			currentTime = currentTime.Add(time.Duration(tt.PauseGenerationOnFailureForMinutes-1) * time.Minute)
   912  			res, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
   913  				Repo:    &argoappv1.Repository{},
   914  				AppName: "test",
   915  				ApplicationSource: &argoappv1.ApplicationSource{
   916  					Path: "./testdata/invalid-helm",
   917  				},
   918  			})
   919  
   920  			// 3) Ensure that the cache still returns a cached copy of the last error
   921  			assert.True(t, err != nil && res == nil)
   922  			assert.True(t, strings.HasPrefix(err.Error(), cachedManifestGenerationPrefix))
   923  
   924  			// 4) Jump forward 2 minutes in time, such that the pause generation time has elapsed and we should return to normal state
   925  			currentTime = currentTime.Add(2 * time.Minute)
   926  
   927  			res, err = service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
   928  				Repo:    &argoappv1.Repository{},
   929  				AppName: "test",
   930  				ApplicationSource: &argoappv1.ApplicationSource{
   931  					Path: "./testdata/invalid-helm",
   932  				},
   933  			})
   934  
   935  			// 5) Ensure that the service no longer returns a cached copy of the last error
   936  			assert.True(t, err != nil && res == nil)
   937  			assert.True(t, !strings.HasPrefix(err.Error(), cachedManifestGenerationPrefix))
   938  
   939  		})
   940  	}
   941  
   942  }
   943  
   944  func TestManifestGenErrorCacheRespectsNoCache(t *testing.T) {
   945  
   946  	service := newService(t, ".")
   947  
   948  	service.initConstants = RepoServerInitConstants{
   949  		ParallelismLimit: 1,
   950  		PauseGenerationAfterFailedGenerationAttempts: 1,
   951  		PauseGenerationOnFailureForMinutes:           0,
   952  		PauseGenerationOnFailureForRequests:          4,
   953  	}
   954  
   955  	// 1) Put the cache into the failure state
   956  	for x := 0; x < 2; x++ {
   957  		res, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
   958  			Repo:    &argoappv1.Repository{},
   959  			AppName: "test",
   960  			ApplicationSource: &argoappv1.ApplicationSource{
   961  				Path: "./testdata/invalid-helm",
   962  			},
   963  		})
   964  
   965  		assert.True(t, err != nil && res == nil)
   966  
   967  		// Ensure that the second invocation is cached
   968  		if x == 1 {
   969  			assert.True(t, strings.HasPrefix(err.Error(), cachedManifestGenerationPrefix))
   970  		}
   971  	}
   972  
   973  	// 2) Call generateManifest with NoCache enabled
   974  	res, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
   975  		Repo:    &argoappv1.Repository{},
   976  		AppName: "test",
   977  		ApplicationSource: &argoappv1.ApplicationSource{
   978  			Path: "./testdata/invalid-helm",
   979  		},
   980  		NoCache: true,
   981  	})
   982  
   983  	// 3) Ensure that the cache returns a new generation attempt, rather than a previous cached error
   984  	assert.True(t, err != nil && res == nil)
   985  	assert.True(t, !strings.HasPrefix(err.Error(), cachedManifestGenerationPrefix))
   986  
   987  	// 4) Call generateManifest
   988  	res, err = service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
   989  		Repo:    &argoappv1.Repository{},
   990  		AppName: "test",
   991  		ApplicationSource: &argoappv1.ApplicationSource{
   992  			Path: "./testdata/invalid-helm",
   993  		},
   994  	})
   995  
   996  	// 5) Ensure that the subsequent invocation, after nocache, is cached
   997  	assert.True(t, err != nil && res == nil)
   998  	assert.True(t, strings.HasPrefix(err.Error(), cachedManifestGenerationPrefix))
   999  
  1000  }
  1001  
  1002  func TestGenerateHelmWithValues(t *testing.T) {
  1003  	service := newService(t, "../../util/helm/testdata/redis")
  1004  
  1005  	res, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
  1006  		Repo:    &argoappv1.Repository{},
  1007  		AppName: "test",
  1008  		ApplicationSource: &argoappv1.ApplicationSource{
  1009  			Path: ".",
  1010  			Helm: &argoappv1.ApplicationSourceHelm{
  1011  				ValueFiles:   []string{"values-production.yaml"},
  1012  				ValuesObject: &runtime.RawExtension{Raw: []byte(`cluster: {slaveCount: 2}`)},
  1013  			},
  1014  		},
  1015  		ProjectName:        "something",
  1016  		ProjectSourceRepos: []string{"*"},
  1017  	})
  1018  
  1019  	assert.NoError(t, err)
  1020  
  1021  	replicasVerified := false
  1022  	for _, src := range res.Manifests {
  1023  		obj := unstructured.Unstructured{}
  1024  		err = json.Unmarshal([]byte(src), &obj)
  1025  		assert.NoError(t, err)
  1026  
  1027  		if obj.GetKind() == "Deployment" && obj.GetName() == "test-redis-slave" {
  1028  			var dep v1.Deployment
  1029  			err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &dep)
  1030  			assert.NoError(t, err)
  1031  			assert.Equal(t, int32(2), *dep.Spec.Replicas)
  1032  			replicasVerified = true
  1033  		}
  1034  	}
  1035  	assert.True(t, replicasVerified)
  1036  
  1037  }
  1038  
  1039  func TestHelmWithMissingValueFiles(t *testing.T) {
  1040  	service := newService(t, "../../util/helm/testdata/redis")
  1041  	missingValuesFile := "values-prod-overrides.yaml"
  1042  
  1043  	req := &apiclient.ManifestRequest{
  1044  		Repo:    &argoappv1.Repository{},
  1045  		AppName: "test",
  1046  		ApplicationSource: &argoappv1.ApplicationSource{
  1047  			Path: ".",
  1048  			Helm: &argoappv1.ApplicationSourceHelm{
  1049  				ValueFiles: []string{"values-production.yaml", missingValuesFile},
  1050  			},
  1051  		},
  1052  		ProjectName:        "something",
  1053  		ProjectSourceRepos: []string{"*"},
  1054  	}
  1055  
  1056  	// Should fail since we're passing a non-existent values file, and error should indicate that
  1057  	_, err := service.GenerateManifest(context.Background(), req)
  1058  	assert.Error(t, err)
  1059  	assert.Contains(t, err.Error(), fmt.Sprintf("%s: no such file or directory", missingValuesFile))
  1060  
  1061  	// Should template without error even if defining a non-existent values file
  1062  	req.ApplicationSource.Helm.IgnoreMissingValueFiles = true
  1063  	_, err = service.GenerateManifest(context.Background(), req)
  1064  	assert.NoError(t, err)
  1065  }
  1066  
  1067  func TestGenerateHelmWithEnvVars(t *testing.T) {
  1068  	service := newService(t, "../../util/helm/testdata/redis")
  1069  
  1070  	res, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
  1071  		Repo:    &argoappv1.Repository{},
  1072  		AppName: "production",
  1073  		ApplicationSource: &argoappv1.ApplicationSource{
  1074  			Path: ".",
  1075  			Helm: &argoappv1.ApplicationSourceHelm{
  1076  				ValueFiles: []string{"values-$ARGOCD_APP_NAME.yaml"},
  1077  			},
  1078  		},
  1079  		ProjectName:        "something",
  1080  		ProjectSourceRepos: []string{"*"},
  1081  	})
  1082  
  1083  	assert.NoError(t, err)
  1084  
  1085  	replicasVerified := false
  1086  	for _, src := range res.Manifests {
  1087  		obj := unstructured.Unstructured{}
  1088  		err = json.Unmarshal([]byte(src), &obj)
  1089  		assert.NoError(t, err)
  1090  
  1091  		if obj.GetKind() == "Deployment" && obj.GetName() == "production-redis-slave" {
  1092  			var dep v1.Deployment
  1093  			err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &dep)
  1094  			assert.NoError(t, err)
  1095  			assert.Equal(t, int32(3), *dep.Spec.Replicas)
  1096  			replicasVerified = true
  1097  		}
  1098  	}
  1099  	assert.True(t, replicasVerified)
  1100  }
  1101  
  1102  // The requested value file (`../minio/values.yaml`) is outside the app path (`./util/helm/testdata/redis`), however
  1103  // since the requested value is still under the repo directory (`~/go/src/github.com/argoproj/argo-cd`), it is allowed
  1104  func TestGenerateHelmWithValuesDirectoryTraversal(t *testing.T) {
  1105  	service := newService(t, "../../util/helm/testdata")
  1106  	_, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
  1107  		Repo:    &argoappv1.Repository{},
  1108  		AppName: "test",
  1109  		ApplicationSource: &argoappv1.ApplicationSource{
  1110  			Path: "./redis",
  1111  			Helm: &argoappv1.ApplicationSourceHelm{
  1112  				ValueFiles:   []string{"../minio/values.yaml"},
  1113  				ValuesObject: &runtime.RawExtension{Raw: []byte(`cluster: {slaveCount: 2}`)},
  1114  			},
  1115  		},
  1116  		ProjectName:        "something",
  1117  		ProjectSourceRepos: []string{"*"},
  1118  	})
  1119  	assert.NoError(t, err)
  1120  
  1121  	// Test the case where the path is "."
  1122  	service = newService(t, "./testdata")
  1123  	_, err = service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
  1124  		Repo:    &argoappv1.Repository{},
  1125  		AppName: "test",
  1126  		ApplicationSource: &argoappv1.ApplicationSource{
  1127  			Path: "./my-chart",
  1128  		},
  1129  		ProjectName:        "something",
  1130  		ProjectSourceRepos: []string{"*"},
  1131  	})
  1132  	assert.NoError(t, err)
  1133  }
  1134  
  1135  func TestChartRepoWithOutOfBoundsSymlink(t *testing.T) {
  1136  	service := newService(t, ".")
  1137  	source := &argoappv1.ApplicationSource{Chart: "out-of-bounds-chart", TargetRevision: ">= 1.0.0"}
  1138  	request := &apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: source, NoCache: true}
  1139  	_, err := service.GenerateManifest(context.Background(), request)
  1140  	assert.ErrorContains(t, err, "chart contains out-of-bounds symlinks")
  1141  }
  1142  
  1143  // This is a Helm first-class app with a values file inside the repo directory
  1144  // (`~/go/src/github.com/argoproj/argo-cd/reposerver/repository`), so it is allowed
  1145  func TestHelmManifestFromChartRepoWithValueFile(t *testing.T) {
  1146  	service := newService(t, ".")
  1147  	source := &argoappv1.ApplicationSource{
  1148  		Chart:          "my-chart",
  1149  		TargetRevision: ">= 1.0.0",
  1150  		Helm: &argoappv1.ApplicationSourceHelm{
  1151  			ValueFiles: []string{"./my-chart-values.yaml"},
  1152  		},
  1153  	}
  1154  	request := &apiclient.ManifestRequest{
  1155  		Repo:               &argoappv1.Repository{},
  1156  		ApplicationSource:  source,
  1157  		NoCache:            true,
  1158  		ProjectName:        "something",
  1159  		ProjectSourceRepos: []string{"*"}}
  1160  	response, err := service.GenerateManifest(context.Background(), request)
  1161  	assert.NoError(t, err)
  1162  	assert.NotNil(t, response)
  1163  	assert.Equal(t, &apiclient.ManifestResponse{
  1164  		Manifests:  []string{"{\"apiVersion\":\"v1\",\"kind\":\"ConfigMap\",\"metadata\":{\"name\":\"my-map\"}}"},
  1165  		Namespace:  "",
  1166  		Server:     "",
  1167  		Revision:   "1.1.0",
  1168  		SourceType: "Helm",
  1169  	}, response)
  1170  }
  1171  
  1172  // This is a Helm first-class app with a values file outside the repo directory
  1173  // (`~/go/src/github.com/argoproj/argo-cd/reposerver/repository`), so it is not allowed
  1174  func TestHelmManifestFromChartRepoWithValueFileOutsideRepo(t *testing.T) {
  1175  	service := newService(t, ".")
  1176  	source := &argoappv1.ApplicationSource{
  1177  		Chart:          "my-chart",
  1178  		TargetRevision: ">= 1.0.0",
  1179  		Helm: &argoappv1.ApplicationSourceHelm{
  1180  			ValueFiles: []string{"../my-chart-2/my-chart-2-values.yaml"},
  1181  		},
  1182  	}
  1183  	request := &apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: source, NoCache: true}
  1184  	_, err := service.GenerateManifest(context.Background(), request)
  1185  	assert.Error(t, err)
  1186  }
  1187  
  1188  func TestHelmManifestFromChartRepoWithValueFileLinks(t *testing.T) {
  1189  	t.Run("Valid symlink", func(t *testing.T) {
  1190  		service := newService(t, ".")
  1191  		source := &argoappv1.ApplicationSource{
  1192  			Chart:          "my-chart",
  1193  			TargetRevision: ">= 1.0.0",
  1194  			Helm: &argoappv1.ApplicationSourceHelm{
  1195  				ValueFiles: []string{"my-chart-link.yaml"},
  1196  			},
  1197  		}
  1198  		request := &apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: source, NoCache: true, ProjectName: "something",
  1199  			ProjectSourceRepos: []string{"*"}}
  1200  		_, err := service.GenerateManifest(context.Background(), request)
  1201  		assert.NoError(t, err)
  1202  	})
  1203  }
  1204  
  1205  func TestGenerateHelmWithURL(t *testing.T) {
  1206  	service := newService(t, "../../util/helm/testdata/redis")
  1207  
  1208  	_, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
  1209  		Repo:    &argoappv1.Repository{},
  1210  		AppName: "test",
  1211  		ApplicationSource: &argoappv1.ApplicationSource{
  1212  			Path: ".",
  1213  			Helm: &argoappv1.ApplicationSourceHelm{
  1214  				ValueFiles:   []string{"https://raw.githubusercontent.com/argoproj/argocd-example-apps/master/helm-guestbook/values.yaml"},
  1215  				ValuesObject: &runtime.RawExtension{Raw: []byte(`cluster: {slaveCount: 2}`)},
  1216  			},
  1217  		},
  1218  		ProjectName:        "something",
  1219  		ProjectSourceRepos: []string{"*"},
  1220  		HelmOptions:        &argoappv1.HelmOptions{ValuesFileSchemes: []string{"https"}},
  1221  	})
  1222  	assert.NoError(t, err)
  1223  }
  1224  
  1225  // The requested value file (`../minio/values.yaml`) is outside the repo directory
  1226  // (`~/go/src/github.com/argoproj/argo-cd/util/helm/testdata/redis`), so it is blocked
  1227  func TestGenerateHelmWithValuesDirectoryTraversalOutsideRepo(t *testing.T) {
  1228  	t.Run("Values file with relative path pointing outside repo root", func(t *testing.T) {
  1229  		service := newService(t, "../../util/helm/testdata/redis")
  1230  		_, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
  1231  			Repo:    &argoappv1.Repository{},
  1232  			AppName: "test",
  1233  			ApplicationSource: &argoappv1.ApplicationSource{
  1234  				Path: ".",
  1235  				Helm: &argoappv1.ApplicationSourceHelm{
  1236  					ValueFiles:   []string{"../minio/values.yaml"},
  1237  					ValuesObject: &runtime.RawExtension{Raw: []byte(`cluster: {slaveCount: 2}`)},
  1238  				},
  1239  			},
  1240  			ProjectName:        "something",
  1241  			ProjectSourceRepos: []string{"*"},
  1242  		})
  1243  		assert.Error(t, err)
  1244  		assert.Contains(t, err.Error(), "outside repository root")
  1245  	})
  1246  
  1247  	t.Run("Values file with relative path pointing inside repo root", func(t *testing.T) {
  1248  		service := newService(t, "./testdata")
  1249  		_, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
  1250  			Repo:    &argoappv1.Repository{},
  1251  			AppName: "test",
  1252  			ApplicationSource: &argoappv1.ApplicationSource{
  1253  				Path: "./my-chart",
  1254  				Helm: &argoappv1.ApplicationSourceHelm{
  1255  					ValueFiles:   []string{"../my-chart/my-chart-values.yaml"},
  1256  					ValuesObject: &runtime.RawExtension{Raw: []byte(`cluster: {slaveCount: 2}`)},
  1257  				},
  1258  			},
  1259  			ProjectName:        "something",
  1260  			ProjectSourceRepos: []string{"*"},
  1261  		})
  1262  		assert.NoError(t, err)
  1263  	})
  1264  
  1265  	t.Run("Values file with absolute path stays within repo root", func(t *testing.T) {
  1266  		service := newService(t, "./testdata")
  1267  		_, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
  1268  			Repo:    &argoappv1.Repository{},
  1269  			AppName: "test",
  1270  			ApplicationSource: &argoappv1.ApplicationSource{
  1271  				Path: "./my-chart",
  1272  				Helm: &argoappv1.ApplicationSourceHelm{
  1273  					ValueFiles:   []string{"/my-chart/my-chart-values.yaml"},
  1274  					ValuesObject: &runtime.RawExtension{Raw: []byte(`cluster: {slaveCount: 2}`)},
  1275  				},
  1276  			},
  1277  			ProjectName:        "something",
  1278  			ProjectSourceRepos: []string{"*"},
  1279  		})
  1280  		assert.NoError(t, err)
  1281  	})
  1282  
  1283  	t.Run("Values file with absolute path using back-references outside repo root", func(t *testing.T) {
  1284  		service := newService(t, "./testdata")
  1285  		_, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
  1286  			Repo:    &argoappv1.Repository{},
  1287  			AppName: "test",
  1288  			ApplicationSource: &argoappv1.ApplicationSource{
  1289  				Path: "./my-chart",
  1290  				Helm: &argoappv1.ApplicationSourceHelm{
  1291  					ValueFiles:   []string{"/../../../my-chart-values.yaml"},
  1292  					ValuesObject: &runtime.RawExtension{Raw: []byte(`cluster: {slaveCount: 2}`)},
  1293  				},
  1294  			},
  1295  			ProjectName:        "something",
  1296  			ProjectSourceRepos: []string{"*"},
  1297  		})
  1298  		assert.Error(t, err)
  1299  		assert.Contains(t, err.Error(), "outside repository root")
  1300  	})
  1301  
  1302  	t.Run("Remote values file from forbidden protocol", func(t *testing.T) {
  1303  		service := newService(t, "./testdata")
  1304  		_, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
  1305  			Repo:    &argoappv1.Repository{},
  1306  			AppName: "test",
  1307  			ApplicationSource: &argoappv1.ApplicationSource{
  1308  				Path: "./my-chart",
  1309  				Helm: &argoappv1.ApplicationSourceHelm{
  1310  					ValueFiles:   []string{"file://../../../../my-chart-values.yaml"},
  1311  					ValuesObject: &runtime.RawExtension{Raw: []byte(`cluster: {slaveCount: 2}`)},
  1312  				},
  1313  			},
  1314  			ProjectName:        "something",
  1315  			ProjectSourceRepos: []string{"*"},
  1316  		})
  1317  		assert.Error(t, err)
  1318  		assert.Contains(t, err.Error(), "is not allowed")
  1319  	})
  1320  
  1321  	t.Run("Remote values file from custom allowed protocol", func(t *testing.T) {
  1322  		service := newService(t, "./testdata")
  1323  		_, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
  1324  			Repo:    &argoappv1.Repository{},
  1325  			AppName: "test",
  1326  			ApplicationSource: &argoappv1.ApplicationSource{
  1327  				Path: "./my-chart",
  1328  				Helm: &argoappv1.ApplicationSourceHelm{
  1329  					ValueFiles: []string{"s3://my-bucket/my-chart-values.yaml"},
  1330  				},
  1331  			},
  1332  			HelmOptions:        &argoappv1.HelmOptions{ValuesFileSchemes: []string{"s3"}},
  1333  			ProjectName:        "something",
  1334  			ProjectSourceRepos: []string{"*"},
  1335  		})
  1336  		assert.Error(t, err)
  1337  		assert.Contains(t, err.Error(), "s3://my-bucket/my-chart-values.yaml: no such file or directory")
  1338  	})
  1339  }
  1340  
  1341  // File parameter should not allow traversal outside of the repository root
  1342  func TestGenerateHelmWithAbsoluteFileParameter(t *testing.T) {
  1343  	service := newService(t, "../..")
  1344  
  1345  	file, err := os.CreateTemp("", "external-secret.txt")
  1346  	assert.NoError(t, err)
  1347  	externalSecretPath := file.Name()
  1348  	defer func() { _ = os.RemoveAll(externalSecretPath) }()
  1349  	expectedFileContent, err := os.ReadFile("../../util/helm/testdata/external/external-secret.txt")
  1350  	assert.NoError(t, err)
  1351  	err = os.WriteFile(externalSecretPath, expectedFileContent, 0644)
  1352  	assert.NoError(t, err)
  1353  	defer func() {
  1354  		if err = file.Close(); err != nil {
  1355  			panic(err)
  1356  		}
  1357  	}()
  1358  
  1359  	_, err = service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
  1360  		Repo:    &argoappv1.Repository{},
  1361  		AppName: "test",
  1362  		ApplicationSource: &argoappv1.ApplicationSource{
  1363  			Path: "./util/helm/testdata/redis",
  1364  			Helm: &argoappv1.ApplicationSourceHelm{
  1365  				ValueFiles:   []string{"values-production.yaml"},
  1366  				ValuesObject: &runtime.RawExtension{Raw: []byte(`cluster: {slaveCount: 2}`)},
  1367  				FileParameters: []argoappv1.HelmFileParameter{{
  1368  					Name: "passwordContent",
  1369  					Path: externalSecretPath,
  1370  				}},
  1371  			},
  1372  		},
  1373  		ProjectName:        "something",
  1374  		ProjectSourceRepos: []string{"*"},
  1375  	})
  1376  	assert.Error(t, err)
  1377  }
  1378  
  1379  // The requested file parameter (`../external/external-secret.txt`) is outside the app path
  1380  // (`./util/helm/testdata/redis`), however since the requested value is still under the repo
  1381  // directory (`~/go/src/github.com/argoproj/argo-cd`), it is allowed. It is used as a means of
  1382  // providing direct content to a helm chart via a specific key.
  1383  func TestGenerateHelmWithFileParameter(t *testing.T) {
  1384  	service := newService(t, "../../util/helm/testdata")
  1385  
  1386  	res, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
  1387  		Repo:    &argoappv1.Repository{},
  1388  		AppName: "test",
  1389  		ApplicationSource: &argoappv1.ApplicationSource{
  1390  			Path: "./redis",
  1391  			Helm: &argoappv1.ApplicationSourceHelm{
  1392  				ValueFiles:   []string{"values-production.yaml"},
  1393  				Values:       `cluster: {slaveCount: 10}`,
  1394  				ValuesObject: &runtime.RawExtension{Raw: []byte(`cluster: {slaveCount: 2}`)},
  1395  				FileParameters: []argoappv1.HelmFileParameter{{
  1396  					Name: "passwordContent",
  1397  					Path: "../external/external-secret.txt",
  1398  				}},
  1399  			},
  1400  		},
  1401  		ProjectName:        "something",
  1402  		ProjectSourceRepos: []string{"*"},
  1403  	})
  1404  	assert.NoError(t, err)
  1405  	assert.Contains(t, res.Manifests[6], `"replicas":2`, "ValuesObject should override Values")
  1406  }
  1407  
  1408  func TestGenerateNullList(t *testing.T) {
  1409  	service := newService(t, ".")
  1410  
  1411  	t.Run("null list", func(t *testing.T) {
  1412  		res1, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
  1413  			Repo:               &argoappv1.Repository{},
  1414  			ApplicationSource:  &argoappv1.ApplicationSource{Path: "./testdata/null-list"},
  1415  			NoCache:            true,
  1416  			ProjectName:        "something",
  1417  			ProjectSourceRepos: []string{"*"},
  1418  		})
  1419  		assert.Nil(t, err)
  1420  		assert.Equal(t, len(res1.Manifests), 1)
  1421  		assert.Contains(t, res1.Manifests[0], "prometheus-operator-operator")
  1422  	})
  1423  
  1424  	t.Run("empty list", func(t *testing.T) {
  1425  		res1, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
  1426  			Repo:               &argoappv1.Repository{},
  1427  			ApplicationSource:  &argoappv1.ApplicationSource{Path: "./testdata/empty-list"},
  1428  			NoCache:            true,
  1429  			ProjectName:        "something",
  1430  			ProjectSourceRepos: []string{"*"},
  1431  		})
  1432  		assert.Nil(t, err)
  1433  		assert.Equal(t, len(res1.Manifests), 1)
  1434  		assert.Contains(t, res1.Manifests[0], "prometheus-operator-operator")
  1435  	})
  1436  
  1437  	t.Run("weird list", func(t *testing.T) {
  1438  		res1, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
  1439  			Repo:               &argoappv1.Repository{},
  1440  			ApplicationSource:  &argoappv1.ApplicationSource{Path: "./testdata/weird-list"},
  1441  			NoCache:            true,
  1442  			ProjectName:        "something",
  1443  			ProjectSourceRepos: []string{"*"},
  1444  		})
  1445  		assert.Nil(t, err)
  1446  		assert.Len(t, res1.Manifests, 2)
  1447  	})
  1448  }
  1449  
  1450  func TestIdentifyAppSourceTypeByAppDirWithKustomizations(t *testing.T) {
  1451  	sourceType, err := GetAppSourceType(context.Background(), &argoappv1.ApplicationSource{}, "./testdata/kustomization_yaml", "./testdata", "testapp", map[string]bool{}, []string{})
  1452  	assert.Nil(t, err)
  1453  	assert.Equal(t, argoappv1.ApplicationSourceTypeKustomize, sourceType)
  1454  
  1455  	sourceType, err = GetAppSourceType(context.Background(), &argoappv1.ApplicationSource{}, "./testdata/kustomization_yml", "./testdata", "testapp", map[string]bool{}, []string{})
  1456  	assert.Nil(t, err)
  1457  	assert.Equal(t, argoappv1.ApplicationSourceTypeKustomize, sourceType)
  1458  
  1459  	sourceType, err = GetAppSourceType(context.Background(), &argoappv1.ApplicationSource{}, "./testdata/Kustomization", "./testdata", "testapp", map[string]bool{}, []string{})
  1460  	assert.Nil(t, err)
  1461  	assert.Equal(t, argoappv1.ApplicationSourceTypeKustomize, sourceType)
  1462  }
  1463  
  1464  func TestGenerateFromUTF16(t *testing.T) {
  1465  	q := apiclient.ManifestRequest{
  1466  		Repo:               &argoappv1.Repository{},
  1467  		ApplicationSource:  &argoappv1.ApplicationSource{},
  1468  		ProjectName:        "something",
  1469  		ProjectSourceRepos: []string{"*"},
  1470  	}
  1471  	res1, err := GenerateManifests(context.Background(), "./testdata/utf-16", "/", "", &q, false, &git.NoopCredsStore{}, resource.MustParse("0"), nil)
  1472  	assert.Nil(t, err)
  1473  	assert.Equal(t, 2, len(res1.Manifests))
  1474  }
  1475  
  1476  func TestListApps(t *testing.T) {
  1477  	service := newService(t, "./testdata")
  1478  
  1479  	res, err := service.ListApps(context.Background(), &apiclient.ListAppsRequest{Repo: &argoappv1.Repository{}})
  1480  	assert.NoError(t, err)
  1481  
  1482  	expectedApps := map[string]string{
  1483  		"Kustomization":                     "Kustomize",
  1484  		"app-parameters/multi":              "Kustomize",
  1485  		"app-parameters/single-app-only":    "Kustomize",
  1486  		"app-parameters/single-global":      "Kustomize",
  1487  		"app-parameters/single-global-helm": "Helm",
  1488  		"in-bounds-values-file-link":        "Helm",
  1489  		"invalid-helm":                      "Helm",
  1490  		"invalid-kustomize":                 "Kustomize",
  1491  		"kustomization_yaml":                "Kustomize",
  1492  		"kustomization_yml":                 "Kustomize",
  1493  		"my-chart":                          "Helm",
  1494  		"my-chart-2":                        "Helm",
  1495  		"oci-dependencies":                  "Helm",
  1496  		"out-of-bounds-values-file-link":    "Helm",
  1497  		"values-files":                      "Helm",
  1498  		"helm-with-dependencies":            "Helm",
  1499  		"helm-with-dependencies-alias":      "Helm",
  1500  	}
  1501  	assert.Equal(t, expectedApps, res.Apps)
  1502  }
  1503  
  1504  func TestGetAppDetailsHelm(t *testing.T) {
  1505  	service := newService(t, "../../util/helm/testdata/dependency")
  1506  
  1507  	res, err := service.GetAppDetails(context.Background(), &apiclient.RepoServerAppDetailsQuery{
  1508  		Repo: &argoappv1.Repository{},
  1509  		Source: &argoappv1.ApplicationSource{
  1510  			Path: ".",
  1511  		},
  1512  	})
  1513  
  1514  	assert.NoError(t, err)
  1515  	assert.NotNil(t, res.Helm)
  1516  
  1517  	assert.Equal(t, "Helm", res.Type)
  1518  	assert.EqualValues(t, []string{"values-production.yaml", "values.yaml"}, res.Helm.ValueFiles)
  1519  }
  1520  
  1521  func TestGetAppDetailsHelmUsesCache(t *testing.T) {
  1522  	service := newService(t, "../../util/helm/testdata/dependency")
  1523  
  1524  	res, err := service.GetAppDetails(context.Background(), &apiclient.RepoServerAppDetailsQuery{
  1525  		Repo: &argoappv1.Repository{},
  1526  		Source: &argoappv1.ApplicationSource{
  1527  			Path: ".",
  1528  		},
  1529  	})
  1530  
  1531  	assert.NoError(t, err)
  1532  	assert.NotNil(t, res.Helm)
  1533  
  1534  	assert.Equal(t, "Helm", res.Type)
  1535  	assert.EqualValues(t, []string{"values-production.yaml", "values.yaml"}, res.Helm.ValueFiles)
  1536  }
  1537  
  1538  func TestGetAppDetailsHelm_WithNoValuesFile(t *testing.T) {
  1539  	service := newService(t, "../../util/helm/testdata/api-versions")
  1540  
  1541  	res, err := service.GetAppDetails(context.Background(), &apiclient.RepoServerAppDetailsQuery{
  1542  		Repo: &argoappv1.Repository{},
  1543  		Source: &argoappv1.ApplicationSource{
  1544  			Path: ".",
  1545  		},
  1546  	})
  1547  
  1548  	assert.NoError(t, err)
  1549  	assert.NotNil(t, res.Helm)
  1550  
  1551  	assert.Equal(t, "Helm", res.Type)
  1552  	assert.Empty(t, res.Helm.ValueFiles)
  1553  	assert.Equal(t, "", res.Helm.Values)
  1554  }
  1555  
  1556  func TestGetAppDetailsKustomize(t *testing.T) {
  1557  	service := newService(t, "../../util/kustomize/testdata/kustomization_yaml")
  1558  
  1559  	res, err := service.GetAppDetails(context.Background(), &apiclient.RepoServerAppDetailsQuery{
  1560  		Repo: &argoappv1.Repository{},
  1561  		Source: &argoappv1.ApplicationSource{
  1562  			Path: ".",
  1563  		},
  1564  	})
  1565  
  1566  	assert.NoError(t, err)
  1567  
  1568  	assert.Equal(t, "Kustomize", res.Type)
  1569  	assert.NotNil(t, res.Kustomize)
  1570  	assert.EqualValues(t, []string{"nginx:1.15.4", "registry.k8s.io/nginx-slim:0.8"}, res.Kustomize.Images)
  1571  }
  1572  
  1573  func TestGetHelmCharts(t *testing.T) {
  1574  	service := newService(t, "../..")
  1575  	res, err := service.GetHelmCharts(context.Background(), &apiclient.HelmChartsRequest{Repo: &argoappv1.Repository{}})
  1576  
  1577  	// fix flakiness
  1578  	sort.Slice(res.Items, func(i, j int) bool {
  1579  		return res.Items[i].Name < res.Items[j].Name
  1580  	})
  1581  
  1582  	assert.NoError(t, err)
  1583  	assert.Len(t, res.Items, 2)
  1584  
  1585  	item := res.Items[0]
  1586  	assert.Equal(t, "my-chart", item.Name)
  1587  	assert.EqualValues(t, []string{"1.0.0", "1.1.0"}, item.Versions)
  1588  
  1589  	item2 := res.Items[1]
  1590  	assert.Equal(t, "out-of-bounds-chart", item2.Name)
  1591  	assert.EqualValues(t, []string{"1.0.0", "1.1.0"}, item2.Versions)
  1592  }
  1593  
  1594  func TestGetRevisionMetadata(t *testing.T) {
  1595  	service, gitClient, _ := newServiceWithMocks(t, "../..", false)
  1596  	now := time.Now()
  1597  
  1598  	gitClient.On("RevisionMetadata", mock.Anything).Return(&git.RevisionMetadata{
  1599  		Message: "test",
  1600  		Author:  "author",
  1601  		Date:    now,
  1602  		Tags:    []string{"tag1", "tag2"},
  1603  	}, nil)
  1604  
  1605  	res, err := service.GetRevisionMetadata(context.Background(), &apiclient.RepoServerRevisionMetadataRequest{
  1606  		Repo:           &argoappv1.Repository{},
  1607  		Revision:       "c0b400fc458875d925171398f9ba9eabd5529923",
  1608  		CheckSignature: true,
  1609  	})
  1610  
  1611  	assert.NoError(t, err)
  1612  	assert.Equal(t, "test", res.Message)
  1613  	assert.Equal(t, now, res.Date.Time)
  1614  	assert.Equal(t, "author", res.Author)
  1615  	assert.EqualValues(t, []string{"tag1", "tag2"}, res.Tags)
  1616  	assert.NotEmpty(t, res.SignatureInfo)
  1617  
  1618  	// Check for truncated revision value
  1619  	res, err = service.GetRevisionMetadata(context.Background(), &apiclient.RepoServerRevisionMetadataRequest{
  1620  		Repo:           &argoappv1.Repository{},
  1621  		Revision:       "c0b400f",
  1622  		CheckSignature: true,
  1623  	})
  1624  
  1625  	assert.NoError(t, err)
  1626  	assert.Equal(t, "test", res.Message)
  1627  	assert.Equal(t, now, res.Date.Time)
  1628  	assert.Equal(t, "author", res.Author)
  1629  	assert.EqualValues(t, []string{"tag1", "tag2"}, res.Tags)
  1630  	assert.NotEmpty(t, res.SignatureInfo)
  1631  
  1632  	// Cache hit - signature info should not be in result
  1633  	res, err = service.GetRevisionMetadata(context.Background(), &apiclient.RepoServerRevisionMetadataRequest{
  1634  		Repo:           &argoappv1.Repository{},
  1635  		Revision:       "c0b400fc458875d925171398f9ba9eabd5529923",
  1636  		CheckSignature: false,
  1637  	})
  1638  	assert.NoError(t, err)
  1639  	assert.Empty(t, res.SignatureInfo)
  1640  
  1641  	// Enforce cache miss - signature info should not be in result
  1642  	res, err = service.GetRevisionMetadata(context.Background(), &apiclient.RepoServerRevisionMetadataRequest{
  1643  		Repo:           &argoappv1.Repository{},
  1644  		Revision:       "da52afd3b2df1ec49470603d8bbb46954dab1091",
  1645  		CheckSignature: false,
  1646  	})
  1647  	assert.NoError(t, err)
  1648  	assert.Empty(t, res.SignatureInfo)
  1649  
  1650  	// Cache hit on previous entry that did not have signature info
  1651  	res, err = service.GetRevisionMetadata(context.Background(), &apiclient.RepoServerRevisionMetadataRequest{
  1652  		Repo:           &argoappv1.Repository{},
  1653  		Revision:       "da52afd3b2df1ec49470603d8bbb46954dab1091",
  1654  		CheckSignature: true,
  1655  	})
  1656  	assert.NoError(t, err)
  1657  	assert.NotEmpty(t, res.SignatureInfo)
  1658  }
  1659  
  1660  func TestGetSignatureVerificationResult(t *testing.T) {
  1661  	// Commit with signature and verification requested
  1662  	{
  1663  		service := newServiceWithSignature(t, "../../manifests/base")
  1664  
  1665  		src := argoappv1.ApplicationSource{Path: "."}
  1666  		q := apiclient.ManifestRequest{
  1667  			Repo:               &argoappv1.Repository{},
  1668  			ApplicationSource:  &src,
  1669  			VerifySignature:    true,
  1670  			ProjectName:        "something",
  1671  			ProjectSourceRepos: []string{"*"},
  1672  		}
  1673  
  1674  		res, err := service.GenerateManifest(context.Background(), &q)
  1675  		assert.NoError(t, err)
  1676  		assert.Equal(t, testSignature, res.VerifyResult)
  1677  	}
  1678  	// Commit with signature and verification not requested
  1679  	{
  1680  		service := newServiceWithSignature(t, "../../manifests/base")
  1681  
  1682  		src := argoappv1.ApplicationSource{Path: "."}
  1683  		q := apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: &src, ProjectName: "something",
  1684  			ProjectSourceRepos: []string{"*"}}
  1685  
  1686  		res, err := service.GenerateManifest(context.Background(), &q)
  1687  		assert.NoError(t, err)
  1688  		assert.Empty(t, res.VerifyResult)
  1689  	}
  1690  	// Commit without signature and verification requested
  1691  	{
  1692  		service := newService(t, "../../manifests/base")
  1693  
  1694  		src := argoappv1.ApplicationSource{Path: "."}
  1695  		q := apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: &src, VerifySignature: true, ProjectName: "something",
  1696  			ProjectSourceRepos: []string{"*"}}
  1697  
  1698  		res, err := service.GenerateManifest(context.Background(), &q)
  1699  		assert.NoError(t, err)
  1700  		assert.Empty(t, res.VerifyResult)
  1701  	}
  1702  	// Commit without signature and verification not requested
  1703  	{
  1704  		service := newService(t, "../../manifests/base")
  1705  
  1706  		src := argoappv1.ApplicationSource{Path: "."}
  1707  		q := apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: &src, VerifySignature: true, ProjectName: "something",
  1708  			ProjectSourceRepos: []string{"*"}}
  1709  
  1710  		res, err := service.GenerateManifest(context.Background(), &q)
  1711  		assert.NoError(t, err)
  1712  		assert.Empty(t, res.VerifyResult)
  1713  	}
  1714  }
  1715  
  1716  func Test_newEnv(t *testing.T) {
  1717  	assert.Equal(t, &argoappv1.Env{
  1718  		&argoappv1.EnvEntry{Name: "ARGOCD_APP_NAME", Value: "my-app-name"},
  1719  		&argoappv1.EnvEntry{Name: "ARGOCD_APP_NAMESPACE", Value: "my-namespace"},
  1720  		&argoappv1.EnvEntry{Name: "ARGOCD_APP_REVISION", Value: "my-revision"},
  1721  		&argoappv1.EnvEntry{Name: "ARGOCD_APP_REVISION_SHORT", Value: "my-revi"},
  1722  		&argoappv1.EnvEntry{Name: "ARGOCD_APP_SOURCE_REPO_URL", Value: "https://github.com/my-org/my-repo"},
  1723  		&argoappv1.EnvEntry{Name: "ARGOCD_APP_SOURCE_PATH", Value: "my-path"},
  1724  		&argoappv1.EnvEntry{Name: "ARGOCD_APP_SOURCE_TARGET_REVISION", Value: "my-target-revision"},
  1725  	}, newEnv(&apiclient.ManifestRequest{
  1726  		AppName:   "my-app-name",
  1727  		Namespace: "my-namespace",
  1728  		Repo:      &argoappv1.Repository{Repo: "https://github.com/my-org/my-repo"},
  1729  		ApplicationSource: &argoappv1.ApplicationSource{
  1730  			Path:           "my-path",
  1731  			TargetRevision: "my-target-revision",
  1732  		},
  1733  	}, "my-revision"))
  1734  }
  1735  
  1736  func TestService_newHelmClientResolveRevision(t *testing.T) {
  1737  	service := newService(t, ".")
  1738  
  1739  	t.Run("EmptyRevision", func(t *testing.T) {
  1740  		_, _, err := service.newHelmClientResolveRevision(&argoappv1.Repository{}, "", "", true)
  1741  		assert.EqualError(t, err, "invalid revision '': improper constraint: ")
  1742  	})
  1743  	t.Run("InvalidRevision", func(t *testing.T) {
  1744  		_, _, err := service.newHelmClientResolveRevision(&argoappv1.Repository{}, "???", "", true)
  1745  		assert.EqualError(t, err, "invalid revision '???': improper constraint: ???", true)
  1746  	})
  1747  }
  1748  
  1749  func TestGetAppDetailsWithAppParameterFile(t *testing.T) {
  1750  	t.Run("No app name set and app specific file exists", func(t *testing.T) {
  1751  		service := newService(t, ".")
  1752  		runWithTempTestdata(t, "multi", func(t *testing.T, path string) {
  1753  			details, err := service.GetAppDetails(context.Background(), &apiclient.RepoServerAppDetailsQuery{
  1754  				Repo: &argoappv1.Repository{},
  1755  				Source: &argoappv1.ApplicationSource{
  1756  					Path: path,
  1757  				},
  1758  			})
  1759  			require.NoError(t, err)
  1760  			assert.EqualValues(t, []string{"gcr.io/heptio-images/ks-guestbook-demo:0.2"}, details.Kustomize.Images)
  1761  		})
  1762  	})
  1763  	t.Run("No app specific override", func(t *testing.T) {
  1764  		service := newService(t, ".")
  1765  		runWithTempTestdata(t, "single-global", func(t *testing.T, path string) {
  1766  			details, err := service.GetAppDetails(context.Background(), &apiclient.RepoServerAppDetailsQuery{
  1767  				Repo: &argoappv1.Repository{},
  1768  				Source: &argoappv1.ApplicationSource{
  1769  					Path: path,
  1770  				},
  1771  				AppName: "testapp",
  1772  			})
  1773  			require.NoError(t, err)
  1774  			assert.EqualValues(t, []string{"gcr.io/heptio-images/ks-guestbook-demo:0.2"}, details.Kustomize.Images)
  1775  		})
  1776  	})
  1777  	t.Run("Only app specific override", func(t *testing.T) {
  1778  		service := newService(t, ".")
  1779  		runWithTempTestdata(t, "single-app-only", func(t *testing.T, path string) {
  1780  			details, err := service.GetAppDetails(context.Background(), &apiclient.RepoServerAppDetailsQuery{
  1781  				Repo: &argoappv1.Repository{},
  1782  				Source: &argoappv1.ApplicationSource{
  1783  					Path: path,
  1784  				},
  1785  				AppName: "testapp",
  1786  			})
  1787  			require.NoError(t, err)
  1788  			assert.EqualValues(t, []string{"gcr.io/heptio-images/ks-guestbook-demo:0.3"}, details.Kustomize.Images)
  1789  		})
  1790  	})
  1791  	t.Run("App specific override", func(t *testing.T) {
  1792  		service := newService(t, ".")
  1793  		runWithTempTestdata(t, "multi", func(t *testing.T, path string) {
  1794  			details, err := service.GetAppDetails(context.Background(), &apiclient.RepoServerAppDetailsQuery{
  1795  				Repo: &argoappv1.Repository{},
  1796  				Source: &argoappv1.ApplicationSource{
  1797  					Path: path,
  1798  				},
  1799  				AppName: "testapp",
  1800  			})
  1801  			require.NoError(t, err)
  1802  			assert.EqualValues(t, []string{"gcr.io/heptio-images/ks-guestbook-demo:0.3"}, details.Kustomize.Images)
  1803  		})
  1804  	})
  1805  	t.Run("App specific overrides containing non-mergeable field", func(t *testing.T) {
  1806  		service := newService(t, ".")
  1807  		runWithTempTestdata(t, "multi", func(t *testing.T, path string) {
  1808  			details, err := service.GetAppDetails(context.Background(), &apiclient.RepoServerAppDetailsQuery{
  1809  				Repo: &argoappv1.Repository{},
  1810  				Source: &argoappv1.ApplicationSource{
  1811  					Path: path,
  1812  				},
  1813  				AppName: "unmergeable",
  1814  			})
  1815  			require.NoError(t, err)
  1816  			assert.EqualValues(t, []string{"gcr.io/heptio-images/ks-guestbook-demo:0.3"}, details.Kustomize.Images)
  1817  		})
  1818  	})
  1819  	t.Run("Broken app-specific overrides", func(t *testing.T) {
  1820  		service := newService(t, ".")
  1821  		runWithTempTestdata(t, "multi", func(t *testing.T, path string) {
  1822  			_, err := service.GetAppDetails(context.Background(), &apiclient.RepoServerAppDetailsQuery{
  1823  				Repo: &argoappv1.Repository{},
  1824  				Source: &argoappv1.ApplicationSource{
  1825  					Path: path,
  1826  				},
  1827  				AppName: "broken",
  1828  			})
  1829  			assert.Error(t, err)
  1830  		})
  1831  	})
  1832  }
  1833  
  1834  // There are unit test that will use kustomize set and by that modify the
  1835  // kustomization.yaml. For proper testing, we need to copy the testdata to a
  1836  // temporary path, run the tests, and then throw the copy away again.
  1837  func mkTempParameters(source string) string {
  1838  	tempDir, err := os.MkdirTemp("./testdata", "app-parameters")
  1839  	if err != nil {
  1840  		panic(err)
  1841  	}
  1842  	cmd := exec.Command("cp", "-R", source, tempDir)
  1843  	err = cmd.Run()
  1844  	if err != nil {
  1845  		os.RemoveAll(tempDir)
  1846  		panic(err)
  1847  	}
  1848  	return tempDir
  1849  }
  1850  
  1851  // Simple wrapper run a test with a temporary copy of the testdata, because
  1852  // the test would modify the data when run.
  1853  func runWithTempTestdata(t *testing.T, path string, runner func(t *testing.T, path string)) {
  1854  	tempDir := mkTempParameters("./testdata/app-parameters")
  1855  	runner(t, filepath.Join(tempDir, "app-parameters", path))
  1856  	os.RemoveAll(tempDir)
  1857  }
  1858  
  1859  func TestGenerateManifestsWithAppParameterFile(t *testing.T) {
  1860  	t.Run("Single global override", func(t *testing.T) {
  1861  		runWithTempTestdata(t, "single-global", func(t *testing.T, path string) {
  1862  			service := newService(t, ".")
  1863  			manifests, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
  1864  				Repo: &argoappv1.Repository{},
  1865  				ApplicationSource: &argoappv1.ApplicationSource{
  1866  					Path: path,
  1867  				},
  1868  				ProjectName:        "something",
  1869  				ProjectSourceRepos: []string{"*"},
  1870  			})
  1871  			require.NoError(t, err)
  1872  			resourceByKindName := make(map[string]*unstructured.Unstructured)
  1873  			for _, manifest := range manifests.Manifests {
  1874  				var un unstructured.Unstructured
  1875  				err := yaml.Unmarshal([]byte(manifest), &un)
  1876  				if !assert.NoError(t, err) {
  1877  					return
  1878  				}
  1879  				resourceByKindName[fmt.Sprintf("%s/%s", un.GetKind(), un.GetName())] = &un
  1880  			}
  1881  			deployment, ok := resourceByKindName["Deployment/guestbook-ui"]
  1882  			require.True(t, ok)
  1883  			containers, ok, _ := unstructured.NestedSlice(deployment.Object, "spec", "template", "spec", "containers")
  1884  			require.True(t, ok)
  1885  			image, ok, _ := unstructured.NestedString(containers[0].(map[string]interface{}), "image")
  1886  			require.True(t, ok)
  1887  			assert.Equal(t, "gcr.io/heptio-images/ks-guestbook-demo:0.2", image)
  1888  		})
  1889  	})
  1890  
  1891  	t.Run("Single global override Helm", func(t *testing.T) {
  1892  		runWithTempTestdata(t, "single-global-helm", func(t *testing.T, path string) {
  1893  			service := newService(t, ".")
  1894  			manifests, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
  1895  				Repo: &argoappv1.Repository{},
  1896  				ApplicationSource: &argoappv1.ApplicationSource{
  1897  					Path: path,
  1898  				},
  1899  				ProjectName:        "something",
  1900  				ProjectSourceRepos: []string{"*"},
  1901  			})
  1902  			require.NoError(t, err)
  1903  			resourceByKindName := make(map[string]*unstructured.Unstructured)
  1904  			for _, manifest := range manifests.Manifests {
  1905  				var un unstructured.Unstructured
  1906  				err := yaml.Unmarshal([]byte(manifest), &un)
  1907  				if !assert.NoError(t, err) {
  1908  					return
  1909  				}
  1910  				resourceByKindName[fmt.Sprintf("%s/%s", un.GetKind(), un.GetName())] = &un
  1911  			}
  1912  			deployment, ok := resourceByKindName["Deployment/guestbook-ui"]
  1913  			require.True(t, ok)
  1914  			containers, ok, _ := unstructured.NestedSlice(deployment.Object, "spec", "template", "spec", "containers")
  1915  			require.True(t, ok)
  1916  			image, ok, _ := unstructured.NestedString(containers[0].(map[string]interface{}), "image")
  1917  			require.True(t, ok)
  1918  			assert.Equal(t, "gcr.io/heptio-images/ks-guestbook-demo:0.2", image)
  1919  		})
  1920  	})
  1921  
  1922  	t.Run("Application specific override", func(t *testing.T) {
  1923  		service := newService(t, ".")
  1924  		runWithTempTestdata(t, "single-app-only", func(t *testing.T, path string) {
  1925  			manifests, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
  1926  				Repo: &argoappv1.Repository{},
  1927  				ApplicationSource: &argoappv1.ApplicationSource{
  1928  					Path: path,
  1929  				},
  1930  				AppName:            "testapp",
  1931  				ProjectName:        "something",
  1932  				ProjectSourceRepos: []string{"*"},
  1933  			})
  1934  			require.NoError(t, err)
  1935  			resourceByKindName := make(map[string]*unstructured.Unstructured)
  1936  			for _, manifest := range manifests.Manifests {
  1937  				var un unstructured.Unstructured
  1938  				err := yaml.Unmarshal([]byte(manifest), &un)
  1939  				if !assert.NoError(t, err) {
  1940  					return
  1941  				}
  1942  				resourceByKindName[fmt.Sprintf("%s/%s", un.GetKind(), un.GetName())] = &un
  1943  			}
  1944  			deployment, ok := resourceByKindName["Deployment/guestbook-ui"]
  1945  			require.True(t, ok)
  1946  			containers, ok, _ := unstructured.NestedSlice(deployment.Object, "spec", "template", "spec", "containers")
  1947  			require.True(t, ok)
  1948  			image, ok, _ := unstructured.NestedString(containers[0].(map[string]interface{}), "image")
  1949  			require.True(t, ok)
  1950  			assert.Equal(t, "gcr.io/heptio-images/ks-guestbook-demo:0.3", image)
  1951  		})
  1952  	})
  1953  
  1954  	t.Run("Multi-source with source as ref only does not generate manifests", func(t *testing.T) {
  1955  		service := newService(t, ".")
  1956  		runWithTempTestdata(t, "single-app-only", func(t *testing.T, path string) {
  1957  			manifests, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
  1958  				Repo: &argoappv1.Repository{},
  1959  				ApplicationSource: &argoappv1.ApplicationSource{
  1960  					Path:  "",
  1961  					Chart: "",
  1962  					Ref:   "test",
  1963  				},
  1964  				AppName:            "testapp-multi-ref-only",
  1965  				ProjectName:        "something",
  1966  				ProjectSourceRepos: []string{"*"},
  1967  				HasMultipleSources: true,
  1968  			})
  1969  			assert.NoError(t, err)
  1970  			assert.Empty(t, manifests.Manifests)
  1971  			assert.NotEmpty(t, manifests.Revision)
  1972  		})
  1973  	})
  1974  
  1975  	t.Run("Application specific override for other app", func(t *testing.T) {
  1976  		service := newService(t, ".")
  1977  		runWithTempTestdata(t, "single-app-only", func(t *testing.T, path string) {
  1978  			manifests, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
  1979  				Repo: &argoappv1.Repository{},
  1980  				ApplicationSource: &argoappv1.ApplicationSource{
  1981  					Path: path,
  1982  				},
  1983  				AppName:            "testapp2",
  1984  				ProjectName:        "something",
  1985  				ProjectSourceRepos: []string{"*"},
  1986  			})
  1987  			require.NoError(t, err)
  1988  			resourceByKindName := make(map[string]*unstructured.Unstructured)
  1989  			for _, manifest := range manifests.Manifests {
  1990  				var un unstructured.Unstructured
  1991  				err := yaml.Unmarshal([]byte(manifest), &un)
  1992  				if !assert.NoError(t, err) {
  1993  					return
  1994  				}
  1995  				resourceByKindName[fmt.Sprintf("%s/%s", un.GetKind(), un.GetName())] = &un
  1996  			}
  1997  			deployment, ok := resourceByKindName["Deployment/guestbook-ui"]
  1998  			require.True(t, ok)
  1999  			containers, ok, _ := unstructured.NestedSlice(deployment.Object, "spec", "template", "spec", "containers")
  2000  			require.True(t, ok)
  2001  			image, ok, _ := unstructured.NestedString(containers[0].(map[string]interface{}), "image")
  2002  			require.True(t, ok)
  2003  			assert.Equal(t, "gcr.io/heptio-images/ks-guestbook-demo:0.1", image)
  2004  		})
  2005  	})
  2006  
  2007  	t.Run("Override info does not appear in cache key", func(t *testing.T) {
  2008  		service := newService(t, ".")
  2009  		runWithTempTestdata(t, "single-global", func(t *testing.T, path string) {
  2010  			source := &argoappv1.ApplicationSource{
  2011  				Path: path,
  2012  			}
  2013  			sourceCopy := source.DeepCopy() // make a copy in case GenerateManifest mutates it.
  2014  			_, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
  2015  				Repo:               &argoappv1.Repository{},
  2016  				ApplicationSource:  sourceCopy,
  2017  				AppName:            "test",
  2018  				ProjectName:        "something",
  2019  				ProjectSourceRepos: []string{"*"},
  2020  			})
  2021  			assert.NoError(t, err)
  2022  			res := &cache.CachedManifestResponse{}
  2023  			// Try to pull from the cache with a `source` that does not include any overrides. Overrides should not be
  2024  			// part of the cache key, because you can't get the overrides without a repo operation. And avoiding repo
  2025  			// operations is the point of the cache.
  2026  			err = service.cache.GetManifests(mock.Anything, source, argoappv1.RefTargetRevisionMapping{}, &argoappv1.ClusterInfo{}, "", "", "", "test", res, nil)
  2027  			assert.NoError(t, err)
  2028  		})
  2029  	})
  2030  }
  2031  
  2032  func TestGenerateManifestWithAnnotatedAndRegularGitTagHashes(t *testing.T) {
  2033  	regularGitTagHash := "632039659e542ed7de0c170a4fcc1c571b288fc0"
  2034  	annotatedGitTaghash := "95249be61b028d566c29d47b19e65c5603388a41"
  2035  	invalidGitTaghash := "invalid-tag"
  2036  	actualCommitSHA := "632039659e542ed7de0c170a4fcc1c571b288fc0"
  2037  
  2038  	tests := []struct {
  2039  		name            string
  2040  		ctx             context.Context
  2041  		manifestRequest *apiclient.ManifestRequest
  2042  		wantError       bool
  2043  		service         *Service
  2044  	}{
  2045  		{
  2046  			name: "Case: Git tag hash matches latest commit SHA (regular tag)",
  2047  			ctx:  context.Background(),
  2048  			manifestRequest: &apiclient.ManifestRequest{
  2049  				Repo: &argoappv1.Repository{},
  2050  				ApplicationSource: &argoappv1.ApplicationSource{
  2051  					TargetRevision: regularGitTagHash,
  2052  				},
  2053  				NoCache:            true,
  2054  				ProjectName:        "something",
  2055  				ProjectSourceRepos: []string{"*"},
  2056  			},
  2057  			wantError: false,
  2058  			service:   newServiceWithCommitSHA(t, ".", regularGitTagHash),
  2059  		},
  2060  
  2061  		{
  2062  			name: "Case: Git tag hash does not match latest commit SHA (annotated tag)",
  2063  			ctx:  context.Background(),
  2064  			manifestRequest: &apiclient.ManifestRequest{
  2065  				Repo: &argoappv1.Repository{},
  2066  				ApplicationSource: &argoappv1.ApplicationSource{
  2067  					TargetRevision: annotatedGitTaghash,
  2068  				},
  2069  				NoCache:            true,
  2070  				ProjectName:        "something",
  2071  				ProjectSourceRepos: []string{"*"},
  2072  			},
  2073  			wantError: false,
  2074  			service:   newServiceWithCommitSHA(t, ".", annotatedGitTaghash),
  2075  		},
  2076  
  2077  		{
  2078  			name: "Case: Git tag hash is invalid",
  2079  			ctx:  context.Background(),
  2080  			manifestRequest: &apiclient.ManifestRequest{
  2081  				Repo: &argoappv1.Repository{},
  2082  				ApplicationSource: &argoappv1.ApplicationSource{
  2083  					TargetRevision: invalidGitTaghash,
  2084  				},
  2085  				NoCache:            true,
  2086  				ProjectName:        "something",
  2087  				ProjectSourceRepos: []string{"*"},
  2088  			},
  2089  			wantError: true,
  2090  			service:   newServiceWithCommitSHA(t, ".", invalidGitTaghash),
  2091  		},
  2092  	}
  2093  	for _, tt := range tests {
  2094  		t.Run(tt.name, func(t *testing.T) {
  2095  			manifestResponse, err := tt.service.GenerateManifest(tt.ctx, tt.manifestRequest)
  2096  			if !tt.wantError {
  2097  				if err == nil {
  2098  					assert.Equal(t, manifestResponse.Revision, actualCommitSHA)
  2099  				} else {
  2100  					t.Errorf("unexpected error")
  2101  				}
  2102  			} else {
  2103  				if err == nil {
  2104  					t.Errorf("expected an error but did not throw one")
  2105  				}
  2106  			}
  2107  
  2108  		})
  2109  	}
  2110  }
  2111  
  2112  func TestGenerateManifestWithAnnotatedTagsAndMultiSourceApp(t *testing.T) {
  2113  	annotatedGitTaghash := "95249be61b028d566c29d47b19e65c5603388a41"
  2114  
  2115  	service := newServiceWithCommitSHA(t, ".", annotatedGitTaghash)
  2116  
  2117  	refSources := map[string]*argoappv1.RefTarget{}
  2118  
  2119  	refSources["$global"] = &argoappv1.RefTarget{
  2120  		TargetRevision: annotatedGitTaghash,
  2121  	}
  2122  
  2123  	refSources["$default"] = &argoappv1.RefTarget{
  2124  		TargetRevision: annotatedGitTaghash,
  2125  	}
  2126  
  2127  	manifestRequest := &apiclient.ManifestRequest{
  2128  		Repo: &argoappv1.Repository{},
  2129  		ApplicationSource: &argoappv1.ApplicationSource{
  2130  			TargetRevision: annotatedGitTaghash,
  2131  			Helm: &argoappv1.ApplicationSourceHelm{
  2132  				ValueFiles: []string{"$global/values.yaml", "$default/secrets.yaml"},
  2133  			},
  2134  		},
  2135  		HasMultipleSources: true,
  2136  		NoCache:            true,
  2137  		RefSources:         refSources,
  2138  	}
  2139  
  2140  	response, err := service.GenerateManifest(context.Background(), manifestRequest)
  2141  	if err != nil {
  2142  		t.Errorf("unexpected %s", err)
  2143  	}
  2144  
  2145  	if response.Revision != annotatedGitTaghash {
  2146  		t.Errorf("returned SHA %s is different from expected annotated tag %s", response.Revision, annotatedGitTaghash)
  2147  	}
  2148  }
  2149  
  2150  func TestFindResources(t *testing.T) {
  2151  	testCases := []struct {
  2152  		name          string
  2153  		include       string
  2154  		exclude       string
  2155  		expectedNames []string
  2156  	}{{
  2157  		name:          "Include One Match",
  2158  		include:       "subdir/deploymentSub.yaml",
  2159  		expectedNames: []string{"nginx-deployment-sub"},
  2160  	}, {
  2161  		name:          "Include Everything",
  2162  		include:       "*.yaml",
  2163  		expectedNames: []string{"nginx-deployment", "nginx-deployment-sub"},
  2164  	}, {
  2165  		name:          "Include Subdirectory",
  2166  		include:       "**/*.yaml",
  2167  		expectedNames: []string{"nginx-deployment-sub"},
  2168  	}, {
  2169  		name:          "Include No Matches",
  2170  		include:       "nothing.yaml",
  2171  		expectedNames: []string{},
  2172  	}, {
  2173  		name:          "Exclude - One Match",
  2174  		exclude:       "subdir/deploymentSub.yaml",
  2175  		expectedNames: []string{"nginx-deployment"},
  2176  	}, {
  2177  		name:          "Exclude - Everything",
  2178  		exclude:       "*.yaml",
  2179  		expectedNames: []string{},
  2180  	}}
  2181  	for i := range testCases {
  2182  		tc := testCases[i]
  2183  		t.Run(tc.name, func(t *testing.T) {
  2184  			objs, err := findManifests(&log.Entry{}, "testdata/app-include-exclude", ".", nil, argoappv1.ApplicationSourceDirectory{
  2185  				Recurse: true,
  2186  				Include: tc.include,
  2187  				Exclude: tc.exclude,
  2188  			}, map[string]bool{}, resource.MustParse("0"))
  2189  			if !assert.NoError(t, err) {
  2190  				return
  2191  			}
  2192  			var names []string
  2193  			for i := range objs {
  2194  				names = append(names, objs[i].GetName())
  2195  			}
  2196  			assert.ElementsMatch(t, tc.expectedNames, names)
  2197  		})
  2198  	}
  2199  }
  2200  
  2201  func TestFindManifests_Exclude(t *testing.T) {
  2202  	objs, err := findManifests(&log.Entry{}, "testdata/app-include-exclude", ".", nil, argoappv1.ApplicationSourceDirectory{
  2203  		Recurse: true,
  2204  		Exclude: "subdir/deploymentSub.yaml",
  2205  	}, map[string]bool{}, resource.MustParse("0"))
  2206  
  2207  	if !assert.NoError(t, err) || !assert.Len(t, objs, 1) {
  2208  		return
  2209  	}
  2210  
  2211  	assert.Equal(t, "nginx-deployment", objs[0].GetName())
  2212  }
  2213  
  2214  func TestFindManifests_Exclude_NothingMatches(t *testing.T) {
  2215  	objs, err := findManifests(&log.Entry{}, "testdata/app-include-exclude", ".", nil, argoappv1.ApplicationSourceDirectory{
  2216  		Recurse: true,
  2217  		Exclude: "nothing.yaml",
  2218  	}, map[string]bool{}, resource.MustParse("0"))
  2219  
  2220  	if !assert.NoError(t, err) || !assert.Len(t, objs, 2) {
  2221  		return
  2222  	}
  2223  
  2224  	assert.ElementsMatch(t,
  2225  		[]string{"nginx-deployment", "nginx-deployment-sub"}, []string{objs[0].GetName(), objs[1].GetName()})
  2226  }
  2227  
  2228  func tempDir(t *testing.T) string {
  2229  	dir, err := os.MkdirTemp(".", "")
  2230  	require.NoError(t, err)
  2231  	t.Cleanup(func() {
  2232  		err = os.RemoveAll(dir)
  2233  		if err != nil {
  2234  			panic(err)
  2235  		}
  2236  	})
  2237  	absDir, err := filepath.Abs(dir)
  2238  	require.NoError(t, err)
  2239  	return absDir
  2240  }
  2241  
  2242  func walkFor(t *testing.T, root string, testPath string, run func(info fs.FileInfo)) {
  2243  	var hitExpectedPath = false
  2244  	err := filepath.Walk(root, func(path string, info fs.FileInfo, err error) error {
  2245  		if path == testPath {
  2246  			require.NoError(t, err)
  2247  			hitExpectedPath = true
  2248  			run(info)
  2249  		}
  2250  		return nil
  2251  	})
  2252  	require.NoError(t, err)
  2253  	assert.True(t, hitExpectedPath, "did not hit expected path when walking directory")
  2254  }
  2255  
  2256  func Test_getPotentiallyValidManifestFile(t *testing.T) {
  2257  	// These tests use filepath.Walk instead of os.Stat to get file info, because FileInfo from os.Stat does not return
  2258  	// true for IsSymlink like os.Walk does.
  2259  
  2260  	// These tests do not use t.TempDir() because those directories can contain symlinks which cause test to fail
  2261  	// InBound checks.
  2262  
  2263  	t.Run("non-JSON/YAML is skipped with an empty ignore message", func(t *testing.T) {
  2264  		appDir := tempDir(t)
  2265  		filePath := filepath.Join(appDir, "not-json-or-yaml")
  2266  		file, err := os.OpenFile(filePath, os.O_RDONLY|os.O_CREATE, 0644)
  2267  		require.NoError(t, err)
  2268  		err = file.Close()
  2269  		require.NoError(t, err)
  2270  
  2271  		walkFor(t, appDir, filePath, func(info fs.FileInfo) {
  2272  			realFileInfo, ignoreMessage, err := getPotentiallyValidManifestFile(filePath, info, appDir, appDir, "", "")
  2273  			assert.Nil(t, realFileInfo)
  2274  			assert.Empty(t, ignoreMessage)
  2275  			assert.NoError(t, err)
  2276  		})
  2277  	})
  2278  
  2279  	t.Run("circular link should throw an error", func(t *testing.T) {
  2280  		appDir := tempDir(t)
  2281  
  2282  		aPath := filepath.Join(appDir, "a.json")
  2283  		bPath := filepath.Join(appDir, "b.json")
  2284  		err := os.Symlink(bPath, aPath)
  2285  		require.NoError(t, err)
  2286  		err = os.Symlink(aPath, bPath)
  2287  		require.NoError(t, err)
  2288  
  2289  		walkFor(t, appDir, aPath, func(info fs.FileInfo) {
  2290  			realFileInfo, ignoreMessage, err := getPotentiallyValidManifestFile(aPath, info, appDir, appDir, "", "")
  2291  			assert.Nil(t, realFileInfo)
  2292  			assert.Empty(t, ignoreMessage)
  2293  			assert.ErrorContains(t, err, "too many links")
  2294  		})
  2295  	})
  2296  
  2297  	t.Run("symlink with missing destination should throw an error", func(t *testing.T) {
  2298  		appDir := tempDir(t)
  2299  
  2300  		aPath := filepath.Join(appDir, "a.json")
  2301  		bPath := filepath.Join(appDir, "b.json")
  2302  		err := os.Symlink(bPath, aPath)
  2303  		require.NoError(t, err)
  2304  
  2305  		walkFor(t, appDir, aPath, func(info fs.FileInfo) {
  2306  			realFileInfo, ignoreMessage, err := getPotentiallyValidManifestFile(aPath, info, appDir, appDir, "", "")
  2307  			assert.Nil(t, realFileInfo)
  2308  			assert.NotEmpty(t, ignoreMessage)
  2309  			assert.NoError(t, err)
  2310  		})
  2311  	})
  2312  
  2313  	t.Run("out-of-bounds symlink should throw an error", func(t *testing.T) {
  2314  		appDir := tempDir(t)
  2315  
  2316  		linkPath := filepath.Join(appDir, "a.json")
  2317  		err := os.Symlink("..", linkPath)
  2318  		require.NoError(t, err)
  2319  
  2320  		walkFor(t, appDir, linkPath, func(info fs.FileInfo) {
  2321  			realFileInfo, ignoreMessage, err := getPotentiallyValidManifestFile(linkPath, info, appDir, appDir, "", "")
  2322  			assert.Nil(t, realFileInfo)
  2323  			assert.Empty(t, ignoreMessage)
  2324  			assert.ErrorContains(t, err, "illegal filepath in symlink")
  2325  		})
  2326  	})
  2327  
  2328  	t.Run("symlink to a non-regular file should be skipped with warning", func(t *testing.T) {
  2329  		appDir := tempDir(t)
  2330  
  2331  		dirPath := filepath.Join(appDir, "test.dir")
  2332  		err := os.MkdirAll(dirPath, 0644)
  2333  		require.NoError(t, err)
  2334  		linkPath := filepath.Join(appDir, "test.json")
  2335  		err = os.Symlink(dirPath, linkPath)
  2336  		require.NoError(t, err)
  2337  
  2338  		walkFor(t, appDir, linkPath, func(info fs.FileInfo) {
  2339  			realFileInfo, ignoreMessage, err := getPotentiallyValidManifestFile(linkPath, info, appDir, appDir, "", "")
  2340  			assert.Nil(t, realFileInfo)
  2341  			assert.Contains(t, ignoreMessage, "non-regular file")
  2342  			assert.NoError(t, err)
  2343  		})
  2344  	})
  2345  
  2346  	t.Run("non-included file should be skipped with no message", func(t *testing.T) {
  2347  		appDir := tempDir(t)
  2348  
  2349  		filePath := filepath.Join(appDir, "not-included.yaml")
  2350  		file, err := os.OpenFile(filePath, os.O_RDONLY|os.O_CREATE, 0644)
  2351  		require.NoError(t, err)
  2352  		err = file.Close()
  2353  		require.NoError(t, err)
  2354  
  2355  		walkFor(t, appDir, filePath, func(info fs.FileInfo) {
  2356  			realFileInfo, ignoreMessage, err := getPotentiallyValidManifestFile(filePath, info, appDir, appDir, "*.json", "")
  2357  			assert.Nil(t, realFileInfo)
  2358  			assert.Empty(t, ignoreMessage)
  2359  			assert.NoError(t, err)
  2360  		})
  2361  	})
  2362  
  2363  	t.Run("excluded file should be skipped with no message", func(t *testing.T) {
  2364  		appDir := tempDir(t)
  2365  
  2366  		filePath := filepath.Join(appDir, "excluded.json")
  2367  		file, err := os.OpenFile(filePath, os.O_RDONLY|os.O_CREATE, 0644)
  2368  		require.NoError(t, err)
  2369  		err = file.Close()
  2370  		require.NoError(t, err)
  2371  
  2372  		walkFor(t, appDir, filePath, func(info fs.FileInfo) {
  2373  			realFileInfo, ignoreMessage, err := getPotentiallyValidManifestFile(filePath, info, appDir, appDir, "", "excluded.*")
  2374  			assert.Nil(t, realFileInfo)
  2375  			assert.Empty(t, ignoreMessage)
  2376  			assert.NoError(t, err)
  2377  		})
  2378  	})
  2379  
  2380  	t.Run("symlink to a regular file is potentially valid", func(t *testing.T) {
  2381  		appDir := tempDir(t)
  2382  
  2383  		filePath := filepath.Join(appDir, "regular-file")
  2384  		file, err := os.OpenFile(filePath, os.O_RDONLY|os.O_CREATE, 0644)
  2385  		require.NoError(t, err)
  2386  		err = file.Close()
  2387  		require.NoError(t, err)
  2388  
  2389  		linkPath := filepath.Join(appDir, "link.json")
  2390  		err = os.Symlink(filePath, linkPath)
  2391  		require.NoError(t, err)
  2392  
  2393  		walkFor(t, appDir, linkPath, func(info fs.FileInfo) {
  2394  			realFileInfo, ignoreMessage, err := getPotentiallyValidManifestFile(linkPath, info, appDir, appDir, "", "")
  2395  			assert.NotNil(t, realFileInfo)
  2396  			assert.Empty(t, ignoreMessage)
  2397  			assert.NoError(t, err)
  2398  		})
  2399  	})
  2400  
  2401  	t.Run("a regular file is potentially valid", func(t *testing.T) {
  2402  		appDir := tempDir(t)
  2403  
  2404  		filePath := filepath.Join(appDir, "regular-file.json")
  2405  		file, err := os.OpenFile(filePath, os.O_RDONLY|os.O_CREATE, 0644)
  2406  		require.NoError(t, err)
  2407  		err = file.Close()
  2408  		require.NoError(t, err)
  2409  
  2410  		walkFor(t, appDir, filePath, func(info fs.FileInfo) {
  2411  			realFileInfo, ignoreMessage, err := getPotentiallyValidManifestFile(filePath, info, appDir, appDir, "", "")
  2412  			assert.NotNil(t, realFileInfo)
  2413  			assert.Empty(t, ignoreMessage)
  2414  			assert.NoError(t, err)
  2415  		})
  2416  	})
  2417  
  2418  	t.Run("realFileInfo is for the destination rather than the symlink", func(t *testing.T) {
  2419  		appDir := tempDir(t)
  2420  
  2421  		filePath := filepath.Join(appDir, "regular-file")
  2422  		file, err := os.OpenFile(filePath, os.O_RDONLY|os.O_CREATE, 0644)
  2423  		require.NoError(t, err)
  2424  		err = file.Close()
  2425  		require.NoError(t, err)
  2426  
  2427  		linkPath := filepath.Join(appDir, "link.json")
  2428  		err = os.Symlink(filePath, linkPath)
  2429  		require.NoError(t, err)
  2430  
  2431  		walkFor(t, appDir, linkPath, func(info fs.FileInfo) {
  2432  			realFileInfo, ignoreMessage, err := getPotentiallyValidManifestFile(linkPath, info, appDir, appDir, "", "")
  2433  			assert.NotNil(t, realFileInfo)
  2434  			assert.Equal(t, filepath.Base(filePath), realFileInfo.Name())
  2435  			assert.Empty(t, ignoreMessage)
  2436  			assert.NoError(t, err)
  2437  		})
  2438  	})
  2439  }
  2440  
  2441  func Test_getPotentiallyValidManifests(t *testing.T) {
  2442  	// Tests which return no manifests and an error check to make sure the directory exists before running. A missing
  2443  	// directory would produce those same results.
  2444  
  2445  	logCtx := log.WithField("test", "test")
  2446  
  2447  	t.Run("unreadable file throws error", func(t *testing.T) {
  2448  		appDir := t.TempDir()
  2449  		unreadablePath := filepath.Join(appDir, "unreadable.json")
  2450  		err := os.WriteFile(unreadablePath, []byte{}, 0666)
  2451  		require.NoError(t, err)
  2452  		err = os.Chmod(appDir, 0000)
  2453  		require.NoError(t, err)
  2454  
  2455  		manifests, err := getPotentiallyValidManifests(logCtx, appDir, appDir, false, "", "", resource.MustParse("0"))
  2456  		assert.Empty(t, manifests)
  2457  		assert.Error(t, err)
  2458  
  2459  		// allow cleanup
  2460  		err = os.Chmod(appDir, 0777)
  2461  		if err != nil {
  2462  			panic(err)
  2463  		}
  2464  	})
  2465  
  2466  	t.Run("no recursion when recursion is disabled", func(t *testing.T) {
  2467  		manifests, err := getPotentiallyValidManifests(logCtx, "./testdata/recurse", "./testdata/recurse", false, "", "", resource.MustParse("0"))
  2468  		assert.Len(t, manifests, 1)
  2469  		assert.NoError(t, err)
  2470  	})
  2471  
  2472  	t.Run("recursion when recursion is enabled", func(t *testing.T) {
  2473  		manifests, err := getPotentiallyValidManifests(logCtx, "./testdata/recurse", "./testdata/recurse", true, "", "", resource.MustParse("0"))
  2474  		assert.Len(t, manifests, 2)
  2475  		assert.NoError(t, err)
  2476  	})
  2477  
  2478  	t.Run("non-JSON/YAML is skipped", func(t *testing.T) {
  2479  		manifests, err := getPotentiallyValidManifests(logCtx, "./testdata/non-manifest-file", "./testdata/non-manifest-file", false, "", "", resource.MustParse("0"))
  2480  		assert.Empty(t, manifests)
  2481  		assert.NoError(t, err)
  2482  	})
  2483  
  2484  	t.Run("circular link should throw an error", func(t *testing.T) {
  2485  		const testDir = "./testdata/circular-link"
  2486  		require.DirExists(t, testDir)
  2487  		require.NoError(t, fileutil.CreateSymlink(t, testDir, "a.json", "b.json"))
  2488  		defer os.Remove(path.Join(testDir, "a.json"))
  2489  		require.NoError(t, fileutil.CreateSymlink(t, testDir, "b.json", "a.json"))
  2490  		defer os.Remove(path.Join(testDir, "b.json"))
  2491  		manifests, err := getPotentiallyValidManifests(logCtx, "./testdata/circular-link", "./testdata/circular-link", false, "", "", resource.MustParse("0"))
  2492  		assert.Empty(t, manifests)
  2493  		assert.Error(t, err)
  2494  	})
  2495  
  2496  	t.Run("out-of-bounds symlink should throw an error", func(t *testing.T) {
  2497  		require.DirExists(t, "./testdata/out-of-bounds-link")
  2498  		manifests, err := getPotentiallyValidManifests(logCtx, "./testdata/out-of-bounds-link", "./testdata/out-of-bounds-link", false, "", "", resource.MustParse("0"))
  2499  		assert.Empty(t, manifests)
  2500  		assert.Error(t, err)
  2501  	})
  2502  
  2503  	t.Run("symlink to a regular file works", func(t *testing.T) {
  2504  		repoRoot, err := filepath.Abs("./testdata/in-bounds-link")
  2505  		require.NoError(t, err)
  2506  		appPath, err := filepath.Abs("./testdata/in-bounds-link/app")
  2507  		require.NoError(t, err)
  2508  		manifests, err := getPotentiallyValidManifests(logCtx, appPath, repoRoot, false, "", "", resource.MustParse("0"))
  2509  		assert.Len(t, manifests, 1)
  2510  		assert.NoError(t, err)
  2511  	})
  2512  
  2513  	t.Run("symlink to nowhere should be ignored", func(t *testing.T) {
  2514  		manifests, err := getPotentiallyValidManifests(logCtx, "./testdata/link-to-nowhere", "./testdata/link-to-nowhere", false, "", "", resource.MustParse("0"))
  2515  		assert.Empty(t, manifests)
  2516  		assert.NoError(t, err)
  2517  	})
  2518  
  2519  	t.Run("link to over-sized manifest fails", func(t *testing.T) {
  2520  		repoRoot, err := filepath.Abs("./testdata/in-bounds-link")
  2521  		require.NoError(t, err)
  2522  		appPath, err := filepath.Abs("./testdata/in-bounds-link/app")
  2523  		require.NoError(t, err)
  2524  		// The file is 35 bytes.
  2525  		manifests, err := getPotentiallyValidManifests(logCtx, appPath, repoRoot, false, "", "", resource.MustParse("34"))
  2526  		assert.Empty(t, manifests)
  2527  		assert.ErrorIs(t, err, ErrExceededMaxCombinedManifestFileSize)
  2528  	})
  2529  
  2530  	t.Run("group of files should be limited at precisely the sum of their size", func(t *testing.T) {
  2531  		// There is a total of 10 files, ech file being 10 bytes.
  2532  		manifests, err := getPotentiallyValidManifests(logCtx, "./testdata/several-files", "./testdata/several-files", false, "", "", resource.MustParse("365"))
  2533  		assert.Len(t, manifests, 10)
  2534  		assert.NoError(t, err)
  2535  
  2536  		manifests, err = getPotentiallyValidManifests(logCtx, "./testdata/several-files", "./testdata/several-files", false, "", "", resource.MustParse("100"))
  2537  		assert.Empty(t, manifests)
  2538  		assert.ErrorIs(t, err, ErrExceededMaxCombinedManifestFileSize)
  2539  	})
  2540  }
  2541  
  2542  func Test_findManifests(t *testing.T) {
  2543  	logCtx := log.WithField("test", "test")
  2544  	noRecurse := argoappv1.ApplicationSourceDirectory{Recurse: false}
  2545  
  2546  	t.Run("unreadable file throws error", func(t *testing.T) {
  2547  		appDir := t.TempDir()
  2548  		unreadablePath := filepath.Join(appDir, "unreadable.json")
  2549  		err := os.WriteFile(unreadablePath, []byte{}, 0666)
  2550  		require.NoError(t, err)
  2551  		err = os.Chmod(appDir, 0000)
  2552  		require.NoError(t, err)
  2553  
  2554  		manifests, err := findManifests(logCtx, appDir, appDir, nil, noRecurse, nil, resource.MustParse("0"))
  2555  		assert.Empty(t, manifests)
  2556  		assert.Error(t, err)
  2557  
  2558  		// allow cleanup
  2559  		err = os.Chmod(appDir, 0777)
  2560  		if err != nil {
  2561  			panic(err)
  2562  		}
  2563  	})
  2564  
  2565  	t.Run("no recursion when recursion is disabled", func(t *testing.T) {
  2566  		manifests, err := findManifests(logCtx, "./testdata/recurse", "./testdata/recurse", nil, noRecurse, nil, resource.MustParse("0"))
  2567  		assert.Len(t, manifests, 2)
  2568  		assert.NoError(t, err)
  2569  	})
  2570  
  2571  	t.Run("recursion when recursion is enabled", func(t *testing.T) {
  2572  		recurse := argoappv1.ApplicationSourceDirectory{Recurse: true}
  2573  		manifests, err := findManifests(logCtx, "./testdata/recurse", "./testdata/recurse", nil, recurse, nil, resource.MustParse("0"))
  2574  		assert.Len(t, manifests, 4)
  2575  		assert.NoError(t, err)
  2576  	})
  2577  
  2578  	t.Run("non-JSON/YAML is skipped", func(t *testing.T) {
  2579  		manifests, err := findManifests(logCtx, "./testdata/non-manifest-file", "./testdata/non-manifest-file", nil, noRecurse, nil, resource.MustParse("0"))
  2580  		assert.Empty(t, manifests)
  2581  		assert.NoError(t, err)
  2582  	})
  2583  
  2584  	t.Run("circular link should throw an error", func(t *testing.T) {
  2585  		const testDir = "./testdata/circular-link"
  2586  		require.DirExists(t, testDir)
  2587  		require.NoError(t, fileutil.CreateSymlink(t, testDir, "a.json", "b.json"))
  2588  		defer os.Remove(path.Join(testDir, "a.json"))
  2589  		require.NoError(t, fileutil.CreateSymlink(t, testDir, "b.json", "a.json"))
  2590  		defer os.Remove(path.Join(testDir, "b.json"))
  2591  		manifests, err := findManifests(logCtx, "./testdata/circular-link", "./testdata/circular-link", nil, noRecurse, nil, resource.MustParse("0"))
  2592  		assert.Empty(t, manifests)
  2593  		assert.Error(t, err)
  2594  	})
  2595  
  2596  	t.Run("out-of-bounds symlink should throw an error", func(t *testing.T) {
  2597  		require.DirExists(t, "./testdata/out-of-bounds-link")
  2598  		manifests, err := findManifests(logCtx, "./testdata/out-of-bounds-link", "./testdata/out-of-bounds-link", nil, noRecurse, nil, resource.MustParse("0"))
  2599  		assert.Empty(t, manifests)
  2600  		assert.Error(t, err)
  2601  	})
  2602  
  2603  	t.Run("symlink to a regular file works", func(t *testing.T) {
  2604  		repoRoot, err := filepath.Abs("./testdata/in-bounds-link")
  2605  		require.NoError(t, err)
  2606  		appPath, err := filepath.Abs("./testdata/in-bounds-link/app")
  2607  		require.NoError(t, err)
  2608  		manifests, err := findManifests(logCtx, appPath, repoRoot, nil, noRecurse, nil, resource.MustParse("0"))
  2609  		assert.Len(t, manifests, 1)
  2610  		assert.NoError(t, err)
  2611  	})
  2612  
  2613  	t.Run("symlink to nowhere should be ignored", func(t *testing.T) {
  2614  		manifests, err := findManifests(logCtx, "./testdata/link-to-nowhere", "./testdata/link-to-nowhere", nil, noRecurse, nil, resource.MustParse("0"))
  2615  		assert.Empty(t, manifests)
  2616  		assert.NoError(t, err)
  2617  	})
  2618  
  2619  	t.Run("link to over-sized manifest fails", func(t *testing.T) {
  2620  		repoRoot, err := filepath.Abs("./testdata/in-bounds-link")
  2621  		require.NoError(t, err)
  2622  		appPath, err := filepath.Abs("./testdata/in-bounds-link/app")
  2623  		require.NoError(t, err)
  2624  		// The file is 35 bytes.
  2625  		manifests, err := findManifests(logCtx, appPath, repoRoot, nil, noRecurse, nil, resource.MustParse("34"))
  2626  		assert.Empty(t, manifests)
  2627  		assert.ErrorIs(t, err, ErrExceededMaxCombinedManifestFileSize)
  2628  	})
  2629  
  2630  	t.Run("group of files should be limited at precisely the sum of their size", func(t *testing.T) {
  2631  		// There is a total of 10 files, each file being 10 bytes.
  2632  		manifests, err := findManifests(logCtx, "./testdata/several-files", "./testdata/several-files", nil, noRecurse, nil, resource.MustParse("365"))
  2633  		assert.Len(t, manifests, 10)
  2634  		assert.NoError(t, err)
  2635  
  2636  		manifests, err = findManifests(logCtx, "./testdata/several-files", "./testdata/several-files", nil, noRecurse, nil, resource.MustParse("364"))
  2637  		assert.Empty(t, manifests)
  2638  		assert.ErrorIs(t, err, ErrExceededMaxCombinedManifestFileSize)
  2639  	})
  2640  
  2641  	t.Run("jsonnet isn't counted against size limit", func(t *testing.T) {
  2642  		// Each file is 36 bytes. Only the 36-byte json file should be counted against the limit.
  2643  		manifests, err := findManifests(logCtx, "./testdata/jsonnet-and-json", "./testdata/jsonnet-and-json", nil, noRecurse, nil, resource.MustParse("36"))
  2644  		assert.Len(t, manifests, 2)
  2645  		assert.NoError(t, err)
  2646  
  2647  		manifests, err = findManifests(logCtx, "./testdata/jsonnet-and-json", "./testdata/jsonnet-and-json", nil, noRecurse, nil, resource.MustParse("35"))
  2648  		assert.Empty(t, manifests)
  2649  		assert.ErrorIs(t, err, ErrExceededMaxCombinedManifestFileSize)
  2650  	})
  2651  
  2652  	t.Run("partially valid YAML file throws an error", func(t *testing.T) {
  2653  		require.DirExists(t, "./testdata/partially-valid-yaml")
  2654  		manifests, err := findManifests(logCtx, "./testdata/partially-valid-yaml", "./testdata/partially-valid-yaml", nil, noRecurse, nil, resource.MustParse("0"))
  2655  		assert.Empty(t, manifests)
  2656  		assert.Error(t, err)
  2657  	})
  2658  
  2659  	t.Run("invalid manifest throws an error", func(t *testing.T) {
  2660  		require.DirExists(t, "./testdata/invalid-manifests")
  2661  		manifests, err := findManifests(logCtx, "./testdata/invalid-manifests", "./testdata/invalid-manifests", nil, noRecurse, nil, resource.MustParse("0"))
  2662  		assert.Empty(t, manifests)
  2663  		assert.Error(t, err)
  2664  	})
  2665  
  2666  	t.Run("irrelevant YAML gets skipped, relevant YAML gets parsed", func(t *testing.T) {
  2667  		manifests, err := findManifests(logCtx, "./testdata/irrelevant-yaml", "./testdata/irrelevant-yaml", nil, noRecurse, nil, resource.MustParse("0"))
  2668  		assert.Len(t, manifests, 1)
  2669  		assert.NoError(t, err)
  2670  	})
  2671  
  2672  	t.Run("multiple JSON objects in one file throws an error", func(t *testing.T) {
  2673  		require.DirExists(t, "./testdata/json-list")
  2674  		manifests, err := findManifests(logCtx, "./testdata/json-list", "./testdata/json-list", nil, noRecurse, nil, resource.MustParse("0"))
  2675  		assert.Empty(t, manifests)
  2676  		assert.Error(t, err)
  2677  	})
  2678  
  2679  	t.Run("invalid JSON throws an error", func(t *testing.T) {
  2680  		require.DirExists(t, "./testdata/invalid-json")
  2681  		manifests, err := findManifests(logCtx, "./testdata/invalid-json", "./testdata/invalid-json", nil, noRecurse, nil, resource.MustParse("0"))
  2682  		assert.Empty(t, manifests)
  2683  		assert.Error(t, err)
  2684  	})
  2685  
  2686  	t.Run("valid JSON returns manifest and no error", func(t *testing.T) {
  2687  		manifests, err := findManifests(logCtx, "./testdata/valid-json", "./testdata/valid-json", nil, noRecurse, nil, resource.MustParse("0"))
  2688  		assert.Len(t, manifests, 1)
  2689  		assert.NoError(t, err)
  2690  	})
  2691  
  2692  	t.Run("YAML with an empty document doesn't throw an error", func(t *testing.T) {
  2693  		manifests, err := findManifests(logCtx, "./testdata/yaml-with-empty-document", "./testdata/yaml-with-empty-document", nil, noRecurse, nil, resource.MustParse("0"))
  2694  		assert.Len(t, manifests, 1)
  2695  		assert.NoError(t, err)
  2696  	})
  2697  }
  2698  
  2699  func TestTestRepoOCI(t *testing.T) {
  2700  	service := newService(t, ".")
  2701  	_, err := service.TestRepository(context.Background(), &apiclient.TestRepositoryRequest{
  2702  		Repo: &argoappv1.Repository{
  2703  			Repo:      "https://demo.goharbor.io",
  2704  			Type:      "helm",
  2705  			EnableOCI: true,
  2706  		},
  2707  	})
  2708  	require.Error(t, err)
  2709  	assert.Contains(t, err.Error(), "OCI Helm repository URL should include hostname and port only")
  2710  }
  2711  
  2712  func Test_getHelmDependencyRepos(t *testing.T) {
  2713  	repo1 := "https://charts.bitnami.com/bitnami"
  2714  	repo2 := "https://eventstore.github.io/EventStore.Charts"
  2715  
  2716  	repos, err := getHelmDependencyRepos("../../util/helm/testdata/dependency")
  2717  	assert.NoError(t, err)
  2718  	assert.Equal(t, len(repos), 2)
  2719  	assert.Equal(t, repos[0].Repo, repo1)
  2720  	assert.Equal(t, repos[1].Repo, repo2)
  2721  }
  2722  
  2723  func TestResolveRevision(t *testing.T) {
  2724  
  2725  	service := newService(t, ".")
  2726  	repo := &argoappv1.Repository{Repo: "https://github.com/argoproj/argo-cd"}
  2727  	app := &argoappv1.Application{Spec: argoappv1.ApplicationSpec{Source: &argoappv1.ApplicationSource{}}}
  2728  	resolveRevisionResponse, err := service.ResolveRevision(context.Background(), &apiclient.ResolveRevisionRequest{
  2729  		Repo:              repo,
  2730  		App:               app,
  2731  		AmbiguousRevision: "v2.2.2",
  2732  	})
  2733  
  2734  	expectedResolveRevisionResponse := &apiclient.ResolveRevisionResponse{
  2735  		Revision:          "03b17e0233e64787ffb5fcf65c740cc2a20822ba",
  2736  		AmbiguousRevision: "v2.2.2 (03b17e0233e64787ffb5fcf65c740cc2a20822ba)",
  2737  	}
  2738  
  2739  	assert.NotNil(t, resolveRevisionResponse.Revision)
  2740  	assert.Nil(t, err)
  2741  	assert.Equal(t, expectedResolveRevisionResponse, resolveRevisionResponse)
  2742  
  2743  }
  2744  
  2745  func TestResolveRevisionNegativeScenarios(t *testing.T) {
  2746  
  2747  	service := newService(t, ".")
  2748  	repo := &argoappv1.Repository{Repo: "https://github.com/argoproj/argo-cd"}
  2749  	app := &argoappv1.Application{Spec: argoappv1.ApplicationSpec{Source: &argoappv1.ApplicationSource{}}}
  2750  	resolveRevisionResponse, err := service.ResolveRevision(context.Background(), &apiclient.ResolveRevisionRequest{
  2751  		Repo:              repo,
  2752  		App:               app,
  2753  		AmbiguousRevision: "v2.a.2",
  2754  	})
  2755  
  2756  	expectedResolveRevisionResponse := &apiclient.ResolveRevisionResponse{
  2757  		Revision:          "",
  2758  		AmbiguousRevision: "",
  2759  	}
  2760  
  2761  	assert.NotNil(t, resolveRevisionResponse.Revision)
  2762  	assert.NotNil(t, err)
  2763  	assert.Equal(t, expectedResolveRevisionResponse, resolveRevisionResponse)
  2764  
  2765  }
  2766  
  2767  func TestDirectoryPermissionInitializer(t *testing.T) {
  2768  	dir := t.TempDir()
  2769  
  2770  	file, err := os.CreateTemp(dir, "")
  2771  	require.NoError(t, err)
  2772  	io.Close(file)
  2773  
  2774  	// remove read permissions
  2775  	assert.NoError(t, os.Chmod(dir, 0000))
  2776  
  2777  	// Remember to restore permissions when the test finishes so dir can
  2778  	// be removed properly.
  2779  	t.Cleanup(func() {
  2780  		require.NoError(t, os.Chmod(dir, 0777))
  2781  	})
  2782  
  2783  	// make sure permission are restored
  2784  	closer := directoryPermissionInitializer(dir)
  2785  	_, err = os.ReadFile(file.Name())
  2786  	require.NoError(t, err)
  2787  
  2788  	// make sure permission are removed by closer
  2789  	io.Close(closer)
  2790  	_, err = os.ReadFile(file.Name())
  2791  	require.Error(t, err)
  2792  }
  2793  
  2794  func addHelmToGitRepo(t *testing.T, options newGitRepoOptions) {
  2795  	err := os.WriteFile(filepath.Join(options.path, "Chart.yaml"), []byte("name: test\nversion: v1.0.0"), 0777)
  2796  	assert.NoError(t, err)
  2797  	for valuesFileName, values := range options.helmChartOptions.valuesFiles {
  2798  		valuesFileContents, err := yaml.Marshal(values)
  2799  		assert.NoError(t, err)
  2800  		err = os.WriteFile(filepath.Join(options.path, valuesFileName), valuesFileContents, 0777)
  2801  		assert.NoError(t, err)
  2802  	}
  2803  	assert.NoError(t, err)
  2804  	cmd := exec.Command("git", "add", "-A")
  2805  	cmd.Dir = options.path
  2806  	assert.NoError(t, cmd.Run())
  2807  	cmd = exec.Command("git", "commit", "-m", "Initial commit")
  2808  	cmd.Dir = options.path
  2809  	assert.NoError(t, cmd.Run())
  2810  }
  2811  
  2812  func initGitRepo(t *testing.T, options newGitRepoOptions) (revision string) {
  2813  	if options.createPath {
  2814  		assert.NoError(t, os.Mkdir(options.path, 0755))
  2815  	}
  2816  
  2817  	cmd := exec.Command("git", "init", "-b", "main", options.path)
  2818  	cmd.Dir = options.path
  2819  	assert.NoError(t, cmd.Run())
  2820  
  2821  	if options.remote != "" {
  2822  		cmd = exec.Command("git", "remote", "add", "origin", options.path)
  2823  		cmd.Dir = options.path
  2824  		assert.NoError(t, cmd.Run())
  2825  	}
  2826  
  2827  	commitAdded := options.addEmptyCommit || options.helmChartOptions.chartName != ""
  2828  	if options.addEmptyCommit {
  2829  		cmd = exec.Command("git", "commit", "-m", "Initial commit", "--allow-empty")
  2830  		cmd.Dir = options.path
  2831  		assert.NoError(t, cmd.Run())
  2832  	} else if options.helmChartOptions.chartName != "" {
  2833  		addHelmToGitRepo(t, options)
  2834  	}
  2835  
  2836  	if commitAdded {
  2837  		var revB bytes.Buffer
  2838  		cmd = exec.Command("git", "rev-parse", "HEAD", options.path)
  2839  		cmd.Dir = options.path
  2840  		cmd.Stdout = &revB
  2841  		assert.NoError(t, cmd.Run())
  2842  		revision = strings.Split(revB.String(), "\n")[0]
  2843  	}
  2844  	return revision
  2845  }
  2846  
  2847  func TestInit(t *testing.T) {
  2848  	dir := t.TempDir()
  2849  
  2850  	// service.Init sets permission to 0300. Restore permissions when the test
  2851  	// finishes so dir can be removed properly.
  2852  	t.Cleanup(func() {
  2853  		require.NoError(t, os.Chmod(dir, 0777))
  2854  	})
  2855  
  2856  	repoPath := path.Join(dir, "repo1")
  2857  	initGitRepo(t, newGitRepoOptions{path: repoPath, remote: "https://github.com/argo-cd/test-repo1", createPath: true, addEmptyCommit: false})
  2858  
  2859  	service := newService(t, ".")
  2860  	service.rootDir = dir
  2861  
  2862  	require.NoError(t, service.Init())
  2863  
  2864  	_, err := os.ReadDir(dir)
  2865  	require.Error(t, err)
  2866  	initGitRepo(t, newGitRepoOptions{path: path.Join(dir, "repo2"), remote: "https://github.com/argo-cd/test-repo2", createPath: true, addEmptyCommit: false})
  2867  }
  2868  
  2869  // TestCheckoutRevisionCanGetNonstandardRefs shows that we can fetch a revision that points to a non-standard ref. In
  2870  // other words, we haven't regressed and caused this issue again: https://github.com/argoproj/argo-cd/issues/4935
  2871  func TestCheckoutRevisionCanGetNonstandardRefs(t *testing.T) {
  2872  	rootPath := t.TempDir()
  2873  
  2874  	sourceRepoPath, err := os.MkdirTemp(rootPath, "")
  2875  	require.NoError(t, err)
  2876  
  2877  	// Create a repo such that one commit is on a non-standard ref _and nowhere else_. This is meant to simulate, for
  2878  	// example, a GitHub ref for a pull into one repo from a fork of that repo.
  2879  	runGit(t, sourceRepoPath, "init")
  2880  	runGit(t, sourceRepoPath, "checkout", "-b", "main") // make sure there's a main branch to switch back to
  2881  	runGit(t, sourceRepoPath, "commit", "-m", "empty", "--allow-empty")
  2882  	runGit(t, sourceRepoPath, "checkout", "-b", "branch")
  2883  	runGit(t, sourceRepoPath, "commit", "-m", "empty", "--allow-empty")
  2884  	sha := runGit(t, sourceRepoPath, "rev-parse", "HEAD")
  2885  	runGit(t, sourceRepoPath, "update-ref", "refs/pull/123/head", strings.TrimSuffix(sha, "\n"))
  2886  	runGit(t, sourceRepoPath, "checkout", "main")
  2887  	runGit(t, sourceRepoPath, "branch", "-D", "branch")
  2888  
  2889  	destRepoPath, err := os.MkdirTemp(rootPath, "")
  2890  	require.NoError(t, err)
  2891  
  2892  	gitClient, err := git.NewClientExt("file://"+sourceRepoPath, destRepoPath, &git.NopCreds{}, true, false, "")
  2893  	require.NoError(t, err)
  2894  
  2895  	pullSha, err := gitClient.LsRemote("refs/pull/123/head")
  2896  	require.NoError(t, err)
  2897  
  2898  	err = checkoutRevision(gitClient, "does-not-exist", false)
  2899  	assert.Error(t, err)
  2900  
  2901  	err = checkoutRevision(gitClient, pullSha, false)
  2902  	assert.NoError(t, err)
  2903  }
  2904  
  2905  // runGit runs a git command in the given working directory. If the command succeeds, it returns the combined standard
  2906  // and error output. If it fails, it stops the test with a failure message.
  2907  func runGit(t *testing.T, workDir string, args ...string) string {
  2908  	cmd := exec.Command("git", args...)
  2909  	cmd.Dir = workDir
  2910  	out, err := cmd.CombinedOutput()
  2911  	stringOut := string(out)
  2912  	require.NoError(t, err, stringOut)
  2913  	return stringOut
  2914  }
  2915  
  2916  func Test_walkHelmValueFilesInPath(t *testing.T) {
  2917  	t.Run("does not exist", func(t *testing.T) {
  2918  		var files []string
  2919  		root := "/obviously/does/not/exist"
  2920  		err := filepath.Walk(root, walkHelmValueFilesInPath(root, &files))
  2921  		assert.Error(t, err)
  2922  		assert.Empty(t, files)
  2923  	})
  2924  	t.Run("values files", func(t *testing.T) {
  2925  		var files []string
  2926  		root := "./testdata/values-files"
  2927  		err := filepath.Walk(root, walkHelmValueFilesInPath(root, &files))
  2928  		assert.NoError(t, err)
  2929  		assert.Len(t, files, 5)
  2930  	})
  2931  	t.Run("unrelated root", func(t *testing.T) {
  2932  		var files []string
  2933  		root := "./testdata/values-files"
  2934  		unrelated_root := "/different/root/path"
  2935  		err := filepath.Walk(root, walkHelmValueFilesInPath(unrelated_root, &files))
  2936  		assert.Error(t, err)
  2937  	})
  2938  }
  2939  
  2940  func Test_populateHelmAppDetails(t *testing.T) {
  2941  	var emptyTempPaths = io.NewRandomizedTempPaths(t.TempDir())
  2942  	res := apiclient.RepoAppDetailsResponse{}
  2943  	q := apiclient.RepoServerAppDetailsQuery{
  2944  		Repo: &argoappv1.Repository{},
  2945  		Source: &argoappv1.ApplicationSource{
  2946  			Helm: &argoappv1.ApplicationSourceHelm{ValueFiles: []string{"exclude.yaml", "has-the-word-values.yaml"}},
  2947  		},
  2948  	}
  2949  	appPath, err := filepath.Abs("./testdata/values-files/")
  2950  	require.NoError(t, err)
  2951  	err = populateHelmAppDetails(&res, appPath, appPath, &q, emptyTempPaths)
  2952  	require.NoError(t, err)
  2953  	assert.Len(t, res.Helm.Parameters, 3)
  2954  	assert.Len(t, res.Helm.ValueFiles, 5)
  2955  }
  2956  
  2957  func Test_populateHelmAppDetails_values_symlinks(t *testing.T) {
  2958  	var emptyTempPaths = io.NewRandomizedTempPaths(t.TempDir())
  2959  	t.Run("inbound", func(t *testing.T) {
  2960  		res := apiclient.RepoAppDetailsResponse{}
  2961  		q := apiclient.RepoServerAppDetailsQuery{Repo: &argoappv1.Repository{}, Source: &argoappv1.ApplicationSource{}}
  2962  		err := populateHelmAppDetails(&res, "./testdata/in-bounds-values-file-link/", "./testdata/in-bounds-values-file-link/", &q, emptyTempPaths)
  2963  		require.NoError(t, err)
  2964  		assert.NotEmpty(t, res.Helm.Values)
  2965  		assert.NotEmpty(t, res.Helm.Parameters)
  2966  	})
  2967  
  2968  	t.Run("out of bounds", func(t *testing.T) {
  2969  		res := apiclient.RepoAppDetailsResponse{}
  2970  		q := apiclient.RepoServerAppDetailsQuery{Repo: &argoappv1.Repository{}, Source: &argoappv1.ApplicationSource{}}
  2971  		err := populateHelmAppDetails(&res, "./testdata/out-of-bounds-values-file-link/", "./testdata/out-of-bounds-values-file-link/", &q, emptyTempPaths)
  2972  		require.NoError(t, err)
  2973  		assert.Empty(t, res.Helm.Values)
  2974  		assert.Empty(t, res.Helm.Parameters)
  2975  	})
  2976  }
  2977  
  2978  func TestGetHelmRepos_OCIDependencies(t *testing.T) {
  2979  	src := argoappv1.ApplicationSource{Path: "."}
  2980  	q := apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: &src, HelmRepoCreds: []*argoappv1.RepoCreds{
  2981  		{URL: "example.com", Username: "test", Password: "test", EnableOCI: true},
  2982  	}}
  2983  
  2984  	helmRepos, err := getHelmRepos("./testdata/oci-dependencies", q.Repos, q.HelmRepoCreds)
  2985  	assert.Nil(t, err)
  2986  
  2987  	assert.Equal(t, len(helmRepos), 1)
  2988  	assert.Equal(t, helmRepos[0].Username, "test")
  2989  	assert.Equal(t, helmRepos[0].EnableOci, true)
  2990  	assert.Equal(t, helmRepos[0].Repo, "example.com/myrepo")
  2991  }
  2992  
  2993  func TestGetHelmRepo_NamedRepos(t *testing.T) {
  2994  	src := argoappv1.ApplicationSource{Path: "."}
  2995  	q := apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: &src, Repos: []*argoappv1.Repository{{
  2996  		Name:     "custom-repo",
  2997  		Repo:     "https://example.com",
  2998  		Username: "test",
  2999  	}}}
  3000  
  3001  	helmRepos, err := getHelmRepos("./testdata/helm-with-dependencies", q.Repos, q.HelmRepoCreds)
  3002  	assert.Nil(t, err)
  3003  
  3004  	assert.Equal(t, len(helmRepos), 1)
  3005  	assert.Equal(t, helmRepos[0].Username, "test")
  3006  	assert.Equal(t, helmRepos[0].Repo, "https://example.com")
  3007  }
  3008  
  3009  func TestGetHelmRepo_NamedReposAlias(t *testing.T) {
  3010  	src := argoappv1.ApplicationSource{Path: "."}
  3011  	q := apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: &src, Repos: []*argoappv1.Repository{{
  3012  		Name:     "custom-repo-alias",
  3013  		Repo:     "https://example.com",
  3014  		Username: "test-alias",
  3015  	}}}
  3016  
  3017  	helmRepos, err := getHelmRepos("./testdata/helm-with-dependencies-alias", q.Repos, q.HelmRepoCreds)
  3018  	assert.Nil(t, err)
  3019  
  3020  	assert.Equal(t, len(helmRepos), 1)
  3021  	assert.Equal(t, helmRepos[0].Username, "test-alias")
  3022  	assert.Equal(t, helmRepos[0].Repo, "https://example.com")
  3023  }
  3024  
  3025  func Test_getResolvedValueFiles(t *testing.T) {
  3026  	tempDir := t.TempDir()
  3027  	paths := io.NewRandomizedTempPaths(tempDir)
  3028  	paths.Add(git.NormalizeGitURL("https://github.com/org/repo1"), path.Join(tempDir, "repo1"))
  3029  
  3030  	testCases := []struct {
  3031  		name         string
  3032  		rawPath      string
  3033  		env          *argoappv1.Env
  3034  		refSources   map[string]*argoappv1.RefTarget
  3035  		expectedPath string
  3036  		expectedErr  bool
  3037  	}{
  3038  		{
  3039  			name:         "simple path",
  3040  			rawPath:      "values.yaml",
  3041  			env:          &argoappv1.Env{},
  3042  			refSources:   map[string]*argoappv1.RefTarget{},
  3043  			expectedPath: path.Join(tempDir, "main-repo", "values.yaml"),
  3044  		},
  3045  		{
  3046  			name:    "simple ref",
  3047  			rawPath: "$ref/values.yaml",
  3048  			env:     &argoappv1.Env{},
  3049  			refSources: map[string]*argoappv1.RefTarget{
  3050  				"$ref": {
  3051  					Repo: argoappv1.Repository{
  3052  						Repo: "https://github.com/org/repo1",
  3053  					},
  3054  				},
  3055  			},
  3056  			expectedPath: path.Join(tempDir, "repo1", "values.yaml"),
  3057  		},
  3058  		{
  3059  			name:    "only ref",
  3060  			rawPath: "$ref",
  3061  			env:     &argoappv1.Env{},
  3062  			refSources: map[string]*argoappv1.RefTarget{
  3063  				"$ref": {
  3064  					Repo: argoappv1.Repository{
  3065  						Repo: "https://github.com/org/repo1",
  3066  					},
  3067  				},
  3068  			},
  3069  			expectedErr: true,
  3070  		},
  3071  		{
  3072  			name:    "attempted traversal",
  3073  			rawPath: "$ref/../values.yaml",
  3074  			env:     &argoappv1.Env{},
  3075  			refSources: map[string]*argoappv1.RefTarget{
  3076  				"$ref": {
  3077  					Repo: argoappv1.Repository{
  3078  						Repo: "https://github.com/org/repo1",
  3079  					},
  3080  				},
  3081  			},
  3082  			expectedErr: true,
  3083  		},
  3084  		{
  3085  			// Since $ref doesn't resolve to a ref target, we assume it's an env var. Since the env var isn't specified,
  3086  			// it's replaced with an empty string. This is necessary for backwards compatibility with behavior before
  3087  			// ref targets were introduced.
  3088  			name:         "ref doesn't exist",
  3089  			rawPath:      "$ref/values.yaml",
  3090  			env:          &argoappv1.Env{},
  3091  			refSources:   map[string]*argoappv1.RefTarget{},
  3092  			expectedPath: path.Join(tempDir, "main-repo", "values.yaml"),
  3093  		},
  3094  		{
  3095  			name:    "repo doesn't exist",
  3096  			rawPath: "$ref/values.yaml",
  3097  			env:     &argoappv1.Env{},
  3098  			refSources: map[string]*argoappv1.RefTarget{
  3099  				"$ref": {
  3100  					Repo: argoappv1.Repository{
  3101  						Repo: "https://github.com/org/repo2",
  3102  					},
  3103  				},
  3104  			},
  3105  			expectedErr: true,
  3106  		},
  3107  		{
  3108  			name:    "env var is resolved",
  3109  			rawPath: "$ref/$APP_PATH/values.yaml",
  3110  			env: &argoappv1.Env{
  3111  				&argoappv1.EnvEntry{
  3112  					Name:  "APP_PATH",
  3113  					Value: "app-path",
  3114  				},
  3115  			},
  3116  			refSources: map[string]*argoappv1.RefTarget{
  3117  				"$ref": {
  3118  					Repo: argoappv1.Repository{
  3119  						Repo: "https://github.com/org/repo1",
  3120  					},
  3121  				},
  3122  			},
  3123  			expectedPath: path.Join(tempDir, "repo1", "app-path", "values.yaml"),
  3124  		},
  3125  		{
  3126  			name:    "traversal in env var is blocked",
  3127  			rawPath: "$ref/$APP_PATH/values.yaml",
  3128  			env: &argoappv1.Env{
  3129  				&argoappv1.EnvEntry{
  3130  					Name:  "APP_PATH",
  3131  					Value: "..",
  3132  				},
  3133  			},
  3134  			refSources: map[string]*argoappv1.RefTarget{
  3135  				"$ref": {
  3136  					Repo: argoappv1.Repository{
  3137  						Repo: "https://github.com/org/repo1",
  3138  					},
  3139  				},
  3140  			},
  3141  			expectedErr: true,
  3142  		},
  3143  		{
  3144  			name:    "env var prefix",
  3145  			rawPath: "$APP_PATH/values.yaml",
  3146  			env: &argoappv1.Env{
  3147  				&argoappv1.EnvEntry{
  3148  					Name:  "APP_PATH",
  3149  					Value: "app-path",
  3150  				},
  3151  			},
  3152  			refSources:   map[string]*argoappv1.RefTarget{},
  3153  			expectedPath: path.Join(tempDir, "main-repo", "app-path", "values.yaml"),
  3154  		},
  3155  		{
  3156  			name:         "unresolved env var",
  3157  			rawPath:      "$APP_PATH/values.yaml",
  3158  			env:          &argoappv1.Env{},
  3159  			refSources:   map[string]*argoappv1.RefTarget{},
  3160  			expectedPath: path.Join(tempDir, "main-repo", "values.yaml"),
  3161  		},
  3162  	}
  3163  
  3164  	for _, tc := range testCases {
  3165  		tcc := tc
  3166  		t.Run(tcc.name, func(t *testing.T) {
  3167  			t.Parallel()
  3168  			resolvedPaths, err := getResolvedValueFiles(path.Join(tempDir, "main-repo"), path.Join(tempDir, "main-repo"), tcc.env, []string{}, []string{tcc.rawPath}, tcc.refSources, paths, false)
  3169  			if !tcc.expectedErr {
  3170  				assert.NoError(t, err)
  3171  				require.Len(t, resolvedPaths, 1)
  3172  				assert.Equal(t, tcc.expectedPath, string(resolvedPaths[0]))
  3173  			} else {
  3174  				assert.Error(t, err)
  3175  				assert.Empty(t, resolvedPaths)
  3176  			}
  3177  		})
  3178  	}
  3179  }
  3180  func TestErrorGetGitDirectories(t *testing.T) {
  3181  	type fields struct {
  3182  		service *Service
  3183  	}
  3184  	type args struct {
  3185  		ctx     context.Context
  3186  		request *apiclient.GitDirectoriesRequest
  3187  	}
  3188  	tests := []struct {
  3189  		name    string
  3190  		fields  fields
  3191  		args    args
  3192  		want    *apiclient.GitDirectoriesResponse
  3193  		wantErr assert.ErrorAssertionFunc
  3194  	}{
  3195  		{name: "InvalidRepo", fields: fields{service: newService(t, ".")}, args: args{
  3196  			ctx: context.TODO(),
  3197  			request: &apiclient.GitDirectoriesRequest{
  3198  				Repo:             nil,
  3199  				SubmoduleEnabled: false,
  3200  				Revision:         "HEAD",
  3201  			},
  3202  		}, want: nil, wantErr: assert.Error},
  3203  		{name: "InvalidResolveRevision", fields: fields{service: func() *Service {
  3204  			s, _, _ := newServiceWithOpt(t, func(gitClient *gitmocks.Client, helmClient *helmmocks.Client, paths *iomocks.TempPaths) {
  3205  				gitClient.On("Checkout", mock.Anything, mock.Anything).Return(nil)
  3206  				gitClient.On("LsRemote", mock.Anything).Return("", fmt.Errorf("ah error"))
  3207  				paths.On("GetPath", mock.Anything).Return(".", nil)
  3208  				paths.On("GetPathIfExists", mock.Anything).Return(".", nil)
  3209  			}, ".")
  3210  			return s
  3211  		}()}, args: args{
  3212  			ctx: context.TODO(),
  3213  			request: &apiclient.GitDirectoriesRequest{
  3214  				Repo:             &argoappv1.Repository{Repo: "not-a-valid-url"},
  3215  				SubmoduleEnabled: false,
  3216  				Revision:         "sadfsadf",
  3217  			},
  3218  		}, want: nil, wantErr: assert.Error},
  3219  	}
  3220  	for _, tt := range tests {
  3221  		t.Run(tt.name, func(t *testing.T) {
  3222  			s := tt.fields.service
  3223  			got, err := s.GetGitDirectories(tt.args.ctx, tt.args.request)
  3224  			if !tt.wantErr(t, err, fmt.Sprintf("GetGitDirectories(%v, %v)", tt.args.ctx, tt.args.request)) {
  3225  				return
  3226  			}
  3227  			assert.Equalf(t, tt.want, got, "GetGitDirectories(%v, %v)", tt.args.ctx, tt.args.request)
  3228  		})
  3229  	}
  3230  }
  3231  
  3232  func TestGetGitDirectories(t *testing.T) {
  3233  	// test not using the cache
  3234  	root := "./testdata/git-files-dirs"
  3235  	s, _, cacheMocks := newServiceWithOpt(t, func(gitClient *gitmocks.Client, helmClient *helmmocks.Client, paths *iomocks.TempPaths) {
  3236  		gitClient.On("Init").Return(nil)
  3237  		gitClient.On("Fetch", mock.Anything).Return(nil)
  3238  		gitClient.On("Checkout", mock.Anything, mock.Anything).Once().Return(nil)
  3239  		gitClient.On("LsRemote", "HEAD").Return("632039659e542ed7de0c170a4fcc1c571b288fc0", nil)
  3240  		gitClient.On("Root").Return(root)
  3241  		paths.On("GetPath", mock.Anything).Return(root, nil)
  3242  		paths.On("GetPathIfExists", mock.Anything).Return(root, nil)
  3243  	}, root)
  3244  	dirRequest := &apiclient.GitDirectoriesRequest{
  3245  		Repo:             &argoappv1.Repository{Repo: "a-url.com"},
  3246  		SubmoduleEnabled: false,
  3247  		Revision:         "HEAD",
  3248  	}
  3249  	directories, err := s.GetGitDirectories(context.TODO(), dirRequest)
  3250  	assert.Nil(t, err)
  3251  	assert.ElementsMatch(t, directories.GetPaths(), []string{"app", "app/bar", "app/foo/bar", "somedir", "app/foo"})
  3252  
  3253  	// do the same request again to use the cache
  3254  	// we only allow CheckOut to be called once in the mock
  3255  	directories, err = s.GetGitDirectories(context.TODO(), dirRequest)
  3256  	assert.Nil(t, err)
  3257  	assert.ElementsMatch(t, []string{"app", "app/bar", "app/foo/bar", "somedir", "app/foo"}, directories.GetPaths())
  3258  	cacheMocks.mockCache.AssertCacheCalledTimes(t, &repositorymocks.CacheCallCounts{
  3259  		ExternalSets: 1,
  3260  		ExternalGets: 2,
  3261  	})
  3262  }
  3263  
  3264  func TestErrorGetGitFiles(t *testing.T) {
  3265  	type fields struct {
  3266  		service *Service
  3267  	}
  3268  	type args struct {
  3269  		ctx     context.Context
  3270  		request *apiclient.GitFilesRequest
  3271  	}
  3272  	tests := []struct {
  3273  		name    string
  3274  		fields  fields
  3275  		args    args
  3276  		want    *apiclient.GitFilesResponse
  3277  		wantErr assert.ErrorAssertionFunc
  3278  	}{
  3279  		{name: "InvalidRepo", fields: fields{service: newService(t, ".")}, args: args{
  3280  			ctx: context.TODO(),
  3281  			request: &apiclient.GitFilesRequest{
  3282  				Repo:             nil,
  3283  				SubmoduleEnabled: false,
  3284  				Revision:         "HEAD",
  3285  			},
  3286  		}, want: nil, wantErr: assert.Error},
  3287  		{name: "InvalidResolveRevision", fields: fields{service: func() *Service {
  3288  			s, _, _ := newServiceWithOpt(t, func(gitClient *gitmocks.Client, helmClient *helmmocks.Client, paths *iomocks.TempPaths) {
  3289  				gitClient.On("Checkout", mock.Anything, mock.Anything).Return(nil)
  3290  				gitClient.On("LsRemote", mock.Anything).Return("", fmt.Errorf("ah error"))
  3291  				paths.On("GetPath", mock.Anything).Return(".", nil)
  3292  				paths.On("GetPathIfExists", mock.Anything).Return(".", nil)
  3293  			}, ".")
  3294  			return s
  3295  		}()}, args: args{
  3296  			ctx: context.TODO(),
  3297  			request: &apiclient.GitFilesRequest{
  3298  				Repo:             &argoappv1.Repository{Repo: "not-a-valid-url"},
  3299  				SubmoduleEnabled: false,
  3300  				Revision:         "sadfsadf",
  3301  			},
  3302  		}, want: nil, wantErr: assert.Error},
  3303  	}
  3304  	for _, tt := range tests {
  3305  		t.Run(tt.name, func(t *testing.T) {
  3306  			s := tt.fields.service
  3307  			got, err := s.GetGitFiles(tt.args.ctx, tt.args.request)
  3308  			if !tt.wantErr(t, err, fmt.Sprintf("GetGitFiles(%v, %v)", tt.args.ctx, tt.args.request)) {
  3309  				return
  3310  			}
  3311  			assert.Equalf(t, tt.want, got, "GetGitFiles(%v, %v)", tt.args.ctx, tt.args.request)
  3312  		})
  3313  	}
  3314  }
  3315  
  3316  func TestGetGitFiles(t *testing.T) {
  3317  	// test not using the cache
  3318  	files := []string{"./testdata/git-files-dirs/somedir/config.yaml",
  3319  		"./testdata/git-files-dirs/config.yaml", "./testdata/git-files-dirs/config.yaml", "./testdata/git-files-dirs/app/foo/bar/config.yaml"}
  3320  	root := ""
  3321  	s, _, cacheMocks := newServiceWithOpt(t, func(gitClient *gitmocks.Client, helmClient *helmmocks.Client, paths *iomocks.TempPaths) {
  3322  		gitClient.On("Init").Return(nil)
  3323  		gitClient.On("Fetch", mock.Anything).Return(nil)
  3324  		gitClient.On("Checkout", mock.Anything, mock.Anything).Once().Return(nil)
  3325  		gitClient.On("LsRemote", "HEAD").Return("632039659e542ed7de0c170a4fcc1c571b288fc0", nil)
  3326  		gitClient.On("Root").Return(root)
  3327  		gitClient.On("LsFiles", mock.Anything, mock.Anything).Once().Return(files, nil)
  3328  		paths.On("GetPath", mock.Anything).Return(root, nil)
  3329  		paths.On("GetPathIfExists", mock.Anything).Return(root, nil)
  3330  	}, root)
  3331  	filesRequest := &apiclient.GitFilesRequest{
  3332  		Repo:             &argoappv1.Repository{Repo: "a-url.com"},
  3333  		SubmoduleEnabled: false,
  3334  		Revision:         "HEAD",
  3335  	}
  3336  
  3337  	// expected map
  3338  	expected := make(map[string][]byte)
  3339  	for _, filePath := range files {
  3340  		fileContents, err := os.ReadFile(filePath)
  3341  		assert.Nil(t, err)
  3342  		expected[filePath] = fileContents
  3343  	}
  3344  
  3345  	fileResponse, err := s.GetGitFiles(context.TODO(), filesRequest)
  3346  	assert.Nil(t, err)
  3347  	assert.Equal(t, fileResponse.GetMap(), expected)
  3348  
  3349  	// do the same request again to use the cache
  3350  	// we only allow LsFiles to be called once in the mock
  3351  	fileResponse, err = s.GetGitFiles(context.TODO(), filesRequest)
  3352  	assert.Nil(t, err)
  3353  	assert.Equal(t, expected, fileResponse.GetMap())
  3354  	cacheMocks.mockCache.AssertCacheCalledTimes(t, &repositorymocks.CacheCallCounts{
  3355  		ExternalSets: 1,
  3356  		ExternalGets: 2,
  3357  	})
  3358  }
  3359  
  3360  func Test_getRepoSanitizerRegex(t *testing.T) {
  3361  	r := getRepoSanitizerRegex("/tmp/_argocd-repo")
  3362  	msg := r.ReplaceAllString("error message containing /tmp/_argocd-repo/SENSITIVE and other stuff", "<path to cached source>")
  3363  	assert.Equal(t, "error message containing <path to cached source> and other stuff", msg)
  3364  	msg = r.ReplaceAllString("error message containing /tmp/_argocd-repo/SENSITIVE/with/trailing/path and other stuff", "<path to cached source>")
  3365  	assert.Equal(t, "error message containing <path to cached source>/with/trailing/path and other stuff", msg)
  3366  }
  3367  
  3368  func TestGetRevisionChartDetails(t *testing.T) {
  3369  	t.Run("Test revision semvar", func(t *testing.T) {
  3370  		root := t.TempDir()
  3371  		service := newService(t, root)
  3372  		_, err := service.GetRevisionChartDetails(context.Background(), &apiclient.RepoServerRevisionChartDetailsRequest{
  3373  			Repo: &v1alpha1.Repository{
  3374  				Repo: fmt.Sprintf("file://%s", root),
  3375  				Name: "test-repo-name",
  3376  				Type: "helm",
  3377  			},
  3378  			Name:     "test-name",
  3379  			Revision: "test-revision",
  3380  		})
  3381  		assert.ErrorContains(t, err, "invalid revision")
  3382  	})
  3383  
  3384  	t.Run("Test GetRevisionChartDetails", func(t *testing.T) {
  3385  		root := t.TempDir()
  3386  		service := newService(t, root)
  3387  		repoUrl := fmt.Sprintf("file://%s", root)
  3388  		err := service.cache.SetRevisionChartDetails(repoUrl, "my-chart", "1.1.0", &argoappv1.ChartDetails{
  3389  			Description: "test-description",
  3390  			Home:        "test-home",
  3391  			Maintainers: []string{"test-maintainer"},
  3392  		})
  3393  		assert.NoError(t, err)
  3394  		chartDetails, err := service.GetRevisionChartDetails(context.Background(), &apiclient.RepoServerRevisionChartDetailsRequest{
  3395  			Repo: &v1alpha1.Repository{
  3396  				Repo: fmt.Sprintf("file://%s", root),
  3397  				Name: "test-repo-name",
  3398  				Type: "helm",
  3399  			},
  3400  			Name:     "my-chart",
  3401  			Revision: "1.1.0",
  3402  		})
  3403  		assert.NoError(t, err)
  3404  		assert.Equal(t, "test-description", chartDetails.Description)
  3405  		assert.Equal(t, "test-home", chartDetails.Home)
  3406  		assert.Equal(t, []string{"test-maintainer"}, chartDetails.Maintainers)
  3407  	})
  3408  }