1
0
mirror of https://github.com/imgproxy/imgproxy.git synced 2026-04-24 19:54:05 +02:00
Files
2026-02-27 16:57:11 +01:00

328 lines
9.3 KiB
Go

package conditionalheaders
import (
"encoding/base64"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/imgproxy/imgproxy/v3/httpheaders"
"github.com/imgproxy/imgproxy/v3/testutil"
"github.com/stretchr/testify/suite"
)
type RequestSuite struct {
testutil.LazySuite
Factory testutil.LazyObj[*Factory]
Config testutil.LazyObj[*Config]
}
func (s *RequestSuite) SetupSuite() {
s.Config, _ = testutil.NewLazySuiteObj(s, func() (*Config, error) {
cfg := NewDefaultConfig()
return &cfg, nil
})
s.Factory, _ = testutil.NewLazySuiteObj(s, func() (*Factory, error) {
return NewFactory(s.Config())
})
}
func (s *RequestSuite) SetupSubTest() {
s.ResetLazyObjects()
}
// helper creates a minimal request object with given config and optional headers
func (s *RequestSuite) makeReq(ifMod, ifNone string) *http.Request {
req := &http.Request{Header: make(http.Header)}
if len(ifMod) > 0 {
req.Header.Set(httpheaders.IfModifiedSince, ifMod)
}
if len(ifNone) > 0 {
req.Header.Set(httpheaders.IfNoneMatch, ifNone)
}
return req
}
func (s *RequestSuite) TestNewFromRequest() {
inMod := "Mon, 02 Jan 2006 15:04:05 GMT"
inNone := `"etag"`
r := s.makeReq(inMod, inNone)
c := s.Factory().NewRequest(r)
req := s.Require()
req.Equal(inMod, c.ifModifiedSince)
req.Equal(inNone, c.ifNoneMatch)
}
func (s *RequestSuite) TestInjectImageRequestHeaders() {
buster := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)
inModAfterBuster := buster.Add(+time.Hour).Format(http.TimeFormat)
inModBeforeBuster := buster.Add(-time.Hour).Format(http.TimeFormat)
s.Config().LastModifiedBuster = buster
cases := []struct {
name string
ifModifiedSince string
ifNoneMatch string
etagSentToSource string
lastModifiedEnabled bool
lastModifiedBuster time.Time
etagEnabled bool
etagBuster string
wantIfModifiedSince bool
wantIfNoneMatch bool
}{
{
name: "all disabled",
},
{
name: "last modified enabled",
ifModifiedSince: inModAfterBuster,
lastModifiedEnabled: true,
wantIfModifiedSince: true,
},
{
name: "etag enabled",
ifNoneMatch: "etag1",
etagSentToSource: "etag1",
etagEnabled: true,
wantIfNoneMatch: true,
},
{
name: "etag/lm enabled with no buster",
ifModifiedSince: inModAfterBuster,
ifNoneMatch: "etag1",
etagSentToSource: "etag1",
lastModifiedEnabled: true,
etagEnabled: true,
wantIfModifiedSince: true,
wantIfNoneMatch: true,
},
{
name: "last modified after buster",
ifModifiedSince: inModAfterBuster,
lastModifiedEnabled: true,
lastModifiedBuster: buster,
wantIfModifiedSince: true,
},
{
name: "last modified before buster",
ifModifiedSince: inModBeforeBuster,
lastModifiedEnabled: true,
lastModifiedBuster: buster,
wantIfModifiedSince: false,
},
{
name: "last modified enabled with buster, invalid header",
ifModifiedSince: "not-a-time",
lastModifiedEnabled: true,
lastModifiedBuster: buster,
wantIfModifiedSince: false, // invalid header should be treated as if it's before buster
},
{
name: "etag buster",
ifNoneMatch: "buster/" + base64.RawURLEncoding.EncodeToString([]byte("etag1")),
etagSentToSource: "etag1",
etagEnabled: true,
etagBuster: "buster",
wantIfNoneMatch: true,
},
{
name: "both enabled, but last modified buster check fails, etag still works",
ifModifiedSince: inModBeforeBuster,
ifNoneMatch: "buster/" + base64.RawURLEncoding.EncodeToString([]byte("etag1")),
etagSentToSource: "etag1",
lastModifiedEnabled: true,
lastModifiedBuster: buster,
etagEnabled: true,
etagBuster: "buster",
wantIfModifiedSince: false, // should not be sent because of buster
wantIfNoneMatch: true, // should not be sent because of buster
},
{
name: "both enabled, if-none-match with invalid base64",
ifModifiedSince: inModAfterBuster,
ifNoneMatch: "buster/@@@",
lastModifiedEnabled: true,
lastModifiedBuster: buster,
etagEnabled: true,
etagBuster: "buster",
wantIfModifiedSince: false, // should be sent because last modified buster check passes
wantIfNoneMatch: false, // should not be sent because of etag buster check fails
},
{
name: "both enabled, but etag buster different",
ifModifiedSince: inModAfterBuster,
ifNoneMatch: "buster/" + base64.RawURLEncoding.EncodeToString([]byte("etag1")),
lastModifiedEnabled: true,
lastModifiedBuster: buster,
etagEnabled: true,
etagBuster: "different-buster",
wantIfModifiedSince: false, // should be sent because last modified buster check passes
wantIfNoneMatch: false, // should not be sent because of etag buster check fails
},
{
name: "both enabled, but etag with no buster",
ifModifiedSince: inModAfterBuster,
ifNoneMatch: "W/etag1",
lastModifiedEnabled: true,
lastModifiedBuster: buster,
etagEnabled: true,
etagBuster: "buster",
wantIfModifiedSince: false, // should be sent because last modified buster check passes
wantIfNoneMatch: false, // should not be sent because of etag buster check fails
},
}
for _, c := range cases {
s.Run(c.name, func() {
s.Config().LastModifiedEnabled = c.lastModifiedEnabled
s.Config().LastModifiedBuster = c.lastModifiedBuster
s.Config().ETagEnabled = c.etagEnabled
s.Config().ETagBuster = c.etagBuster
r := s.makeReq(c.ifModifiedSince, c.ifNoneMatch)
ch := s.Factory().NewRequest(r)
h := make(http.Header)
ch.InjectImageRequestHeaders(h)
if c.wantIfModifiedSince {
s.Require().Equal(c.ifModifiedSince, h.Get(httpheaders.IfModifiedSince))
} else {
s.Require().Empty(h.Get(httpheaders.IfModifiedSince))
}
if c.wantIfNoneMatch {
s.Require().Equal(c.etagSentToSource, h.Get(httpheaders.IfNoneMatch))
} else {
s.Require().Empty(h.Get(httpheaders.IfNoneMatch))
}
})
}
}
func (s *RequestSuite) TestInjectUserResponseHeaders() {
buster := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)
busterFmt := buster.Format(http.TimeFormat)
lastModAfterBuster := buster.Add(time.Hour).Format(http.TimeFormat)
lastModBeforeBuster := buster.Add(-time.Hour).Format(http.TimeFormat)
cases := []struct {
name string
imageEtag string
imageLastModified string
lastModifiedEnabled bool
lastModifiedBuster time.Time
etagEnabled bool
etagBuster string
wantLastModified string
wantEtag string
}{
{
name: "all disabled",
lastModifiedEnabled: false,
etagEnabled: false,
},
{
name: "lastmod enabled no header",
lastModifiedEnabled: true,
},
{
name: "lastmod older than buster",
imageLastModified: lastModBeforeBuster,
lastModifiedEnabled: true,
lastModifiedBuster: buster,
wantLastModified: busterFmt,
},
{
name: "lastmod after buster",
imageLastModified: lastModAfterBuster,
lastModifiedEnabled: true,
lastModifiedBuster: buster,
wantLastModified: lastModAfterBuster,
},
{
name: "lastmod invalid value",
imageLastModified: "not-a-time",
lastModifiedEnabled: true,
lastModifiedBuster: buster,
wantLastModified: "",
},
{
name: "etag enabled empty",
etagEnabled: true,
},
{
name: "etag enabled with value",
imageEtag: "etag1",
etagEnabled: true,
etagBuster: "buster",
wantEtag: `"buster/` + base64.RawURLEncoding.EncodeToString([]byte("etag1")) + `"`,
},
{
name: "both enabled, both present, lm older",
imageLastModified: lastModBeforeBuster,
imageEtag: "etag2",
lastModifiedEnabled: true,
lastModifiedBuster: buster,
etagEnabled: true,
etagBuster: "buster",
wantLastModified: busterFmt,
wantEtag: `"buster/` + base64.RawURLEncoding.EncodeToString([]byte("etag2")) + `"`,
},
}
for _, c := range cases {
s.Run(c.name, func() {
s.Config().LastModifiedEnabled = c.lastModifiedEnabled
s.Config().LastModifiedBuster = c.lastModifiedBuster
s.Config().ETagEnabled = c.etagEnabled
s.Config().ETagBuster = c.etagBuster
// prepare image response headers
imageRes := make(http.Header)
if len(c.imageEtag) > 0 {
imageRes.Set(httpheaders.Etag, c.imageEtag)
}
if len(c.imageLastModified) > 0 {
imageRes.Set(httpheaders.LastModified, c.imageLastModified)
}
// prepare request and response writer
r := s.makeReq("", "")
rec := httptest.NewRecorder()
ch := s.Factory().NewRequest(r)
ch.SetOriginHeaders(imageRes)
ch.InjectUserResponseHeaders(rec)
// assert Last-Modified
lm := rec.Header().Get(httpheaders.LastModified)
if c.wantLastModified == "" {
s.Require().Empty(lm)
} else {
s.Require().Equal(c.wantLastModified, lm)
}
// assert ETag
etag := rec.Header().Get(httpheaders.Etag)
if c.wantEtag == "" {
s.Require().Empty(etag)
} else {
s.Require().Equal(c.wantEtag, etag)
}
})
}
}
func TestConditionalHeadersSuite(t *testing.T) {
suite.Run(t, new(RequestSuite))
}