Files
Jesse HallamandGitHub edf2075381 MM-68149: Upgrade to Go 1.26.2 (#36418)
* MM-68149: upgrade to Go 1.26.2

Update go directive in go.mod and .go-version.

* MM-68149: replace pointer helpers with Go 1.26 new()

Go 1.26 extends the built-in new() to accept an initial value expression,
making typed-pointer helpers like model.NewPointer(x), bToP(x), and boolPtr(x)
redundant. Replace every call site with new(x) and remove the now-unused
helper functions and their //go:fix inline directives.

* MM-68149: apply go fix for reflect API and format-string changes

- reflect.Ptr → reflect.Pointer (renamed in Go 1.18, deprecated alias removed in 1.26)
- reflect range-over-struct: for i := 0; i < t.NumField(); i++ → for field := range t.Fields()
  and the equivalent for Methods() and interface types
- Fix format-string concatenation and variadic-arg mismatches flagged by go vet

* MM-68149: update JPEG fixtures and test infrastructure for Go 1.26 encoder

Go 1.26 ships a new image/jpeg encoder that produces slightly different output.
Regenerate all JPEG fixture files and switch the comparison helpers from
byte-equality to pixel-level comparison with a small per-channel tolerance,
so minor encoder drift across patch versions is handled automatically.

Add -update-fixtures flag to make it easy to regenerate fixtures after future
major Go upgrades. Document the update procedure in tests/README.md.

* MM-68149: CI check that go fix ./... produces no changes

* Fix real bugs flagged by CodeRabbit review

- group.go: set newGroup.MemberCount not group.MemberCount (member count
  was populated on the wrong variable and lost before publish/return)
- file_test.go: guard compareImage(GetFilePreview) on the preview slice
  length, not the thumbnail slice length (copy-paste error)
- config_test.go: remove duplicate MinimumLength assignment

* fixup! Fix real bugs flagged by CodeRabbit review
2026-05-12 15:59:12 +00:00

479 lines
14 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package imageproxy
import (
"bytes"
"io"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/httpservice"
"github.com/mattermost/mattermost/server/v8/channels/utils/testutils"
)
func makeTestLocalProxy() *ImageProxy {
configService := &testutils.StaticConfigService{
Cfg: &model.Config{
ServiceSettings: model.ServiceSettings{
SiteURL: new("https://mattermost.example.com"),
AllowedUntrustedInternalConnections: new("127.0.0.1"),
},
ImageProxySettings: model.ImageProxySettings{
Enable: new(true),
ImageProxyType: model.NewPointer(model.ImageProxyTypeLocal),
},
},
}
return MakeImageProxy(configService, httpservice.MakeHTTPService(configService), nil)
}
func TestLocalBackend_GetImage(t *testing.T) {
t.Run("image", func(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "max-age=2592000, private")
w.Header().Set("Content-Type", "image/png")
w.Header().Set("Content-Length", "10")
w.WriteHeader(http.StatusOK)
w.Write([]byte("1111111111"))
})
mock := httptest.NewServer(handler)
defer mock.Close()
proxy := makeTestLocalProxy()
recorder := httptest.NewRecorder()
request, _ := http.NewRequest(http.MethodGet, "", nil)
proxy.GetImage(recorder, request, mock.URL+"/image.png")
resp := recorder.Result()
require.Equal(t, http.StatusOK, resp.StatusCode)
assert.Equal(t, "max-age=2592000, private", resp.Header.Get("Cache-Control"))
assert.Equal(t, "10", resp.Header.Get("Content-Length"))
respBody, _ := io.ReadAll(resp.Body)
assert.Equal(t, []byte("1111111111"), respBody)
})
t.Run("not an image", func(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotAcceptable)
})
mock := httptest.NewServer(handler)
defer mock.Close()
proxy := makeTestLocalProxy()
recorder := httptest.NewRecorder()
request, _ := http.NewRequest(http.MethodGet, "", nil)
proxy.GetImage(recorder, request, mock.URL+"/file.pdf")
resp := recorder.Result()
require.Equal(t, http.StatusNotAcceptable, resp.StatusCode)
})
t.Run("not an image, but remote server ignores accept header", func(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "max-age=2592000, private")
w.Header().Set("Content-Type", "application/pdf")
w.Header().Set("Content-Length", "10")
w.WriteHeader(http.StatusOK)
w.Write([]byte("1111111111"))
})
mock := httptest.NewServer(handler)
defer mock.Close()
proxy := makeTestLocalProxy()
recorder := httptest.NewRecorder()
request, _ := http.NewRequest(http.MethodGet, "", nil)
proxy.GetImage(recorder, request, mock.URL+"/file.pdf")
resp := recorder.Result()
require.Equal(t, http.StatusForbidden, resp.StatusCode)
})
t.Run("not found", func(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
})
mock := httptest.NewServer(handler)
defer mock.Close()
proxy := makeTestLocalProxy()
recorder := httptest.NewRecorder()
request, _ := http.NewRequest(http.MethodGet, "", nil)
proxy.GetImage(recorder, request, mock.URL+"/image.png")
resp := recorder.Result()
require.Equal(t, http.StatusNotFound, resp.StatusCode)
})
t.Run("other server error", func(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
})
mock := httptest.NewServer(handler)
defer mock.Close()
proxy := makeTestLocalProxy()
recorder := httptest.NewRecorder()
request, _ := http.NewRequest(http.MethodGet, "", nil)
proxy.GetImage(recorder, request, mock.URL+"/image.png")
resp := recorder.Result()
require.Equal(t, http.StatusInternalServerError, resp.StatusCode)
})
t.Run("timeout", func(t *testing.T) {
wait := make(chan bool, 1)
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
<-wait
})
mock := httptest.NewServer(handler)
defer mock.Close()
proxy := makeTestLocalProxy()
// Modify the timeout to be much shorter than the default 30 seconds
proxy.backend.(*LocalBackend).client.Timeout = time.Millisecond
recorder := httptest.NewRecorder()
request, _ := http.NewRequest(http.MethodGet, "", nil)
proxy.GetImage(recorder, request, mock.URL+"/image.png")
resp := recorder.Result()
require.Equal(t, http.StatusGatewayTimeout, resp.StatusCode)
wait <- true
})
t.Run("unsupported SVG content type", func(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "max-age=2592000, private")
w.Header().Set("Content-Type", "image/svg+xml")
w.Header().Set("Content-Length", "10")
w.WriteHeader(http.StatusOK)
w.Write([]byte("1111111111"))
})
mock := httptest.NewServer(handler)
defer mock.Close()
proxy := makeTestLocalProxy()
recorder := httptest.NewRecorder()
request, err := http.NewRequest(http.MethodGet, "", nil)
require.NoError(t, err)
proxy.GetImage(recorder, request, mock.URL+"/test.svg")
resp := recorder.Result()
assert.Equal(t, http.StatusForbidden, resp.StatusCode)
})
t.Run("SVG body with image/png content type", func(t *testing.T) {
body := []byte(`<svg xmlns="http://www.w3.org/2000/svg"><rect width="100" height="100"/></svg>`)
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "image/png")
w.WriteHeader(http.StatusOK)
w.Write(body)
})
mock := httptest.NewServer(handler)
defer mock.Close()
proxy := makeTestLocalProxy()
recorder := httptest.NewRecorder()
request, err := http.NewRequest(http.MethodGet, "", nil)
require.NoError(t, err)
proxy.GetImage(recorder, request, mock.URL+"/image.png")
resp := recorder.Result()
assert.Equal(t, http.StatusForbidden, resp.StatusCode)
})
t.Run("XML-based SVG with image/png content type", func(t *testing.T) {
body := []byte(`<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg"><rect/></svg>`)
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "image/png")
w.WriteHeader(http.StatusOK)
w.Write(body)
})
mock := httptest.NewServer(handler)
defer mock.Close()
proxy := makeTestLocalProxy()
recorder := httptest.NewRecorder()
request, err := http.NewRequest(http.MethodGet, "", nil)
require.NoError(t, err)
proxy.GetImage(recorder, request, mock.URL+"/image.png")
resp := recorder.Result()
assert.Equal(t, http.StatusForbidden, resp.StatusCode)
})
t.Run("UTF-16 LE BOM SVG with image/png content type", func(t *testing.T) {
// Build a UTF-16 LE payload with BOM: 0xFF 0xFE followed by each ASCII
// character of the SVG tag as a two-byte little-endian code unit.
svgASCII := `<svg xmlns="http://www.w3.org/2000/svg"><rect/></svg>`
body := []byte{0xFF, 0xFE} // UTF-16 LE BOM
for _, c := range svgASCII {
body = append(body, byte(c), 0x00)
}
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "image/png")
w.WriteHeader(http.StatusOK)
w.Write(body)
})
mock := httptest.NewServer(handler)
defer mock.Close()
proxy := makeTestLocalProxy()
recorder := httptest.NewRecorder()
request, err := http.NewRequest(http.MethodGet, "", nil)
require.NoError(t, err)
proxy.GetImage(recorder, request, mock.URL+"/image.png")
resp := recorder.Result()
assert.Equal(t, http.StatusForbidden, resp.StatusCode)
})
t.Run("UTF-16 BE BOM SVG with image/png content type", func(t *testing.T) {
// Build a UTF-16 BE payload with BOM: 0xFE 0xFF followed by each ASCII
// character as a two-byte big-endian code unit.
svgASCII := `<svg xmlns="http://www.w3.org/2000/svg"><rect/></svg>`
body := []byte{0xFE, 0xFF} // UTF-16 BE BOM
for _, c := range svgASCII {
body = append(body, 0x00, byte(c))
}
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "image/png")
w.WriteHeader(http.StatusOK)
w.Write(body)
})
mock := httptest.NewServer(handler)
defer mock.Close()
proxy := makeTestLocalProxy()
recorder := httptest.NewRecorder()
request, err := http.NewRequest(http.MethodGet, "", nil)
require.NoError(t, err)
proxy.GetImage(recorder, request, mock.URL+"/image.png")
resp := recorder.Result()
assert.Equal(t, http.StatusForbidden, resp.StatusCode)
})
t.Run("SVG body with leading whitespace prefix", func(t *testing.T) {
prefix := bytes.Repeat([]byte(" "), 600)
body := append(prefix, []byte(`<svg xmlns="http://www.w3.org/2000/svg"><rect/></svg>`)...)
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "image/png")
w.WriteHeader(http.StatusOK)
w.Write(body)
})
mock := httptest.NewServer(handler)
defer mock.Close()
proxy := makeTestLocalProxy()
recorder := httptest.NewRecorder()
request, err := http.NewRequest(http.MethodGet, "", nil)
require.NoError(t, err)
proxy.GetImage(recorder, request, mock.URL+"/image.png")
resp := recorder.Result()
assert.Equal(t, http.StatusForbidden, resp.StatusCode)
})
t.Run("Redirect", func(t *testing.T) {
var mock *httptest.Server
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/image.png":
w.Header().Set("Location", mock.URL+"/image2.png")
w.WriteHeader(http.StatusMovedPermanently)
case "/image2.png":
w.Header().Set("Cache-Control", "max-age=2592000, private")
w.Header().Set("Content-Type", "image/png")
w.Header().Set("Content-Length", "10")
w.WriteHeader(http.StatusOK)
w.Write([]byte("1111111111"))
}
})
mock = httptest.NewServer(handler)
defer mock.Close()
proxy := makeTestLocalProxy()
recorder := httptest.NewRecorder()
request, _ := http.NewRequest(http.MethodGet, "", nil)
proxy.GetImage(recorder, request, mock.URL+"/image.png")
resp := recorder.Result()
require.Equal(t, http.StatusOK, resp.StatusCode)
assert.Equal(t, "10", resp.Header.Get("Content-Length"))
})
}
func TestLocalBackend_GetImageDirect(t *testing.T) {
t.Run("image", func(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "max-age=2592000, private")
w.Header().Set("Content-Type", "image/png")
w.Header().Set("Content-Length", "10")
w.WriteHeader(http.StatusOK)
w.Write([]byte("1111111111"))
})
mock := httptest.NewServer(handler)
defer mock.Close()
proxy := makeTestLocalProxy()
body, contentType, err := proxy.GetImageDirect(mock.URL + "/image.png")
assert.NoError(t, err)
assert.Equal(t, "image/png", contentType)
respBody, _ := io.ReadAll(body)
assert.Equal(t, []byte("1111111111"), respBody)
})
t.Run("not an image", func(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotAcceptable)
})
mock := httptest.NewServer(handler)
defer mock.Close()
proxy := makeTestLocalProxy()
body, contentType, err := proxy.GetImageDirect(mock.URL + "/file.pdf")
assert.Error(t, err)
assert.Equal(t, "", contentType)
assert.Equal(t, ErrLocalRequestFailed, err)
assert.Nil(t, body)
})
t.Run("not an image, but remote server ignores accept header", func(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "max-age=2592000, private")
w.Header().Set("Content-Type", "application/pdf")
w.Header().Set("Content-Length", "10")
w.WriteHeader(http.StatusOK)
w.Write([]byte("1111111111"))
})
mock := httptest.NewServer(handler)
defer mock.Close()
proxy := makeTestLocalProxy()
body, contentType, err := proxy.GetImageDirect(mock.URL + "/file.pdf")
assert.Error(t, err)
assert.Equal(t, "", contentType)
assert.Equal(t, ErrLocalRequestFailed, err)
assert.Nil(t, body)
})
t.Run("not found", func(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
})
mock := httptest.NewServer(handler)
defer mock.Close()
proxy := makeTestLocalProxy()
body, contentType, err := proxy.GetImageDirect(mock.URL + "/image.png")
assert.Error(t, err)
assert.Equal(t, "", contentType)
assert.Equal(t, ErrLocalRequestFailed, err)
assert.Nil(t, body)
})
t.Run("other server error", func(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
})
mock := httptest.NewServer(handler)
defer mock.Close()
proxy := makeTestLocalProxy()
body, contentType, err := proxy.GetImageDirect(mock.URL + "/image.png")
assert.Error(t, err)
assert.Equal(t, "", contentType)
assert.Equal(t, ErrLocalRequestFailed, err)
assert.Nil(t, body)
})
t.Run("timeout", func(t *testing.T) {
wait := make(chan bool, 1)
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
<-wait
})
mock := httptest.NewServer(handler)
defer mock.Close()
proxy := makeTestLocalProxy()
// Modify the timeout to be much shorter than the default 30 seconds
proxy.backend.(*LocalBackend).client.Timeout = time.Millisecond
body, contentType, err := proxy.GetImageDirect(mock.URL + "/image.png")
assert.Error(t, err)
assert.Equal(t, "", contentType)
assert.Equal(t, ErrLocalRequestFailed, err)
assert.Nil(t, body)
wait <- true
})
}