- Decode: clean values byte for byte, nil, non-string refusal, the 35 unsafe payloads stripped with notices exactly when changed - notices order, ten-name cut, plural forms in lang/en and lang/pl - LooksLikeHTML, options, ListValue, Upload sniffing and safe names - StoreMedia layout, suffixes, no overwrite, size and folder rules - editor corpus and policy cross-check extended in place: more negatives, notices only when stripped, stable second pass - TestPostsEditorHooks: create and update, default and non-default writes, no writes, no writer - TestCategoriesAdminBlogEditor (Docker host): untouched saves keep imported markup and NULL, clean edit, strip and report, blank to NULL, editor upload
165 lines
5.6 KiB
Go
165 lines
5.6 KiB
Go
package classes
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.golem15.com/golem15/summercms/modules/lagoon/attach"
|
|
"gocloud.dev/blob/memblob"
|
|
)
|
|
|
|
func TestMediaObjectPath(t *testing.T) {
|
|
got := mediaObjectPath("../etc", "cover.png")
|
|
if !strings.HasPrefix(got, "/journal/") {
|
|
t.Fatalf("path = %q, want /journal/ prefix", got)
|
|
}
|
|
if strings.Contains(got, "..") {
|
|
t.Fatalf("path still contains ..: %q", got)
|
|
}
|
|
if got := mediaObjectPath("photos", "a.png"); got != "/journal/photos/a.png" {
|
|
t.Fatalf("nested = %q", got)
|
|
}
|
|
if got := mediaObjectPath("journal/photos", "a.png"); got != "/journal/photos/a.png" {
|
|
t.Fatalf("prefixed folder = %q", got)
|
|
}
|
|
if got := mediaObjectPath("", "a.png"); got != "/journal/a.png" {
|
|
t.Fatalf("no folder = %q", got)
|
|
}
|
|
}
|
|
|
|
func TestMediaFolderPatternRejectsDotDot(t *testing.T) {
|
|
folder := "../etc"
|
|
if MediaFolderPattern.MatchString(folder) && !strings.Contains(folder, "..") {
|
|
t.Fatal("folder ../etc should be rejected")
|
|
}
|
|
if !strings.Contains(folder, "..") {
|
|
t.Fatal("test fixture lost ..")
|
|
}
|
|
if MediaFolderPattern.MatchString("ok_folder-1") {
|
|
return
|
|
}
|
|
t.Fatal("ok_folder-1 should match")
|
|
}
|
|
|
|
// failingReader fails every read.
|
|
type failingReader struct{}
|
|
|
|
func (failingReader) Read([]byte) (int, error) { return 0, errors.New("disk on fire") }
|
|
|
|
func TestStoreMedia(t *testing.T) {
|
|
ctx := context.Background()
|
|
bucket := memblob.OpenBucket(nil)
|
|
t.Cleanup(func() { _ = bucket.Close() })
|
|
mediaErr := func(t *testing.T, err error, field, message string) {
|
|
t.Helper()
|
|
var refused *MediaError
|
|
if !errors.As(err, &refused) || refused.Field != field || refused.Message != message || refused.Error() != message {
|
|
t.Fatalf("error = %v, want MediaError on %s %q", err, field, message)
|
|
}
|
|
}
|
|
|
|
t.Run("allowed extensions, key layout and URL", func(t *testing.T) {
|
|
for _, tc := range []struct{ folder, name, path string }{
|
|
{"photos", "a.jpg", "/journal/photos/a.jpg"},
|
|
{"/journal/photos/", "b.JPEG", "/journal/photos/b.JPEG"},
|
|
{"", "c.png", "/journal/c.png"},
|
|
{"x//y", "d.gif", "/journal/x/y/d.gif"},
|
|
{"Journal", "e.webp", "/journal/e.webp"},
|
|
{"posts", `C:\uploads\f.png`, "/journal/posts/f.png"},
|
|
{"posts", "../../g.png", "/journal/posts/g.png"},
|
|
} {
|
|
got, err := StoreMedia(ctx, bucket, tc.folder, tc.name, bytes.NewReader(testPNG))
|
|
if err != nil {
|
|
t.Fatalf("%s/%s: %v", tc.folder, tc.name, err)
|
|
}
|
|
key := strings.TrimPrefix(tc.path, "/")
|
|
if got.Path != tc.path || got.URL != attach.PublicURL(key) {
|
|
t.Fatalf("%s/%s = %+v, want path %s", tc.folder, tc.name, got, tc.path)
|
|
}
|
|
data, err := bucket.ReadAll(ctx, key)
|
|
if err != nil || !bytes.Equal(data, testPNG) {
|
|
t.Fatalf("stored %s: %v", key, err)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("a taken name gets the next numbered suffix", func(t *testing.T) {
|
|
var paths []string
|
|
for i := 0; i < 3; i++ {
|
|
got, err := StoreMedia(ctx, bucket, "dup", "same.png", bytes.NewReader(testPNG))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
paths = append(paths, got.Path)
|
|
}
|
|
if strings.Join(paths, " ") != "/journal/dup/same.png /journal/dup/same-1.png /journal/dup/same-2.png" {
|
|
t.Fatalf("paths = %v", paths)
|
|
}
|
|
})
|
|
|
|
t.Run("a name with every suffix taken fails instead of overwriting", func(t *testing.T) {
|
|
full := memblob.OpenBucket(nil)
|
|
t.Cleanup(func() { _ = full.Close() })
|
|
keys := []string{"journal/full/a.png"}
|
|
for n := 1; n < mediaMaxSuffix; n++ {
|
|
keys = append(keys, "journal/full/a-"+strconv.Itoa(n)+".png")
|
|
}
|
|
for _, key := range keys {
|
|
if err := full.WriteAll(ctx, key, []byte("old"), nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
_, err := StoreMedia(ctx, full, "full", "a.png", bytes.NewReader(testPNG))
|
|
if err == nil || errors.As(err, new(*MediaError)) {
|
|
t.Fatalf("error = %v, want an opaque error", err)
|
|
}
|
|
for _, key := range keys {
|
|
if data, _ := full.ReadAll(ctx, key); string(data) != "old" {
|
|
t.Fatalf("%s was overwritten", key)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("other extensions are refused", func(t *testing.T) {
|
|
for _, name := range []string{"a.svg", "a.html", "a.bmp", "a", "a.png.exe", ".png.txt"} {
|
|
_, err := StoreMedia(ctx, bucket, "", name, bytes.NewReader(testPNG))
|
|
mediaErr(t, err, "file", "The file must be an image.")
|
|
}
|
|
})
|
|
|
|
t.Run("the size limit is 10240 KiB", func(t *testing.T) {
|
|
if MediaMaxBytes != 10240*1024 {
|
|
t.Fatalf("MediaMaxBytes = %d", MediaMaxBytes)
|
|
}
|
|
exact := make([]byte, MediaMaxBytes)
|
|
if _, err := StoreMedia(ctx, bucket, "size", "exact.png", bytes.NewReader(exact)); err != nil {
|
|
t.Fatalf("exactly MediaMaxBytes: %v", err)
|
|
}
|
|
_, err := StoreMedia(ctx, bucket, "size", "over.png", bytes.NewReader(make([]byte, MediaMaxBytes+1)))
|
|
mediaErr(t, err, "file", "The file may not be greater than 10240 kilobytes.")
|
|
if ok, _ := bucket.Exists(ctx, "journal/size/over.png"); ok {
|
|
t.Fatal("an oversized file was stored")
|
|
}
|
|
})
|
|
|
|
t.Run("the folder rule is checked", func(t *testing.T) {
|
|
for _, folder := range []string{"../up", "a..b", "a b", `a\b`, strings.Repeat("a", 101), "a?b"} {
|
|
_, err := StoreMedia(ctx, bucket, folder, "a.png", bytes.NewReader(testPNG))
|
|
mediaErr(t, err, "folder", "The folder format is invalid.")
|
|
}
|
|
})
|
|
|
|
t.Run("a missing bucket and a read failure are opaque errors", func(t *testing.T) {
|
|
if _, err := StoreMedia(ctx, nil, "", "a.png", bytes.NewReader(testPNG)); err == nil || errors.As(err, new(*MediaError)) {
|
|
t.Fatalf("nil bucket error = %v", err)
|
|
}
|
|
if _, err := StoreMedia(ctx, bucket, "", "a.png", failingReader{}); err == nil || errors.As(err, new(*MediaError)) || !strings.Contains(err.Error(), "disk on fire") {
|
|
t.Fatalf("read failure error = %v", err)
|
|
}
|
|
})
|
|
}
|