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

     1  package cache
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"testing"
     7  	"time"
     8  
     9  	"github.com/go-git/go-git/v5/plumbing"
    10  	"github.com/spf13/cobra"
    11  	"github.com/stretchr/testify/assert"
    12  	"github.com/stretchr/testify/mock"
    13  	"github.com/stretchr/testify/require"
    14  
    15  	"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
    16  	"github.com/argoproj/argo-cd/v3/reposerver/apiclient"
    17  	"github.com/argoproj/argo-cd/v3/reposerver/cache/mocks"
    18  	cacheutil "github.com/argoproj/argo-cd/v3/util/cache"
    19  )
    20  
    21  type MockedCache struct {
    22  	mock.Mock
    23  	*Cache
    24  }
    25  
    26  type fixtures struct {
    27  	mockCache *mocks.MockRepoCache
    28  	cache     *MockedCache
    29  }
    30  
    31  func newFixtures() *fixtures {
    32  	mockCache := mocks.NewMockRepoCache(&mocks.MockCacheOptions{RevisionCacheExpiration: 1 * time.Minute, RepoCacheExpiration: 1 * time.Minute})
    33  	newBaseCache := cacheutil.NewCache(mockCache.RedisClient)
    34  	baseCache := NewCache(newBaseCache, 1*time.Minute, 1*time.Minute, 10*time.Second)
    35  	return &fixtures{mockCache: mockCache, cache: &MockedCache{Cache: baseCache}}
    36  }
    37  
    38  func TestCache_GetRevisionMetadata(t *testing.T) {
    39  	fixtures := newFixtures()
    40  	t.Cleanup(fixtures.mockCache.StopRedisCallback)
    41  	cache := fixtures.cache
    42  	mockCache := fixtures.mockCache
    43  	// cache miss
    44  	_, err := cache.GetRevisionMetadata("my-repo-url", "my-revision")
    45  	require.ErrorIs(t, err, ErrCacheMiss)
    46  	mockCache.RedisClient.AssertCalled(t, "Get", mock.Anything, mock.Anything)
    47  	// populate cache
    48  	err = cache.SetRevisionMetadata("my-repo-url", "my-revision", &v1alpha1.RevisionMetadata{Message: "my-message"})
    49  	require.NoError(t, err)
    50  	// cache miss
    51  	_, err = cache.GetRevisionMetadata("other-repo-url", "my-revision")
    52  	require.ErrorIs(t, err, ErrCacheMiss)
    53  	// cache miss
    54  	_, err = cache.GetRevisionMetadata("my-repo-url", "other-revision")
    55  	require.ErrorIs(t, err, ErrCacheMiss)
    56  	// cache hit
    57  	value, err := cache.GetRevisionMetadata("my-repo-url", "my-revision")
    58  	require.NoError(t, err)
    59  	assert.Equal(t, &v1alpha1.RevisionMetadata{Message: "my-message"}, value)
    60  	mockCache.AssertCacheCalledTimes(t, &mocks.CacheCallCounts{ExternalSets: 1, ExternalGets: 4})
    61  }
    62  
    63  func TestCache_ListApps(t *testing.T) {
    64  	fixtures := newFixtures()
    65  	t.Cleanup(fixtures.mockCache.StopRedisCallback)
    66  	cache := fixtures.cache
    67  	mockCache := fixtures.mockCache
    68  	// cache miss
    69  	_, err := cache.ListApps("my-repo-url", "my-revision")
    70  	require.ErrorIs(t, err, ErrCacheMiss)
    71  	// populate cache
    72  	err = cache.SetApps("my-repo-url", "my-revision", map[string]string{"foo": "bar"})
    73  	require.NoError(t, err)
    74  	// cache miss
    75  	_, err = cache.ListApps("other-repo-url", "my-revision")
    76  	require.ErrorIs(t, err, ErrCacheMiss)
    77  	// cache miss
    78  	_, err = cache.ListApps("my-repo-url", "other-revision")
    79  	require.ErrorIs(t, err, ErrCacheMiss)
    80  	// cache hit
    81  	value, err := cache.ListApps("my-repo-url", "my-revision")
    82  	require.NoError(t, err)
    83  	assert.Equal(t, map[string]string{"foo": "bar"}, value)
    84  	mockCache.AssertCacheCalledTimes(t, &mocks.CacheCallCounts{ExternalSets: 1, ExternalGets: 4})
    85  }
    86  
    87  func TestCache_GetManifests(t *testing.T) {
    88  	fixtures := newFixtures()
    89  	t.Cleanup(fixtures.mockCache.StopRedisCallback)
    90  	cache := fixtures.cache
    91  	mockCache := fixtures.mockCache
    92  	// cache miss
    93  	q := &apiclient.ManifestRequest{}
    94  	value := &CachedManifestResponse{}
    95  	err := cache.GetManifests("my-revision", &v1alpha1.ApplicationSource{}, q.RefSources, q, "my-namespace", "", "my-app-label-key", "my-app-label-value", value, nil, "")
    96  	require.ErrorIs(t, err, ErrCacheMiss)
    97  	// populate cache
    98  	res := &CachedManifestResponse{ManifestResponse: &apiclient.ManifestResponse{SourceType: "my-source-type"}}
    99  	err = cache.SetManifests("my-revision", &v1alpha1.ApplicationSource{}, q.RefSources, q, "my-namespace", "", "my-app-label-key", "my-app-label-value", res, nil, "")
   100  	require.NoError(t, err)
   101  	t.Run("expect cache miss because of changed revision", func(t *testing.T) {
   102  		err = cache.GetManifests("other-revision", &v1alpha1.ApplicationSource{}, q.RefSources, q, "my-namespace", "", "my-app-label-key", "my-app-label-value", value, nil, "")
   103  		require.ErrorIs(t, err, ErrCacheMiss)
   104  	})
   105  	t.Run("expect cache miss because of changed path", func(t *testing.T) {
   106  		err = cache.GetManifests("my-revision", &v1alpha1.ApplicationSource{Path: "other-path"}, q.RefSources, q, "my-namespace", "", "my-app-label-key", "my-app-label-value", value, nil, "")
   107  		require.ErrorIs(t, err, ErrCacheMiss)
   108  	})
   109  	t.Run("expect cache miss because of changed namespace", func(t *testing.T) {
   110  		err = cache.GetManifests("my-revision", &v1alpha1.ApplicationSource{}, q.RefSources, q, "other-namespace", "", "my-app-label-key", "my-app-label-value", value, nil, "")
   111  		require.ErrorIs(t, err, ErrCacheMiss)
   112  	})
   113  	t.Run("expect cache miss because of changed app label key", func(t *testing.T) {
   114  		err = cache.GetManifests("my-revision", &v1alpha1.ApplicationSource{}, q.RefSources, q, "my-namespace", "", "other-app-label-key", "my-app-label-value", value, nil, "")
   115  		require.ErrorIs(t, err, ErrCacheMiss)
   116  	})
   117  	t.Run("expect cache miss because of changed app label value", func(t *testing.T) {
   118  		err = cache.GetManifests("my-revision", &v1alpha1.ApplicationSource{}, q.RefSources, q, "my-namespace", "", "my-app-label-key", "other-app-label-value", value, nil, "")
   119  		require.ErrorIs(t, err, ErrCacheMiss)
   120  	})
   121  	t.Run("expect cache miss because of changed referenced source", func(t *testing.T) {
   122  		err = cache.GetManifests("my-revision", &v1alpha1.ApplicationSource{}, q.RefSources, q, "my-namespace", "", "my-app-label-key", "other-app-label-value", value, map[string]string{"my-referenced-source": "my-referenced-revision"}, "")
   123  		require.ErrorIs(t, err, ErrCacheMiss)
   124  	})
   125  	t.Run("expect cache hit", func(t *testing.T) {
   126  		err = cache.SetManifests(
   127  			"my-revision1", &v1alpha1.ApplicationSource{}, q.RefSources, q, "my-namespace", "", "my-app-label-key", "my-app-label-value",
   128  			&CachedManifestResponse{ManifestResponse: &apiclient.ManifestResponse{SourceType: "my-source-type", Revision: "my-revision2"}}, nil, "")
   129  		require.NoError(t, err)
   130  
   131  		err = cache.GetManifests("my-revision1", &v1alpha1.ApplicationSource{}, q.RefSources, q, "my-namespace", "", "my-app-label-key", "my-app-label-value", value, nil, "")
   132  		require.NoError(t, err)
   133  
   134  		assert.Equal(t, "my-source-type", value.ManifestResponse.SourceType)
   135  		assert.Equal(t, "my-revision1", value.ManifestResponse.Revision)
   136  	})
   137  	mockCache.AssertCacheCalledTimes(t, &mocks.CacheCallCounts{ExternalSets: 2, ExternalGets: 8})
   138  }
   139  
   140  func TestCache_GetAppDetails(t *testing.T) {
   141  	fixtures := newFixtures()
   142  	t.Cleanup(fixtures.mockCache.StopRedisCallback)
   143  	cache := fixtures.cache
   144  	mockCache := fixtures.mockCache
   145  	// cache miss
   146  	value := &apiclient.RepoAppDetailsResponse{}
   147  	emptyRefSources := map[string]*v1alpha1.RefTarget{}
   148  	err := cache.GetAppDetails("my-revision", &v1alpha1.ApplicationSource{}, emptyRefSources, value, "", nil)
   149  	require.ErrorIs(t, err, ErrCacheMiss)
   150  	res := &apiclient.RepoAppDetailsResponse{Type: "my-type"}
   151  	err = cache.SetAppDetails("my-revision", &v1alpha1.ApplicationSource{}, emptyRefSources, res, "", nil)
   152  	require.NoError(t, err)
   153  	// cache miss
   154  	err = cache.GetAppDetails("other-revision", &v1alpha1.ApplicationSource{}, emptyRefSources, value, "", nil)
   155  	require.ErrorIs(t, err, ErrCacheMiss)
   156  	// cache miss
   157  	err = cache.GetAppDetails("my-revision", &v1alpha1.ApplicationSource{Path: "other-path"}, emptyRefSources, value, "", nil)
   158  	require.ErrorIs(t, err, ErrCacheMiss)
   159  	// cache hit
   160  	err = cache.GetAppDetails("my-revision", &v1alpha1.ApplicationSource{}, emptyRefSources, value, "", nil)
   161  	require.NoError(t, err)
   162  	assert.Equal(t, &apiclient.RepoAppDetailsResponse{Type: "my-type"}, value)
   163  	mockCache.AssertCacheCalledTimes(t, &mocks.CacheCallCounts{ExternalSets: 1, ExternalGets: 4})
   164  }
   165  
   166  func TestAddCacheFlagsToCmd(t *testing.T) {
   167  	cache, err := AddCacheFlagsToCmd(&cobra.Command{})()
   168  	require.NoError(t, err)
   169  	assert.Equal(t, 24*time.Hour, cache.repoCacheExpiration)
   170  }
   171  
   172  func TestCachedManifestResponse_HashBehavior(t *testing.T) {
   173  	inMemCache := cacheutil.NewInMemoryCache(1 * time.Hour)
   174  
   175  	repoCache := NewCache(
   176  		cacheutil.NewCache(inMemCache),
   177  		1*time.Minute,
   178  		1*time.Minute,
   179  		10*time.Second,
   180  	)
   181  
   182  	response := apiclient.ManifestResponse{
   183  		Namespace: "default",
   184  		Revision:  "revision",
   185  		Manifests: []string{"sample-text"},
   186  	}
   187  	appSrc := &v1alpha1.ApplicationSource{}
   188  	appKey := "key"
   189  	appValue := "value"
   190  
   191  	// Set the value in the cache
   192  	store := &CachedManifestResponse{
   193  		FirstFailureTimestamp:           0,
   194  		ManifestResponse:                &response,
   195  		MostRecentError:                 "",
   196  		NumberOfCachedResponsesReturned: 0,
   197  		NumberOfConsecutiveFailures:     0,
   198  	}
   199  	q := &apiclient.ManifestRequest{}
   200  	err := repoCache.SetManifests(response.Revision, appSrc, q.RefSources, q, response.Namespace, "", appKey, appValue, store, nil, "")
   201  	require.NoError(t, err)
   202  
   203  	// Get the cache entry of the set value directly from the in memory cache, and check the values
   204  	var cacheKey string
   205  	var cmr *CachedManifestResponse
   206  	{
   207  		items := getInMemoryCacheContents(t, inMemCache)
   208  
   209  		assert.Len(t, items, 1)
   210  
   211  		for key, val := range items {
   212  			cmr = val
   213  			cacheKey = key
   214  		}
   215  		assert.NotEmpty(t, cmr.CacheEntryHash)
   216  		assert.NotNil(t, cmr.ManifestResponse)
   217  		assert.Equal(t, cmr.ManifestResponse, store.ManifestResponse)
   218  
   219  		regeneratedHash, err := cmr.generateCacheEntryHash()
   220  		require.NoError(t, err)
   221  		assert.Equal(t, cmr.CacheEntryHash, regeneratedHash)
   222  	}
   223  
   224  	// Retrieve the value using 'GetManifests' and confirm it works
   225  	retrievedVal := &CachedManifestResponse{}
   226  	err = repoCache.GetManifests(response.Revision, appSrc, q.RefSources, q, response.Namespace, "", appKey, appValue, retrievedVal, nil, "")
   227  	require.NoError(t, err)
   228  	assert.Equal(t, retrievedVal, store)
   229  
   230  	// Corrupt the hash so that it doesn't match
   231  	{
   232  		newCmr := cmr.shallowCopy()
   233  		newCmr.CacheEntryHash = "!bad-hash!"
   234  
   235  		err := inMemCache.Set(&cacheutil.Item{
   236  			Key:    cacheKey,
   237  			Object: &newCmr,
   238  		})
   239  		require.NoError(t, err)
   240  	}
   241  
   242  	// Retrieve the value using GetManifests and confirm it returns a cache miss
   243  	retrievedVal = &CachedManifestResponse{}
   244  	err = repoCache.GetManifests(response.Revision, appSrc, q.RefSources, q, response.Namespace, "", appKey, appValue, retrievedVal, nil, "")
   245  
   246  	assert.Equal(t, err, cacheutil.ErrCacheMiss)
   247  
   248  	// Verify that the hash mismatch item has been deleted
   249  	items := getInMemoryCacheContents(t, inMemCache)
   250  	assert.Empty(t, items)
   251  }
   252  
   253  func getInMemoryCacheContents(t *testing.T, inMemCache *cacheutil.InMemoryCache) map[string]*CachedManifestResponse {
   254  	t.Helper()
   255  	items, err := inMemCache.Items(func() any { return &CachedManifestResponse{} })
   256  	require.NoError(t, err)
   257  
   258  	result := map[string]*CachedManifestResponse{}
   259  	for key, val := range items {
   260  		obj, ok := val.(*CachedManifestResponse)
   261  		require.True(t, ok, "Unexpected type in cache")
   262  
   263  		result[key] = obj
   264  	}
   265  
   266  	return result
   267  }
   268  
   269  func TestCachedManifestResponse_ShallowCopy(t *testing.T) {
   270  	pre := &CachedManifestResponse{
   271  		CacheEntryHash:        "value",
   272  		FirstFailureTimestamp: 1,
   273  		ManifestResponse: &apiclient.ManifestResponse{
   274  			Manifests: []string{"one", "two"},
   275  		},
   276  		MostRecentError:                 "error",
   277  		NumberOfCachedResponsesReturned: 2,
   278  		NumberOfConsecutiveFailures:     3,
   279  	}
   280  
   281  	post := pre.shallowCopy()
   282  	assert.Equal(t, pre, post)
   283  
   284  	unequal := &CachedManifestResponse{
   285  		CacheEntryHash:        "diff-value",
   286  		FirstFailureTimestamp: 1,
   287  		ManifestResponse: &apiclient.ManifestResponse{
   288  			Manifests: []string{"one", "two"},
   289  		},
   290  		MostRecentError:                 "error",
   291  		NumberOfCachedResponsesReturned: 2,
   292  		NumberOfConsecutiveFailures:     3,
   293  	}
   294  	assert.NotEqual(t, pre, unequal)
   295  }
   296  
   297  func TestCachedManifestResponse_ShallowCopyExpectedFields(t *testing.T) {
   298  	// Attempt to ensure that the developer updated CachedManifestResponse.shallowCopy(), by doing a sanity test of the structure here
   299  
   300  	val := &CachedManifestResponse{}
   301  
   302  	str, err := json.Marshal(val)
   303  	if err != nil {
   304  		assert.FailNow(t, "Unable to marshal", err)
   305  		return
   306  	}
   307  
   308  	jsonMap := map[string]any{}
   309  	err = json.Unmarshal(str, &jsonMap)
   310  	if err != nil {
   311  		assert.FailNow(t, "Unable to unmarshal", err)
   312  		return
   313  	}
   314  
   315  	expectedFields := []string{
   316  		"cacheEntryHash", "manifestResponse", "mostRecentError", "firstFailureTimestamp",
   317  		"numberOfConsecutiveFailures", "numberOfCachedResponsesReturned",
   318  	}
   319  
   320  	assert.Len(t, jsonMap, len(expectedFields))
   321  
   322  	// If this test failed, you probably also forgot to update CachedManifestResponse.shallowCopy(), so
   323  	// go do that first :)
   324  
   325  	for _, expectedField := range expectedFields {
   326  		assert.Containsf(t, string(str), "\""+expectedField+"\"", "Missing field: %s", expectedField)
   327  	}
   328  }
   329  
   330  func TestGetGitReferences(t *testing.T) {
   331  	t.Run("Valid args, nothing in cache, in-memory only", func(t *testing.T) {
   332  		fixtures := newFixtures()
   333  		t.Cleanup(fixtures.mockCache.StopRedisCallback)
   334  		cache := fixtures.cache
   335  		var references []*plumbing.Reference
   336  		lockOwner, err := cache.GetGitReferences("test-repo", &references)
   337  		require.NoError(t, err, "Error is cache miss handled inside function")
   338  		assert.Empty(t, lockOwner, "Lock owner should be empty")
   339  		assert.Nil(t, references)
   340  		fixtures.mockCache.AssertCacheCalledTimes(t, &mocks.CacheCallCounts{ExternalGets: 1})
   341  	})
   342  
   343  	t.Run("Valid args, nothing in cache, external only", func(t *testing.T) {
   344  		fixtures := newFixtures()
   345  		t.Cleanup(fixtures.mockCache.StopRedisCallback)
   346  		cache := fixtures.cache
   347  		var references []*plumbing.Reference
   348  		lockOwner, err := cache.GetGitReferences("test-repo", &references)
   349  		require.NoError(t, err, "Error is cache miss handled inside function")
   350  		assert.Empty(t, lockOwner, "Lock owner should be empty")
   351  		assert.Nil(t, references)
   352  		fixtures.mockCache.AssertCacheCalledTimes(t, &mocks.CacheCallCounts{ExternalGets: 1})
   353  	})
   354  
   355  	t.Run("Valid args, value in cache, in-memory only", func(t *testing.T) {
   356  		fixtures := newFixtures()
   357  		t.Cleanup(fixtures.mockCache.StopRedisCallback)
   358  		cache := fixtures.cache
   359  		err := cache.SetGitReferences("test-repo", *GitRefCacheItemToReferences([][2]string{{"test-repo", "ref: test"}}))
   360  		require.NoError(t, err)
   361  		var references []*plumbing.Reference
   362  		lockOwner, err := cache.GetGitReferences("test-repo", &references)
   363  		require.NoError(t, err)
   364  		assert.Empty(t, lockOwner, "Lock owner should be empty")
   365  		assert.Len(t, references, 1)
   366  		assert.Equal(t, "test", (references)[0].Target().String())
   367  		assert.Equal(t, "test-repo", (references)[0].Name().String())
   368  		fixtures.mockCache.AssertCacheCalledTimes(t, &mocks.CacheCallCounts{ExternalSets: 1, ExternalGets: 1})
   369  	})
   370  
   371  	t.Run("cache error", func(t *testing.T) {
   372  		fixtures := newFixtures()
   373  		t.Cleanup(fixtures.mockCache.StopRedisCallback)
   374  		cache := fixtures.cache
   375  		fixtures.mockCache.RedisClient.On("Get", mock.Anything, mock.Anything).Unset()
   376  		fixtures.mockCache.RedisClient.On("Get", mock.Anything, mock.Anything).Return(errors.New("test cache error"))
   377  		var references []*plumbing.Reference
   378  		lockOwner, err := cache.GetGitReferences("test-repo", &references)
   379  		require.ErrorContains(t, err, "test cache error", "Error should be propagated")
   380  		assert.Empty(t, lockOwner, "Lock owner should be empty")
   381  		assert.Nil(t, references)
   382  		fixtures.mockCache.AssertCacheCalledTimes(t, &mocks.CacheCallCounts{ExternalGets: 1})
   383  	})
   384  }
   385  
   386  func TestGitRefCacheItemToReferences_DataChecks(t *testing.T) {
   387  	references := *GitRefCacheItemToReferences(nil)
   388  	assert.Empty(t, references, "No data should be handled gracefully by returning an empty slice")
   389  	references = *GitRefCacheItemToReferences([][2]string{{"", ""}})
   390  	assert.Empty(t, references, "Empty data should be discarded")
   391  	references = *GitRefCacheItemToReferences([][2]string{{"test", ""}})
   392  	assert.Len(t, references, 1, "Just the key being set should not be discarded")
   393  	assert.Equal(t, "test", references[0].Name().String(), "Name should be set and equal test")
   394  	references = *GitRefCacheItemToReferences([][2]string{{"", "ref: test1"}})
   395  	assert.Len(t, references, 1, "Just the value being set should not be discarded")
   396  	assert.Equal(t, "test1", references[0].Target().String(), "Target should be set and equal test1")
   397  	references = *GitRefCacheItemToReferences([][2]string{{"test2", "ref: test2"}})
   398  	assert.Len(t, references, 1, "Valid data is should be preserved")
   399  	assert.Equal(t, "test2", references[0].Name().String(), "Name should be set and equal test2")
   400  	assert.Equal(t, "test2", references[0].Target().String(), "Target should be set and equal test2")
   401  	references = *GitRefCacheItemToReferences([][2]string{{"test3", "ref: test3"}, {"test4", "ref: test4"}})
   402  	assert.Len(t, references, 2, "Valid data is should be preserved")
   403  	assert.Equal(t, "test3", references[0].Name().String(), "Name should be set and equal test3")
   404  	assert.Equal(t, "test3", references[0].Target().String(), "Target should be set and equal test3")
   405  	assert.Equal(t, "test4", references[1].Name().String(), "Name should be set and equal test4")
   406  	assert.Equal(t, "test4", references[1].Target().String(), "Target should be set and equal test4")
   407  }
   408  
   409  func TestTryLockGitRefCache_OwnershipFlows(t *testing.T) {
   410  	fixtures := newFixtures()
   411  	t.Cleanup(fixtures.mockCache.StopRedisCallback)
   412  	cache := fixtures.cache
   413  	utilCache := cache.cache
   414  	var references []*plumbing.Reference
   415  	// Test setting the lock
   416  	_, err := cache.TryLockGitRefCache("my-repo-url", "my-lock-id", &references)
   417  	fixtures.mockCache.AssertCacheCalledTimes(t, &mocks.CacheCallCounts{ExternalSets: 1, ExternalGets: 1})
   418  	require.NoError(t, err)
   419  	var output [][2]string
   420  	key := "git-refs|" + "my-repo-url"
   421  	err = utilCache.GetItem(key, &output)
   422  	fixtures.mockCache.AssertCacheCalledTimes(t, &mocks.CacheCallCounts{ExternalSets: 1, ExternalGets: 2})
   423  	require.NoError(t, err)
   424  	assert.Equal(t, "locked", output[0][0], "The lock should be set")
   425  	assert.Equal(t, "my-lock-id", output[0][1], "The lock should be set to the provided lock id")
   426  	// Test not being able to overwrite the lock
   427  	_, err = cache.TryLockGitRefCache("my-repo-url", "other-lock-id", &references)
   428  	fixtures.mockCache.AssertCacheCalledTimes(t, &mocks.CacheCallCounts{ExternalSets: 2, ExternalGets: 3})
   429  	require.NoError(t, err)
   430  	err = utilCache.GetItem(key, &output)
   431  	fixtures.mockCache.AssertCacheCalledTimes(t, &mocks.CacheCallCounts{ExternalSets: 2, ExternalGets: 4})
   432  	require.NoError(t, err)
   433  	assert.Equal(t, "locked", output[0][0], "The lock should not have changed")
   434  	assert.Equal(t, "my-lock-id", output[0][1], "The lock should not have changed")
   435  	// Test can overwrite once there is nothing set
   436  	err = utilCache.SetItem(key, [][2]string{}, &cacheutil.CacheActionOpts{Expiration: 0, Delete: true})
   437  	fixtures.mockCache.AssertCacheCalledTimes(t, &mocks.CacheCallCounts{ExternalSets: 2, ExternalGets: 4, ExternalDeletes: 1})
   438  	require.NoError(t, err)
   439  	_, err = cache.TryLockGitRefCache("my-repo-url", "other-lock-id", &references)
   440  	fixtures.mockCache.AssertCacheCalledTimes(t, &mocks.CacheCallCounts{ExternalSets: 3, ExternalGets: 5, ExternalDeletes: 1})
   441  	require.NoError(t, err)
   442  	err = utilCache.GetItem(key, &output)
   443  	require.NoError(t, err)
   444  	assert.Equal(t, "locked", output[0][0], "The lock should be set")
   445  	assert.Equal(t, "other-lock-id", output[0][1], "The lock id should have changed to other-lock-id")
   446  	fixtures.mockCache.AssertCacheCalledTimes(t, &mocks.CacheCallCounts{ExternalSets: 3, ExternalGets: 6, ExternalDeletes: 1})
   447  }
   448  
   449  func TestGetOrLockGitReferences(t *testing.T) {
   450  	t.Run("Test cache lock get lock", func(t *testing.T) {
   451  		fixtures := newFixtures()
   452  		t.Cleanup(fixtures.mockCache.StopRedisCallback)
   453  		cache := fixtures.cache
   454  		var references []*plumbing.Reference
   455  		lockId, err := cache.GetOrLockGitReferences("test-repo", "test-lock-id", &references)
   456  		require.NoError(t, err)
   457  		assert.Equal(t, "test-lock-id", lockId)
   458  		assert.NotEmpty(t, lockId, "Lock id should be set")
   459  		fixtures.mockCache.AssertCacheCalledTimes(t, &mocks.CacheCallCounts{ExternalSets: 1, ExternalGets: 2})
   460  	})
   461  
   462  	t.Run("Test cache lock, cache hit local", func(t *testing.T) {
   463  		fixtures := newFixtures()
   464  		t.Cleanup(fixtures.mockCache.StopRedisCallback)
   465  		cache := fixtures.cache
   466  		err := cache.SetGitReferences("test-repo", *GitRefCacheItemToReferences([][2]string{{"test-repo", "ref: test"}}))
   467  		require.NoError(t, err)
   468  		var references []*plumbing.Reference
   469  		lockId, err := cache.GetOrLockGitReferences("test-repo", "test-lock-id", &references)
   470  		require.NoError(t, err)
   471  		assert.NotEqual(t, "test-lock-id", lockId)
   472  		assert.Empty(t, lockId, "Lock id should not be set")
   473  		assert.Equal(t, "test-repo", references[0].Name().String())
   474  		assert.Equal(t, "test", references[0].Target().String())
   475  		fixtures.mockCache.AssertCacheCalledTimes(t, &mocks.CacheCallCounts{ExternalSets: 1, ExternalGets: 1})
   476  	})
   477  
   478  	t.Run("Test cache lock, cache hit remote", func(t *testing.T) {
   479  		fixtures := newFixtures()
   480  		t.Cleanup(fixtures.mockCache.StopRedisCallback)
   481  		cache := fixtures.cache
   482  		err := fixtures.cache.cache.SetItem(
   483  			"git-refs|test-repo",
   484  			[][2]string{{"test-repo", "ref: test"}},
   485  			&cacheutil.CacheActionOpts{
   486  				Expiration: 30 * time.Second,
   487  			})
   488  		require.NoError(t, err)
   489  		var references []*plumbing.Reference
   490  		lockId, err := cache.GetOrLockGitReferences("test-repo", "test-lock-id", &references)
   491  		require.NoError(t, err)
   492  		assert.NotEqual(t, "test-lock-id", lockId)
   493  		assert.Empty(t, lockId, "Lock id should not be set")
   494  		assert.Equal(t, "test-repo", references[0].Name().String())
   495  		assert.Equal(t, "test", references[0].Target().String())
   496  		fixtures.mockCache.AssertCacheCalledTimes(t, &mocks.CacheCallCounts{ExternalSets: 1, ExternalGets: 1})
   497  	})
   498  
   499  	t.Run("Test miss, populated by external", func(t *testing.T) {
   500  		// Tests the case where another process populates the external cache when trying
   501  		// to obtain the lock
   502  		fixtures := newFixtures()
   503  		t.Cleanup(fixtures.mockCache.StopRedisCallback)
   504  		cache := fixtures.cache
   505  		fixtures.mockCache.RedisClient.On("Get", mock.Anything, mock.Anything).Unset()
   506  		fixtures.mockCache.RedisClient.On("Get", mock.Anything, mock.Anything).Return(cacheutil.ErrCacheMiss).Once().Run(func(_ mock.Arguments) {
   507  			err := cache.SetGitReferences("test-repo", *GitRefCacheItemToReferences([][2]string{{"test-repo", "ref: test"}}))
   508  			require.NoError(t, err)
   509  		}).On("Get", mock.Anything, mock.Anything).Return(nil)
   510  		var references []*plumbing.Reference
   511  		lockId, err := cache.GetOrLockGitReferences("test-repo", "test-lock-id", &references)
   512  		require.NoError(t, err)
   513  		assert.NotEqual(t, "test-lock-id", lockId)
   514  		assert.Empty(t, lockId, "Lock id should not be set")
   515  		fixtures.mockCache.AssertCacheCalledTimes(t, &mocks.CacheCallCounts{ExternalSets: 2, ExternalGets: 2})
   516  	})
   517  
   518  	t.Run("Test cache lock timeout", func(t *testing.T) {
   519  		fixtures := newFixtures()
   520  		t.Cleanup(fixtures.mockCache.StopRedisCallback)
   521  		cache := fixtures.cache
   522  		// Create conditions for cache hit, which would result in false on updateCache if we weren't reaching the timeout
   523  		err := cache.SetGitReferences("test-repo", *GitRefCacheItemToReferences([][2]string{{"test-repo", "ref: test"}}))
   524  		require.NoError(t, err)
   525  		cache.revisionCacheLockTimeout = -1 * time.Second
   526  		var references []*plumbing.Reference
   527  		lockId, err := cache.GetOrLockGitReferences("test-repo", "test-lock-id", &references)
   528  		require.NoError(t, err)
   529  		assert.Equal(t, "test-lock-id", lockId)
   530  		assert.NotEmpty(t, lockId, "Lock id should be set")
   531  		cache.revisionCacheLockTimeout = 10 * time.Second
   532  		fixtures.mockCache.AssertCacheCalledTimes(t, &mocks.CacheCallCounts{ExternalSets: 1})
   533  	})
   534  
   535  	t.Run("Test cache lock error", func(t *testing.T) {
   536  		fixtures := newFixtures()
   537  		t.Cleanup(fixtures.mockCache.StopRedisCallback)
   538  		cache := fixtures.cache
   539  		fixtures.cache.revisionCacheLockTimeout = 10 * time.Second
   540  		fixtures.mockCache.RedisClient.On("Set", mock.Anything).Unset()
   541  		fixtures.mockCache.RedisClient.On("Set", mock.Anything).Return(errors.New("test cache error")).Once().
   542  			On("Set", mock.Anything).Return(nil)
   543  		var references []*plumbing.Reference
   544  		lockId, err := cache.GetOrLockGitReferences("test-repo", "test-lock-id", &references)
   545  		require.NoError(t, err)
   546  		assert.Equal(t, "test-lock-id", lockId)
   547  		assert.NotEmpty(t, lockId, "Lock id should be set")
   548  		fixtures.mockCache.RedisClient.AssertNumberOfCalls(t, "Set", 2)
   549  		fixtures.mockCache.RedisClient.AssertNumberOfCalls(t, "Get", 4)
   550  	})
   551  }
   552  
   553  func TestUnlockGitReferences(t *testing.T) {
   554  	fixtures := newFixtures()
   555  	t.Cleanup(fixtures.mockCache.StopRedisCallback)
   556  	cache := fixtures.cache
   557  
   558  	t.Run("Test not locked", func(t *testing.T) {
   559  		err := cache.UnlockGitReferences("test-repo", "")
   560  		assert.ErrorContains(t, err, "key is missing")
   561  	})
   562  
   563  	t.Run("Test unlock", func(t *testing.T) {
   564  		// Get lock
   565  		var references []*plumbing.Reference
   566  		lockId, err := cache.GetOrLockGitReferences("test-repo", "test-lock-id", &references)
   567  		require.NoError(t, err)
   568  		assert.Equal(t, "test-lock-id", lockId)
   569  		assert.NotEmpty(t, lockId, "Lock id should be set")
   570  		// Release lock
   571  		err = cache.UnlockGitReferences("test-repo", lockId)
   572  		require.NoError(t, err)
   573  	})
   574  }
   575  
   576  func TestSetHelmIndex(t *testing.T) {
   577  	t.Run("SetHelmIndex with valid data", func(t *testing.T) {
   578  		fixtures := newFixtures()
   579  		t.Cleanup(fixtures.mockCache.StopRedisCallback)
   580  		err := fixtures.cache.SetHelmIndex("test-repo", []byte("test-data"))
   581  		require.NoError(t, err)
   582  		fixtures.mockCache.AssertCacheCalledTimes(t, &mocks.CacheCallCounts{ExternalSets: 1})
   583  	})
   584  	t.Run("SetHelmIndex with nil", func(t *testing.T) {
   585  		fixtures := newFixtures()
   586  		t.Cleanup(fixtures.mockCache.StopRedisCallback)
   587  		err := fixtures.cache.SetHelmIndex("test-repo", nil)
   588  		require.Error(t, err, "nil data should not be cached")
   589  		var indexData []byte
   590  		err = fixtures.cache.GetHelmIndex("test-repo", &indexData)
   591  		require.Error(t, err)
   592  		fixtures.mockCache.AssertCacheCalledTimes(t, &mocks.CacheCallCounts{ExternalGets: 1})
   593  	})
   594  }
   595  
   596  func TestRevisionChartDetails(t *testing.T) {
   597  	t.Run("GetRevisionChartDetails cache miss", func(t *testing.T) {
   598  		fixtures := newFixtures()
   599  		t.Cleanup(fixtures.mockCache.StopRedisCallback)
   600  		details, err := fixtures.cache.GetRevisionChartDetails("test-repo", "test-revision", "v1.0.0")
   601  		require.ErrorIs(t, err, ErrCacheMiss)
   602  		assert.Equal(t, &v1alpha1.ChartDetails{}, details)
   603  		fixtures.mockCache.AssertCacheCalledTimes(t, &mocks.CacheCallCounts{ExternalGets: 1})
   604  	})
   605  	t.Run("GetRevisionChartDetails cache miss local", func(t *testing.T) {
   606  		fixtures := newFixtures()
   607  		t.Cleanup(fixtures.mockCache.StopRedisCallback)
   608  		cache := fixtures.cache
   609  		expectedItem := &v1alpha1.ChartDetails{
   610  			Description: "test-chart",
   611  			Home:        "v1.0.0",
   612  			Maintainers: []string{"test-maintainer"},
   613  		}
   614  		err := cache.cache.SetItem(
   615  			revisionChartDetailsKey("test-repo", "test-revision", "v1.0.0"),
   616  			expectedItem,
   617  			&cacheutil.CacheActionOpts{Expiration: 30 * time.Second})
   618  		require.NoError(t, err)
   619  		details, err := fixtures.cache.GetRevisionChartDetails("test-repo", "test-revision", "v1.0.0")
   620  		require.NoError(t, err)
   621  		assert.Equal(t, expectedItem, details)
   622  		fixtures.mockCache.AssertCacheCalledTimes(t, &mocks.CacheCallCounts{ExternalGets: 1, ExternalSets: 1})
   623  	})
   624  
   625  	t.Run("GetRevisionChartDetails cache hit local", func(t *testing.T) {
   626  		fixtures := newFixtures()
   627  		t.Cleanup(fixtures.mockCache.StopRedisCallback)
   628  		cache := fixtures.cache
   629  		expectedItem := &v1alpha1.ChartDetails{
   630  			Description: "test-chart",
   631  			Home:        "v1.0.0",
   632  			Maintainers: []string{"test-maintainer"},
   633  		}
   634  		err := cache.cache.SetItem(
   635  			revisionChartDetailsKey("test-repo", "test-revision", "v1.0.0"),
   636  			expectedItem,
   637  			&cacheutil.CacheActionOpts{Expiration: 30 * time.Second})
   638  		require.NoError(t, err)
   639  		details, err := fixtures.cache.GetRevisionChartDetails("test-repo", "test-revision", "v1.0.0")
   640  		require.NoError(t, err)
   641  		assert.Equal(t, expectedItem, details)
   642  		fixtures.mockCache.AssertCacheCalledTimes(t, &mocks.CacheCallCounts{ExternalGets: 1, ExternalSets: 1})
   643  	})
   644  
   645  	t.Run("SetRevisionChartDetails", func(t *testing.T) {
   646  		fixtures := newFixtures()
   647  		t.Cleanup(fixtures.mockCache.StopRedisCallback)
   648  		expectedItem := &v1alpha1.ChartDetails{
   649  			Description: "test-chart",
   650  			Home:        "v1.0.0",
   651  			Maintainers: []string{"test-maintainer"},
   652  		}
   653  		err := fixtures.cache.SetRevisionChartDetails("test-repo", "test-revision", "v1.0.0", expectedItem)
   654  		require.NoError(t, err)
   655  		details, err := fixtures.cache.GetRevisionChartDetails("test-repo", "test-revision", "v1.0.0")
   656  		require.NoError(t, err)
   657  		assert.Equal(t, expectedItem, details)
   658  		fixtures.mockCache.AssertCacheCalledTimes(t, &mocks.CacheCallCounts{ExternalGets: 1, ExternalSets: 1})
   659  	})
   660  }
   661  
   662  func TestGetGitDirectories(t *testing.T) {
   663  	t.Run("GetGitDirectories cache miss", func(t *testing.T) {
   664  		fixtures := newFixtures()
   665  		t.Cleanup(fixtures.mockCache.StopRedisCallback)
   666  		directories, err := fixtures.cache.GetGitDirectories("test-repo", "test-revision")
   667  		require.ErrorIs(t, err, ErrCacheMiss)
   668  		assert.Empty(t, directories)
   669  		fixtures.mockCache.AssertCacheCalledTimes(t, &mocks.CacheCallCounts{ExternalGets: 1})
   670  	})
   671  	t.Run("GetGitDirectories cache miss local", func(t *testing.T) {
   672  		fixtures := newFixtures()
   673  		t.Cleanup(fixtures.mockCache.StopRedisCallback)
   674  		cache := fixtures.cache
   675  		expectedItem := []string{"test/dir", "test/dir2"}
   676  		err := cache.cache.SetItem(
   677  			gitDirectoriesKey("test-repo", "test-revision"),
   678  			expectedItem,
   679  			&cacheutil.CacheActionOpts{Expiration: 30 * time.Second})
   680  		require.NoError(t, err)
   681  		directories, err := fixtures.cache.GetGitDirectories("test-repo", "test-revision")
   682  		require.NoError(t, err)
   683  		assert.Equal(t, expectedItem, directories)
   684  		fixtures.mockCache.AssertCacheCalledTimes(t, &mocks.CacheCallCounts{ExternalGets: 1, ExternalSets: 1})
   685  	})
   686  
   687  	t.Run("GetGitDirectories cache hit local", func(t *testing.T) {
   688  		fixtures := newFixtures()
   689  		t.Cleanup(fixtures.mockCache.StopRedisCallback)
   690  		cache := fixtures.cache
   691  		expectedItem := []string{"test/dir", "test/dir2"}
   692  		err := cache.cache.SetItem(
   693  			gitDirectoriesKey("test-repo", "test-revision"),
   694  			expectedItem,
   695  			&cacheutil.CacheActionOpts{Expiration: 30 * time.Second})
   696  		require.NoError(t, err)
   697  		directories, err := fixtures.cache.GetGitDirectories("test-repo", "test-revision")
   698  		require.NoError(t, err)
   699  		assert.Equal(t, expectedItem, directories)
   700  		fixtures.mockCache.AssertCacheCalledTimes(t, &mocks.CacheCallCounts{ExternalGets: 1, ExternalSets: 1})
   701  	})
   702  
   703  	t.Run("SetGitDirectories", func(t *testing.T) {
   704  		fixtures := newFixtures()
   705  		t.Cleanup(fixtures.mockCache.StopRedisCallback)
   706  		expectedItem := []string{"test/dir", "test/dir2"}
   707  		err := fixtures.cache.SetGitDirectories("test-repo", "test-revision", expectedItem)
   708  		require.NoError(t, err)
   709  		directories, err := fixtures.cache.GetGitDirectories("test-repo", "test-revision")
   710  		require.NoError(t, err)
   711  		assert.Equal(t, expectedItem, directories)
   712  		fixtures.mockCache.AssertCacheCalledTimes(t, &mocks.CacheCallCounts{ExternalGets: 1, ExternalSets: 1})
   713  	})
   714  }
   715  
   716  func TestGetGitFiles(t *testing.T) {
   717  	t.Run("GetGitFiles cache miss", func(t *testing.T) {
   718  		fixtures := newFixtures()
   719  		t.Cleanup(fixtures.mockCache.StopRedisCallback)
   720  		directories, err := fixtures.cache.GetGitFiles("test-repo", "test-revision", "*.json")
   721  		require.ErrorIs(t, err, ErrCacheMiss)
   722  		assert.Empty(t, directories)
   723  		fixtures.mockCache.AssertCacheCalledTimes(t, &mocks.CacheCallCounts{ExternalGets: 1})
   724  	})
   725  	t.Run("GetGitFiles cache hit", func(t *testing.T) {
   726  		fixtures := newFixtures()
   727  		t.Cleanup(fixtures.mockCache.StopRedisCallback)
   728  		cache := fixtures.cache
   729  		expectedItem := map[string][]byte{"test/file.json": []byte("\"test\":\"contents\""), "test/file1.json": []byte("\"test1\":\"contents1\"")}
   730  		err := cache.cache.SetItem(
   731  			gitFilesKey("test-repo", "test-revision", "*.json"),
   732  			expectedItem,
   733  			&cacheutil.CacheActionOpts{Expiration: 30 * time.Second})
   734  		require.NoError(t, err)
   735  		files, err := fixtures.cache.GetGitFiles("test-repo", "test-revision", "*.json")
   736  		require.NoError(t, err)
   737  		assert.Equal(t, expectedItem, files)
   738  		fixtures.mockCache.AssertCacheCalledTimes(t, &mocks.CacheCallCounts{ExternalGets: 1, ExternalSets: 1})
   739  	})
   740  
   741  	t.Run("SetGitFiles", func(t *testing.T) {
   742  		fixtures := newFixtures()
   743  		t.Cleanup(fixtures.mockCache.StopRedisCallback)
   744  		expectedItem := map[string][]byte{"test/file.json": []byte("\"test\":\"contents\""), "test/file1.json": []byte("\"test1\":\"contents1\"")}
   745  		err := fixtures.cache.SetGitFiles("test-repo", "test-revision", "*.json", expectedItem)
   746  		require.NoError(t, err)
   747  		files, err := fixtures.cache.GetGitFiles("test-repo", "test-revision", "*.json")
   748  		require.NoError(t, err)
   749  		assert.Equal(t, expectedItem, files)
   750  		fixtures.mockCache.AssertCacheCalledTimes(t, &mocks.CacheCallCounts{ExternalGets: 1, ExternalSets: 1})
   751  	})
   752  }