[bugfix] Accept non-multipart forms for account updates (#1896)

* [bugfix] Update Swagger schema per max_profile_fields addition

* [bugfix] Accept non-multipart forms for account updates
This commit is contained in:
Umar Getagazov 2023-06-16 12:16:04 +03:00 committed by GitHub
parent 827cc4df56
commit 0fa06c0cde
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 128 additions and 1 deletions

View File

@ -1147,6 +1147,13 @@ definitions:
format: int64 format: int64
type: integer type: integer
x-go-name: MaxFeaturedTags x-go-name: MaxFeaturedTags
max_profile_fields:
description: |-
The maximum number of profile fields allowed for each account.
Currently not configurable, so this is hardcoded to 6. (https://github.com/superseriousbusiness/gotosocial/issues/1876)
format: int64
type: integer
x-go-name: MaxProfileFields
title: InstanceConfigurationAccounts models instance account config parameters. title: InstanceConfigurationAccounts models instance account config parameters.
type: object type: object
x-go-name: InstanceConfigurationAccounts x-go-name: InstanceConfigurationAccounts
@ -3144,6 +3151,7 @@ paths:
patch: patch:
consumes: consumes:
- multipart/form-data - multipart/form-data
- application/x-www-form-urlencoded
- application/json - application/json
operationId: accountUpdate operationId: accountUpdate
parameters: parameters:

View File

@ -43,6 +43,7 @@ import (
// //
// consumes: // consumes:
// - multipart/form-data // - multipart/form-data
// - application/x-www-form-urlencoded
// - application/json // - application/json
// //
// produces: // produces:
@ -213,6 +214,17 @@ func parseUpdateAccountForm(c *gin.Context) (*apimodel.UpdateCredentialsRequest,
if err != nil { if err != nil {
return nil, fmt.Errorf("custom json binding failed: %w", err) return nil, fmt.Errorf("custom json binding failed: %w", err)
} }
case binding.MIMEPOSTForm:
// Bind with default form binding first.
if err := c.ShouldBindWith(form, binding.FormPost); err != nil {
return nil, err
}
// Now use custom form binding for
// field attributes in the form data.
if err := c.ShouldBindWith(form, fieldsAttributesFormBinding{}); err != nil {
return nil, fmt.Errorf("custom form binding failed: %w", err)
}
case binding.MIMEMultipartPOSTForm: case binding.MIMEMultipartPOSTForm:
// Bind with default form binding first. // Bind with default form binding first.
if err := c.ShouldBindWith(form, binding.FormMultipart); err != nil { if err := c.ShouldBindWith(form, binding.FormMultipart); err != nil {
@ -225,7 +237,7 @@ func parseUpdateAccountForm(c *gin.Context) (*apimodel.UpdateCredentialsRequest,
return nil, fmt.Errorf("custom form binding failed: %w", err) return nil, fmt.Errorf("custom form binding failed: %w", err)
} }
default: default:
err := fmt.Errorf("content-type %s not supported for this endpoint; supported content-types are %s, %s", ct, binding.MIMEJSON, binding.MIMEMultipartPOSTForm) err := fmt.Errorf("content-type %s not supported for this endpoint; supported content-types are %s, %s, %s", ct, binding.MIMEJSON, binding.MIMEPOSTForm, binding.MIMEMultipartPOSTForm)
return nil, err return nil, err
} }

View File

@ -24,6 +24,7 @@ import (
"io" "io"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"net/url"
"testing" "testing"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
@ -37,6 +38,14 @@ type AccountUpdateTestSuite struct {
AccountStandardTestSuite AccountStandardTestSuite
} }
func (suite *AccountUpdateTestSuite) updateAccountFromForm(data map[string]string, expectedHTTPStatus int, expectedBody string) (*apimodel.Account, error) {
form := url.Values{}
for key, val := range data {
form[key] = []string{val}
}
return suite.updateAccount([]byte(form.Encode()), "application/x-www-form-urlencoded", expectedHTTPStatus, expectedBody)
}
func (suite *AccountUpdateTestSuite) updateAccountFromFormData(data map[string]string, expectedHTTPStatus int, expectedBody string) (*apimodel.Account, error) { func (suite *AccountUpdateTestSuite) updateAccountFromFormData(data map[string]string, expectedHTTPStatus int, expectedBody string) (*apimodel.Account, error) {
requestBody, w, err := testrig.CreateMultipartFormData("", "", data) requestBody, w, err := testrig.CreateMultipartFormData("", "", data)
if err != nil { if err != nil {
@ -106,6 +115,32 @@ func (suite *AccountUpdateTestSuite) updateAccount(
return resp, nil return resp, nil
} }
func (suite *AccountUpdateTestSuite) TestUpdateAccountBasicForm() {
data := map[string]string{
"note": "this is my new bio read it and weep",
"fields_attributes[0][name]": "pronouns",
"fields_attributes[0][value]": "they/them",
"fields_attributes[1][name]": "Website",
"fields_attributes[1][value]": "https://example.com",
}
apimodelAccount, err := suite.updateAccountFromForm(data, http.StatusOK, "")
if err != nil {
suite.FailNow(err.Error())
}
suite.Equal("<p>this is my new bio read it and weep</p>", apimodelAccount.Note)
suite.Equal("this is my new bio read it and weep", apimodelAccount.Source.Note)
if l := len(apimodelAccount.Fields); l != 2 {
suite.FailNow("", "expected %d fields, got %d", 2, l)
}
suite.Equal(`pronouns`, apimodelAccount.Fields[0].Name)
suite.Equal(`they/them`, apimodelAccount.Fields[0].Value)
suite.Equal(`Website`, apimodelAccount.Fields[1].Name)
suite.Equal(`<a href="https://example.com" rel="nofollow noreferrer noopener" target="_blank">https://example.com</a>`, apimodelAccount.Fields[1].Value)
}
func (suite *AccountUpdateTestSuite) TestUpdateAccountBasicFormData() { func (suite *AccountUpdateTestSuite) TestUpdateAccountBasicFormData() {
data := map[string]string{ data := map[string]string{
"note": "this is my new bio read it and weep", "note": "this is my new bio read it and weep",
@ -166,6 +201,19 @@ func (suite *AccountUpdateTestSuite) TestUpdateAccountBasicJSON() {
suite.Equal(`<a href="https://example.com" rel="nofollow noreferrer noopener" target="_blank">https://example.com</a>`, apimodelAccount.Fields[1].Value) suite.Equal(`<a href="https://example.com" rel="nofollow noreferrer noopener" target="_blank">https://example.com</a>`, apimodelAccount.Fields[1].Value)
} }
func (suite *AccountUpdateTestSuite) TestUpdateAccountLockForm() {
data := map[string]string{
"locked": "true",
}
apimodelAccount, err := suite.updateAccountFromForm(data, http.StatusOK, "")
if err != nil {
suite.FailNow(err.Error())
}
suite.True(apimodelAccount.Locked)
}
func (suite *AccountUpdateTestSuite) TestUpdateAccountLockFormData() { func (suite *AccountUpdateTestSuite) TestUpdateAccountLockFormData() {
data := map[string]string{ data := map[string]string{
"locked": "true", "locked": "true",
@ -193,6 +241,19 @@ func (suite *AccountUpdateTestSuite) TestUpdateAccountLockJSON() {
suite.True(apimodelAccount.Locked) suite.True(apimodelAccount.Locked)
} }
func (suite *AccountUpdateTestSuite) TestUpdateAccountUnlockForm() {
data := map[string]string{
"locked": "false",
}
apimodelAccount, err := suite.updateAccountFromForm(data, http.StatusOK, "")
if err != nil {
suite.FailNow(err.Error())
}
suite.False(apimodelAccount.Locked)
}
func (suite *AccountUpdateTestSuite) TestUpdateAccountUnlockFormData() { func (suite *AccountUpdateTestSuite) TestUpdateAccountUnlockFormData() {
data := map[string]string{ data := map[string]string{
"locked": "false", "locked": "false",
@ -240,6 +301,24 @@ func (suite *AccountUpdateTestSuite) TestUpdateAccountCache() {
suite.Equal("<p>this is my new bio read it and weep</p>", apimodelAccount.Note) suite.Equal("<p>this is my new bio read it and weep</p>", apimodelAccount.Note)
} }
func (suite *AccountUpdateTestSuite) TestUpdateAccountDiscoverableForm() {
data := map[string]string{
"discoverable": "false",
}
apimodelAccount, err := suite.updateAccountFromForm(data, http.StatusOK, "")
if err != nil {
suite.FailNow(err.Error())
}
suite.False(apimodelAccount.Discoverable)
// Check the account in the database too.
dbZork, err := suite.db.GetAccountByID(context.Background(), apimodelAccount.ID)
suite.NoError(err)
suite.False(*dbZork.Discoverable)
}
func (suite *AccountUpdateTestSuite) TestUpdateAccountDiscoverableFormData() { func (suite *AccountUpdateTestSuite) TestUpdateAccountDiscoverableFormData() {
data := map[string]string{ data := map[string]string{
"discoverable": "false", "discoverable": "false",
@ -302,6 +381,15 @@ func (suite *AccountUpdateTestSuite) TestUpdateAccountWithImageFormData() {
suite.NotEqual("http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg", apimodelAccount.HeaderStatic) suite.NotEqual("http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg", apimodelAccount.HeaderStatic)
} }
func (suite *AccountUpdateTestSuite) TestUpdateAccountEmptyForm() {
data := make(map[string]string)
_, err := suite.updateAccountFromForm(data, http.StatusBadRequest, `{"error":"Bad Request: empty form submitted"}`)
if err != nil {
suite.FailNow(err.Error())
}
}
func (suite *AccountUpdateTestSuite) TestUpdateAccountEmptyFormData() { func (suite *AccountUpdateTestSuite) TestUpdateAccountEmptyFormData() {
data := make(map[string]string) data := make(map[string]string)
@ -311,6 +399,25 @@ func (suite *AccountUpdateTestSuite) TestUpdateAccountEmptyFormData() {
} }
} }
func (suite *AccountUpdateTestSuite) TestUpdateAccountSourceForm() {
data := map[string]string{
"source[privacy]": string(apimodel.VisibilityPrivate),
"source[language]": "de",
"source[sensitive]": "true",
"locked": "true",
}
apimodelAccount, err := suite.updateAccountFromForm(data, http.StatusOK, "")
if err != nil {
suite.FailNow(err.Error())
}
suite.Equal(data["source[language]"], apimodelAccount.Source.Language)
suite.EqualValues(apimodel.VisibilityPrivate, apimodelAccount.Source.Privacy)
suite.True(apimodelAccount.Source.Sensitive)
suite.True(apimodelAccount.Locked)
}
func (suite *AccountUpdateTestSuite) TestUpdateAccountSourceFormData() { func (suite *AccountUpdateTestSuite) TestUpdateAccountSourceFormData() {
data := map[string]string{ data := map[string]string{
"source[privacy]": string(apimodel.VisibilityPrivate), "source[privacy]": string(apimodel.VisibilityPrivate),