package imgcache import ( "bytes" "errors" "io" "os" "path/filepath" "testing" ) func TestContentStorage_StoreAndLoad(t *testing.T) { t.Parallel() 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) _, err = os.Stat(expectedPath) if 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 func() { _ = 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) { t.Parallel() 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) { t.Parallel() tmpDir := t.TempDir() storage, err := NewContentStorage(tmpDir) if err != nil { t.Fatalf("NewContentStorage() error = %v", err) } _, err = storage.Load(ContentHash("nonexistent")) if !errors.Is(err, ErrNotFound) { t.Errorf("Load() error = %v, want ErrNotFound", err) } } func TestContentStorage_Delete(t *testing.T) { t.Parallel() 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") } err = storage.Delete(hash) if 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) { t.Parallel() tmpDir := t.TempDir() storage, err := NewContentStorage(tmpDir) if err != nil { t.Fatalf("NewContentStorage() error = %v", err) } // Should not error err = storage.Delete(ContentHash("nonexistent")) if err != nil { t.Errorf("Delete() error = %v, want nil", err) } } func TestContentStorage_HashToPath(t *testing.T) { t.Parallel() 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) _, err = os.Stat(expectedPath) if err != nil { t.Errorf("File not at expected path %s: %v", expectedPath, err) } } func TestMetadataStorage_StoreAndLoad(t *testing.T) { t.Parallel() tmpDir := t.TempDir() storage, err := NewMetadataStorage(tmpDir) if err != nil { t.Fatalf("NewMetadataStorage() error = %v", err) } meta := &SourceMetadata{ Host: testHostCDN, Path: testPathCat, ContentHash: "abc123", StatusCode: 200, ContentType: testContentTypeJPEG, FetchedAt: 1704067200, ETag: `"etag123"`, } pathHash := HashPath(testPathCat) err = storage.Store(testHostCDN, pathHash, meta) if err != nil { t.Fatalf("Store() error = %v", err) } // Verify file exists at expected path expectedPath := filepath.Join(tmpDir, testHostCDN, string(pathHash)+".json") _, err = os.Stat(expectedPath) if err != nil { t.Errorf("File not at expected path %s: %v", expectedPath, err) } // Load and verify loaded, err := storage.Load(testHostCDN, 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) { t.Parallel() tmpDir := t.TempDir() storage, err := NewMetadataStorage(tmpDir) if err != nil { t.Fatalf("NewMetadataStorage() error = %v", err) } _, err = storage.Load(testHostExample, PathHash("nonexistent")) if !errors.Is(err, ErrNotFound) { t.Errorf("Load() error = %v, want ErrNotFound", err) } } func TestMetadataStorage_Delete(t *testing.T) { t.Parallel() tmpDir := t.TempDir() storage, err := NewMetadataStorage(tmpDir) if err != nil { t.Fatalf("NewMetadataStorage() error = %v", err) } meta := &SourceMetadata{ Host: testHostExample, Path: "/test.jpg", StatusCode: 200, } pathHash := HashPath("/test.jpg") err = storage.Store(testHostExample, pathHash, meta) if err != nil { t.Fatalf("Store() error = %v", err) } if !storage.Exists(testHostExample, pathHash) { t.Error("Exists() = false, want true") } err = storage.Delete(testHostExample, pathHash) if err != nil { t.Fatalf("Delete() error = %v", err) } if storage.Exists(testHostExample, pathHash) { t.Error("Exists() = true after delete, want false") } } func TestHashPath(t *testing.T) { t.Parallel() // Same input should produce same hash hash1 := HashPath(testPathCat) hash2 := HashPath(testPathCat) 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) { t.Parallel() req1 := &ImageRequest{ SourceHost: testHostCDN, SourcePath: testPathCat, SourceQuery: "", Size: Size{Width: 800, Height: 600}, Format: FormatWebP, Quality: 85, FitMode: FitCover, } req2 := &ImageRequest{ SourceHost: testHostCDN, SourcePath: testPathCat, 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: testHostCDN, SourcePath: testPathCat, 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: testHostCDN, SourcePath: testPathCat, 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: testHostCDN, SourcePath: testPathCat, 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") } }