- attach.UploadWriterOptions (was the unexported uploadWriterOptions) gives code that writes an upload to the bucket itself the same Content-Type and Content-Disposition: attachment for non-inline types that attach.Store stores - writeBlob calls it; Store behaviour unchanged - TestUploadWriterOptions table test - lagoon README, docs/database/attachments.md and docs/services/storage.md describe it for writers that bypass attach.Store
156 lines
4.4 KiB
Go
156 lines
4.4 KiB
Go
package attach
|
|
|
|
import (
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
"gocloud.dev/blob"
|
|
"gocloud.dev/blob/fileblob"
|
|
"gocloud.dev/blob/memblob"
|
|
)
|
|
|
|
func TestIsInlineContentType(t *testing.T) {
|
|
cases := []struct {
|
|
in string
|
|
want bool
|
|
}{
|
|
{"image/jpeg", true},
|
|
{"image/png", true},
|
|
{"image/gif", true},
|
|
{"image/webp", true},
|
|
{"image/avif", true},
|
|
{"IMAGE/WEBP; charset=binary", true},
|
|
{"text/html; charset=utf-8", false},
|
|
{"image/svg+xml", false},
|
|
{"application/pdf", false},
|
|
{"text/plain", false},
|
|
{"application/octet-stream", false},
|
|
{"application/zip", false},
|
|
{"image/bmp", false},
|
|
{"image/x-icon", false},
|
|
{"video/mp4", false},
|
|
{"audio/mpeg", false},
|
|
{"", false},
|
|
{"not a media type;;", false},
|
|
}
|
|
for _, tc := range cases {
|
|
if got := IsInlineContentType(tc.in); got != tc.want {
|
|
t.Errorf("IsInlineContentType(%q) = %v, want %v", tc.in, got, tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestUploadWriterOptions(t *testing.T) {
|
|
cases := []struct {
|
|
in string
|
|
disposition string
|
|
}{
|
|
{"image/png", ""},
|
|
{"image/jpeg", ""},
|
|
{"image/gif", ""},
|
|
{"image/avif", ""},
|
|
{"IMAGE/WEBP; charset=binary", ""},
|
|
{"text/html", "attachment"},
|
|
{"text/xml", "attachment"},
|
|
{"image/svg+xml", "attachment"},
|
|
{"application/pdf", "attachment"},
|
|
{"text/plain", "attachment"},
|
|
{"application/octet-stream", "attachment"},
|
|
{"", "attachment"},
|
|
}
|
|
for _, tc := range cases {
|
|
got := UploadWriterOptions(tc.in)
|
|
want := &blob.WriterOptions{ContentType: tc.in, ContentDisposition: tc.disposition}
|
|
if !reflect.DeepEqual(got, want) {
|
|
t.Errorf("UploadWriterOptions(%q) = %+v, want %+v", tc.in, got, want)
|
|
}
|
|
}
|
|
|
|
first := UploadWriterOptions("text/html")
|
|
second := UploadWriterOptions("text/html")
|
|
if first == second {
|
|
t.Fatal("UploadWriterOptions returned the same pointer twice")
|
|
}
|
|
first.ContentDisposition = "inline"
|
|
first.CacheControl = "no-store"
|
|
if third := UploadWriterOptions("text/html"); third.ContentDisposition != "attachment" || third.CacheControl != "" {
|
|
t.Errorf("mutating one result changed the next: %+v", third)
|
|
}
|
|
}
|
|
|
|
// TestWriteBlobDisposition is the driver half of the public-bucket fix: a
|
|
// public bucket serves the headers Store stores with the object, so the
|
|
// attachment disposition must survive the write on the real drivers. The
|
|
// metadata-skip fileblob bucket shows that a driver which does not keep
|
|
// attributes still accepts the option without an error.
|
|
func TestWriteBlobDisposition(t *testing.T) {
|
|
cases := []struct {
|
|
name, ct, disposition string
|
|
}{
|
|
{"html", "text/html", "attachment"},
|
|
{"text", "text/plain", "attachment"},
|
|
{"pdf", "application/pdf", "attachment"},
|
|
{"svg", "image/svg+xml", "attachment"},
|
|
{"zip", "application/zip", "attachment"},
|
|
{"png", "image/png", ""},
|
|
{"jpeg", "image/jpeg", ""},
|
|
{"avif", "image/avif", ""},
|
|
}
|
|
sidecar, err := fileblob.OpenBucket(t.TempDir(), nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
noMeta, err := fileblob.OpenBucket(t.TempDir(), &fileblob.Options{Metadata: fileblob.MetadataDontWrite})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
buckets := []struct {
|
|
name string
|
|
bucket *blob.Bucket
|
|
keepsAttrs bool
|
|
}{
|
|
{"memblob", memblob.OpenBucket(nil), true},
|
|
{"fileblob sidecar", sidecar, true},
|
|
{"fileblob metadata skipped", noMeta, false},
|
|
}
|
|
const body = "<html><script>alert(1)</script></html>"
|
|
for _, b := range buckets {
|
|
bucket := b.bucket
|
|
t.Cleanup(func() { _ = bucket.Close() })
|
|
t.Run(b.name, func(t *testing.T) {
|
|
ctx := t.Context()
|
|
for _, tc := range cases {
|
|
key := "abc/def/012/" + tc.name + "-file.bin"
|
|
n, err := writeBlob(ctx, bucket, key, strings.NewReader(body), tc.ct, 0)
|
|
if err != nil {
|
|
t.Fatalf("%s: writeBlob: %v", tc.name, err)
|
|
}
|
|
if n != int64(len(body)) {
|
|
t.Fatalf("%s: wrote %d bytes, want %d", tc.name, n, len(body))
|
|
}
|
|
got, err := bucket.ReadAll(ctx, key)
|
|
if err != nil {
|
|
t.Fatalf("%s: ReadAll: %v", tc.name, err)
|
|
}
|
|
if string(got) != body {
|
|
t.Fatalf("%s: body = %q, want %q", tc.name, got, body)
|
|
}
|
|
if !b.keepsAttrs {
|
|
continue
|
|
}
|
|
attrs, err := bucket.Attributes(ctx, key)
|
|
if err != nil {
|
|
t.Fatalf("%s: Attributes: %v", tc.name, err)
|
|
}
|
|
if attrs.ContentType != tc.ct {
|
|
t.Errorf("%s: ContentType = %q, want %q", tc.name, attrs.ContentType, tc.ct)
|
|
}
|
|
if attrs.ContentDisposition != tc.disposition {
|
|
t.Errorf("%s: ContentDisposition = %q, want %q", tc.name, attrs.ContentDisposition, tc.disposition)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|