package imageproxy import ( "bytes" "io" "os" "testing" "sneak.berlin/go/pixa/internal/imagerequest" ) // getImage calls Get for req and returns the image it answered with and // its cache status. func getImage( t *testing.T, svc *testProxy, req *imagerequest.Request, ) ([]byte, CacheStatus) { t.Helper() resp, err := svc.Get(t.Context(), req) if err != nil { t.Fatalf("Get() of quality %d error = %v", req.Transform.Quality, err) } defer func() { _ = resp.Content.Close() }() body, err := io.ReadAll(resp.Content) if err != nil { t.Fatalf("reading the image of quality %d: %v", req.Transform.Quality, err) } return body, resp.CacheStatus } // fileSize returns the size of the file at path. func fileSize(t *testing.T, path string) int64 { t.Helper() info, err := os.Stat(path) if err != nil { t.Fatalf("stat %s: %v", path, err) } return info.Size() } // cachedSourcePath returns the path of the cached source file req uses. func cachedSourcePath( t *testing.T, svc *testProxy, req *imagerequest.Request, ) string { t.Helper() reader, _, _, err := svc.cache.Source(t.Context(), req.Source) if err != nil { t.Fatalf("Source() error = %v; want the cached source", err) } _ = reader.Close() return sourceFilePath(svc, req.Source.Key()) } // TestProxy_Get_FetchesAgainForRemovedCachedSource removes the cached // source's row and file, as the reconciliation pass does when the file is // empty or cut short. internal/imgcache tests that the pass does so. The // test then asks for two new sizes of the source. The first must fetch it // again and cache it whole, so the second is made from the cache without a // third fetch. func TestProxy_Get_FetchesAgainForRemovedCachedSource(t *testing.T) { t.Parallel() svc, fixtures := setupTestProxy(t) getImage(t, svc, photoVariant(fixtures, 85, imagerequest.FitCover)) source := photoVariant(fixtures, 85, imagerequest.FitCover).Source path := cachedSourcePath(t, svc, photoVariant(fixtures, 85, imagerequest.FitCover)) wholeSize := fileSize(t, path) // Removes the source as the reconciliation pass does: its row, then its file _, err := svc.db.ExecContext(t.Context(), `DELETE FROM source_content WHERE source_key = ?`, string(source.Key())) if err != nil { t.Fatalf("deleting the source's row: %v", err) } err = os.Remove(path) if err != nil { t.Fatalf("removing the source's file: %v", err) } getImage(t, svc, photoVariant(fixtures, 40, imagerequest.FitCover)) getImage(t, svc, photoVariant(fixtures, 30, imagerequest.FitCover)) fetches := readCounters(t, svc).upstreamFetchCount t.Logf("upstream fetches = %d", fetches) if fetches != 2 { t.Errorf("upstream fetches = %d, want 2", fetches) } size := fileSize(t, path) if size != wholeSize { t.Errorf("cached source is %d bytes, want %d", size, wholeSize) } } // TestProxy_Get_ConvertsAgainForRemovedCachedVariant caches a variant of // the photo and removes its row and file, as the reconciliation pass does // when the file is empty or cut short. internal/imgcache tests that the // pass does so. The test then checks that Get converts the image again, and // that the next Get serves the whole variant from the cache. func TestProxy_Get_ConvertsAgainForRemovedCachedVariant(t *testing.T) { t.Parallel() svc, fixtures := setupTestProxy(t) req := photoVariant(fixtures, 85, imagerequest.FitCover) want, _ := getImage(t, svc, req) key := imagerequest.CacheKey(req) // Removes the variant as the reconciliation pass does: its row, then its file _, err := svc.db.ExecContext(t.Context(), `DELETE FROM variant_content WHERE cache_key = ?`, string(key)) if err != nil { t.Fatalf("deleting the variant's row: %v", err) } err = os.Remove(variantFilePath(svc, key)) if err != nil { t.Fatalf("removing the variant's file: %v", err) } got, status := getImage(t, svc, req) t.Logf("after the removal: %d bytes, %s", len(got), status) if status != CacheMiss || !bytes.Equal(got, want) { t.Errorf("after the removal: Get() = %d bytes, %s; want the %d bytes "+ "converted again, %s", len(got), status, len(want), CacheMiss) } got, status = getImage(t, svc, req) t.Logf("next request: %d bytes, %s", len(got), status) if status != CacheHit || !bytes.Equal(got, want) { t.Errorf("next request: Get() = %d bytes, %s; want the %d bytes, %s", len(got), status, len(want), CacheHit) } }