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 }