navidrome/utils/cache/simple_cache_test.go
Deluan Quintão 76042ba173
feat(ui): add Now Playing panel for admins (#4209)
* feat(ui): add Now Playing panel and integrate now playing count updates

Signed-off-by: Deluan <deluan@navidrome.org>

* fix: check return value in test to satisfy linter

* fix: format React code with prettier

* fix: resolve race condition in play tracker test

* fix: log error when fetching now playing data fails

Signed-off-by: Deluan <deluan@navidrome.org>

* feat(ui): refactor Now Playing panel with new components and error handling

Signed-off-by: Deluan <deluan@navidrome.org>

* fix(ui): adjust padding and height in Now Playing panel for improved layout

Signed-off-by: Deluan <deluan@navidrome.org>

* fix(cache): add automatic cleanup to prevent goroutine leak on cache garbage collection

Signed-off-by: Deluan <deluan@navidrome.org>

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2025-06-10 17:22:13 -04:00

162 lines
4.2 KiB
Go

package cache
import (
"errors"
"fmt"
"time"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("SimpleCache", func() {
var (
cache SimpleCache[string, string]
)
BeforeEach(func() {
cache = NewSimpleCache[string, string]()
})
Describe("Add and Get", func() {
It("should add and retrieve a value", func() {
err := cache.Add("key", "value")
Expect(err).NotTo(HaveOccurred())
value, err := cache.Get("key")
Expect(err).NotTo(HaveOccurred())
Expect(value).To(Equal("value"))
})
})
Describe("AddWithTTL and Get", func() {
It("should add a value with TTL and retrieve it", func() {
err := cache.AddWithTTL("key", "value", 1*time.Minute)
Expect(err).NotTo(HaveOccurred())
value, err := cache.Get("key")
Expect(err).NotTo(HaveOccurred())
Expect(value).To(Equal("value"))
})
It("should not retrieve a value after its TTL has expired", func() {
err := cache.AddWithTTL("key", "value", 10*time.Millisecond)
Expect(err).NotTo(HaveOccurred())
time.Sleep(50 * time.Millisecond)
_, err = cache.Get("key")
Expect(err).To(HaveOccurred())
})
})
Describe("GetWithLoader", func() {
It("should retrieve a value using the loader function", func() {
loader := func(key string) (string, time.Duration, error) {
return fmt.Sprintf("%s=value", key), 1 * time.Minute, nil
}
value, err := cache.GetWithLoader("key", loader)
Expect(err).NotTo(HaveOccurred())
Expect(value).To(Equal("key=value"))
})
It("should return the error returned by the loader function", func() {
loader := func(key string) (string, time.Duration, error) {
return "", 0, errors.New("some error")
}
_, err := cache.GetWithLoader("key", loader)
Expect(err).To(HaveOccurred())
})
})
Describe("Keys and Values", func() {
It("should return all keys and all values", func() {
err := cache.Add("key1", "value1")
Expect(err).NotTo(HaveOccurred())
err = cache.Add("key2", "value2")
Expect(err).NotTo(HaveOccurred())
keys := cache.Keys()
Expect(keys).To(ConsistOf("key1", "key2"))
values := cache.Values()
Expect(values).To(ConsistOf("value1", "value2"))
})
Context("when there are expired items in the cache", func() {
It("should not return expired items", func() {
Expect(cache.Add("key0", "value0")).To(Succeed())
for i := 1; i <= 3; i++ {
err := cache.AddWithTTL(fmt.Sprintf("key%d", i), fmt.Sprintf("value%d", i), 10*time.Millisecond)
Expect(err).NotTo(HaveOccurred())
}
time.Sleep(50 * time.Millisecond)
Expect(cache.Keys()).To(ConsistOf("key0"))
Expect(cache.Values()).To(ConsistOf("value0"))
})
})
})
Describe("Options", func() {
Context("when size limit is set", func() {
BeforeEach(func() {
cache = NewSimpleCache[string, string](Options{
SizeLimit: 2,
})
})
It("should drop the oldest item when the size limit is reached", func() {
for i := 1; i <= 3; i++ {
err := cache.Add(fmt.Sprintf("key%d", i), fmt.Sprintf("value%d", i))
Expect(err).NotTo(HaveOccurred())
}
Expect(cache.Keys()).To(ConsistOf("key2", "key3"))
})
})
Context("when default TTL is set", func() {
BeforeEach(func() {
cache = NewSimpleCache[string, string](Options{
DefaultTTL: 10 * time.Millisecond,
})
})
It("should expire items after the default TTL", func() {
Expect(cache.AddWithTTL("key0", "value0", 1*time.Minute)).To(Succeed())
for i := 1; i <= 3; i++ {
err := cache.Add(fmt.Sprintf("key%d", i), fmt.Sprintf("value%d", i))
Expect(err).NotTo(HaveOccurred())
}
time.Sleep(50 * time.Millisecond)
for i := 1; i <= 3; i++ {
_, err := cache.Get(fmt.Sprintf("key%d", i))
Expect(err).To(HaveOccurred())
}
Expect(cache.Get("key0")).To(Equal("value0"))
})
})
Describe("OnExpiration", func() {
It("should call callback when item expires", func() {
cache = NewSimpleCache[string, string]()
expired := make(chan struct{})
cache.OnExpiration(func(k, v string) { close(expired) })
Expect(cache.AddWithTTL("key", "value", 10*time.Millisecond)).To(Succeed())
select {
case <-expired:
case <-time.After(100 * time.Millisecond):
Fail("expiration callback not called")
}
})
})
})
})