mirror of
https://github.com/navidrome/navidrome.git
synced 2026-06-02 07:01:36 +00:00
* feat(conf): add Dir type with lazy directory creation Introduces the Dir type that wraps a directory path string and defers os.MkdirAll until the first call to Path() or MustPath(), using sync.Once to ensure the creation happens exactly once. Implements fmt.Stringer, encoding.TextMarshaler, and encoding.TextUnmarshaler for config integration. Includes Ginkgo/Gomega tests covering all methods and error paths. * refactor(conf): replace eager dir creation with lazy Dir type Change DataFolder, CacheFolder, Plugins.Folder, and Backup.Path from string to Dir. Remove all os.MkdirAll calls from Load() so directories are created lazily on first Path()/MustPath() call. Artwork folder creation was already handled at point-of-use in image_upload.go. Add SnapshotConfig() to conf package for safe test config save/restore that avoids copying sync.Once inside Dir fields. Fix copy-lock vet warning in nativeapi/config.go by marshalling pointer instead of value. * refactor(conf): migrate tests and db init to lazy Dir type Update all test files to use conf.NewDir() for Dir field assignments. Ensure DataFolder is created lazily when the database is first opened in db.Db(). Remove eager directory creation from conf.Load() tests. * fix(conf): address review findings for Dir type - Use os.ModePerm for DataFolder/CacheFolder (was 0700, should match original behavior). Add NewDirWithPerm for PluginsFolder (0700). - Use Path() instead of MustPath() in db.Prune() to avoid logFatal from background cron job. - Panic on marshal/unmarshal errors in SnapshotConfig (test helper). - Clean up redundant String()/MustPath() calls in plugin manager. - Remove dead code in dir_test.go. Signed-off-by: Deluan <deluan@navidrome.org> * fix(conf): add GoString to Dir for clean config dump output Implement fmt.GoStringer on Dir so pretty.Sprintf shows the path string instead of internal struct fields (sync.Once, perm, err). Also add TODO comment to configtest about removing the indirection. * fix(dir): improve error logging in MustPath method Signed-off-by: Deluan <deluan@navidrome.org> * refactor(tests): remove redundant tests for unwritable DataFolder and CacheFolder Signed-off-by: Deluan <deluan@navidrome.org> * fix(conf): address PR review feedback - Ensure Plugins.Folder always uses 0700, even when user-configured (previously only the derived default got restrictive permissions). - Create LogFile parent directory before opening, so LogFile paths inside a not-yet-created DataFolder work correctly. --------- Signed-off-by: Deluan <deluan@navidrome.org>
602 lines
16 KiB
Go
602 lines
16 KiB
Go
//go:build !windows
|
|
|
|
package plugins
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"github.com/navidrome/navidrome/conf"
|
|
"github.com/navidrome/navidrome/conf/configtest"
|
|
"github.com/navidrome/navidrome/model"
|
|
"github.com/navidrome/navidrome/tests"
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
)
|
|
|
|
var _ = Describe("CacheService", func() {
|
|
var service *cacheServiceImpl
|
|
var ctx context.Context
|
|
|
|
BeforeEach(func() {
|
|
ctx = context.Background()
|
|
service = newCacheService("test_plugin")
|
|
})
|
|
|
|
AfterEach(func() {
|
|
if service != nil {
|
|
service.Close()
|
|
}
|
|
})
|
|
|
|
Describe("getTTL", func() {
|
|
It("returns default TTL when seconds is 0", func() {
|
|
ttl := service.getTTL(0)
|
|
Expect(ttl).To(Equal(defaultCacheTTL))
|
|
})
|
|
|
|
It("returns default TTL when seconds is negative", func() {
|
|
ttl := service.getTTL(-10)
|
|
Expect(ttl).To(Equal(defaultCacheTTL))
|
|
})
|
|
|
|
It("returns correct duration when seconds is positive", func() {
|
|
ttl := service.getTTL(60)
|
|
Expect(ttl).To(Equal(time.Minute))
|
|
})
|
|
})
|
|
|
|
Describe("Plugin Isolation", func() {
|
|
It("isolates keys between plugins", func() {
|
|
service1 := newCacheService("plugin1")
|
|
defer service1.Close()
|
|
service2 := newCacheService("plugin2")
|
|
defer service2.Close()
|
|
|
|
// Both plugins set same key
|
|
err := service1.SetString(ctx, "shared", "value1", 0)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
err = service2.SetString(ctx, "shared", "value2", 0)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
// Each plugin should get their own value
|
|
val1, exists1, err := service1.GetString(ctx, "shared")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(exists1).To(BeTrue())
|
|
Expect(val1).To(Equal("value1"))
|
|
|
|
val2, exists2, err := service2.GetString(ctx, "shared")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(exists2).To(BeTrue())
|
|
Expect(val2).To(Equal("value2"))
|
|
})
|
|
})
|
|
|
|
Describe("String Operations", func() {
|
|
It("sets and gets a string value", func() {
|
|
err := service.SetString(ctx, "string_key", "test_value", 300)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
value, exists, err := service.GetString(ctx, "string_key")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(exists).To(BeTrue())
|
|
Expect(value).To(Equal("test_value"))
|
|
})
|
|
|
|
It("returns not exists for missing key", func() {
|
|
value, exists, err := service.GetString(ctx, "missing_key")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(exists).To(BeFalse())
|
|
Expect(value).To(Equal(""))
|
|
})
|
|
})
|
|
|
|
Describe("Integer Operations", func() {
|
|
It("sets and gets an integer value", func() {
|
|
err := service.SetInt(ctx, "int_key", 42, 300)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
value, exists, err := service.GetInt(ctx, "int_key")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(exists).To(BeTrue())
|
|
Expect(value).To(Equal(int64(42)))
|
|
})
|
|
|
|
It("returns not exists for missing key", func() {
|
|
value, exists, err := service.GetInt(ctx, "missing_int_key")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(exists).To(BeFalse())
|
|
Expect(value).To(Equal(int64(0)))
|
|
})
|
|
})
|
|
|
|
Describe("Float Operations", func() {
|
|
It("sets and gets a float value", func() {
|
|
err := service.SetFloat(ctx, "float_key", 3.14, 300)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
value, exists, err := service.GetFloat(ctx, "float_key")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(exists).To(BeTrue())
|
|
Expect(value).To(Equal(3.14))
|
|
})
|
|
|
|
It("returns not exists for missing key", func() {
|
|
value, exists, err := service.GetFloat(ctx, "missing_float_key")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(exists).To(BeFalse())
|
|
Expect(value).To(Equal(float64(0)))
|
|
})
|
|
})
|
|
|
|
Describe("Bytes Operations", func() {
|
|
It("sets and gets a bytes value", func() {
|
|
byteData := []byte("hello world")
|
|
err := service.SetBytes(ctx, "bytes_key", byteData, 300)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
value, exists, err := service.GetBytes(ctx, "bytes_key")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(exists).To(BeTrue())
|
|
Expect(value).To(Equal(byteData))
|
|
})
|
|
|
|
It("returns not exists for missing key", func() {
|
|
value, exists, err := service.GetBytes(ctx, "missing_bytes_key")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(exists).To(BeFalse())
|
|
Expect(value).To(BeNil())
|
|
})
|
|
})
|
|
|
|
Describe("Type mismatch handling", func() {
|
|
It("returns not exists when type doesn't match the getter", func() {
|
|
// Set string
|
|
err := service.SetString(ctx, "mixed_key", "string value", 0)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
// Try to get as int
|
|
value, exists, err := service.GetInt(ctx, "mixed_key")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(exists).To(BeFalse())
|
|
Expect(value).To(Equal(int64(0)))
|
|
})
|
|
|
|
It("returns not exists when getting string as float", func() {
|
|
err := service.SetString(ctx, "str_as_float", "not a float", 0)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
value, exists, err := service.GetFloat(ctx, "str_as_float")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(exists).To(BeFalse())
|
|
Expect(value).To(Equal(float64(0)))
|
|
})
|
|
|
|
It("returns not exists when getting int as bytes", func() {
|
|
err := service.SetInt(ctx, "int_as_bytes", 123, 0)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
value, exists, err := service.GetBytes(ctx, "int_as_bytes")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(exists).To(BeFalse())
|
|
Expect(value).To(BeNil())
|
|
})
|
|
})
|
|
|
|
Describe("Has Operation", func() {
|
|
It("returns true for existing key", func() {
|
|
err := service.SetString(ctx, "existing_key", "exists", 0)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
exists, err := service.Has(ctx, "existing_key")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(exists).To(BeTrue())
|
|
})
|
|
|
|
It("returns false for non-existing key", func() {
|
|
exists, err := service.Has(ctx, "non_existing_key")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(exists).To(BeFalse())
|
|
})
|
|
})
|
|
|
|
Describe("Remove Operation", func() {
|
|
It("removes a value from the cache", func() {
|
|
// Set a value
|
|
err := service.SetString(ctx, "remove_key", "to be removed", 0)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
// Verify it exists
|
|
exists, err := service.Has(ctx, "remove_key")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(exists).To(BeTrue())
|
|
|
|
// Remove it
|
|
err = service.Remove(ctx, "remove_key")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
// Verify it's gone
|
|
exists, err = service.Has(ctx, "remove_key")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(exists).To(BeFalse())
|
|
})
|
|
|
|
It("does not error when removing non-existing key", func() {
|
|
err := service.Remove(ctx, "never_existed")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
})
|
|
})
|
|
|
|
Describe("TTL Behavior", func() {
|
|
It("uses default TTL when 0 is provided", func() {
|
|
err := service.SetString(ctx, "default_ttl", "value", 0)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
// Value should exist immediately
|
|
exists, err := service.Has(ctx, "default_ttl")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(exists).To(BeTrue())
|
|
})
|
|
|
|
It("uses custom TTL when provided", func() {
|
|
err := service.SetString(ctx, "custom_ttl", "value", 300)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
// Value should exist immediately
|
|
exists, err := service.Has(ctx, "custom_ttl")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(exists).To(BeTrue())
|
|
})
|
|
})
|
|
|
|
Describe("Close", func() {
|
|
It("removes all cache entries for the plugin", func() {
|
|
// Use a dedicated service for this test
|
|
closeService := newCacheService("close_test_plugin")
|
|
|
|
// Set multiple values
|
|
err := closeService.SetString(ctx, "key1", "value1", 0)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
err = closeService.SetInt(ctx, "key2", 42, 0)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
err = closeService.SetFloat(ctx, "key3", 3.14, 0)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
// Verify they exist
|
|
exists, _ := closeService.Has(ctx, "key1")
|
|
Expect(exists).To(BeTrue())
|
|
exists, _ = closeService.Has(ctx, "key2")
|
|
Expect(exists).To(BeTrue())
|
|
exists, _ = closeService.Has(ctx, "key3")
|
|
Expect(exists).To(BeTrue())
|
|
|
|
// Close the service
|
|
err = closeService.Close()
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
// All entries should be gone
|
|
exists, _ = closeService.Has(ctx, "key1")
|
|
Expect(exists).To(BeFalse())
|
|
exists, _ = closeService.Has(ctx, "key2")
|
|
Expect(exists).To(BeFalse())
|
|
exists, _ = closeService.Has(ctx, "key3")
|
|
Expect(exists).To(BeFalse())
|
|
})
|
|
|
|
It("does not affect other plugins' cache entries", func() {
|
|
// Create two services for different plugins
|
|
service1 := newCacheService("plugin_close_test1")
|
|
service2 := newCacheService("plugin_close_test2")
|
|
defer service2.Close()
|
|
|
|
// Set values for both plugins
|
|
err := service1.SetString(ctx, "key", "value1", 0)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
err = service2.SetString(ctx, "key", "value2", 0)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
// Close only service1
|
|
err = service1.Close()
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
// service1's key should be gone
|
|
exists, _ := service1.Has(ctx, "key")
|
|
Expect(exists).To(BeFalse())
|
|
|
|
// service2's key should still exist
|
|
exists, _ = service2.Has(ctx, "key")
|
|
Expect(exists).To(BeTrue())
|
|
})
|
|
})
|
|
})
|
|
|
|
var _ = Describe("CacheService Integration", Ordered, func() {
|
|
var (
|
|
manager *Manager
|
|
tmpDir string
|
|
)
|
|
|
|
BeforeAll(func() {
|
|
var err error
|
|
tmpDir, err = os.MkdirTemp("", "cache-test-*")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
// Copy the test-cache-plugin
|
|
srcPath := filepath.Join(testdataDir, "test-cache-plugin"+PackageExtension)
|
|
destPath := filepath.Join(tmpDir, "test-cache-plugin"+PackageExtension)
|
|
data, err := os.ReadFile(srcPath)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
err = os.WriteFile(destPath, data, 0600)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
// Compute SHA256 for the plugin
|
|
hash := sha256.Sum256(data)
|
|
hashHex := hex.EncodeToString(hash[:])
|
|
|
|
// Setup config
|
|
DeferCleanup(configtest.SetupConfig())
|
|
conf.Server.Plugins.Enabled = true
|
|
conf.Server.Plugins.Folder = conf.NewDir(tmpDir)
|
|
conf.Server.Plugins.AutoReload = false
|
|
|
|
// Setup mock DataStore with pre-enabled plugin
|
|
mockPluginRepo := tests.CreateMockPluginRepo()
|
|
mockPluginRepo.Permitted = true
|
|
mockPluginRepo.SetData(model.Plugins{{
|
|
ID: "test-cache-plugin",
|
|
Path: destPath,
|
|
SHA256: hashHex,
|
|
Enabled: true,
|
|
}})
|
|
dataStore := &tests.MockDataStore{MockedPlugin: mockPluginRepo}
|
|
|
|
// Create and start manager
|
|
manager = &Manager{
|
|
plugins: make(map[string]*plugin),
|
|
ds: dataStore,
|
|
subsonicRouter: http.NotFoundHandler(),
|
|
}
|
|
err = manager.Start(GinkgoT().Context())
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
DeferCleanup(func() {
|
|
_ = manager.Stop()
|
|
_ = os.RemoveAll(tmpDir)
|
|
})
|
|
})
|
|
|
|
Describe("Plugin Loading", func() {
|
|
It("should load plugin with cache permission", func() {
|
|
manager.mu.RLock()
|
|
p, ok := manager.plugins["test-cache-plugin"]
|
|
manager.mu.RUnlock()
|
|
Expect(ok).To(BeTrue())
|
|
Expect(p.manifest.Permissions).ToNot(BeNil())
|
|
Expect(p.manifest.Permissions.Cache).ToNot(BeNil())
|
|
})
|
|
})
|
|
|
|
Describe("Cache Operations via Plugin", func() {
|
|
type testCacheInput struct {
|
|
Operation string `json:"operation"`
|
|
Key string `json:"key"`
|
|
StringVal string `json:"string_val,omitempty"`
|
|
IntVal int64 `json:"int_val,omitempty"`
|
|
FloatVal float64 `json:"float_val,omitempty"`
|
|
BytesVal []byte `json:"bytes_val,omitempty"`
|
|
TTLSeconds int64 `json:"ttl_seconds,omitempty"`
|
|
}
|
|
type testCacheOutput struct {
|
|
StringVal string `json:"string_val,omitempty"`
|
|
IntVal int64 `json:"int_val,omitempty"`
|
|
FloatVal float64 `json:"float_val,omitempty"`
|
|
BytesVal []byte `json:"bytes_val,omitempty"`
|
|
Exists bool `json:"exists,omitempty"`
|
|
Error *string `json:"error,omitempty"`
|
|
}
|
|
|
|
callTestCache := func(ctx context.Context, input testCacheInput) (*testCacheOutput, error) {
|
|
manager.mu.RLock()
|
|
p := manager.plugins["test-cache-plugin"]
|
|
manager.mu.RUnlock()
|
|
|
|
instance, err := p.instance(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer instance.Close(ctx)
|
|
|
|
inputBytes, _ := json.Marshal(input)
|
|
_, outputBytes, err := instance.Call("nd_test_cache", inputBytes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var output testCacheOutput
|
|
if err := json.Unmarshal(outputBytes, &output); err != nil {
|
|
return nil, err
|
|
}
|
|
if output.Error != nil {
|
|
return nil, errors.New(*output.Error)
|
|
}
|
|
return &output, nil
|
|
}
|
|
|
|
It("should set and get string value", func() {
|
|
ctx := GinkgoT().Context()
|
|
|
|
// Set string
|
|
_, err := callTestCache(ctx, testCacheInput{
|
|
Operation: "set_string",
|
|
Key: "test_string",
|
|
StringVal: "hello world",
|
|
TTLSeconds: 300,
|
|
})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
// Get string
|
|
output, err := callTestCache(ctx, testCacheInput{
|
|
Operation: "get_string",
|
|
Key: "test_string",
|
|
})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(output.Exists).To(BeTrue())
|
|
Expect(output.StringVal).To(Equal("hello world"))
|
|
})
|
|
|
|
It("should set and get integer value", func() {
|
|
ctx := GinkgoT().Context()
|
|
|
|
// Set int
|
|
_, err := callTestCache(ctx, testCacheInput{
|
|
Operation: "set_int",
|
|
Key: "test_int",
|
|
IntVal: 42,
|
|
TTLSeconds: 300,
|
|
})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
// Get int
|
|
output, err := callTestCache(ctx, testCacheInput{
|
|
Operation: "get_int",
|
|
Key: "test_int",
|
|
})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(output.Exists).To(BeTrue())
|
|
Expect(output.IntVal).To(Equal(int64(42)))
|
|
})
|
|
|
|
It("should set and get float value", func() {
|
|
ctx := GinkgoT().Context()
|
|
|
|
// Set float
|
|
_, err := callTestCache(ctx, testCacheInput{
|
|
Operation: "set_float",
|
|
Key: "test_float",
|
|
FloatVal: 3.14159,
|
|
TTLSeconds: 300,
|
|
})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
// Get float
|
|
output, err := callTestCache(ctx, testCacheInput{
|
|
Operation: "get_float",
|
|
Key: "test_float",
|
|
})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(output.Exists).To(BeTrue())
|
|
Expect(output.FloatVal).To(Equal(3.14159))
|
|
})
|
|
|
|
It("should set and get bytes value", func() {
|
|
ctx := GinkgoT().Context()
|
|
testBytes := []byte{0x01, 0x02, 0x03, 0x04}
|
|
|
|
// Set bytes
|
|
_, err := callTestCache(ctx, testCacheInput{
|
|
Operation: "set_bytes",
|
|
Key: "test_bytes",
|
|
BytesVal: testBytes,
|
|
TTLSeconds: 300,
|
|
})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
// Get bytes
|
|
output, err := callTestCache(ctx, testCacheInput{
|
|
Operation: "get_bytes",
|
|
Key: "test_bytes",
|
|
})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(output.Exists).To(BeTrue())
|
|
Expect(output.BytesVal).To(Equal(testBytes))
|
|
})
|
|
|
|
It("should handle binary data with null bytes through WASM", func() {
|
|
ctx := GinkgoT().Context()
|
|
|
|
// Binary data with null bytes, high bytes, and other edge cases
|
|
binaryData := []byte{0x00, 0x01, 0x02, 0xFF, 0xFE, 0x00, 0x80, 0x7F}
|
|
|
|
// Set binary bytes
|
|
_, err := callTestCache(ctx, testCacheInput{
|
|
Operation: "set_bytes",
|
|
Key: "binary_test",
|
|
BytesVal: binaryData,
|
|
TTLSeconds: 300,
|
|
})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
// Get binary bytes and verify exact match
|
|
output, err := callTestCache(ctx, testCacheInput{
|
|
Operation: "get_bytes",
|
|
Key: "binary_test",
|
|
})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(output.Exists).To(BeTrue())
|
|
Expect(output.BytesVal).To(Equal(binaryData))
|
|
})
|
|
|
|
It("should check if key exists", func() {
|
|
ctx := GinkgoT().Context()
|
|
|
|
// Set a value
|
|
_, err := callTestCache(ctx, testCacheInput{
|
|
Operation: "set_string",
|
|
Key: "exists_test",
|
|
StringVal: "value",
|
|
})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
// Check has
|
|
output, err := callTestCache(ctx, testCacheInput{
|
|
Operation: "has",
|
|
Key: "exists_test",
|
|
})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(output.Exists).To(BeTrue())
|
|
|
|
// Check non-existent
|
|
output, err = callTestCache(ctx, testCacheInput{
|
|
Operation: "has",
|
|
Key: "nonexistent",
|
|
})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(output.Exists).To(BeFalse())
|
|
})
|
|
|
|
It("should remove a key", func() {
|
|
ctx := GinkgoT().Context()
|
|
|
|
// Set a value
|
|
_, err := callTestCache(ctx, testCacheInput{
|
|
Operation: "set_string",
|
|
Key: "remove_test",
|
|
StringVal: "value",
|
|
})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
// Remove it
|
|
_, err = callTestCache(ctx, testCacheInput{
|
|
Operation: "remove",
|
|
Key: "remove_test",
|
|
})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
// Verify it's gone
|
|
output, err := callTestCache(ctx, testCacheInput{
|
|
Operation: "has",
|
|
Key: "remove_test",
|
|
})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(output.Exists).To(BeFalse())
|
|
})
|
|
})
|
|
})
|