Define ContentHash, VariantKey, and PathHash types to replace raw strings, providing compile-time type safety for storage operations. Update storage layer to use typed parameters, refactor cache to use variant storage keyed by VariantKey, and implement source content reuse on cache misses.
360 lines
8.6 KiB
Go
360 lines
8.6 KiB
Go
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
|
|
hashStr := string(hash)
|
|
expectedPath := filepath.Join(tmpDir, hashStr[0:2], hashStr[2:4], hashStr)
|
|
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(ContentHash("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(ContentHash("nonexistent")); err != nil {
|
|
t.Errorf("Delete() error = %v, want nil", err)
|
|
}
|
|
}
|
|
|
|
func TestContentStorage_HashToPath(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
storage, err := NewContentStorage(tmpDir)
|
|
if err != nil {
|
|
t.Fatalf("NewContentStorage() error = %v", err)
|
|
}
|
|
|
|
// Test by storing and verifying the resulting path structure
|
|
content := []byte("test content for path verification")
|
|
hash, _, err := storage.Store(bytes.NewReader(content))
|
|
if err != nil {
|
|
t.Fatalf("Store() error = %v", err)
|
|
}
|
|
|
|
hashStr := string(hash)
|
|
expectedPath := filepath.Join(tmpDir, hashStr[0:2], hashStr[2:4], hashStr)
|
|
if _, err := os.Stat(expectedPath); err != nil {
|
|
t.Errorf("File not at expected path %s: %v", expectedPath, err)
|
|
}
|
|
}
|
|
|
|
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", string(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", PathHash("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(string(hash1)) != 64 {
|
|
t.Errorf("HashPath() length = %d, want 64", len(string(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(string(key1)) != 64 {
|
|
t.Errorf("CacheKey() length = %d, want 64", len(string(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")
|
|
}
|
|
}
|