Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb78b881a6 | ||
|
|
58566b78cc | ||
|
|
51528c92b4 | ||
|
|
1911489815 | ||
|
|
8c9623f56c | ||
|
|
3a06283a94 |
No files matched your search
@@ -473,11 +473,9 @@ nor change what it asks for.
|
|||||||
`jxl` for a form with no format.
|
`jxl` for a form with no format.
|
||||||
|
|
||||||
The token holds the source's host, path and query and the size, format, quality,
|
The token holds the source's host, path and query and the size, format, quality,
|
||||||
fit and expiry, encrypted with a key derived from `signing_key`. The host is
|
fit and expiry, encrypted with a key derived from `signing_key`. The source
|
||||||
kept in lower case and an empty path as `/`, as for `/v1/image/`, so
|
URL's scheme is not kept: the image is fetched like any other (see Routes), and
|
||||||
`https://CDN.Example.com` and `https://cdn.example.com/` are one source image,
|
the blocked networks still apply.
|
||||||
fetched and cached once. The source URL's scheme is not kept: the image is
|
|
||||||
fetched like any other (see Routes), and the blocked networks still apply.
|
|
||||||
|
|
||||||
How long the URL lasts is chosen on the page, from 1 minute to 1 year, or never.
|
How long the URL lasts is chosen on the page, from 1 minute to 1 year, or never.
|
||||||
The expiry is fixed in the token when the URL is made and cannot be changed or
|
The expiry is fixed in the token when the URL is made and cannot be changed or
|
||||||
@@ -536,13 +534,8 @@ Where:
|
|||||||
- `query` — source query string as under Routes, empty string if none, with `%`
|
- `query` — source query string as under Routes, empty string if none, with `%`
|
||||||
and `:` written as in `path`; neither then holds a `:`, so path `/a:b` with no
|
and `:` written as in `path`; neither then holds a `:`, so path `/a:b` with no
|
||||||
query and path `/a` with query `b:` are signed differently
|
query and path `/a` with query `b:` are signed differently
|
||||||
- `width` — requested width in pixels, or `0`: the width is then worked out from
|
- `width` — requested width in pixels, `0` for original
|
||||||
the source's proportions and the requested height, rounded down but at least
|
- `height` — requested height in pixels, `0` for original
|
||||||
1, or is the source's own width when the height is `0` too (the `orig` size)
|
|
||||||
- `height` — requested height in pixels, or `0`: the height is then worked out
|
|
||||||
from the source's proportions and the requested width, rounded down but at
|
|
||||||
least 1, or is the source's own height when the width is `0` too (the `orig`
|
|
||||||
size)
|
|
||||||
- `format` — output format, one of those listed under Routes, with `original`
|
- `format` — output format, one of those listed under Routes, with `original`
|
||||||
signed as `orig`, `jpg` as `jpeg`, and no format as `jxl`, so a URL with no
|
signed as `orig`, `jpg` as `jpeg`, and no format as `jxl`, so a URL with no
|
||||||
format has the signature of the same URL ending in `.jxl`; `auto` is signed as
|
format has the signature of the same URL ending in `.jxl`; `auto` is signed as
|
||||||
@@ -555,10 +548,7 @@ Where:
|
|||||||
with 400
|
with 400
|
||||||
- `fit` — the URL's `fit` query parameter (cover, contain, fill, inside,
|
- `fit` — the URL's `fit` query parameter (cover, contain, fill, inside,
|
||||||
outside), or `cover` when the URL has no `fit`; a request whose `fit` is
|
outside), or `cover` when the URL has no `fit`; a request whose `fit` is
|
||||||
anything else, an empty `fit=` included, is refused with 400. `outside` scales
|
anything else, an empty `fit=` included, is refused with 400
|
||||||
the image to cover the requested size, keeping its proportions, so it can come
|
|
||||||
out larger than that size on one side: an 8192x1 source at `100x0`, which is
|
|
||||||
100x1 by its proportions, comes out at 8192x1
|
|
||||||
|
|
||||||
The URL's `sig` is the HMAC-SHA256 result in base64url (the URL-safe alphabet of
|
The URL's `sig` is the HMAC-SHA256 result in base64url (the URL-safe alphabet of
|
||||||
RFC 4648) with the trailing `=` padding kept, 44 characters in all. pixa
|
RFC 4648) with the trailing `=` padding kept, 44 characters in all. pixa
|
||||||
|
|||||||
@@ -38,25 +38,6 @@ P2: security: per-origin rate limiting
|
|||||||
characters as `%25`, `%3F` and `%23`. For a `state_dir` of `/` or one starting
|
characters as `%25`, `%3F` and `%23`. For a `state_dir` of `/` or one starting
|
||||||
with `//` it starts `file:////`, so SQLite no longer reads the first name in
|
with `//` it starts `file:////`, so SQLite no longer reads the first name in
|
||||||
the path as a host.
|
the path as a host.
|
||||||
- 2026-10-16 a size with one side `0` keeps the side computed from the source's
|
|
||||||
proportions at 1 or more (closes #283): it was rounded down, so for a thin
|
|
||||||
source it came out at 0, and libvips then made the size square. An 8192x1
|
|
||||||
source at `100x0` came out at 100x100, and at `1300x0` with `fit=cover` made
|
|
||||||
libvips ask for about 39 GB at once; an 8192x2 source at `100x0` answered 500.
|
|
||||||
`fit=contain` and `fit=inside` work both sides out again from the smaller of
|
|
||||||
the two scales and `fit=outside` from the larger, and each keeps both sides at
|
|
||||||
1 or more too, so a 1x8192 source at `0x100` with `fit=contain`, and a 49x8192
|
|
||||||
source at `0x100` or a 49x49 source at `1x1` with `fit=outside`, no longer
|
|
||||||
answer 500. An 8192x1 source at `1300x0` now comes out at 1300x1, and the
|
|
||||||
process running it alone peaked at about 34 MiB resident. With `fit=outside`
|
|
||||||
the image is scaled to cover the size, so that source at `100x0` comes out at
|
|
||||||
8192x1. `README.md` now says this under Signature Specification, and says how
|
|
||||||
a `0` side is worked out where it said `0` meant the original.
|
|
||||||
- 2026-10-16 the generator page lower-cases the source host and writes an empty
|
|
||||||
source path as `/` before it makes the encrypted URL, as `/v1/image/` does
|
|
||||||
(closes #293). A host written in upper and in lower case, or a source URL with
|
|
||||||
no path and with the path `/`, made two cache keys and two upstream requests
|
|
||||||
for one image.
|
|
||||||
- 2026-10-16 the source URL a request fetches is built with `net/url` from the
|
- 2026-10-16 the source URL a request fetches is built with `net/url` from the
|
||||||
source host, path and query, not joined as text (closes #288). The source
|
source host, path and query, not joined as text (closes #288). The source
|
||||||
path, decoded, is fetched percent-encoded again, so `100%25.png` fetches
|
path, decoded, is fetched percent-encoded again, so `100%25.png` fetches
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ func (s *Handlers) HandleGenerateURL() http.HandlerFunc {
|
|||||||
sourceURL := r.FormValue("url")
|
sourceURL := r.FormValue("url")
|
||||||
|
|
||||||
// Validate source URL
|
// Validate source URL
|
||||||
parsed, err := parseSourceURL(sourceURL)
|
parsed, err := url.Parse(sourceURL)
|
||||||
if err != nil || parsed.Host == "" {
|
if err != nil || parsed.Host == "" {
|
||||||
s.renderGeneratorWithForm(w, r, "Invalid source URL", r.Form,
|
s.renderGeneratorWithForm(w, r, "Invalid source URL", r.Form,
|
||||||
http.StatusBadRequest)
|
http.StatusBadRequest)
|
||||||
@@ -198,25 +198,6 @@ func (s *Handlers) HandleGenerateURL() http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseSourceURL parses the source URL entered on the generator page with its
|
|
||||||
// host lower-cased and an empty path written as "/", as /v1/image/ reads a
|
|
||||||
// source URL, so that each spelling of one source URL makes one cache key and
|
|
||||||
// one upstream request.
|
|
||||||
func parseSourceURL(sourceURL string) (*url.URL, error) {
|
|
||||||
parsed, err := url.Parse(sourceURL)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
parsed.Host = strings.ToLower(parsed.Host)
|
|
||||||
|
|
||||||
if parsed.Path == "" {
|
|
||||||
parsed.Path = "/"
|
|
||||||
}
|
|
||||||
|
|
||||||
return parsed, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// fetchedAsWritten reports whether fetchURL, the URL pixa fetches for an
|
// fetchedAsWritten reports whether fetchURL, the URL pixa fetches for an
|
||||||
// image URL made from the source URL source, is source as it was written,
|
// image URL made from the source URL source, is source as it was written,
|
||||||
// apart from the scheme, which the token does not keep. It is not when source
|
// apart from the scheme, which the token does not keep. It is not when source
|
||||||
|
|||||||
@@ -50,69 +50,3 @@ func TestImageProcessor_RefusesComputedSideAboveMaximum(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestImageProcessor_ComputedSideIsAtLeastOne verifies that, for a size with
|
|
||||||
// one side 0, the side computed from the input's proportions is 1 where it
|
|
||||||
// would round down to 0, and that Process makes that size in every fit mode
|
|
||||||
// but FitOutside, which scales the input to cover the size and so can make
|
|
||||||
// it longer. Each case checks the size targetDimensions gives before it
|
|
||||||
// calls Process: with that side at 0, an 8192x1 input at 1300x0 makes
|
|
||||||
// libvips ask for about 39 GB with FitCover.
|
|
||||||
func TestImageProcessor_ComputedSideIsAtLeastOne(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
inputWidth, inputHeight int
|
|
||||||
size Size
|
|
||||||
wantWidth, wantHeight int
|
|
||||||
}{
|
|
||||||
{"8192x1 at 100x0", 8192, 1, Size{Width: 100}, 100, 1},
|
|
||||||
{"8192x1 at 1300x0", 8192, 1, Size{Width: 1300}, 1300, 1},
|
|
||||||
{"8192x2 at 100x0", 8192, 2, Size{Width: 100}, 100, 1},
|
|
||||||
{"1x8192 at 0x100", 1, 8192, Size{Height: 100}, 1, 100},
|
|
||||||
{"1x8192 at 0x1300", 1, 8192, Size{Height: 1300}, 1, 1300},
|
|
||||||
{"2x8192 at 0x100", 2, 8192, Size{Height: 100}, 1, 100},
|
|
||||||
}
|
|
||||||
|
|
||||||
fits := []FitMode{FitCover, FitContain, FitFill, FitInside, FitOutside}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
for _, fit := range fits {
|
|
||||||
t.Run(tt.name+" "+string(fit), func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
width, height, err := targetDimensions(tt.size,
|
|
||||||
tt.inputWidth, tt.inputHeight)
|
|
||||||
if err != nil || width != tt.wantWidth || height != tt.wantHeight {
|
|
||||||
t.Fatalf("targetDimensions() = %dx%d, %v, want %dx%d",
|
|
||||||
width, height, err, tt.wantWidth, tt.wantHeight)
|
|
||||||
}
|
|
||||||
|
|
||||||
input := createTestJPEG(t, tt.inputWidth, tt.inputHeight)
|
|
||||||
|
|
||||||
result, err := New(Params{}).Process(t.Context(),
|
|
||||||
bytes.NewReader(input),
|
|
||||||
&Request{Size: tt.size, Format: FormatJPEG, FitMode: fit})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Process() error = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
defer func() { _ = result.Content.Close() }()
|
|
||||||
|
|
||||||
t.Logf("Process() made %dx%d", result.Width, result.Height)
|
|
||||||
|
|
||||||
ok := result.Width == tt.wantWidth && result.Height == tt.wantHeight
|
|
||||||
if fit == FitOutside {
|
|
||||||
ok = result.Width >= tt.wantWidth && result.Height >= tt.wantHeight
|
|
||||||
}
|
|
||||||
|
|
||||||
if !ok {
|
|
||||||
t.Errorf("Process() made %dx%d, want %dx%d (at least, for %s)",
|
|
||||||
result.Width, result.Height, tt.wantWidth, tt.wantHeight,
|
|
||||||
FitOutside)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -60,45 +60,3 @@ func TestImageProcessor_RefusesFitOutsideAboveMaximum(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestImageProcessor_FitOutsideSideIsAtLeastOne verifies that FitOutside
|
|
||||||
// keeps each side at 1 or more when it works both sides out again from the
|
|
||||||
// larger scale. A side of 49 scaled to 1 is 49 times 1/49, which in floating
|
|
||||||
// point is just under 1 and so rounded down to 0, a width libvips refuses.
|
|
||||||
func TestImageProcessor_FitOutsideSideIsAtLeastOne(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
inputWidth, inputHeight int
|
|
||||||
size Size
|
|
||||||
wantWidth, wantHeight int
|
|
||||||
}{
|
|
||||||
{"49x8192 at 0x100", 49, 8192, Size{Height: 100}, 1, 167},
|
|
||||||
{"49x49 at 1x1", 49, 49, Size{Width: 1, Height: 1}, 1, 1},
|
|
||||||
}
|
|
||||||
|
|
||||||
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: FitOutside})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Process() error = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
defer func() { _ = result.Content.Close() }()
|
|
||||||
|
|
||||||
t.Logf("Process() made %dx%d", result.Width, result.Height)
|
|
||||||
|
|
||||||
if result.Width != tt.wantWidth || result.Height != tt.wantHeight {
|
|
||||||
t.Errorf("Process() made %dx%d, want %dx%d",
|
|
||||||
result.Width, result.Height, tt.wantWidth, tt.wantHeight)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -311,10 +311,8 @@ func (p *ImageProcessor) Process(
|
|||||||
|
|
||||||
// targetDimensions calculates the output dimensions for a requested size,
|
// targetDimensions calculates the output dimensions for a requested size,
|
||||||
// scaling proportionally when only one dimension is given and keeping the
|
// scaling proportionally when only one dimension is given and keeping the
|
||||||
// original dimensions when both are zero. A side scaled proportionally is
|
// original dimensions when both are zero. A side scaled proportionally can
|
||||||
// rounded down but kept at 1 or more, as for a thin image it would round
|
// come out above MaxOutputDimension, which is ErrOutputTooLarge.
|
||||||
// down to 0, a side of no pixels. It can come out above MaxOutputDimension,
|
|
||||||
// which is ErrOutputTooLarge.
|
|
||||||
func targetDimensions(size Size, origWidth, origHeight int) (int, int, error) {
|
func targetDimensions(size Size, origWidth, origHeight int) (int, int, error) {
|
||||||
width, height := size.Width, size.Height
|
width, height := size.Width, size.Height
|
||||||
|
|
||||||
@@ -324,10 +322,10 @@ func targetDimensions(size Size, origWidth, origHeight int) (int, int, error) {
|
|||||||
return origWidth, origHeight, nil
|
return origWidth, origHeight, nil
|
||||||
case width == 0:
|
case width == 0:
|
||||||
// Only height specified: calculate width proportionally
|
// Only height specified: calculate width proportionally
|
||||||
width = max(1, origWidth*height/origHeight)
|
width = origWidth * height / origHeight
|
||||||
case height == 0:
|
case height == 0:
|
||||||
// Only width specified: calculate height proportionally
|
// Only width specified: calculate height proportionally
|
||||||
height = max(1, origHeight*width/origWidth)
|
height = origHeight * width / origWidth
|
||||||
}
|
}
|
||||||
|
|
||||||
if width > MaxOutputDimension || height > MaxOutputDimension {
|
if width > MaxOutputDimension || height > MaxOutputDimension {
|
||||||
@@ -481,16 +479,13 @@ func (p *ImageProcessor) resize(
|
|||||||
return cropCentreAndResize(img, width, height)
|
return cropCentreAndResize(img, width, height)
|
||||||
|
|
||||||
case FitContain:
|
case FitContain:
|
||||||
// Resize to fit within dimensions, maintaining aspect ratio. Scaled
|
// Resize to fit within dimensions, maintaining aspect ratio
|
||||||
// by the smaller scale, the short side of a thin image can round
|
|
||||||
// down to 0, which libvips refuses as a width, so each side is at
|
|
||||||
// least 1.
|
|
||||||
imgW, imgH := img.Width(), img.Height()
|
imgW, imgH := img.Width(), img.Height()
|
||||||
scaleW := float64(width) / float64(imgW)
|
scaleW := float64(width) / float64(imgW)
|
||||||
scaleH := float64(height) / float64(imgH)
|
scaleH := float64(height) / float64(imgH)
|
||||||
scale := min(scaleW, scaleH)
|
scale := min(scaleW, scaleH)
|
||||||
newW := max(1, int(float64(imgW)*scale))
|
newW := int(float64(imgW) * scale)
|
||||||
newH := max(1, int(float64(imgH)*scale))
|
newH := int(float64(imgH) * scale)
|
||||||
|
|
||||||
return img.Thumbnail(newW, newH, vips.InterestingNone)
|
return img.Thumbnail(newW, newH, vips.InterestingNone)
|
||||||
|
|
||||||
@@ -508,22 +503,19 @@ func (p *ImageProcessor) resize(
|
|||||||
scaleW := float64(width) / float64(imgW)
|
scaleW := float64(width) / float64(imgW)
|
||||||
scaleH := float64(height) / float64(imgH)
|
scaleH := float64(height) / float64(imgH)
|
||||||
scale := min(scaleW, scaleH)
|
scale := min(scaleW, scaleH)
|
||||||
newW := max(1, int(float64(imgW)*scale))
|
newW := int(float64(imgW) * scale)
|
||||||
newH := max(1, int(float64(imgH)*scale))
|
newH := int(float64(imgH) * scale)
|
||||||
|
|
||||||
return img.Thumbnail(newW, newH, vips.InterestingNone)
|
return img.Thumbnail(newW, newH, vips.InterestingNone)
|
||||||
|
|
||||||
case FitOutside:
|
case FitOutside:
|
||||||
// Resize so smallest dimension fits, may exceed target on other
|
// Resize so smallest dimension fits, may exceed target on other dimension
|
||||||
// dimension. A side scaled to 1 can come out just under 1 in
|
|
||||||
// floating point, as 49 times 1/49 does, and round down to 0, which
|
|
||||||
// libvips refuses as a width, so each side is at least 1.
|
|
||||||
imgW, imgH := img.Width(), img.Height()
|
imgW, imgH := img.Width(), img.Height()
|
||||||
scaleW := float64(width) / float64(imgW)
|
scaleW := float64(width) / float64(imgW)
|
||||||
scaleH := float64(height) / float64(imgH)
|
scaleH := float64(height) / float64(imgH)
|
||||||
scale := max(scaleW, scaleH)
|
scale := max(scaleW, scaleH)
|
||||||
newW := max(1, int(float64(imgW)*scale))
|
newW := int(float64(imgW) * scale)
|
||||||
newH := max(1, int(float64(imgH)*scale))
|
newH := int(float64(imgH) * scale)
|
||||||
|
|
||||||
// The dimension that exceeds the target is held to the maximum too
|
// The dimension that exceeds the target is held to the maximum too
|
||||||
if newW > MaxOutputDimension || newH > MaxOutputDimension {
|
if newW > MaxOutputDimension || newH > MaxOutputDimension {
|
||||||
|
|||||||
@@ -29,9 +29,7 @@ var (
|
|||||||
const MaxDimension = imageprocessor.MaxOutputDimension
|
const MaxDimension = imageprocessor.MaxOutputDimension
|
||||||
|
|
||||||
// ValidateDimension checks one requested width or height; name ("width" or
|
// ValidateDimension checks one requested width or height; name ("width" or
|
||||||
// "height") appears in the error. 0 is valid: that side is then worked out from
|
// "height") appears in the error. 0 means "original size" and is valid.
|
||||||
// the source's proportions and the other requested side, rounded down but at
|
|
||||||
// least 1, or is the source's own side when the other side is 0 too.
|
|
||||||
func ValidateDimension(name string, value int) error {
|
func ValidateDimension(name string, value int) error {
|
||||||
if value < 0 {
|
if value < 0 {
|
||||||
return fmt.Errorf("%w: %s is negative", ErrInvalidSize, name)
|
return fmt.Errorf("%w: %s is negative", ErrInvalidSize, name)
|
||||||
|
|||||||
@@ -308,70 +308,6 @@ func TestGeneratedURLFetchesTheSourceURLAsEntered(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestGeneratorMakesOneImageURLForTwoSpellingsOfASourceURL enters two
|
|
||||||
// spellings of one source URL on the generator page, requests the encrypted
|
|
||||||
// URL made for each, and checks that the upstream receives one request and
|
|
||||||
// that the second encrypted URL is served from the cache. One pair writes the
|
|
||||||
// host in upper and in lower case, which /v1/image/ lower-cases. The host is
|
|
||||||
// an IPv6 address, whose hex digits have letter case, because the fetcher
|
|
||||||
// would look a host name up in DNS. It has a port, as the fetcher refuses an
|
|
||||||
// IPv6 address written without one. The other pair is a source URL with no
|
|
||||||
// path and with the path "/", both of which fetch "/".
|
|
||||||
func TestGeneratorMakesOneImageURLForTwoSpellingsOfASourceURL(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
// sourceURLs are the two spellings, after "https://"
|
|
||||||
sourceURLs []string
|
|
||||||
// wantRequest is the path the upstream must receive
|
|
||||||
wantRequest string
|
|
||||||
}{
|
|
||||||
{"host in upper and in lower case",
|
|
||||||
[]string{"[2001:DB8::A]:8443/a.png", "[2001:db8::a]:8443/a.png"},
|
|
||||||
"/a.png"},
|
|
||||||
{"no path and the path written as a slash",
|
|
||||||
[]string{upstreamHost, upstreamHost + "/"}, "/"},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
upstream := startRecordingUpstream(t)
|
|
||||||
s, _, _ := startImageProxy(t, upstream.Server)
|
|
||||||
|
|
||||||
var imageURLs []string
|
|
||||||
|
|
||||||
for _, sourceURL := range tt.sourceURLs {
|
|
||||||
rec := postGenerate(t, s, "https://"+sourceURL)
|
|
||||||
|
|
||||||
match := generatedURLPattern.FindStringSubmatch(rec.Body.String())
|
|
||||||
if rec.Code != http.StatusOK || match == nil {
|
|
||||||
t.Fatalf("POST /generate: %d with no encrypted URL; body %s",
|
|
||||||
rec.Code, rec.Body.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
imageURLs = append(imageURLs, match[1])
|
|
||||||
}
|
|
||||||
|
|
||||||
getImage(t, s, imageURLs[0])
|
|
||||||
|
|
||||||
second := getImage(t, s, imageURLs[1])
|
|
||||||
if got := second.Header().Get("X-Pixa-Cache"); got != string(imgcache.CacheHit) {
|
|
||||||
t.Errorf("second X-Pixa-Cache = %q, want HIT", got)
|
|
||||||
}
|
|
||||||
|
|
||||||
got := upstream.requestTargets()
|
|
||||||
t.Logf("upstream received %q", got)
|
|
||||||
|
|
||||||
if !slices.Equal(got, []string{tt.wantRequest}) {
|
|
||||||
t.Errorf("upstream received %q, want [%q]", got, tt.wantRequest)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestGeneratorRefusesSourceURLItCannotFetchAsWritten submits source URLs the
|
// TestGeneratorRefusesSourceURLItCannotFetchAsWritten submits source URLs the
|
||||||
// generator page must make no encrypted URL for, and checks that it answers
|
// generator page must make no encrypted URL for, and checks that it answers
|
||||||
// 400 and shows why. pixa keeps the source path decoded and fetches it
|
// 400 and shows why. pixa keeps the source path decoded and fetches it
|
||||||
|
|||||||
Reference in new issue
Block a user