Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 39 additions & 4 deletions internal/cache/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,24 @@ type CacheConfig struct {
AutoCache bool `json:"auto_cache"` // Automatically cache new analyses
}

const (
// MinTTL is the minimum allowed time-to-live for cache entries (1 minute)
MinTTL = 1 * time.Minute
// MaxTTL is the maximum allowed time-to-live for cache entries (7 days)
MaxTTL = 7 * 24 * time.Hour
)

// Validate checks if the cache configuration is valid.
func (c CacheConfig) Validate() error {
if c.TTL < MinTTL {
return fmt.Errorf("cache TTL too short: %v (minimum %v)", c.TTL, MinTTL)
}
if c.TTL > MaxTTL {
return fmt.Errorf("cache TTL too long: %v (maximum %v)", c.TTL, MaxTTL)
}
return nil
}

// Cache manages the local cache for analysis results.
// It provides thread-safe operations for storing and retrieving cached data.
type Cache struct {
Expand Down Expand Up @@ -176,7 +194,18 @@ func (c *Cache) loadConfig() {
if err != nil {
return // Use defaults
}
json.Unmarshal(data, &c.config)
var loadedConfig CacheConfig
if err := json.Unmarshal(data, &loadedConfig); err != nil {
return // Use defaults
}

// Validate loaded configuration
if err := loadedConfig.Validate(); err != nil {
// If invalid, we use default TTL but keep other valid settings if possible
// For simplicity, if TTL is invalid, we fallback to default TTL
loadedConfig.TTL = DefaultConfig().TTL
}
c.config = loadedConfig
}

// SaveConfig saves cache configuration
Expand Down Expand Up @@ -418,10 +447,16 @@ func (c *Cache) SetEnabled(enabled bool) {
c.SaveConfig()
}

// SetTTL sets the cache time-to-live
func (c *Cache) SetTTL(ttl time.Duration) {
// SetTTL sets the cache time-to-live.
// Returns an error if the TTL is out of allowed bounds (1m - 7d).
func (c *Cache) SetTTL(ttl time.Duration) error {
newConfig := c.config
newConfig.TTL = ttl
if err := newConfig.Validate(); err != nil {
return err
}
c.config.TTL = ttl
c.SaveConfig()
return c.SaveConfig()
}

// SetAutoCache enables or disables auto-caching
Expand Down
77 changes: 74 additions & 3 deletions internal/cache/cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,7 @@ func TestCache_GetWithoutTTLExpiration(t *testing.T) {
testData := "some-analysis"

// Set with negative TTL so it is immediately expired
// Note: SetWithTTL doesn't use CacheConfig.Validate() to allow for special cases/testing
err = cache.SetWithTTL(testRepo, testData, -1*time.Second)
if err != nil {
t.Fatalf("SetWithTTL() error = %v", err)
Expand All @@ -324,10 +325,80 @@ func TestCache_GetWithoutTTLExpiration(t *testing.T) {
if found {
t.Error("Get() should not find expired entry")
}
if entry == nil {
t.Fatal("GetWithoutTTLExpiration() returned nil entry")
}

cache.Delete(testRepo)
}

func TestCacheConfig_Validate(t *testing.T) {
tests := []struct {
name string
ttl time.Duration
wantErr bool
}{
{"Valid 1h", 1 * time.Hour, false},
{"Valid 24h", 24 * time.Hour, false},
{"Valid 7d", 7 * 24 * time.Hour, false},
{"Valid 1m", 1 * time.Minute, false},
{"Too short 30s", 30 * time.Second, true},
{"Zero", 0, true},
{"Negative", -1 * time.Hour, true},
{"Too long 8d", 8 * 24 * time.Hour, true},
}

// Wait, let me double check my Validate logic in cache.go
/*
func (c CacheConfig) Validate() error {
if c.TTL < MinTTL {
return fmt.Errorf("cache TTL too short: %v (minimum %v)", c.TTL, MinTTL)
}
if c.TTL > MaxTTL {
return fmt.Errorf("cache TTL too long: %v (maximum %v)", c.TTL, MaxTTL)
}
return nil
}
*/
// So 1m IS valid.

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
config := CacheConfig{TTL: tt.ttl}
err := config.Validate()
if (err != nil) != tt.wantErr {
t.Errorf("CacheConfig.Validate() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}

func TestCache_SetTTL(t *testing.T) {
cache, err := NewCache()
if err != nil {
t.Fatalf("NewCache() error = %v", err)
}

tests := []struct {
name string
ttl time.Duration
wantErr bool
}{
{"Valid 2h", 2 * time.Hour, false},
{"Too short 10s", 10 * time.Second, true},
{"Too short 0s", 0, true},
{"Too short -1h", -1 * time.Hour, true},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := cache.SetTTL(tt.ttl)
if (err != nil) != tt.wantErr {
t.Errorf("SetTTL() error = %v, wantErr %v", err, tt.wantErr)
}
if !tt.wantErr {
if cache.GetConfig().TTL != tt.ttl {
t.Errorf("TTL not updated: got %v, want %v", cache.GetConfig().TTL, tt.ttl)
}
}
})
}
}