bleep bloop

This commit is contained in:
tsmethurst
2021-04-09 23:55:57 +02:00
parent a30a1a267b
commit b713ccac9f
15 changed files with 337 additions and 36 deletions

View File

@ -32,7 +32,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/router"
)
const mediaPath = "/api/v1/media"
const basePath = "/api/v1/media"
type mediaModule struct {
mediaHandler media.MediaHandler
@ -46,7 +46,7 @@ type mediaModule struct {
func New(db db.DB, mediaHandler media.MediaHandler, mastoConverter mastotypes.Converter, config *config.Config, log *logrus.Logger) apimodule.ClientAPIModule {
return &mediaModule{
mediaHandler: mediaHandler,
config: config,
config: config,
db: db,
mastoConverter: mastoConverter,
log: log,
@ -55,7 +55,7 @@ func New(db db.DB, mediaHandler media.MediaHandler, mastoConverter mastotypes.Co
// Route satisfies the RESTAPIModule interface
func (m *mediaModule) Route(s router.Router) error {
s.AttachHandler(http.MethodPost, mediaPath, m.mediaCreatePOSTHandler)
s.AttachHandler(http.MethodPost, basePath, m.mediaCreatePOSTHandler)
return nil
}

View File

@ -67,14 +67,13 @@ func (m *mediaModule) mediaCreatePOSTHandler(c *gin.Context) {
return
}
// open the attachment and extract the bytes from it
f, err := form.File.Open()
if err != nil {
l.Debugf("error opening attachment: %s", err)
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("could not open provided attachment: %s", err)})
return
}
// extract the bytes
buf := new(bytes.Buffer)
size, err := io.Copy(buf, f)
if err != nil {
@ -88,6 +87,7 @@ func (m *mediaModule) mediaCreatePOSTHandler(c *gin.Context) {
return
}
// allow the mediaHandler to work its magic of processing the attachment bytes, and putting them in whatever storage backend we're using
attachment, err := m.mediaHandler.ProcessAttachment(buf.Bytes(), authed.Account.ID)
if err != nil {
l.Debugf("error reading attachment: %s", err)
@ -95,7 +95,14 @@ func (m *mediaModule) mediaCreatePOSTHandler(c *gin.Context) {
return
}
// now we need to add extra fields that the attachment processor doesn't know (from the form)
// TODO: handle this inside mediaHandler.ProcessAttachment (just pass more params to it)
// first description
attachment.Description = form.Description
// now parse the focus parameter
// TODO: tidy this up into a separate function and just return an error so all the c.JSON and return calls are obviated
var focusx, focusy float32
if form.Focus != "" {
spl := strings.Split(form.Focus, ",")
@ -106,12 +113,12 @@ func (m *mediaModule) mediaCreatePOSTHandler(c *gin.Context) {
}
xStr := spl[0]
yStr := spl[1]
if xStr == "" || xStr == "" {
if xStr == "" || yStr == "" {
l.Debugf("improperly formatted focus %s", form.Focus)
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("improperly formatted focus %s", form.Focus)})
return
}
fx, err := strconv.ParseFloat(xStr[:4], 32)
fx, err := strconv.ParseFloat(xStr, 32)
if err != nil {
l.Debugf("improperly formatted focus %s: %s", form.Focus, err)
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("improperly formatted focus %s", form.Focus)})
@ -123,7 +130,7 @@ func (m *mediaModule) mediaCreatePOSTHandler(c *gin.Context) {
return
}
focusx = float32(fx)
fy, err := strconv.ParseFloat(yStr[:4], 32)
fy, err := strconv.ParseFloat(yStr, 32)
if err != nil {
l.Debugf("improperly formatted focus %s: %s", form.Focus, err)
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("improperly formatted focus %s", form.Focus)})
@ -136,10 +143,11 @@ func (m *mediaModule) mediaCreatePOSTHandler(c *gin.Context) {
}
focusy = float32(fy)
}
attachment.FileMeta.Focus.X = focusx
attachment.FileMeta.Focus.Y = focusy
// prepare the frontend representation now -- if there are any errors here at least we can bail without
// having already put something in the database and then having to clean it up again (eugh)
mastoAttachment, err := m.mastoConverter.AttachmentToMasto(attachment)
if err != nil {
l.Debugf("error parsing media attachment to frontend type: %s", err)
@ -147,12 +155,14 @@ func (m *mediaModule) mediaCreatePOSTHandler(c *gin.Context) {
return
}
// now we can confidently put the attachment in the database
if err := m.db.Put(attachment); err != nil {
l.Debugf("error storing media attachment in db: %s", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("error storing media attachment in db: %s", err)})
return
}
// and return its frontend representation
c.JSON(http.StatusAccepted, mastoAttachment)
}
@ -162,7 +172,7 @@ func validateCreateMedia(form *mastotypes.AttachmentRequest, config *config.Medi
return errors.New("no attachment given")
}
// a very superficial check to see if no limits are exceeded
// a very superficial check to see if no size limits are exceeded
// we still don't actually know which media types we're dealing with but the other handlers will go into more detail there
maxSize := config.MaxVideoSize
if config.MaxImageSize > maxSize {
@ -171,5 +181,12 @@ func validateCreateMedia(form *mastotypes.AttachmentRequest, config *config.Medi
if form.File.Size > int64(maxSize) {
return fmt.Errorf("file size limit exceeded: limit is %d bytes but attachment was %d bytes", maxSize, form.File.Size)
}
if len(form.Description) < config.MinDescriptionChars || len(form.Description) > config.MaxDescriptionChars {
return fmt.Errorf("image description length must be between %d and %d characters (inclusive), but provided image description was %d chars", config.MinDescriptionChars, config.MaxDescriptionChars, len(form.Description))
}
// TODO: validate focus here
return nil
}

View File

@ -0,0 +1,197 @@
package media
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/suite"
"github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/mastotypes"
mastomodel "github.com/superseriousbusiness/gotosocial/internal/mastotypes/mastomodel"
"github.com/superseriousbusiness/gotosocial/internal/media"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
"github.com/superseriousbusiness/gotosocial/internal/storage"
"github.com/superseriousbusiness/gotosocial/testrig"
)
type MediaCreateTestSuite struct {
suite.Suite
config *config.Config
mockOauthServer *oauth.MockServer
mockStorage *storage.MockStorage
mediaHandler media.MediaHandler
mastoConverter mastotypes.Converter
testTokens map[string]*oauth.Token
testClients map[string]*oauth.Client
testApplications map[string]*gtsmodel.Application
testUsers map[string]*gtsmodel.User
testAccounts map[string]*gtsmodel.Account
log *logrus.Logger
db db.DB
mediaModule *mediaModule
}
/*
TEST INFRASTRUCTURE
*/
// SetupSuite sets some variables on the suite that we can use as consts (more or less) throughout
func (suite *MediaCreateTestSuite) SetupSuite() {
// some of our subsequent entities need a log so create this here
log := logrus.New()
log.SetLevel(logrus.TraceLevel)
suite.log = log
// Direct config to local postgres instance
c := config.Empty()
c.Protocol = "http"
c.Host = "localhost"
c.DBConfig = &config.DBConfig{
Type: "postgres",
Address: "localhost",
Port: 5432,
User: "postgres",
Password: "postgres",
Database: "postgres",
ApplicationName: "gotosocial",
}
c.MediaConfig = &config.MediaConfig{
MaxImageSize: 2 << 20,
}
c.StorageConfig = &config.StorageConfig{
Backend: "local",
BasePath: "/tmp",
ServeProtocol: "http",
ServeHost: "localhost",
ServeBasePath: "/fileserver/media",
}
c.StatusesConfig = &config.StatusesConfig{
MaxChars: 500,
CWMaxChars: 50,
PollMaxOptions: 4,
PollOptionMaxChars: 50,
MaxMediaFiles: 4,
}
suite.config = c
// use an actual database for this, because it's just easier than mocking one out
database, err := db.New(context.Background(), c, log)
if err != nil {
suite.FailNow(err.Error())
}
suite.db = database
suite.mockOauthServer = &oauth.MockServer{}
suite.mockStorage = &storage.MockStorage{}
suite.mockStorage.On("StoreFileAt", mock.AnythingOfType("string"), mock.AnythingOfType("[]uint8")).Return(nil) // just pretend to store
suite.mediaHandler = media.New(suite.config, suite.db, suite.mockStorage, log)
suite.mastoConverter = mastotypes.New(suite.config, suite.db)
suite.mediaModule = New(suite.db, suite.mediaHandler, suite.mastoConverter, suite.config, suite.log).(*mediaModule)
}
func (suite *MediaCreateTestSuite) TearDownSuite() {
if err := suite.db.Stop(context.Background()); err != nil {
logrus.Panicf("error closing db connection: %s", err)
}
}
func (suite *MediaCreateTestSuite) SetupTest() {
if err := testrig.StandardDBSetup(suite.db); err != nil {
panic(err)
}
suite.testTokens = testrig.TestTokens()
suite.testClients = testrig.TestClients()
suite.testApplications = testrig.TestApplications()
suite.testUsers = testrig.TestUsers()
suite.testAccounts = testrig.TestAccounts()
}
// TearDownTest drops tables to make sure there's no data in the db
func (suite *MediaCreateTestSuite) TearDownTest() {
if err := testrig.StandardDBTeardown(suite.db); err != nil {
panic(err)
}
}
/*
ACTUAL TESTS
*/
func (suite *MediaCreateTestSuite) TestStatusCreatePOSTImageHandlerSuccessful() {
t := suite.testTokens["local_account_1"]
oauthToken := oauth.PGTokenToOauthToken(t)
// setup
recorder := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(recorder)
ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
buf, w, err := testrig.CreateMultipartFormData("file", "../../media/test/test-jpeg.jpg", map[string]string{
"description": "this is a test image -- a cool background from somewhere",
"focus": "-0.5,0.5",
})
if err != nil {
panic(err)
}
ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", basePath), bytes.NewReader(buf.Bytes())) // the endpoint we're hitting
ctx.Request.Header.Set("Content-Type", w.FormDataContentType())
suite.mediaModule.mediaCreatePOSTHandler(ctx)
// check response
suite.EqualValues(http.StatusAccepted, recorder.Code)
result := recorder.Result()
defer result.Body.Close()
b, err := ioutil.ReadAll(result.Body)
assert.NoError(suite.T(), err)
fmt.Println(string(b))
attachmentReply := &mastomodel.Attachment{}
err = json.Unmarshal(b, attachmentReply)
assert.NoError(suite.T(), err)
assert.Equal(suite.T(), "this is a test image -- a cool background from somewhere", attachmentReply.Description)
assert.Equal(suite.T(), "image", attachmentReply.Type)
assert.EqualValues(suite.T(), mastomodel.MediaMeta{
Original: mastomodel.MediaDimensions{
Width: 1920,
Height: 1080,
Size: "1920x1080",
Aspect: 1.7777778,
},
Small: mastomodel.MediaDimensions{
Width: 256,
Height: 144,
Size: "256x144",
Aspect: 1.7777778,
},
Focus: mastomodel.MediaFocus{
X: -0.5,
Y: 0.5,
},
}, attachmentReply.Meta)
assert.Equal(suite.T(), "LjCZnlvyRkRn_NvzRjWF?urqV@f9", attachmentReply.Blurhash)
assert.NotEmpty(suite.T(), attachmentReply.ID)
assert.NotEmpty(suite.T(), attachmentReply.URL)
assert.NotEmpty(suite.T(), attachmentReply.PreviewURL)
}
func TestMediaCreateTestSuite(t *testing.T) {
suite.Run(t, new(MediaCreateTestSuite))
}