3
0

Improve attachment uploading & constraints

This commit is contained in:
Denis Arh
2022-02-25 08:35:24 +01:00
parent a86efe3090
commit 6f19f00b29
13 changed files with 591 additions and 25 deletions
+11 -3
View File
@@ -122,6 +122,9 @@ type (
hasResource bool
Resource *types.Record
hasFieldName bool
FieldName string
hasContent bool
Content interface{}
contentString string
@@ -162,6 +165,10 @@ func (h attachmentHandler) Create() *atypes.Function {
Name: "resource",
Types: []string{"ComposeRecord"}, Required: true,
},
{
Name: "fieldName",
Types: []string{"String"},
},
{
Name: "content",
Types: []string{"String", "Reader", "Bytes"}, Required: true,
@@ -179,9 +186,10 @@ func (h attachmentHandler) Create() *atypes.Function {
Handler: func(ctx context.Context, in *expr.Vars) (out *expr.Vars, err error) {
var (
args = &attachmentCreateArgs{
hasName: in.Has("name"),
hasResource: in.Has("resource"),
hasContent: in.Has("content"),
hasName: in.Has("name"),
hasResource: in.Has("resource"),
hasFieldName: in.Has("fieldName"),
hasContent: in.Has("content"),
}
)
+2 -1
View File
@@ -13,7 +13,7 @@ import (
type (
attachmentService interface {
FindByID(ctx context.Context, namespaceID, attachmentID uint64) (*types.Attachment, error)
CreateRecordAttachment(ctx context.Context, namespaceID uint64, name string, size int64, fh io.ReadSeeker, moduleID, recordID uint64) (att *types.Attachment, err error)
CreateRecordAttachment(ctx context.Context, namespaceID uint64, name string, size int64, fh io.ReadSeeker, moduleID, recordID uint64, fieldName string) (att *types.Attachment, err error)
DeleteByID(ctx context.Context, namespaceID uint64, attachmentID uint64) error
OpenOriginal(att *types.Attachment) (io.ReadSeeker, error)
OpenPreview(att *types.Attachment) (io.ReadSeeker, error)
@@ -113,6 +113,7 @@ func (h attachmentHandler) create(ctx context.Context, args *attachmentCreateArg
fh,
args.Resource.ModuleID,
args.Resource.ID,
args.FieldName,
)
default:
return nil, fmt.Errorf("unknown resource")
@@ -39,6 +39,10 @@ functions:
types:
- { wf: ComposeRecord }
fieldName:
types:
- { wf: String }
content:
required: true
types:
+1
View File
@@ -269,6 +269,7 @@ func (ctrl *Record) Upload(ctx context.Context, r *request.RecordUpload) (interf
file,
r.ModuleID,
r.RecordID,
r.FieldName,
)
return makeAttachmentPayload(ctx, a, err)
+140 -15
View File
@@ -8,21 +8,30 @@ import (
"io"
"net/http"
"path"
"regexp"
"strings"
"github.com/cortezaproject/corteza-server/compose/types"
"github.com/cortezaproject/corteza-server/pkg/actionlog"
"github.com/cortezaproject/corteza-server/pkg/auth"
"github.com/cortezaproject/corteza-server/pkg/errors"
"github.com/cortezaproject/corteza-server/pkg/objstore"
"github.com/cortezaproject/corteza-server/store"
systemService "github.com/cortezaproject/corteza-server/system/service"
"github.com/disintegration/imaging"
"github.com/edwvee/exiffix"
"github.com/pkg/errors"
)
const (
attachmentPreviewMaxWidth = 320
attachmentPreviewMaxHeight = 180
// using base 10, it will be less confusing for the non-techie users
megabyte = 1_000_000
)
var (
reMimeType = regexp.MustCompile(`\w+/[-.\w]+(?:\+[-.\w]+)?`)
)
type (
@@ -48,7 +57,7 @@ type (
FindByID(ctx context.Context, namespaceID, attachmentID uint64) (*types.Attachment, error)
Find(ctx context.Context, filter types.AttachmentFilter) (types.AttachmentSet, types.AttachmentFilter, error)
CreatePageAttachment(ctx context.Context, namespaceID uint64, name string, size int64, fh io.ReadSeeker, pageID uint64) (*types.Attachment, error)
CreateRecordAttachment(ctx context.Context, namespaceID uint64, name string, size int64, fh io.ReadSeeker, moduleID, recordID uint64) (*types.Attachment, error)
CreateRecordAttachment(ctx context.Context, namespaceID uint64, name string, size int64, fh io.ReadSeeker, moduleID, recordID uint64, fieldName string) (*types.Attachment, error)
CreateNamespaceAttachment(ctx context.Context, name string, size int64, fh io.ReadSeeker) (*types.Attachment, error)
OpenOriginal(att *types.Attachment) (io.ReadSeeker, error)
OpenPreview(att *types.Attachment) (io.ReadSeeker, error)
@@ -246,8 +255,12 @@ func (svc attachment) CreatePageAttachment(ctx context.Context, namespaceID uint
}
)
err = func() error {
ns, p, err = loadPage(ctx, svc.store, namespaceID, pageID)
err = store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) (err error) {
if size == 0 {
return AttachmentErrNotAllowedToCreateEmptyAttachment()
}
ns, p, err = loadPage(ctx, s, namespaceID, pageID)
if err != nil {
return err
}
@@ -259,20 +272,43 @@ func (svc attachment) CreatePageAttachment(ctx context.Context, namespaceID uint
return AttachmentErrNotAllowedToUpdatePage()
}
{
// Verify size and type of the uploaded page attachment
// Max size & allowed mime-types are pulled from the current settings
var (
maxSize = int64(systemService.CurrentSettings.Compose.Page.Attachments.MaxSize) * megabyte
allowedTypes = systemService.CurrentSettings.Compose.Page.Attachments.Mimetypes
mimeType string
)
if maxSize > 0 && maxSize < size {
return AttachmentErrTooLarge().Apply(
errors.Meta("size", size),
errors.Meta("maxSize", maxSize),
)
}
if mimeType, err = svc.extractMimetype(fh); err != nil {
return err
} else if !svc.checkMimeType(mimeType, allowedTypes...) {
return AttachmentErrNotAllowedToUploadThisType()
}
}
att = &types.Attachment{
NamespaceID: namespaceID,
Name: strings.TrimSpace(name),
Kind: types.PageAttachment,
}
return svc.create(ctx, name, size, fh, att)
}()
return svc.create(ctx, s, name, size, fh, att)
})
return att, svc.recordAction(ctx, aProps, AttachmentActionCreate, err)
}
func (svc attachment) CreateRecordAttachment(ctx context.Context, namespaceID uint64, name string, size int64, fh io.ReadSeeker, moduleID, recordID uint64) (att *types.Attachment, err error) {
func (svc attachment) CreateRecordAttachment(ctx context.Context, namespaceID uint64, name string, size int64, fh io.ReadSeeker, moduleID, recordID uint64, fieldName string) (att *types.Attachment, err error) {
var (
ns *types.Namespace
m *types.Module
@@ -286,6 +322,10 @@ func (svc attachment) CreateRecordAttachment(ctx context.Context, namespaceID ui
)
err = store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) (err error) {
if size == 0 {
return AttachmentErrNotAllowedToCreateEmptyAttachment()
}
ns, m, err = loadModuleWithNamespace(ctx, s, namespaceID, moduleID)
if err != nil {
return err
@@ -320,13 +360,50 @@ func (svc attachment) CreateRecordAttachment(ctx context.Context, namespaceID ui
}
}
{
// Verify size and type of the uploaded record attachment
// Max size & allowed mime-types are pulled from the current settings
var (
maxSize = int64(systemService.CurrentSettings.Compose.Record.Attachments.MaxSize) * megabyte
allowedTypes = systemService.CurrentSettings.Compose.Record.Attachments.Mimetypes
mimeType string
)
f := m.Fields.FindByName(fieldName)
if f == nil || f.Kind != "File" {
return AttachmentErrInvalidModuleField().Apply(
errors.Meta("fieldName", fieldName),
)
}
if aux := f.Options.Int64("maxSize"); aux > 0 {
maxSize = aux * megabyte
}
if aux := f.Options.String("mimetypes"); len(aux) > 0 {
allowedTypes = strings.Split(aux, ",")
}
if maxSize > 0 && maxSize < size {
return AttachmentErrTooLarge().Apply(
errors.Meta("size", size),
errors.Meta("maxSize", maxSize),
)
}
if mimeType, err = svc.extractMimetype(fh); err != nil {
return err
} else if !svc.checkMimeType(mimeType, allowedTypes...) {
return AttachmentErrNotAllowedToUploadThisType().Apply(errors.Meta("mimetype", mimeType))
}
}
att = &types.Attachment{
NamespaceID: namespaceID,
Name: strings.TrimSpace(name),
Kind: types.RecordAttachment,
}
return svc.create(ctx, name, size, fh, att)
return svc.create(ctx, s, name, size, fh, att)
})
return att, svc.recordAction(ctx, aProps, AttachmentActionCreate, err)
@@ -338,23 +415,50 @@ func (svc attachment) CreateNamespaceAttachment(ctx context.Context, name string
)
err = store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) (err error) {
if size == 0 {
return AttachmentErrNotAllowedToCreateEmptyAttachment()
}
if !svc.ac.CanCreateNamespace(ctx) {
return AttachmentErrNotAllowedToUpdateNamespace()
}
{
// Verify file size and
var (
// use max-file-size from page attachments for now
maxSize = int64(systemService.CurrentSettings.Compose.Page.Attachments.MaxSize) * megabyte
mimeType string
)
if maxSize > 0 && maxSize < size {
return AttachmentErrTooLarge().Apply(
errors.Meta("size", size),
errors.Meta("maxSize", maxSize),
)
}
if mimeType, err = svc.extractMimetype(fh); err != nil {
return err
} else if !svc.checkMimeType(mimeType, "image/png", "image/gif", "image/jpeg") {
return AttachmentErrNotAllowedToUploadThisType()
}
}
att = &types.Attachment{
Name: strings.TrimSpace(name),
Kind: types.NamespaceAttachment,
}
return svc.create(ctx, name, size, fh, att)
// @todo limit upload on image/* only!
return svc.create(ctx, s, name, size, fh, att)
})
return att, svc.recordAction(ctx, aProps, AttachmentActionCreate, err)
}
func (svc attachment) create(ctx context.Context, name string, size int64, fh io.ReadSeeker, att *types.Attachment) (err error) {
func (svc attachment) create(ctx context.Context, s store.ComposeAttachments, name string, size int64, fh io.ReadSeeker, att *types.Attachment) (err error) {
var (
aProps = &attachmentActionProps{}
)
@@ -368,7 +472,7 @@ func (svc attachment) create(ctx context.Context, name string, size int64, fh io
}
if svc.objects == nil {
return errors.New("cannot create attachment: store handler not set")
return errors.Internal("cannot create attachment: store handler not set")
}
if size == 0 {
@@ -399,7 +503,7 @@ func (svc attachment) create(ctx context.Context, name string, size int64, fh io
return AttachmentErrFailedToProcessImage(aProps).Wrap(err)
}
if err = store.CreateComposeAttachment(ctx, svc.store, att); err != nil {
if err = store.CreateComposeAttachment(ctx, s, att); err != nil {
return
}
@@ -451,7 +555,7 @@ func (svc attachment) processImage(original io.ReadSeeker, att *types.Attachment
}
if format, err = imaging.FormatFromExtension(att.Meta.Original.Extension); err != nil {
return errors.Wrapf(err, "Could not get format from extension '%s'", att.Meta.Original.Extension)
return errors.Internal("could not get format from extension '%s'", att.Meta.Original.Extension).Wrap(err)
}
previewFormat = format
@@ -472,7 +576,7 @@ func (svc attachment) processImage(original io.ReadSeeker, att *types.Attachment
// Use first image for the preview
preview = cfg.Image[0]
} else {
return errors.Wrapf(err, "Could not decode gif config")
return errors.Internal("Could not decode gif config").Wrap(err)
}
} else {
@@ -487,7 +591,7 @@ func (svc attachment) processImage(original io.ReadSeeker, att *types.Attachment
// other cases are handled here
if preview == nil {
if preview, err = imaging.Decode(original); err != nil {
return errors.Wrapf(err, "Could not decode original image")
return errors.Internal("Could not decode original image").Wrap(err)
}
}
@@ -521,4 +625,25 @@ func (svc attachment) processImage(original io.ReadSeeker, att *types.Attachment
return svc.objects.Save(att.PreviewUrl, buf)
}
func (attachment) checkMimeType(test string, vv ...string) bool {
if len(vv) == 0 {
// return true if there are no type constraints to check against
return true
}
for _, v := range vv {
v = strings.TrimSpace(v)
if !reMimeType.MatchString(v) {
continue
}
if v == test {
return true
}
}
return false
}
var _ AttachmentService = &attachment{}
+109 -2
View File
@@ -11,12 +11,13 @@ package service
import (
"context"
"fmt"
"strings"
"time"
"github.com/cortezaproject/corteza-server/compose/types"
"github.com/cortezaproject/corteza-server/pkg/actionlog"
"github.com/cortezaproject/corteza-server/pkg/errors"
"github.com/cortezaproject/corteza-server/pkg/locale"
"strings"
"time"
)
type (
@@ -781,6 +782,40 @@ func AttachmentErrInvalidModuleID(mm ...*attachmentActionProps) *errors.Error {
return e
}
// AttachmentErrInvalidModuleField returns "compose:attachment.invalidModuleField" as *errors.Error
//
//
// This function is auto-generated.
//
func AttachmentErrInvalidModuleField(mm ...*attachmentActionProps) *errors.Error {
var p = &attachmentActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("invalid module field", nil),
errors.Meta("type", "invalidModuleField"),
errors.Meta("resource", "compose:attachment"),
errors.Meta(attachmentPropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "compose"),
errors.Meta(locale.ErrorMetaKey{}, "attachment.errors.invalidModuleField"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// AttachmentErrInvalidPageID returns "compose:attachment.invalidPageID" as *errors.Error
//
//
@@ -849,6 +884,78 @@ func AttachmentErrInvalidRecordID(mm ...*attachmentActionProps) *errors.Error {
return e
}
// AttachmentErrTooLarge returns "compose:attachment.tooLarge" as *errors.Error
//
//
// This function is auto-generated.
//
func AttachmentErrTooLarge(mm ...*attachmentActionProps) *errors.Error {
var p = &attachmentActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("file too large", nil),
errors.Meta("type", "tooLarge"),
errors.Meta("resource", "compose:attachment"),
// action log entry; no formatting, it will be applied inside recordAction fn.
errors.Meta(attachmentLogMetaKey{}, "could not upload this file, file size too large"),
errors.Meta(attachmentPropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "compose"),
errors.Meta(locale.ErrorMetaKey{}, "attachment.errors.tooLarge"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// AttachmentErrNotAllowedToUploadThisType returns "compose:attachment.notAllowedToUploadThisType" as *errors.Error
//
//
// This function is auto-generated.
//
func AttachmentErrNotAllowedToUploadThisType(mm ...*attachmentActionProps) *errors.Error {
var p = &attachmentActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("file type not allowed", nil),
errors.Meta("type", "notAllowedToUploadThisType"),
errors.Meta("resource", "compose:attachment"),
// action log entry; no formatting, it will be applied inside recordAction fn.
errors.Meta(attachmentLogMetaKey{}, "could not upload this file, file type nto allowed"),
errors.Meta(attachmentPropsMetaKey{}, p),
// translation namespace & key
errors.Meta(locale.ErrorMetaNamespace{}, "compose"),
errors.Meta(locale.ErrorMetaKey{}, "attachment.errors.notAllowedToUploadThisType"),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// AttachmentErrNotAllowedToListAttachments returns "compose:attachment.notAllowedToListAttachments" as *errors.Error
//
//
+12
View File
@@ -85,6 +85,10 @@ errors:
message: "invalid module ID"
severity: warning
- error: invalidModuleField
message: "invalid module field"
severity: warning
- error: invalidPageID
message: "invalid page ID"
severity: warning
@@ -93,6 +97,14 @@ errors:
message: "invalid record ID"
severity: warning
- error: tooLarge
message: "file too large"
log: "could not upload this file, file size too large"
- error: notAllowedToUploadThisType
message: "file type not allowed"
log: "could not upload this file, file type not allowed"
- error: notAllowedToListAttachments
message: "not allowed to list attachments"
log: "could not list attachments; insufficient permissions"
+99
View File
@@ -1,10 +1,12 @@
package compose
import (
"bytes"
"context"
"fmt"
"net/http"
"net/url"
"os"
"testing"
"time"
@@ -12,6 +14,7 @@ import (
"github.com/cortezaproject/corteza-server/compose/types"
"github.com/cortezaproject/corteza-server/pkg/id"
"github.com/cortezaproject/corteza-server/store"
systemService "github.com/cortezaproject/corteza-server/system/service"
"github.com/cortezaproject/corteza-server/tests/helpers"
"github.com/steinfletcher/apitest-jsonpath"
"github.com/stretchr/testify/require"
@@ -343,6 +346,102 @@ func TestPageTreeRead(t *testing.T) {
End()
}
func TestPageAttachment(t *testing.T) {
h := newHelper(t)
h.clearPages()
ns := h.makeNamespace("page attachment testing namespace")
page := h.repoMakePage(ns, "some-page")
helpers.AllowMe(h, types.NamespaceRbacResource(0), "read")
helpers.AllowMe(h, types.PageRbacResource(0, 0), "read", "update")
xxlBlob := bytes.Repeat([]byte("0"), 1_000_001)
testImgFh, err := os.ReadFile("./testdata/test.png")
h.noError(err)
defer func() {
// reset settings after we're done
systemService.CurrentSettings.Compose.Page.Attachments.MaxSize = 0
systemService.CurrentSettings.Compose.Page.Attachments.Mimetypes = nil
}()
// one megabyte limit
systemService.CurrentSettings.Compose.Page.Attachments.MaxSize = 1
systemService.CurrentSettings.Compose.Page.Attachments.Mimetypes = []string{
"application/octet-stream",
}
cc := []struct {
name string
file []byte
fname string
mtype string
form map[string]string
test func(*http.Response, *http.Request) error
}{
{
"empty file",
[]byte(""),
"empty",
"plain/text",
map[string]string{},
helpers.AssertError("attachment.errors.notAllowedToCreateEmptyAttachment"),
},
{
"no file",
nil,
"empty",
"plain/text",
map[string]string{},
helpers.AssertError("attachment.errors.notAllowedToCreateEmptyAttachment"),
},
{
"valid upload, no constraints",
[]byte("."),
"dot",
"plain/text",
map[string]string{},
helpers.AssertNoErrors,
},
{
"global max size - over sized",
xxlBlob,
"numbers",
"plain/text",
map[string]string{},
helpers.AssertError("attachment.errors.tooLarge"),
},
{
"global mimetype - invalid",
testImgFh,
"numbers.gif",
"image/gif",
map[string]string{},
helpers.AssertError("attachment.errors.notAllowedToUploadThisType"),
},
}
for _, c := range cc {
t.Run(c.name, func(t *testing.T) {
h.t = t
helpers.InitFileUpload(t, h.apiInit(),
fmt.Sprintf("/namespace/%d/page/%d/attachment", page.NamespaceID, page.ID),
c.form,
c.file,
c.fname,
c.mtype,
).
Status(http.StatusOK).
Assert(c.test).
End()
})
}
}
func TestPageLabels(t *testing.T) {
h := newHelper(t)
h.clearPages()
+168
View File
@@ -8,6 +8,7 @@ import (
"mime/multipart"
"net/http"
"net/url"
"os"
"strconv"
"testing"
"time"
@@ -17,6 +18,7 @@ import (
"github.com/cortezaproject/corteza-server/compose/types"
"github.com/cortezaproject/corteza-server/pkg/id"
"github.com/cortezaproject/corteza-server/store"
systemService "github.com/cortezaproject/corteza-server/system/service"
"github.com/cortezaproject/corteza-server/tests/helpers"
"github.com/steinfletcher/apitest"
jsonpath "github.com/steinfletcher/apitest-jsonpath"
@@ -763,6 +765,172 @@ func TestRecordDelete(t *testing.T) {
h.a.NotNil(r.DeletedAt)
}
func TestRecordAttachment(t *testing.T) {
h := newHelper(t)
h.clearRecords()
namespace := h.makeNamespace("record attachment testing namespace")
helpers.AllowMe(h, types.NamespaceRbacResource(0), "read")
helpers.AllowMe(h, types.ModuleRbacResource(0, 0), "read", "record.create")
helpers.AllowMe(h, types.RecordRbacResource(0, 0, 0), "read")
helpers.AllowMe(h, types.ModuleFieldRbacResource(0, 0, 0), "record.value.read", "record.value.update")
const maxSizeLimit = 1
module := h.makeModule(
namespace,
"module",
&types.ModuleField{Name: "no_constraints", Kind: "File"},
&types.ModuleField{
Name: "max_size",
Kind: "File",
Options: types.ModuleFieldOptions{"maxSize": maxSizeLimit},
},
&types.ModuleField{
Name: "img_only",
Kind: "File",
Options: types.ModuleFieldOptions{
"maxSize": maxSizeLimit,
"mimetypes": "image/gif, image/png, image/jpeg",
},
},
&types.ModuleField{Name: "str", Kind: "String"},
)
xxlBlob := bytes.Repeat([]byte("0"), maxSizeLimit*1_000_000+1)
testImgFh, err := os.ReadFile("./testdata/test.png")
h.noError(err)
defer func() {
// reset settings after we're done
systemService.CurrentSettings.Compose.Record.Attachments.MaxSize = 0
systemService.CurrentSettings.Compose.Record.Attachments.Mimetypes = nil
}()
systemService.CurrentSettings.Compose.Record.Attachments.MaxSize = maxSizeLimit
systemService.CurrentSettings.Compose.Record.Attachments.Mimetypes = []string{
"application/octet-stream",
}
cc := []struct {
name string
file []byte
fname string
mtype string
form map[string]string
test func(*http.Response, *http.Request) error
}{
{
"empty file",
[]byte(""),
"empty",
"plain/text",
map[string]string{"fieldName": "no_constraints"},
helpers.AssertError("attachment.errors.notAllowedToCreateEmptyAttachment"),
},
{
"no file",
nil,
"empty",
"plain/text",
map[string]string{"fieldName": "no_constraints"},
helpers.AssertError("attachment.errors.notAllowedToCreateEmptyAttachment"),
},
{
"no field",
[]byte("."),
"dot",
"plain/text",
nil,
helpers.AssertError("attachment.errors.invalidModuleField"),
},
{
"invalid field",
[]byte("."),
"dot",
"plain/text",
map[string]string{"fieldName": "str"},
helpers.AssertError("attachment.errors.invalidModuleField"),
},
{
"valid upload, no constraints",
[]byte("."),
"dot",
"plain/text",
map[string]string{"fieldName": "no_constraints"},
helpers.AssertNoErrors,
},
{
"global max size - over sized",
xxlBlob,
"numbers",
"plain/text",
map[string]string{"fieldName": "no_constraints"},
helpers.AssertError("attachment.errors.tooLarge"),
},
{
"field max size - ok",
[]byte("12345"),
"numbers",
"plain/text",
map[string]string{"fieldName": "max_size"},
helpers.AssertNoErrors,
},
{
"field max size - over sized",
xxlBlob,
"numbers",
"plain/text",
map[string]string{"fieldName": "max_size"},
helpers.AssertError("attachment.errors.tooLarge"),
},
{
"global mimetype - invalid",
testImgFh,
"numbers.gif",
"image/gif",
map[string]string{"fieldName": "no_constraints"},
helpers.AssertError("attachment.errors.notAllowedToUploadThisType"),
},
{
"field mimetype - ok",
testImgFh,
"image.png",
"image/gif",
map[string]string{"fieldName": "img_only"},
helpers.AssertNoErrors,
},
{
"field mimetype - invalid",
testImgFh,
"image.png",
"image/gif",
map[string]string{"fieldName": "img_only"},
helpers.AssertNoErrors,
},
}
for _, c := range cc {
t.Run(c.name, func(t *testing.T) {
h.t = t
helpers.InitFileUpload(t, h.apiInit(),
fmt.Sprintf("/namespace/%d/module/%d/record/attachment", module.NamespaceID, module.ID),
c.form,
c.file,
c.fname,
c.mtype,
).
Status(http.StatusOK).
Assert(c.test).
End()
})
}
}
func TestRecordExport(t *testing.T) {
h := newHelper(t)
h.clearRecords()
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

+40
View File
@@ -0,0 +1,40 @@
package helpers
import (
"bytes"
"fmt"
"mime/multipart"
"net/textproto"
"testing"
"github.com/steinfletcher/apitest"
"github.com/stretchr/testify/require"
)
func InitFileUpload(t *testing.T, apiTest *apitest.APITest, endpoint string, form map[string]string, file []byte, name, mimetype string) *apitest.Response {
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
for k, v := range form {
require.NoError(t, writer.WriteField(k, v))
}
header := make(textproto.MIMEHeader)
header.Set("Content-Disposition",
fmt.Sprintf(`form-data; name="%s"; filename="%s"`,
"upload", name))
header.Set("Content-Type", mimetype)
part, err := writer.CreatePart(header)
require.NoError(t, err)
_, err = part.Write(file)
require.NoError(t, err)
require.NoError(t, writer.Close())
return apiTest.
Post(endpoint).
Body(body.String()).
Header("Content-Type", writer.FormDataContentType()).
Header("Accept", "application/json").
Expect(t)
}
@@ -6,4 +6,4 @@ modules:
mod1:
name: Module#1
fields:
f1: { label: 'Field1' }
f1: { label: 'Field1', kind: 'File' }
@@ -27,9 +27,10 @@ workflows:
kind: function
ref: attachmentCreate
arguments:
- { target: content, type: String, expr: "base64blackGif" }
- { target: name, type: String, value: "black-from-base64-string.gif" }
- { target: resource, type: ComposeRecord, expr: "attachable" }
- { target: content, type: String, expr: "base64blackGif" }
- { target: name, type: String, value: "black-from-base64-string.gif" }
- { target: resource, type: ComposeRecord, expr: "attachable" }
- { target: fieldName, type: String, value: "f1" }
results:
- { target: storedAttBlackGif, expr: "attachment" }