test: cover encrypted-URL and generator validation gaps (#62)
Failing tests for the missing validation on the encrypted-URL path and the token generator: an encrypted token carrying an over-limit dimension or an unrecognized fit mode must be rejected with 400, and POST /generate with a non-numeric or over-limit width must return 400 rather than minting a token. Also covers the shared imgcache validator directly. Model: opus-4-8
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"maps"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// generatePost submits the /generate form with a valid session and CSRF token
|
||||
// plus the caller's extra fields, returning the recorder.
|
||||
func generatePost(
|
||||
t *testing.T, extra url.Values,
|
||||
) *httptest.ResponseRecorder {
|
||||
t.Helper()
|
||||
|
||||
h, srv := newCSRFTestRouter(t)
|
||||
|
||||
sessionCookie := newSessionCookie(t, h)
|
||||
cookies, token := csrfCredentials(t, srv, []*http.Cookie{sessionCookie})
|
||||
cookies = append(cookies, sessionCookie)
|
||||
|
||||
form := url.Values{
|
||||
sourceURLField: {testSourceURL},
|
||||
csrfTokenField: {token},
|
||||
}
|
||||
maps.Copy(form, extra)
|
||||
|
||||
return postForm(srv, "/generate", cookies, form)
|
||||
}
|
||||
|
||||
// TestGeneratePostRejectsNonNumericWidth verifies that a non-numeric width is
|
||||
// rejected with 400 naming the field rather than being coerced to 0 and
|
||||
// minting a 0-width token.
|
||||
func TestGeneratePostRejectsNonNumericWidth(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rec := generatePost(t, url.Values{"width": {"abc"}})
|
||||
|
||||
if rec.Code != http.StatusBadRequest {
|
||||
t.Fatalf("status = %d, want %d", rec.Code, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if strings.Contains(rec.Body.String(), "/v1/e/") {
|
||||
t.Error("a token was generated for non-numeric width")
|
||||
}
|
||||
}
|
||||
|
||||
// TestGeneratePostRejectsOverLimitWidth verifies that a width beyond
|
||||
// MaxDimension is rejected at generation time so an unusable token cannot be
|
||||
// minted.
|
||||
func TestGeneratePostRejectsOverLimitWidth(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rec := generatePost(t, url.Values{"width": {"100000"}})
|
||||
|
||||
if rec.Code != http.StatusBadRequest {
|
||||
t.Fatalf("status = %d, want %d", rec.Code, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if strings.Contains(rec.Body.String(), "/v1/e/") {
|
||||
t.Error("a token was generated for an over-limit width")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"sneak.berlin/go/pixa/internal/encurl"
|
||||
"sneak.berlin/go/pixa/internal/imgcache"
|
||||
)
|
||||
|
||||
// newEncTestServer builds a router serving the encrypted-URL route with a
|
||||
// generator seeded by the shared test signing key. The image service is left
|
||||
// nil: these tests exercise validation that rejects a token before any image
|
||||
// is fetched, so the handler must never reach the service.
|
||||
func newEncTestServer(t *testing.T) (*encurl.Generator, http.Handler) {
|
||||
t.Helper()
|
||||
|
||||
encGen, err := encurl.NewGenerator(testSigningKey)
|
||||
if err != nil {
|
||||
t.Fatalf("encurl.NewGenerator() error = %v", err)
|
||||
}
|
||||
|
||||
h := &Handlers{
|
||||
log: slog.New(slog.DiscardHandler),
|
||||
encGen: encGen,
|
||||
}
|
||||
|
||||
r := chi.NewRouter()
|
||||
r.Get("/v1/e/{token}/*", h.HandleImageEnc())
|
||||
|
||||
return encGen, r
|
||||
}
|
||||
|
||||
// getEncToken issues a GET for the given token and returns the recorder.
|
||||
func getEncToken(srv http.Handler, token string) *httptest.ResponseRecorder {
|
||||
req := httptest.NewRequestWithContext(
|
||||
context.Background(), http.MethodGet, "/v1/e/"+token+"/img.jpg", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
srv.ServeHTTP(rec, req)
|
||||
|
||||
return rec
|
||||
}
|
||||
|
||||
// TestHandleImageEnc_OverLimitDimension_Returns400 verifies that a decrypted
|
||||
// token requesting a dimension beyond MaxDimension is rejected with 400
|
||||
// instead of reaching the image processor and libvips.
|
||||
func TestHandleImageEnc_OverLimitDimension_Returns400(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
encGen, srv := newEncTestServer(t)
|
||||
|
||||
token, err := encGen.Generate(&encurl.Payload{
|
||||
SourceHost: "cdn.example.com",
|
||||
SourcePath: "/photo.jpg",
|
||||
Width: 100000,
|
||||
Height: 100000,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Generate() error = %v", err)
|
||||
}
|
||||
|
||||
rec := getEncToken(srv, token)
|
||||
|
||||
if rec.Code != http.StatusBadRequest {
|
||||
t.Fatalf("status = %d, want %d", rec.Code, http.StatusBadRequest)
|
||||
}
|
||||
}
|
||||
|
||||
// TestHandleImageEnc_InvalidFitMode_Returns400 verifies that a decrypted token
|
||||
// carrying an unrecognized fit mode is rejected with 400 rather than surfacing
|
||||
// as a 500 from the image processor's default branch.
|
||||
func TestHandleImageEnc_InvalidFitMode_Returns400(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
encGen, srv := newEncTestServer(t)
|
||||
|
||||
token, err := encGen.Generate(&encurl.Payload{
|
||||
SourceHost: "cdn.example.com",
|
||||
SourcePath: "/photo.jpg",
|
||||
Width: 800,
|
||||
Height: 600,
|
||||
FitMode: imgcache.FitMode("bogus"),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Generate() error = %v", err)
|
||||
}
|
||||
|
||||
rec := getEncToken(srv, token)
|
||||
|
||||
if rec.Code != http.StatusBadRequest {
|
||||
t.Fatalf("status = %d, want %d", rec.Code, http.StatusBadRequest)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package imgcache
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestValidateImageRequest(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
req ImageRequest
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "within bounds",
|
||||
req: ImageRequest{Size: Size{Width: 800, Height: 600}, FitMode: FitCover},
|
||||
},
|
||||
{
|
||||
name: "original size and empty fit",
|
||||
req: ImageRequest{Size: Size{Width: 0, Height: 0}},
|
||||
},
|
||||
{
|
||||
name: "width over limit",
|
||||
req: ImageRequest{Size: Size{Width: MaxDimension + 1, Height: 600}},
|
||||
wantErr: ErrDimensionTooLarge,
|
||||
},
|
||||
{
|
||||
name: "height over limit",
|
||||
req: ImageRequest{Size: Size{Width: 800, Height: MaxDimension + 1}},
|
||||
wantErr: ErrDimensionTooLarge,
|
||||
},
|
||||
{
|
||||
name: "negative width",
|
||||
req: ImageRequest{Size: Size{Width: -1, Height: 600}},
|
||||
wantErr: ErrInvalidSize,
|
||||
},
|
||||
{
|
||||
name: "invalid fit mode",
|
||||
req: ImageRequest{Size: Size{Width: 800, Height: 600}, FitMode: "bogus"},
|
||||
wantErr: ErrInvalidFitMode,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
err := ValidateImageRequest(&tt.req)
|
||||
if tt.wantErr == nil {
|
||||
if err != nil {
|
||||
t.Fatalf("ValidateImageRequest() error = %v, want nil", err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if !errors.Is(err, tt.wantErr) {
|
||||
t.Fatalf("ValidateImageRequest() error = %v, want %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user