package imgcache import ( "bytes" "io" "os" "path/filepath" "testing" ) func TestContentStorage_StoreAndLoad(t *testing.T) { tmpDir := t.TempDir() storage, err := NewContentStorage(tmpDir) if err != nil { t.Fatalf("NewContentStorage() error = %v", err) } content := []byte("hello world") hash, size, err := storage.Store(bytes.NewReader(content)) if err != nil { t.Fatalf("Store() error = %v", err) } if size != int64(len(content)) { t.Errorf("Store() size = %d, want %d", size, len(content)) } if hash == "" { t.Error("Store() returned empty hash") } // Verify file exists at expected path expectedPath := filepath.Join(tmpDir, hash[0:2], hash[2:4], hash) if _, err := os.Stat(expectedPath); err != nil { t.Errorf("File not at expected path %s: %v", expectedPath, err) } // Load and verify content r, err := storage.Load(hash) if err != nil { t.Fatalf("Load() error = %v", err) } defer r.Close() loaded, err := io.ReadAll(r) if err != nil { t.Fatalf("ReadAll() error = %v", err) } if !bytes.Equal(loaded, content) { t.Errorf("Load() content = %q, want %q", loaded, content) } } func TestContentStorage_StoreIdempotent(t *testing.T) { tmpDir := t.TempDir() storage, err := NewContentStorage(tmpDir) if err != nil { t.Fatalf("NewContentStorage() error = %v", err) } content := []byte("same content") hash1, _, err := storage.Store(bytes.NewReader(content)) if err != nil { t.Fatalf("Store() first error = %v", err) } hash2, _, err := storage.Store(bytes.NewReader(content)) if err != nil { t.Fatalf("Store() second error = %v", err) } if hash1 != hash2 { t.Errorf("Store() hashes differ: %s vs %s", hash1, hash2) } } func TestContentStorage_LoadNotFound(t *testing.T) { tmpDir := t.TempDir() storage, err := NewContentStorage(tmpDir) if err != nil { t.Fatalf("NewContentStorage() error = %v", err) } _, err = storage.Load("nonexistent") if err != ErrNotFound { t.Errorf("Load() error = %v, want ErrNotFound", err) } } func TestContentStorage_Delete(t *testing.T) { tmpDir := t.TempDir() storage, err := NewContentStorage(tmpDir) if err != nil { t.Fatalf("NewContentStorage() error = %v", err) } content := []byte("to be deleted") hash, _, err := storage.Store(bytes.NewReader(content)) if err != nil { t.Fatalf("Store() error = %v", err) } if !storage.Exists(hash) { t.Error("Exists() = false, want true") } if err := storage.Delete(hash); err != nil { t.Fatalf("Delete() error = %v", err) } if storage.Exists(hash) { t.Error("Exists() = true after delete, want false") } } func TestContentStorage_DeleteNonexistent(t *testing.T) { tmpDir := t.TempDir() storage, err := NewContentStorage(tmpDir) if err != nil { t.Fatalf("NewContentStorage() error = %v", err) } // Should not error if err := storage.Delete("nonexistent"); err != nil { t.Errorf("Delete() error = %v, want nil", err) } } func TestContentStorage_Path(t *testing.T) { tmpDir := t.TempDir() storage, err := NewContentStorage(tmpDir) if err != nil { t.Fatalf("NewContentStorage() error = %v", err) } hash := "abcdef0123456789" path := storage.Path(hash) expected := filepath.Join(tmpDir, "ab", "cd", hash) if path != expected { t.Errorf("Path() = %q, want %q", path, expected) } } func TestMetadataStorage_StoreAndLoad(t *testing.T) { tmpDir := t.TempDir() storage, err := NewMetadataStorage(tmpDir) if err != nil { t.Fatalf("NewMetadataStorage() error = %v", err) } meta := &SourceMetadata{ Host: "cdn.example.com", Path: "/photos/cat.jpg", ContentHash: "abc123", StatusCode: 200, ContentType: "image/jpeg", FetchedAt: 1704067200, ETag: `"etag123"`, } pathHash := HashPath("/photos/cat.jpg") err = storage.Store("cdn.example.com", pathHash, meta) if err != nil { t.Fatalf("Store() error = %v", err) } // Verify file exists at expected path expectedPath := filepath.Join(tmpDir, "cdn.example.com", pathHash+".json") if _, err := os.Stat(expectedPath); err != nil { t.Errorf("File not at expected path %s: %v", expectedPath, err) } // Load and verify loaded, err := storage.Load("cdn.example.com", pathHash) if err != nil { t.Fatalf("Load() error = %v", err) } if loaded.Host != meta.Host { t.Errorf("Host = %q, want %q", loaded.Host, meta.Host) } if loaded.Path != meta.Path { t.Errorf("Path = %q, want %q", loaded.Path, meta.Path) } if loaded.ContentHash != meta.ContentHash { t.Errorf("ContentHash = %q, want %q", loaded.ContentHash, meta.ContentHash) } if loaded.StatusCode != meta.StatusCode { t.Errorf("StatusCode = %d, want %d", loaded.StatusCode, meta.StatusCode) } if loaded.ETag != meta.ETag { t.Errorf("ETag = %q, want %q", loaded.ETag, meta.ETag) } } func TestMetadataStorage_LoadNotFound(t *testing.T) { tmpDir := t.TempDir() storage, err := NewMetadataStorage(tmpDir) if err != nil { t.Fatalf("NewMetadataStorage() error = %v", err) } _, err = storage.Load("example.com", "nonexistent") if err != ErrNotFound { t.Errorf("Load() error = %v, want ErrNotFound", err) } } func TestMetadataStorage_Delete(t *testing.T) { tmpDir := t.TempDir() storage, err := NewMetadataStorage(tmpDir) if err != nil { t.Fatalf("NewMetadataStorage() error = %v", err) } meta := &SourceMetadata{ Host: "example.com", Path: "/test.jpg", StatusCode: 200, } pathHash := HashPath("/test.jpg") err = storage.Store("example.com", pathHash, meta) if err != nil { t.Fatalf("Store() error = %v", err) } if !storage.Exists("example.com", pathHash) { t.Error("Exists() = false, want true") } if err := storage.Delete("example.com", pathHash); err != nil { t.Fatalf("Delete() error = %v", err) } if storage.Exists("example.com", pathHash) { t.Error("Exists() = true after delete, want false") } } func TestHashPath(t *testing.T) { // Same input should produce same hash hash1 := HashPath("/photos/cat.jpg") hash2 := HashPath("/photos/cat.jpg") if hash1 != hash2 { t.Errorf("HashPath() not deterministic: %s vs %s", hash1, hash2) } // Different input should produce different hash hash3 := HashPath("/photos/dog.jpg") if hash1 == hash3 { t.Error("HashPath() produced same hash for different inputs") } // Hash should be 64 hex chars (256 bits) if len(hash1) != 64 { t.Errorf("HashPath() length = %d, want 64", len(hash1)) } } func TestCacheKey(t *testing.T) { req1 := &ImageRequest{ SourceHost: "cdn.example.com", SourcePath: "/photos/cat.jpg", SourceQuery: "", Size: Size{Width: 800, Height: 600}, Format: FormatWebP, Quality: 85, FitMode: FitCover, } req2 := &ImageRequest{ SourceHost: "cdn.example.com", SourcePath: "/photos/cat.jpg", SourceQuery: "", Size: Size{Width: 800, Height: 600}, Format: FormatWebP, Quality: 85, FitMode: FitCover, } // Same request should produce same key key1 := CacheKey(req1) key2 := CacheKey(req2) if key1 != key2 { t.Errorf("CacheKey() not deterministic: %s vs %s", key1, key2) } // Key should be 64 hex chars if len(key1) != 64 { t.Errorf("CacheKey() length = %d, want 64", len(key1)) } // Different size should produce different key req3 := &ImageRequest{ SourceHost: "cdn.example.com", SourcePath: "/photos/cat.jpg", SourceQuery: "", Size: Size{Width: 400, Height: 300}, // Different size Format: FormatWebP, Quality: 85, FitMode: FitCover, } key3 := CacheKey(req3) if key1 == key3 { t.Error("CacheKey() produced same key for different sizes") } // Different format should produce different key req4 := &ImageRequest{ SourceHost: "cdn.example.com", SourcePath: "/photos/cat.jpg", SourceQuery: "", Size: Size{Width: 800, Height: 600}, Format: FormatPNG, // Different format Quality: 85, FitMode: FitCover, } key4 := CacheKey(req4) if key1 == key4 { t.Error("CacheKey() produced same key for different formats") } // Different quality should produce different key req5 := &ImageRequest{ SourceHost: "cdn.example.com", SourcePath: "/photos/cat.jpg", SourceQuery: "", Size: Size{Width: 800, Height: 600}, Format: FormatWebP, Quality: 50, // Different quality FitMode: FitCover, } key5 := CacheKey(req5) if key1 == key5 { t.Error("CacheKey() produced same key for different quality") } }