package models
import (
"strings"
"testing"
)
func TestCacheEntryValidate(t *testing.T) {
valid := CacheEntry{Key: "go-mod-v1", Hash: []string{"go.sum", "sub/dir/package-lock.json"}, Paths: []string{"/workspace/go/pkg/mod", "/root/.cache"}}
if err := valid.Validate(); err != nil {
t.Fatalf("valid entry: %v", err)
}
cases := []struct {
name string
entry CacheEntry
want string
}{
{"empty key", CacheEntry{Key: "", Paths: []string{"/x"}}, "invalid key"},
{"key with slash", CacheEntry{Key: "a/b", Paths: []string{"/x"}}, "invalid key"},
{"no paths", CacheEntry{Key: "ok"}, "no paths"},
{"empty path", CacheEntry{Key: "ok", Paths: []string{""}}, "empty path"},
{"path with space", CacheEntry{Key: "ok", Paths: []string{"/my dir"}}, "unsupported characters"},
{"path with quote", CacheEntry{Key: "ok", Paths: []string{"/x'$(rm)"}}, "unsupported characters"},
{"path with shell syntax", CacheEntry{Key: "ok", Paths: []string{"/x;touch-pwn"}}, "unsupported characters"},
{"path traversal", CacheEntry{Key: "ok", Paths: []string{"/x/../y"}}, ".."},
{"duplicate path", CacheEntry{Key: "ok", Paths: []string{"/x", "/x"}}, "duplicate"},
{"level too high", CacheEntry{Key: "ok", Paths: []string{"/x"}, CompressionLevel: 20}, "out of range"},
{"negative level", CacheEntry{Key: "ok", Paths: []string{"/x"}, CompressionLevel: -1}, "out of range"},
{"bad when", CacheEntry{Key: "ok", Paths: []string{"/x"}, When: "sometimes"}, "not one of"},
{"absolute hash path", CacheEntry{Key: "ok", Hash: []string{"/go.sum"}, Paths: []string{"/x"}}, "not repo-relative"},
{"empty hash path", CacheEntry{Key: "ok", Hash: []string{""}, Paths: []string{"/x"}}, "not repo-relative"},
{"hash path traversal", CacheEntry{Key: "ok", Hash: []string{"../secret"}, Paths: []string{"/x"}}, "not repo-relative"},
{"hash path inner traversal", CacheEntry{Key: "ok", Hash: []string{"a/../../b"}, Paths: []string{"/x"}}, "unsupported characters"},
{"hash path with colon", CacheEntry{Key: "ok", Hash: []string{"rev:go.sum"}, Paths: []string{"/x"}}, "unsupported characters"},
{"hash path with space", CacheEntry{Key: "ok", Hash: []string{"my lock"}, Paths: []string{"/x"}}, "unsupported characters"},
{"hash path with glob", CacheEntry{Key: "ok", Hash: []string{"*.sum"}, Paths: []string{"/x"}}, "unsupported characters"},
}
for _, tc := range cases {
err := tc.entry.Validate()
if err == nil || !strings.Contains(err.Error(), tc.want) {
t.Errorf("%s: got %v, want error containing %q", tc.name, err, tc.want)
}
}
ok := CacheEntry{Key: "go-mod", Hash: []string{"go.sum"}, Paths: []string{"node_modules", "/root/.cache"}, CompressionLevel: 19, When: "always"}
if err := ok.Validate(); err != nil {
t.Errorf("relative and absolute paths should validate, got %v", err)
}
}
func TestParseCacheEntries(t *testing.T) {
entries, err := ParseCacheEntries(`
cache:
- key: go-mod
hash: [go.sum]
paths: [/root/go/pkg/mod]
compression-level: 7
when: always
`)
if err != nil {
t.Fatalf("ParseCacheEntries: %v", err)
}
if len(entries) != 1 || entries[0].Key != "go-mod" || entries[0].CompressionLevel != 7 || entries[0].When != "always" {
t.Fatalf("parsed entries = %+v", entries)
}
_, err = ParseCacheEntries(`
cache:
- key: deps
paths: [/deps]
- key: deps
paths: [/other]
`)
if err == nil || !strings.Contains(err.Error(), "duplicate key") {
t.Fatalf("duplicate cache keys error = %v", err)
}
}