213 lines
4.3 KiB
Go
213 lines
4.3 KiB
Go
/*
|
|
©AngelaMos | 2026
|
|
cache_test.go
|
|
|
|
Tests for Cache get/set, freshness, touch, clear, and path traversal safety in cache.go
|
|
|
|
Tests:
|
|
TestCacheGetSetRoundTrip - stored entries match retrieved entries field by field
|
|
TestCacheGetMiss - returns false for keys that were never written
|
|
TestCacheIsFresh - TTL boundary check for fresh vs stale entries
|
|
TestCacheTouch - Touch updates CachedAt without changing stored data
|
|
TestCacheClear - removes all .json files from the cache directory
|
|
TestCachePathTraversal - traversal keys are confined to the cache directory
|
|
*/
|
|
|
|
package pypi
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestCacheGetSetRoundTrip(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
dir := t.TempDir()
|
|
cache, err := NewCache(dir, time.Hour)
|
|
if err != nil {
|
|
t.Fatalf("NewCache: %v", err)
|
|
}
|
|
|
|
entry := &CacheEntry{
|
|
ETag: `"abc123"`,
|
|
Versions: []string{"1.0", "1.1", "2.0"},
|
|
CachedAt: time.Now(),
|
|
}
|
|
|
|
if err := cache.Set("requests", entry); err != nil {
|
|
t.Fatalf("Set: %v", err)
|
|
}
|
|
|
|
got, ok := cache.Get("requests")
|
|
if !ok {
|
|
t.Fatal("Get returned false after Set")
|
|
}
|
|
if got.ETag != entry.ETag {
|
|
t.Errorf("ETag = %q, want %q", got.ETag, entry.ETag)
|
|
}
|
|
if len(got.Versions) != len(entry.Versions) {
|
|
t.Fatalf(
|
|
"Versions len = %d, want %d",
|
|
len(got.Versions), len(entry.Versions),
|
|
)
|
|
}
|
|
for i, v := range got.Versions {
|
|
if v != entry.Versions[i] {
|
|
t.Errorf(
|
|
"Versions[%d] = %q, want %q",
|
|
i, v, entry.Versions[i],
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCacheGetMiss(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
dir := t.TempDir()
|
|
cache, err := NewCache(dir, time.Hour)
|
|
if err != nil {
|
|
t.Fatalf("NewCache: %v", err)
|
|
}
|
|
|
|
_, ok := cache.Get("nonexistent")
|
|
if ok {
|
|
t.Fatal("Get returned true for missing key")
|
|
}
|
|
}
|
|
|
|
func TestCacheIsFresh(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
dir := t.TempDir()
|
|
cache, err := NewCache(dir, time.Hour)
|
|
if err != nil {
|
|
t.Fatalf("NewCache: %v", err)
|
|
}
|
|
|
|
fresh := &CacheEntry{CachedAt: time.Now()}
|
|
if !cache.IsFresh(fresh) {
|
|
t.Error("entry cached just now should be fresh")
|
|
}
|
|
|
|
stale := &CacheEntry{
|
|
CachedAt: time.Now().Add(-2 * time.Hour),
|
|
}
|
|
if cache.IsFresh(stale) {
|
|
t.Error("entry cached 2 hours ago should be stale")
|
|
}
|
|
}
|
|
|
|
func TestCacheTouch(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
dir := t.TempDir()
|
|
cache, err := NewCache(dir, time.Hour)
|
|
if err != nil {
|
|
t.Fatalf("NewCache: %v", err)
|
|
}
|
|
|
|
old := time.Now().Add(-30 * time.Minute)
|
|
entry := &CacheEntry{
|
|
ETag: `"xyz"`,
|
|
Versions: []string{"1.0"},
|
|
CachedAt: old,
|
|
}
|
|
if err := cache.Set("flask", entry); err != nil {
|
|
t.Fatalf("Set: %v", err)
|
|
}
|
|
|
|
cache.Touch("flask")
|
|
|
|
got, ok := cache.Get("flask")
|
|
if !ok {
|
|
t.Fatal("Get returned false after Touch")
|
|
}
|
|
if !got.CachedAt.After(old) {
|
|
t.Error("Touch did not update CachedAt")
|
|
}
|
|
}
|
|
|
|
func TestCacheClear(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
dir := t.TempDir()
|
|
cache, err := NewCache(dir, time.Hour)
|
|
if err != nil {
|
|
t.Fatalf("NewCache: %v", err)
|
|
}
|
|
|
|
for _, name := range []string{"a", "b", "c"} {
|
|
setErr := cache.Set(name, &CacheEntry{
|
|
Versions: []string{"1.0"},
|
|
CachedAt: time.Now(),
|
|
})
|
|
if setErr != nil {
|
|
t.Fatalf("Set %s: %v", name, setErr)
|
|
}
|
|
}
|
|
|
|
if clearErr := cache.Clear(); clearErr != nil {
|
|
t.Fatalf("Clear: %v", clearErr)
|
|
}
|
|
|
|
entries, err := os.ReadDir(dir)
|
|
if err != nil {
|
|
t.Fatalf("ReadDir: %v", err)
|
|
}
|
|
|
|
jsonCount := 0
|
|
for _, e := range entries {
|
|
if filepath.Ext(e.Name()) == ".json" {
|
|
jsonCount++
|
|
}
|
|
}
|
|
if jsonCount != 0 {
|
|
t.Errorf("Clear left %d .json files", jsonCount)
|
|
}
|
|
}
|
|
|
|
func TestCachePathTraversal(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
dir := t.TempDir()
|
|
cache, err := NewCache(dir, time.Hour)
|
|
if err != nil {
|
|
t.Fatalf("NewCache: %v", err)
|
|
}
|
|
|
|
entry := &CacheEntry{
|
|
Versions: []string{"1.0"},
|
|
CachedAt: time.Now(),
|
|
}
|
|
if err := cache.Set("../../../etc/passwd", entry); err != nil {
|
|
t.Fatalf("Set: %v", err)
|
|
}
|
|
|
|
got, ok := cache.Get("../../../etc/passwd")
|
|
if !ok {
|
|
t.Fatal("Get returned false for traversal key")
|
|
}
|
|
if got.Versions[0] != "1.0" {
|
|
t.Error("wrong version returned")
|
|
}
|
|
|
|
path := filepath.Join(dir, "passwd.json")
|
|
if _, err := os.Stat(path); err != nil {
|
|
t.Errorf(
|
|
"expected file at safe path %s, got: %v",
|
|
path, err,
|
|
)
|
|
}
|
|
|
|
unsafePath := filepath.Join(
|
|
dir, "../../../etc/passwd.json",
|
|
)
|
|
if _, err := os.Stat(unsafePath); err == nil {
|
|
t.Error("file created at unsafe traversal path")
|
|
}
|
|
}
|