diff --git a/internal/imageprocessor/computed_side_internal_test.go b/internal/imageprocessor/computed_side_internal_test.go new file mode 100644 index 0000000..f5c3d5a --- /dev/null +++ b/internal/imageprocessor/computed_side_internal_test.go @@ -0,0 +1,52 @@ +package imageprocessor + +import ( + "bytes" + "errors" + "testing" +) + +// TestImageProcessor_RefusesComputedSideAboveMaximum verifies that a size +// with one side 0, which Process computes from the input's proportions, is +// served when that side comes out at MaxOutputDimension and refused with +// ErrOutputTooLarge when it would be above. A 4096x1 or 1x4096 input given +// 2 for its short side has 8192 for its long side. +func TestImageProcessor_RefusesComputedSideAboveMaximum(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + inputWidth, inputHeight int + size Size + wantErr error + }{ + {"width at the maximum", 4096, 1, Size{Height: 2}, nil}, + {"width above the maximum", 4096, 1, Size{Height: 3}, ErrOutputTooLarge}, + {"height at the maximum", 1, 4096, Size{Width: 2}, nil}, + {"height above the maximum", 1, 4096, Size{Width: 3}, ErrOutputTooLarge}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + input := createTestJPEG(t, tt.inputWidth, tt.inputHeight) + + result, err := New(Params{}).Process(t.Context(), + bytes.NewReader(input), + &Request{Size: tt.size, Format: FormatJPEG, FitMode: FitCover}) + if result != nil { + t.Logf("Process() made %dx%d", result.Width, result.Height) + + if max(result.Width, result.Height) != MaxOutputDimension { + t.Errorf("Process() made %dx%d, want a side of %d", + result.Width, result.Height, MaxOutputDimension) + } + } + + if !errors.Is(err, tt.wantErr) { + t.Errorf("Process() error = %v, want %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/server/computed_side_internal_test.go b/internal/server/computed_side_internal_test.go new file mode 100644 index 0000000..ded083f --- /dev/null +++ b/internal/server/computed_side_internal_test.go @@ -0,0 +1,153 @@ +package server + +import ( + "bytes" + "encoding/json" + "image/jpeg" + "net/http" + "net/http/httptest" + "testing" + + "sneak.berlin/go/pixa/internal/encurl" + "sneak.berlin/go/pixa/internal/imgcache" +) + +// TestImageProxyRefusesComputedSideAboveMaximum requests sizes of a 3000x600 +// and a 600x3000 PNG through both image routes. A size with one side 0, whose +// side computed from the source's proportions would be above 8192, is refused +// with 400 and the JSON error body. The sizes after those refusals are served +// as before, which also shows that a refusal is not cached as a failed fetch of +// the source: orig, a size with both sides given, and a size with one side 0 +// whose computed side stays within 8192. +func TestImageProxyRefusesComputedSideAboveMaximum(t *testing.T) { + t.Parallel() + + // The upstream paths of the two sources + const ( + widePath = "/wide.png" + tallPath = "/tall.png" + ) + + wide := encodeTestPNG(t, 3000, 600) + tall := encodeTestPNG(t, 600, 3000) + + upstream := httptest.NewServer(http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "image/png") + + if r.URL.Path == tallPath { + _, _ = w.Write(tall) + + return + } + + _, _ = w.Write(wide) + })) + t.Cleanup(upstream.Close) + + s, _, _ := startImageProxy(t, upstream) + + generator, err := encurl.NewGenerator(testSigningKey) + if err != nil { + t.Fatalf("encurl.NewGenerator: %v", err) + } + + tests := []struct { + path string + // size is the size of the /v1/image/ URL, and width and height are + // those of the encrypted URL + size string + width, height int + // wantWidth and wantHeight are the served image's, 0 for a refusal + wantWidth, wantHeight int + }{ + {widePath, "0x8192", 0, 8192, 0, 0}, + {tallPath, "8192x0", 8192, 0, 0, 0}, + {widePath, "orig", 0, 0, 3000, 600}, + {widePath, "300x200", 300, 200, 300, 200}, + {widePath, "0x300", 0, 300, 1500, 300}, + {tallPath, "300x0", 300, 0, 300, 1500}, + } + + for _, tt := range tests { + token, err := generator.Generate(&encurl.Payload{ + SourceHost: upstreamHost, SourcePath: tt.path, + Width: tt.width, Height: tt.height, Format: imgcache.FormatJPEG, + }) + if err != nil { + t.Fatalf("encurl Generate: %v", err) + } + + for _, target := range []string{ + "/v1/image/" + upstreamHost + tt.path + "/" + tt.size + ".jpeg", + "/v1/e/" + token + "/img.jpg", + } { + rec := httptest.NewRecorder() + s.ServeHTTP(rec, httptest.NewRequestWithContext( + t.Context(), http.MethodGet, target, nil)) + t.Logf("GET %s: %d, Content-Type %s", + target, rec.Code, rec.Header().Get("Content-Type")) + + if tt.wantWidth == 0 { + checkSizeRefused(t, rec) + } else { + checkServedSize(t, rec, tt.wantWidth, tt.wantHeight) + } + } + } +} + +// checkSizeRefused checks that rec answers 400 with the JSON error body that +// refuses a size whose side computed from the source's proportions is above +// 8192. +func checkSizeRefused(t *testing.T, rec *httptest.ResponseRecorder) { + t.Helper() + + const wantError = "size too large for the source: " + + "the side computed from its proportions is above 8192" + + var body struct { + Error string `json:"error"` + Status int `json:"status"` + } + + err := json.Unmarshal(rec.Body.Bytes(), &body) + if err != nil { + t.Errorf("status %d, body is not JSON: %v", rec.Code, err) + + return + } + + if rec.Code != http.StatusBadRequest || body.Status != http.StatusBadRequest || + body.Error != wantError { + t.Errorf("status %d, body %+v, want %d and the error %q", + rec.Code, body, http.StatusBadRequest, wantError) + } +} + +// checkServedSize checks that rec answers 200 with a JPEG of wantWidth by +// wantHeight. +func checkServedSize( + t *testing.T, rec *httptest.ResponseRecorder, wantWidth, wantHeight int, +) { + t.Helper() + + if rec.Code != http.StatusOK { + t.Errorf("status %d, want %d; body %s", + rec.Code, http.StatusOK, rec.Body.String()) + + return + } + + served, err := jpeg.DecodeConfig(bytes.NewReader(rec.Body.Bytes())) + if err != nil { + t.Errorf("decoding the served JPEG: %v", err) + + return + } + + if served.Width != wantWidth || served.Height != wantHeight { + t.Errorf("served %dx%d, want %dx%d", + served.Width, served.Height, wantWidth, wantHeight) + } +}