Remove banned encoding/json import; Entry.Data is now string using core.JSONMarshal/JSONMarshalString. Rename ds→separator, cwd→workingDirectory throughout. Add missing Good/Bad/Ugly tests for all public functions (29 tests, all pass). Co-Authored-By: Virgil <virgil@lethean.io>
465 lines
12 KiB
Go
465 lines
12 KiB
Go
// SPDX-License-Identifier: EUPL-1.2
|
|
|
|
package cache_test
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"dappco.re/go/core"
|
|
"dappco.re/go/core/cache"
|
|
coreio "dappco.re/go/core/io"
|
|
)
|
|
|
|
func newTestCache(t *testing.T, baseDir string, ttl time.Duration) (*cache.Cache, *coreio.MockMedium) {
|
|
t.Helper()
|
|
|
|
m := coreio.NewMockMedium()
|
|
c, err := cache.New(m, baseDir, ttl)
|
|
if err != nil {
|
|
t.Fatalf("failed to create cache: %v", err)
|
|
}
|
|
|
|
return c, m
|
|
}
|
|
|
|
func readEntry(t *testing.T, raw string) cache.Entry {
|
|
t.Helper()
|
|
|
|
var entry cache.Entry
|
|
result := core.JSONUnmarshalString(raw, &entry)
|
|
if !result.OK {
|
|
t.Fatalf("failed to unmarshal cache entry: %v", result.Value)
|
|
}
|
|
|
|
return entry
|
|
}
|
|
|
|
func TestCache_New_Good(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
t.Chdir(tmpDir)
|
|
|
|
c, m := newTestCache(t, "", 0)
|
|
|
|
const key = "defaults"
|
|
if err := c.Set(key, map[string]string{"foo": "bar"}); err != nil {
|
|
t.Fatalf("Set failed: %v", err)
|
|
}
|
|
|
|
path, err := c.Path(key)
|
|
if err != nil {
|
|
t.Fatalf("Path failed: %v", err)
|
|
}
|
|
|
|
wantPath := core.JoinPath(tmpDir, ".core", "cache", key+".json")
|
|
if path != wantPath {
|
|
t.Fatalf("expected default path %q, got %q", wantPath, path)
|
|
}
|
|
|
|
raw, err := m.Read(path)
|
|
if err != nil {
|
|
t.Fatalf("Read failed: %v", err)
|
|
}
|
|
|
|
entry := readEntry(t, raw)
|
|
ttl := entry.ExpiresAt.Sub(entry.CachedAt)
|
|
if ttl < cache.DefaultTTL || ttl > cache.DefaultTTL+time.Second {
|
|
t.Fatalf("expected ttl near %v, got %v", cache.DefaultTTL, ttl)
|
|
}
|
|
}
|
|
|
|
func TestCache_New_Bad(t *testing.T) {
|
|
_, err := cache.New(coreio.NewMockMedium(), "/tmp/cache-negative-ttl", -time.Second)
|
|
if err == nil {
|
|
t.Fatal("expected New to reject negative ttl, got nil")
|
|
}
|
|
}
|
|
|
|
func TestCache_New_Ugly(t *testing.T) {
|
|
// New with zero ttl falls back to DefaultTTL; verify a set entry uses it.
|
|
c, m := newTestCache(t, "/tmp/cache-default-ttl", 0)
|
|
|
|
const key = "ugly-key"
|
|
if err := c.Set(key, map[string]string{"x": "y"}); err != nil {
|
|
t.Fatalf("Set failed: %v", err)
|
|
}
|
|
|
|
path, err := c.Path(key)
|
|
if err != nil {
|
|
t.Fatalf("Path failed: %v", err)
|
|
}
|
|
|
|
raw, err := m.Read(path)
|
|
if err != nil {
|
|
t.Fatalf("Read failed: %v", err)
|
|
}
|
|
|
|
entry := readEntry(t, raw)
|
|
appliedTTL := entry.ExpiresAt.Sub(entry.CachedAt)
|
|
if appliedTTL < cache.DefaultTTL || appliedTTL > cache.DefaultTTL+time.Second {
|
|
t.Fatalf("expected ttl near %v, got %v", cache.DefaultTTL, appliedTTL)
|
|
}
|
|
}
|
|
|
|
func TestCache_Path_Good(t *testing.T) {
|
|
c, _ := newTestCache(t, "/tmp/cache-path", time.Minute)
|
|
|
|
path, err := c.Path("github/acme/repos")
|
|
if err != nil {
|
|
t.Fatalf("Path failed: %v", err)
|
|
}
|
|
|
|
want := "/tmp/cache-path/github/acme/repos.json"
|
|
if path != want {
|
|
t.Fatalf("expected path %q, got %q", want, path)
|
|
}
|
|
}
|
|
|
|
func TestCache_Path_Bad(t *testing.T) {
|
|
c, _ := newTestCache(t, "/tmp/cache-traversal", time.Minute)
|
|
|
|
_, err := c.Path("../../etc/passwd")
|
|
if err == nil {
|
|
t.Fatal("expected error for path traversal key, got nil")
|
|
}
|
|
}
|
|
|
|
func TestCache_Path_Ugly(t *testing.T) {
|
|
// Path on a nil receiver returns an error rather than panicking.
|
|
var c *cache.Cache
|
|
_, err := c.Path("any-key")
|
|
if err == nil {
|
|
t.Fatal("expected Path to fail on nil receiver")
|
|
}
|
|
}
|
|
|
|
func TestCache_Get_Good(t *testing.T) {
|
|
c, _ := newTestCache(t, "/tmp/cache", time.Minute)
|
|
|
|
key := "test-key"
|
|
data := map[string]string{"foo": "bar"}
|
|
|
|
if err := c.Set(key, data); err != nil {
|
|
t.Fatalf("Set failed: %v", err)
|
|
}
|
|
|
|
var retrieved map[string]string
|
|
found, err := c.Get(key, &retrieved)
|
|
if err != nil {
|
|
t.Fatalf("Get failed: %v", err)
|
|
}
|
|
if !found {
|
|
t.Fatal("expected to find cached item")
|
|
}
|
|
if retrieved["foo"] != "bar" {
|
|
t.Errorf("expected foo=bar, got %v", retrieved["foo"])
|
|
}
|
|
}
|
|
|
|
func TestCache_Get_Bad(t *testing.T) {
|
|
// Get on a missing key returns (false, nil) — not an error.
|
|
c, _ := newTestCache(t, "/tmp/cache-get-bad", time.Minute)
|
|
|
|
var retrieved map[string]string
|
|
found, err := c.Get("nonexistent-key", &retrieved)
|
|
if err != nil {
|
|
t.Fatalf("expected no error for missing key, got: %v", err)
|
|
}
|
|
if found {
|
|
t.Fatal("expected found=false for missing key")
|
|
}
|
|
}
|
|
|
|
func TestCache_Get_Ugly(t *testing.T) {
|
|
c, _ := newTestCache(t, "/tmp/cache-expiry", 10*time.Millisecond)
|
|
|
|
if err := c.Set("test-key", map[string]string{"foo": "bar"}); err != nil {
|
|
t.Fatalf("Set for expiry test failed: %v", err)
|
|
}
|
|
|
|
time.Sleep(50 * time.Millisecond)
|
|
|
|
var retrieved map[string]string
|
|
found, err := c.Get("test-key", &retrieved)
|
|
if err != nil {
|
|
t.Fatalf("Get for expired item returned an unexpected error: %v", err)
|
|
}
|
|
if found {
|
|
t.Error("expected item to be expired")
|
|
}
|
|
}
|
|
|
|
func TestCache_Age_Good(t *testing.T) {
|
|
c, _ := newTestCache(t, "/tmp/cache-age", time.Minute)
|
|
|
|
if err := c.Set("test-key", map[string]string{"foo": "bar"}); err != nil {
|
|
t.Fatalf("Set failed: %v", err)
|
|
}
|
|
|
|
if age := c.Age("test-key"); age < 0 {
|
|
t.Errorf("expected age >= 0, got %v", age)
|
|
}
|
|
}
|
|
|
|
func TestCache_Age_Bad(t *testing.T) {
|
|
// Age returns -1 for a key that was never written.
|
|
c, _ := newTestCache(t, "/tmp/cache-age-bad", time.Minute)
|
|
|
|
if age := c.Age("missing-key"); age != -1 {
|
|
t.Errorf("expected -1 for missing key, got %v", age)
|
|
}
|
|
}
|
|
|
|
func TestCache_Age_Ugly(t *testing.T) {
|
|
// Age returns -1 when the stored file contains corrupt JSON.
|
|
c, medium := newTestCache(t, "/tmp/cache-age-ugly", time.Minute)
|
|
|
|
path, err := c.Path("corrupt-key")
|
|
if err != nil {
|
|
t.Fatalf("Path failed: %v", err)
|
|
}
|
|
|
|
medium.Files[path] = "not-valid-json"
|
|
|
|
if age := c.Age("corrupt-key"); age != -1 {
|
|
t.Errorf("expected -1 for corrupt entry, got %v", age)
|
|
}
|
|
}
|
|
|
|
func TestCache_Set_Good(t *testing.T) {
|
|
// Set writes a valid entry that Get can later retrieve.
|
|
c, _ := newTestCache(t, "/tmp/cache-set-good", time.Minute)
|
|
|
|
if err := c.Set("set-key", map[string]int{"count": 42}); err != nil {
|
|
t.Fatalf("Set failed: %v", err)
|
|
}
|
|
|
|
var retrieved map[string]int
|
|
found, err := c.Get("set-key", &retrieved)
|
|
if err != nil {
|
|
t.Fatalf("Get after Set failed: %v", err)
|
|
}
|
|
if !found {
|
|
t.Fatal("expected to find item after Set")
|
|
}
|
|
if retrieved["count"] != 42 {
|
|
t.Errorf("expected count=42, got %v", retrieved["count"])
|
|
}
|
|
}
|
|
|
|
func TestCache_Set_Bad(t *testing.T) {
|
|
// Set on a nil receiver returns an error rather than panicking.
|
|
var c *cache.Cache
|
|
if err := c.Set("key", map[string]string{"a": "b"}); err == nil {
|
|
t.Fatal("expected Set to fail on nil receiver")
|
|
}
|
|
}
|
|
|
|
func TestCache_Set_Ugly(t *testing.T) {
|
|
// Set overwrites an existing entry; subsequent Get returns the new value.
|
|
c, _ := newTestCache(t, "/tmp/cache-set-ugly", time.Minute)
|
|
|
|
if err := c.Set("overwrite-key", map[string]string{"v": "first"}); err != nil {
|
|
t.Fatalf("first Set failed: %v", err)
|
|
}
|
|
if err := c.Set("overwrite-key", map[string]string{"v": "second"}); err != nil {
|
|
t.Fatalf("second Set failed: %v", err)
|
|
}
|
|
|
|
var retrieved map[string]string
|
|
found, err := c.Get("overwrite-key", &retrieved)
|
|
if err != nil {
|
|
t.Fatalf("Get failed: %v", err)
|
|
}
|
|
if !found {
|
|
t.Fatal("expected to find item")
|
|
}
|
|
if retrieved["v"] != "second" {
|
|
t.Errorf("expected v=second after overwrite, got %q", retrieved["v"])
|
|
}
|
|
}
|
|
|
|
func TestCache_NilReceiver_Good(t *testing.T) {
|
|
var c *cache.Cache
|
|
var target map[string]string
|
|
|
|
if _, err := c.Path("x"); err == nil {
|
|
t.Fatal("expected Path to fail on nil receiver")
|
|
}
|
|
|
|
if _, err := c.Get("x", &target); err == nil {
|
|
t.Fatal("expected Get to fail on nil receiver")
|
|
}
|
|
|
|
if err := c.Set("x", map[string]string{"foo": "bar"}); err == nil {
|
|
t.Fatal("expected Set to fail on nil receiver")
|
|
}
|
|
|
|
if err := c.Delete("x"); err == nil {
|
|
t.Fatal("expected Delete to fail on nil receiver")
|
|
}
|
|
|
|
if err := c.Clear(); err == nil {
|
|
t.Fatal("expected Clear to fail on nil receiver")
|
|
}
|
|
|
|
if age := c.Age("x"); age != -1 {
|
|
t.Fatalf("expected Age to return -1 on nil receiver, got %v", age)
|
|
}
|
|
}
|
|
|
|
func TestCache_ZeroValue_Ugly(t *testing.T) {
|
|
var c cache.Cache
|
|
var target map[string]string
|
|
|
|
if _, err := c.Path("x"); err == nil {
|
|
t.Fatal("expected Path to fail on zero-value cache")
|
|
}
|
|
|
|
if _, err := c.Get("x", &target); err == nil {
|
|
t.Fatal("expected Get to fail on zero-value cache")
|
|
}
|
|
|
|
if err := c.Set("x", map[string]string{"foo": "bar"}); err == nil {
|
|
t.Fatal("expected Set to fail on zero-value cache")
|
|
}
|
|
|
|
if err := c.Delete("x"); err == nil {
|
|
t.Fatal("expected Delete to fail on zero-value cache")
|
|
}
|
|
|
|
if err := c.Clear(); err == nil {
|
|
t.Fatal("expected Clear to fail on zero-value cache")
|
|
}
|
|
|
|
if age := c.Age("x"); age != -1 {
|
|
t.Fatalf("expected Age to return -1 on zero-value cache, got %v", age)
|
|
}
|
|
}
|
|
|
|
func TestCache_Delete_Good(t *testing.T) {
|
|
c, _ := newTestCache(t, "/tmp/cache-delete", time.Minute)
|
|
|
|
if err := c.Set("test-key", map[string]string{"foo": "bar"}); err != nil {
|
|
t.Fatalf("Set failed: %v", err)
|
|
}
|
|
|
|
if err := c.Delete("test-key"); err != nil {
|
|
t.Fatalf("Delete failed: %v", err)
|
|
}
|
|
|
|
var retrieved map[string]string
|
|
found, err := c.Get("test-key", &retrieved)
|
|
if err != nil {
|
|
t.Fatalf("Get after delete returned an unexpected error: %v", err)
|
|
}
|
|
if found {
|
|
t.Error("expected item to be deleted")
|
|
}
|
|
}
|
|
|
|
func TestCache_Delete_Bad(t *testing.T) {
|
|
// Delete of a key that does not exist returns nil — idempotent.
|
|
c, _ := newTestCache(t, "/tmp/cache-delete-bad", time.Minute)
|
|
|
|
if err := c.Delete("nonexistent-key"); err != nil {
|
|
t.Fatalf("expected Delete of missing key to return nil, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestCache_Delete_Ugly(t *testing.T) {
|
|
// Delete rejects a path traversal key with an error.
|
|
c, _ := newTestCache(t, "/tmp/cache-delete-ugly", time.Minute)
|
|
|
|
if err := c.Delete("../../etc/shadow"); err == nil {
|
|
t.Fatal("expected Delete to reject path traversal key")
|
|
}
|
|
}
|
|
|
|
func TestCache_Clear_Good(t *testing.T) {
|
|
c, _ := newTestCache(t, "/tmp/cache-clear", time.Minute)
|
|
data := map[string]string{"foo": "bar"}
|
|
|
|
if err := c.Set("key1", data); err != nil {
|
|
t.Fatalf("Set for clear test failed for key1: %v", err)
|
|
}
|
|
if err := c.Set("key2", data); err != nil {
|
|
t.Fatalf("Set for clear test failed for key2: %v", err)
|
|
}
|
|
if err := c.Clear(); err != nil {
|
|
t.Fatalf("Clear failed: %v", err)
|
|
}
|
|
|
|
var retrieved map[string]string
|
|
found, err := c.Get("key1", &retrieved)
|
|
if err != nil {
|
|
t.Fatalf("Get after clear returned an unexpected error: %v", err)
|
|
}
|
|
if found {
|
|
t.Error("expected key1 to be cleared")
|
|
}
|
|
}
|
|
|
|
func TestCache_Clear_Bad(t *testing.T) {
|
|
// Clear on a nil receiver returns an error rather than panicking.
|
|
var c *cache.Cache
|
|
if err := c.Clear(); err == nil {
|
|
t.Fatal("expected Clear to fail on nil receiver")
|
|
}
|
|
}
|
|
|
|
func TestCache_Clear_Ugly(t *testing.T) {
|
|
// Clear on an empty cache (no entries written) returns nil — idempotent.
|
|
c, _ := newTestCache(t, "/tmp/cache-clear-ugly", time.Minute)
|
|
|
|
if err := c.Clear(); err != nil {
|
|
t.Fatalf("expected Clear on empty cache to return nil, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestCache_GitHubReposKey_Good(t *testing.T) {
|
|
key := cache.GitHubReposKey("myorg")
|
|
if key != "github/myorg/repos" {
|
|
t.Errorf("unexpected GitHubReposKey: %q", key)
|
|
}
|
|
}
|
|
|
|
func TestCache_GitHubReposKey_Bad(t *testing.T) {
|
|
// GitHubReposKey with an empty org still produces a structurally valid path.
|
|
key := cache.GitHubReposKey("")
|
|
if key == "" {
|
|
t.Error("expected a non-empty path even for empty org")
|
|
}
|
|
}
|
|
|
|
func TestCache_GitHubReposKey_Ugly(t *testing.T) {
|
|
// GitHubReposKey with an org containing slashes produces a deterministic path.
|
|
key := cache.GitHubReposKey("org/sub")
|
|
if key == "" {
|
|
t.Error("expected a non-empty path for org with slash")
|
|
}
|
|
}
|
|
|
|
func TestCache_GitHubRepoKey_Good(t *testing.T) {
|
|
key := cache.GitHubRepoKey("myorg", "myrepo")
|
|
if key != "github/myorg/myrepo/meta" {
|
|
t.Errorf("unexpected GitHubRepoKey: %q", key)
|
|
}
|
|
}
|
|
|
|
func TestCache_GitHubRepoKey_Bad(t *testing.T) {
|
|
// GitHubRepoKey with empty args still returns a non-empty path.
|
|
key := cache.GitHubRepoKey("", "")
|
|
if key == "" {
|
|
t.Error("expected a non-empty path even for empty org and repo")
|
|
}
|
|
}
|
|
|
|
func TestCache_GitHubRepoKey_Ugly(t *testing.T) {
|
|
// GitHubRepoKey differentiates between two repositories in the same org.
|
|
keyA := cache.GitHubRepoKey("org", "repo-a")
|
|
keyB := cache.GitHubRepoKey("org", "repo-b")
|
|
if keyA == keyB {
|
|
t.Errorf("expected different keys for different repos, both got %q", keyA)
|
|
}
|
|
}
|