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
This commit is contained in:
78
classes/classes_test.go
Normal file
78
classes/classes_test.go
Normal file
@@ -0,0 +1,78 @@
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user