Files
sm-feedback-plugin/classes/classes_test.go
Jakub Zych 92e1b4e09c feat: feedback submissions with screenshots, the hide-widget preference and its user payload key
- POST /_feedback/api/v1/{key}/submit validates as PHP, refuses non-image screenshots (ImageContentGuard copy) and stores the submission, its public screenshot and the G15Office job in one transaction
- PUT /_feedback/api/v1/me/hidden (jwt.auth) stores the caller's preference; feedback_widget_hidden joins every golem15.user payload
- preflights stay with the framework CORS layer
2026-10-04 00:10:15 +02:00

79 lines
2.9 KiB
Go

package classes_test
import (
"bytes"
"encoding/hex"
"image"
"image/color"
"image/gif"
"image/jpeg"
"image/png"
"testing"
"git.golem15.com/golem15/sm-feedback-plugin/classes"
"git.golem15.com/golem15/summercms/modules/lagoon/attach"
)
func mustHex(t *testing.T, s string) []byte {
t.Helper()
b, err := hex.DecodeString(s)
if err != nil {
t.Fatal(err)
}
return b
}
func encoded(t *testing.T, enc func(*bytes.Buffer, image.Image) error) []byte {
t.Helper()
img := image.NewRGBA(image.Rect(0, 0, 3, 2))
img.Set(1, 1, color.RGBA{R: 200, A: 255})
var buf bytes.Buffer
if err := enc(&buf, img); err != nil {
t.Fatal(err)
}
return buf.Bytes()
}
// TestImageGuardCopy pins the feedback copy of ImageContentGuard: the
// shared vectors (the PHP-recorded screenshot fixtures among them) get the
// verdicts PHP gave and the framework's own guard (attach.IsAllowedImage)
// gives, and the sniffed type of each accepted image is one of the four.
func TestImageGuardCopy(t *testing.T) {
cases := []struct {
name string
data []byte
want bool
}{
{"png", encoded(t, func(b *bytes.Buffer, i image.Image) error { return png.Encode(b, i) }), true},
{"jpeg", encoded(t, func(b *bytes.Buffer, i image.Image) error { return jpeg.Encode(b, i, nil) }), true},
{"gif", encoded(t, func(b *bytes.Buffer, i image.Image) error { return gif.Encode(b, i, nil) }), true},
// The parity fixtures: a PNG and a WebP PHP accepted, and a JPEG
// header without an image PHP refused ("The file is not a valid
// image.").
{"fixture-png", mustHex(t, "89504e470d0a1a0a0000000d49484452000000080000000808020000004b6d29dc0000001949444154789c626160a8906360c0442c20021b189c128001005509023d9970f43c0000000049454e44ae426082"), true},
{"fixture-webp", mustHex(t, "524946464800000057454250565038203c0000005003009d012a180010003e6d2c9245a422a19804004006c4a009d3284600fc000108a000feec35853fff497f283d1d7ff3f3396879687ee0b7790000"), true},
{"fixture-spoofed-jpeg", mustHex(t, "ffd8ffe000104a46494600010100000100010000706f6c79676c6f743a2061204a5045472068656164657220776974686f757420616e7920696d616765ffd9"), false},
{"text", []byte("plain text, not a picture"), false},
{"empty", nil, false},
{"svg", []byte(`<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1"></svg>`), false},
{"bmp", append([]byte("BM"), make([]byte, 60)...), false},
{"png-header-only", mustHex(t, "89504e470d0a1a0a"), false},
}
for _, c := range cases {
got := classes.IsAllowedImage(c.data)
if got != c.want {
t.Errorf("%s: IsAllowedImage = %v, want %v", c.name, got, c.want)
}
if fw := attach.IsAllowedImage(c.data); fw != got {
t.Errorf("%s: the framework guard says %v, the copy %v", c.name, fw, got)
}
if got {
switch classes.SniffImageMIME(c.data) {
case "image/png", "image/jpeg", "image/gif", "image/webp":
default:
t.Errorf("%s: sniffed %q", c.name, classes.SniffImageMIME(c.data))
}
}
}
}