ContentStorage stores blobs at <dir>/<ab>/<cd>/<sha256> paths. MetadataStorage stores JSON at <dir>/<host>/<path_hash>.json. CacheKey generates unique keys from request parameters.
354 lines
8.2 KiB
Go
354 lines
8.2 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
|
|
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")
|
|
}
|
|
}
|