check / check (push) Waiting to run
magic.DetectFormat returns an imagerequest.Format. ValidateMagicBytes returns the format it checked against the upstream Content-Type. The cache reads a cached source's format from its file's first bytes, as it reads a variant's. The proxy hands the format to Process. Process refuses a format that is not a source format, and input libvips would load with another loader, such as an SVG passing as AVIF. Format.MIMEType declares each MIME string once, and the fetcher, the cache's variant read and the Accept matching use it. magic.MIMEType, magic.ImageFormat, MIMEToImageFormat, magic's SVG detection, detectFormat, formatFromString, FormatToMIME and Result.InputFormat are gone. Model: opus-5-5
239 lines
7.4 KiB
Go
239 lines
7.4 KiB
Go
package imgcache
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"image/color"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"sneak.berlin/go/pixa/internal/imageprocessor"
|
|
"sneak.berlin/go/pixa/internal/imagerequest"
|
|
"sneak.berlin/go/pixa/internal/magic"
|
|
)
|
|
|
|
// TestPutSource_StoresIdenticalBytesOfTwoURLsTwice stores the same bytes
|
|
// for two source URLs and checks that each URL has a source_content row of
|
|
// its own and a file of its own, named by its source key under
|
|
// cache/sources/<ab>/<cd>/.
|
|
func TestPutSource_StoresIdenticalBytesOfTwoURLsTwice(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cache, stateDir := newEvictionTestCache(t, 1<<30)
|
|
content := bytes.Repeat([]byte{0x5A}, 400)
|
|
|
|
keys := map[string]imagerequest.SourceKey{
|
|
"/a.jpg": storeEvictionTestSource(t, cache, testHostCDN, "/a.jpg", content),
|
|
"/b.jpg": storeEvictionTestSource(t, cache, testHostCDN, "/b.jpg", content),
|
|
}
|
|
t.Logf("source keys: %v", keys)
|
|
|
|
if keys["/a.jpg"] == keys["/b.jpg"] {
|
|
t.Fatalf("both source URLs got the key %s", keys["/a.jpg"])
|
|
}
|
|
|
|
for sourcePath, key := range keys {
|
|
rows := countRows(t, cache,
|
|
`SELECT COUNT(*) FROM source_content WHERE source_key = ?`, string(key))
|
|
if rows != 1 {
|
|
t.Errorf("%s has %d source_content rows, want 1", sourcePath, rows)
|
|
}
|
|
|
|
name := string(key)
|
|
file := filepath.Join(stateDir, "cache", "sources", name[0:2], name[2:4], name)
|
|
|
|
stored, err := os.ReadFile(file) //nolint:gosec // path in the test's state dir
|
|
if err != nil || !bytes.Equal(stored, content) {
|
|
t.Errorf("file of %s at %s holds %d bytes (error %v), want the %d stored",
|
|
sourcePath, file, len(stored), err, len(content))
|
|
}
|
|
}
|
|
|
|
usage, err := cache.UsageBytes(t.Context())
|
|
if err != nil {
|
|
t.Fatalf("UsageBytes() error = %v", err)
|
|
}
|
|
|
|
if usage != 800 {
|
|
t.Errorf("UsageBytes() = %d, want 800, the 400 bytes once for each URL", usage)
|
|
}
|
|
}
|
|
|
|
// TestEvictSourceBlob_LeavesSourceWithIdenticalBytesServed stores the same
|
|
// bytes for two source URLs and evicts the first. Its row and file must be
|
|
// gone, and the second must still be found and read from the cache.
|
|
func TestEvictSourceBlob_LeavesSourceWithIdenticalBytesServed(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cache, _ := newEvictionTestCache(t, 1<<30)
|
|
content := generateTestJPEG(t, 20, 20, color.RGBA{0x5B, 0x5B, 0x5B, 255})
|
|
|
|
evicted := storeEvictionTestSource(t, cache, testHostCDN, "/evicted.jpg", content)
|
|
storeEvictionTestSource(t, cache, testHostCDN, "/kept.jpg", content)
|
|
|
|
err := cache.evictSourceBlob(t.Context(), evicted)
|
|
if err != nil {
|
|
t.Fatalf("evictSourceBlob() error = %v", err)
|
|
}
|
|
|
|
_, err = os.Stat(cache.srcContent.keyToPath(string(evicted)))
|
|
if err == nil {
|
|
t.Error("the evicted source's file is still on disk")
|
|
}
|
|
|
|
if n := countRows(t, cache,
|
|
`SELECT COUNT(*) FROM source_content WHERE source_key = ?`, string(evicted),
|
|
); n != 0 {
|
|
t.Errorf("the evicted source still has %d source_content rows", n)
|
|
}
|
|
|
|
reader, _, _, err := cache.Source(t.Context(),
|
|
imagerequest.Source{Host: testHostCDN, Path: "/kept.jpg"})
|
|
t.Logf("Source() of the kept source: error %v", err)
|
|
|
|
if err != nil {
|
|
t.Fatalf("Source() of the kept source error = %v; want it cached", err)
|
|
}
|
|
|
|
defer func() { _ = reader.Close() }()
|
|
|
|
served, err := io.ReadAll(reader)
|
|
if err != nil {
|
|
t.Fatalf("reading the kept source: %v", err)
|
|
}
|
|
|
|
if !bytes.Equal(served, content) {
|
|
t.Errorf("the kept source reads as %d bytes, want the %d stored",
|
|
len(served), len(content))
|
|
}
|
|
}
|
|
|
|
// TestPutSource_FailedRowWriteLeavesNoFile drops source_content, so that
|
|
// PutSource cannot write the source's row, and checks that the store fails
|
|
// and leaves no file under the source's key.
|
|
func TestPutSource_FailedRowWriteLeavesNoFile(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cache, _ := newEvictionTestCache(t, 1<<30)
|
|
|
|
_, err := cache.db.ExecContext(t.Context(), `DROP TABLE source_content`)
|
|
if err != nil {
|
|
t.Fatalf("failed to drop source_content: %v", err)
|
|
}
|
|
|
|
req := &imagerequest.Request{
|
|
Source: imagerequest.Source{Host: testHostCDN, Path: testPathCat},
|
|
}
|
|
key, err := cache.PutSource(t.Context(), req.Source,
|
|
bytes.NewReader([]byte("source bytes")))
|
|
t.Logf("PutSource() = %q, error = %v", key, err)
|
|
|
|
if err == nil {
|
|
t.Error("PutSource() error = nil, want the failed row write")
|
|
}
|
|
|
|
_, err = os.Stat(cache.srcContent.keyToPath(string(req.Source.Key())))
|
|
if err == nil {
|
|
t.Error("the source's file is on disk although its row was not written")
|
|
}
|
|
}
|
|
|
|
// TestSource_FormatFromFirstBytes stores an image in each source format
|
|
// under one source URL, in turn. After each store, Source must return that
|
|
// image whole, with its format read from the file.
|
|
func TestSource_FormatFromFirstBytes(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cache, _ := newEvictionTestCache(t, 1<<30)
|
|
processor := imageprocessor.New(imageprocessor.Params{
|
|
MaxInputBytes: 50 << 20,
|
|
MaxConcurrentProcessing: 1,
|
|
})
|
|
jpegImage := generateTestJPEG(t, 40, 30, color.RGBA{200, 40, 40, 255})
|
|
src := imagerequest.Source{Host: testHostCDN, Path: testPathCat}
|
|
|
|
for _, format := range []imagerequest.Format{
|
|
imagerequest.FormatJPEG, imagerequest.FormatPNG, imagerequest.FormatGIF,
|
|
imagerequest.FormatWebP, imagerequest.FormatAVIF, imagerequest.FormatJXL,
|
|
} {
|
|
result, err := processor.Process(t.Context(), bytes.NewReader(jpegImage),
|
|
imagerequest.FormatJPEG, &imagerequest.Transform{Format: format})
|
|
if err != nil {
|
|
t.Fatalf("making a %s image: %v", format, err)
|
|
}
|
|
|
|
image, err := io.ReadAll(result.Content)
|
|
if err != nil {
|
|
t.Fatalf("reading the %s image: %v", format, err)
|
|
}
|
|
|
|
_, err = cache.PutSource(t.Context(), src, bytes.NewReader(image))
|
|
if err != nil {
|
|
t.Fatalf("PutSource(%s) error = %v", format, err)
|
|
}
|
|
|
|
reader, size, got, err := cache.Source(t.Context(), src)
|
|
if err != nil {
|
|
t.Fatalf("Source() of the %s image error = %v", format, err)
|
|
}
|
|
|
|
served, err := io.ReadAll(reader)
|
|
_ = reader.Close()
|
|
|
|
if err != nil {
|
|
t.Fatalf("reading the %s source: %v", format, err)
|
|
}
|
|
|
|
t.Logf("%s: %d bytes read as %s", format, size, got)
|
|
|
|
if got != format {
|
|
t.Errorf("Source() of the %s image format = %q, want %q",
|
|
format, got, format)
|
|
}
|
|
|
|
if size != int64(len(image)) || !bytes.Equal(served, image) {
|
|
t.Errorf("%s source read as %d bytes, size %d, want the %d bytes stored",
|
|
format, len(served), size, len(image))
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestSource_RefusesFileOfNoSourceFormat checks that Source refuses a
|
|
// cached source whose file's first bytes are not those of a source format,
|
|
// with an error other than ErrNotFound.
|
|
func TestSource_RefusesFileOfNoSourceFormat(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cache, _ := newEvictionTestCache(t, 1<<30)
|
|
jpegImage := generateTestJPEG(t, 10, 10, color.RGBA{0, 0, 255, 255})
|
|
|
|
tests := []struct {
|
|
name string
|
|
content []byte
|
|
wantErr error
|
|
}{
|
|
{"text", []byte("this is text, not an image"), magic.ErrUnknownFormat},
|
|
{"SVG", []byte(`<svg xmlns="http://www.w3.org/2000/svg"></svg>`),
|
|
magic.ErrUnknownFormat},
|
|
{"JPEG cut to 10 bytes", jpegImage[:10], io.EOF},
|
|
{"empty file", nil, io.EOF},
|
|
}
|
|
|
|
for i, tt := range tests {
|
|
path := fmt.Sprintf("/refused%d.jpg", i)
|
|
storeEvictionTestSource(t, cache, testHostCDN, path, tt.content)
|
|
|
|
reader, _, format, err := cache.Source(t.Context(),
|
|
imagerequest.Source{Host: testHostCDN, Path: path})
|
|
t.Logf("%s: Source() error = %v", tt.name, err)
|
|
|
|
if reader != nil || format != "" || !errors.Is(err, tt.wantErr) {
|
|
t.Errorf("%s: Source() = reader %v, format %q, error %v; "+
|
|
"want no reader and %v", tt.name, reader, format, err, tt.wantErr)
|
|
}
|
|
}
|
|
}
|