check / check (push) Waiting to run
The fetcher accepts image/svg+xml; internal/magic detects SVG. orig of an SVG at the orig size is the SVG unchanged, cached, with a Content-Security-Policy allowing only its styles and data: images; at another size, ErrSVGNotOriginalSize (400). Any other format renders it at the least size covering the output, at most 8192 on a side, so it stays sharp. Rendering refuses an SVG with a side over 8192 times the other (ErrSVGTooThin, 415), and the orig size of one above 8192 on a side (ErrSVGTooLargeForOrigSize, 400). The cache reads up to 1024 first bytes to find a file's format. Tests pinning the SVG refusal now expect SVG. Render time and memory are unbounded: P1 in TODO.md until the owner decides on #68. Model: opus-5-5
126 lines
4.1 KiB
Go
126 lines
4.1 KiB
Go
package server
|
|
|
|
import (
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"sync/atomic"
|
|
"testing"
|
|
|
|
"sneak.berlin/go/pixa/internal/imageproxy"
|
|
"sneak.berlin/go/pixa/internal/imagerequest"
|
|
"sneak.berlin/go/pixa/internal/magic"
|
|
"sneak.berlin/go/pixa/internal/middleware"
|
|
)
|
|
|
|
// testSVG is the SVG the upstream serves in the SVG tests below.
|
|
const testSVG = `<svg xmlns="http://www.w3.org/2000/svg" width="64" height="48">` +
|
|
`<rect width="64" height="48"/></svg>`
|
|
|
|
// startSVGUpstream starts an upstream that answers every request with
|
|
// testSVG as image/svg+xml, and returns it with its count of requests.
|
|
func startSVGUpstream(t *testing.T) (*httptest.Server, *atomic.Int64) {
|
|
t.Helper()
|
|
|
|
var requests atomic.Int64
|
|
|
|
upstream := httptest.NewServer(http.HandlerFunc(
|
|
func(w http.ResponseWriter, _ *http.Request) {
|
|
requests.Add(1)
|
|
w.Header().Set("Content-Type", "image/svg+xml")
|
|
_, _ = io.WriteString(w, testSVG)
|
|
}))
|
|
t.Cleanup(upstream.Close)
|
|
|
|
return upstream, &requests
|
|
}
|
|
|
|
// TestImageProxyServesSVGAsItIs requests an SVG source in its own format
|
|
// (orig) at the orig size through pixa's router, upstream fetcher, image
|
|
// processor and cache, twice. Both answers are the SVG itself, never another
|
|
// format, the second from the cache; the upstream is asked once. Both carry
|
|
// the Content-Security-Policy of an SVG response in place of the
|
|
// middleware's. orig at another size is refused with 400.
|
|
func TestImageProxyServesSVGAsItIs(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const wantPolicy = "default-src 'none'; style-src 'unsafe-inline'; " +
|
|
"img-src data:; frame-ancestors 'none'; sandbox"
|
|
|
|
upstream, requests := startSVGUpstream(t)
|
|
s, _, _ := startImageProxy(t, upstream)
|
|
target := "/v1/image/" + upstreamHost + "/drawing.svg/orig.orig"
|
|
|
|
for _, wantCache := range []imageproxy.CacheStatus{
|
|
imageproxy.CacheMiss, imageproxy.CacheHit,
|
|
} {
|
|
rec := getImage(t, s, target)
|
|
|
|
if rec.Header().Get("Content-Type") != "image/svg+xml" ||
|
|
rec.Body.String() != testSVG {
|
|
t.Errorf("GET %s: %s %q, want image/svg+xml %q", target,
|
|
rec.Header().Get("Content-Type"), rec.Body.String(), testSVG)
|
|
}
|
|
|
|
if got := rec.Header().Get("X-Pixa-Cache"); got != string(wantCache) {
|
|
t.Errorf("GET %s: X-Pixa-Cache = %q, want %q", target, got, wantCache)
|
|
}
|
|
|
|
policy := rec.Header().Get("Content-Security-Policy")
|
|
if policy != wantPolicy {
|
|
t.Errorf("GET %s: Content-Security-Policy = %q, want %q", target,
|
|
policy, wantPolicy)
|
|
}
|
|
}
|
|
|
|
if requests.Load() != 1 {
|
|
t.Errorf("upstream requests = %d, want 1", requests.Load())
|
|
}
|
|
|
|
resized := "/v1/image/" + upstreamHost + "/drawing.svg/32x24.orig"
|
|
|
|
rec := httptest.NewRecorder()
|
|
s.ServeHTTP(rec, httptest.NewRequestWithContext(
|
|
t.Context(), http.MethodGet, resized, nil))
|
|
t.Logf("GET %s: %d %s", resized, rec.Code, rec.Body.String())
|
|
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Errorf("GET %s: %d %s, want 400", resized, rec.Code, rec.Body.String())
|
|
}
|
|
}
|
|
|
|
// TestImageProxyRendersSVG requests an SVG source as each raster format
|
|
// through pixa's router, upstream fetcher and image processor. Each answer
|
|
// is an image in that format, with the middleware's Content-Security-Policy.
|
|
func TestImageProxyRendersSVG(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
upstream, _ := startSVGUpstream(t)
|
|
s, _, _ := startImageProxy(t, upstream)
|
|
|
|
for _, format := range []imagerequest.Format{
|
|
imagerequest.FormatJPEG, imagerequest.FormatPNG, imagerequest.FormatGIF,
|
|
imagerequest.FormatWebP, imagerequest.FormatAVIF, imagerequest.FormatJXL,
|
|
} {
|
|
target := "/v1/image/" + upstreamHost + "/drawing.svg/32x24." +
|
|
string(format)
|
|
|
|
rec := getImage(t, s, target)
|
|
detected, err := magic.DetectFormat(rec.Body.Bytes())
|
|
t.Logf("GET %s: %s, body in %q (%v)", target,
|
|
rec.Header().Get("Content-Type"), detected, err)
|
|
|
|
if rec.Header().Get("Content-Type") != format.MIMEType() ||
|
|
err != nil || detected != format {
|
|
t.Errorf("GET %s: %s, body in %q (%v); want %s", target,
|
|
rec.Header().Get("Content-Type"), detected, err, format.MIMEType())
|
|
}
|
|
|
|
policy := rec.Header().Get("Content-Security-Policy")
|
|
if policy != middleware.ContentSecurityPolicyValue {
|
|
t.Errorf("GET %s: Content-Security-Policy = %q, want %q", target,
|
|
policy, middleware.ContentSecurityPolicyValue)
|
|
}
|
|
}
|
|
}
|