package cache import ( "errors" "os" "path/filepath" "testing" "time" ) func openTestCache(t *testing.T) (*Cache, func()) { t.Helper() path := filepath.Join(t.TempDir(), "orca_cache.db") c, err := Open(path) if err != nil { t.Fatalf("open cache: %v", err) } return c, func() { _ = c.Close() } } func TestCache_Hit(t *testing.T) { c, cleanup := openTestCache(t) defer cleanup() want := []byte("hello-orca") if err := c.Set("nodes", "list", want, 30*time.Second); err != nil { t.Fatalf("set: %v", err) } got, inserted, err := c.Get("nodes", "list") if err != nil { t.Fatalf("get: %v", err) } if string(got) != string(want) { t.Errorf("get value = %q, want %q", got, want) } if inserted.IsZero() { t.Errorf("inserted time is zero") } } func TestCache_Miss(t *testing.T) { c, cleanup := openTestCache(t) defer cleanup() got, _, err := c.Get("nodes", "missing") if !errors.Is(err, ErrCacheMiss) { t.Fatalf("get miss: err = %v, want ErrCacheMiss", err) } if got != nil { t.Errorf("get miss value = %v, want nil", got) } } func TestCache_Invalidate(t *testing.T) { c, cleanup := openTestCache(t) defer cleanup() if err := c.Set("nodes", "list", []byte("a"), 30*time.Second); err != nil { t.Fatalf("set a: %v", err) } if err := c.Set("nodes", "other", []byte("b"), 30*time.Second); err != nil { t.Fatalf("set b: %v", err) } if err := c.Set("jobs", "list", []byte("c"), 30*time.Second); err != nil { t.Fatalf("set c: %v", err) } if err := c.Invalidate("nodes"); err != nil { t.Fatalf("invalidate: %v", err) } if _, _, err := c.Get("nodes", "list"); !errors.Is(err, ErrCacheMiss) { t.Errorf("nodes/list after invalidate: err = %v, want ErrCacheMiss", err) } if _, _, err := c.Get("nodes", "other"); !errors.Is(err, ErrCacheMiss) { t.Errorf("nodes/other after invalidate: err = %v, want ErrCacheMiss", err) } if _, _, err := c.Get("jobs", "list"); err != nil { t.Errorf("jobs/list after nodes invalidate: err = %v, want nil", err) } } func TestCache_InvalidateKey(t *testing.T) { c, cleanup := openTestCache(t) defer cleanup() if err := c.Set("nodes", "list", []byte("a"), 30*time.Second); err != nil { t.Fatalf("set: %v", err) } if err := c.InvalidateKey("nodes", "list"); err != nil { t.Fatalf("invalidate key: %v", err) } if _, _, err := c.Get("nodes", "list"); !errors.Is(err, ErrCacheMiss) { t.Errorf("get after invalidate key: err = %v, want ErrCacheMiss", err) } } func TestCache_TTLExpiry(t *testing.T) { c, cleanup := openTestCache(t) defer cleanup() if err := c.Set("jobs", "list", []byte("stale"), 1*time.Millisecond); err != nil { t.Fatalf("set: %v", err) } time.Sleep(10 * time.Millisecond) if _, _, err := c.Get("jobs", "list"); !errors.Is(err, ErrCacheMiss) { t.Errorf("get after ttl expiry: err = %v, want ErrCacheMiss", err) } } func TestCache_TTLZeroNeverExpires(t *testing.T) { c, cleanup := openTestCache(t) defer cleanup() if err := c.Set("namespaces", "list", []byte("forever"), 0); err != nil { t.Fatalf("set: %v", err) } time.Sleep(5 * time.Millisecond) got, _, err := c.Get("namespaces", "list") if err != nil { t.Fatalf("get ttl=0: %v", err) } if string(got) != "forever" { t.Errorf("get ttl=0 value = %q, want %q", got, "forever") } } func TestCache_Overwrite(t *testing.T) { c, cleanup := openTestCache(t) defer cleanup() if err := c.Set("nodes", "list", []byte("v1"), 30*time.Second); err != nil { t.Fatalf("set v1: %v", err) } if err := c.Set("nodes", "list", []byte("v2"), 30*time.Second); err != nil { t.Fatalf("set v2: %v", err) } got, _, err := c.Get("nodes", "list") if err != nil { t.Fatalf("get: %v", err) } if string(got) != "v2" { t.Errorf("get after overwrite = %q, want %q", got, "v2") } } func TestCache_InvalidateAll(t *testing.T) { c, cleanup := openTestCache(t) defer cleanup() _ = c.Set("nodes", "list", []byte("a"), 30*time.Second) _ = c.Set("jobs", "list", []byte("b"), 30*time.Second) if err := c.InvalidateAll(); err != nil { t.Fatalf("invalidate all: %v", err) } if _, _, err := c.Get("nodes", "list"); !errors.Is(err, ErrCacheMiss) { t.Errorf("nodes/list after invalidate-all: err = %v, want ErrCacheMiss", err) } if _, _, err := c.Get("jobs", "list"); !errors.Is(err, ErrCacheMiss) { t.Errorf("jobs/list after invalidate-all: err = %v, want ErrCacheMiss", err) } } func TestCache_Stats(t *testing.T) { c, cleanup := openTestCache(t) defer cleanup() _ = c.Set("nodes", "list", []byte("aaaa"), 30*time.Second) _ = c.Set("jobs", "list", []byte("bb"), 30*time.Second) stats, err := c.Stats() if err != nil { t.Fatalf("stats: %v", err) } if len(stats) != 2 { t.Fatalf("stats len = %d, want 2", len(stats)) } var nodes, jobs *ClassStats for i := range stats { switch stats[i].Class { case "nodes": nodes = &stats[i] case "jobs": jobs = &stats[i] } } if nodes == nil || nodes.Count != 1 || nodes.Bytes != 4 { t.Errorf("nodes stats = %+v, want count=1 bytes=4", nodes) } if jobs == nil || jobs.Count != 1 || jobs.Bytes != 2 { t.Errorf("jobs stats = %+v, want count=1 bytes=2", jobs) } } func TestCache_OpenDefaultPath(t *testing.T) { dir := t.TempDir() t.Setenv("ORCA_HOME", dir) c, err := Open("") if err != nil { t.Fatalf("open default path: %v", err) } defer c.Close() if err := c.Set("nodes", "list", []byte("ok"), 0); err != nil { t.Fatalf("set: %v", err) } got, _, err := c.Get("nodes", "list") if err != nil { t.Fatalf("get: %v", err) } if string(got) != "ok" { t.Errorf("get = %q, want %q", got, "ok") } } func BenchmarkCacheHit(b *testing.B) { path := filepath.Join(b.TempDir(), "orca_cache.db") c, err := Open(path) if err != nil { b.Fatalf("open: %v", err) } defer c.Close() if err := c.Set("nodes", "list", []byte("bench"), 0); err != nil { b.Fatalf("set: %v", err) } b.ResetTimer() for i := 0; i < b.N; i++ { if _, _, err := c.Get("nodes", "list"); err != nil { b.Fatalf("get: %v", err) } } } // TestCache_FileMode0600 verifies that the cache DB file is created // with mode 0600 (not the default umask 0644) (REQ-158, P09 T4). func TestCache_FileMode0600(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "orca_cache.db") c, err := Open(path) if err != nil { t.Fatalf("open: %v", err) } defer c.Close() info, err := os.Stat(path) if err != nil { t.Fatalf("stat cache db: %v", err) } got := info.Mode().Perm() if got != 0o600 { t.Errorf("cache db mode = %04o, want 0600", got) } } // TestCache_FileMode0600DefaultPath verifies that the cache DB at the // default path (ORCA_HOME) also gets 0600 (REQ-158, P09 T4). func TestCache_FileMode0600DefaultPath(t *testing.T) { dir := t.TempDir() t.Setenv("ORCA_HOME", dir) c, err := Open("") if err != nil { t.Fatalf("open default path: %v", err) } defer c.Close() // The default path is paths.CacheDB() which is under ORCA_HOME. // Find the db file. dbPath := filepath.Join(dir, "orca_cache.db") info, err := os.Stat(dbPath) if err != nil { t.Fatalf("stat cache db at %s: %v", dbPath, err) } got := info.Mode().Perm() if got != 0o600 { t.Errorf("cache db mode = %04o, want 0600", got) } }