diff options
| author | 2023-11-10 17:42:48 +0100 | |
|---|---|---|
| committer | 2023-11-10 16:42:48 +0000 | |
| commit | c7ecab9e6fb76bb10da26c803fc5838419642423 (patch) | |
| tree | 41410d639bdb8b19a93e972a6d05937f82ab4299 /internal | |
| parent | [bugfix] Don't try to update suspended accounts (#2348) (diff) | |
| download | gotosocial-c7ecab9e6fb76bb10da26c803fc5838419642423.tar.xz | |
[chore/bugfix/horror] Allow `expires_in` and poll choices to be parsed from strings (#2346)
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/api/auth/token_test.go | 44 | ||||
| -rw-r--r-- | internal/api/client/accounts/accountdelete_test.go | 10 | ||||
| -rw-r--r-- | internal/api/client/accounts/accountupdate_test.go | 114 | ||||
| -rw-r--r-- | internal/api/client/admin/emojicreate_test.go | 22 | ||||
| -rw-r--r-- | internal/api/client/admin/emojiupdate_test.go | 58 | ||||
| -rw-r--r-- | internal/api/client/instance/instancepatch_test.go | 46 | ||||
| -rw-r--r-- | internal/api/client/media/mediacreate_test.go | 24 | ||||
| -rw-r--r-- | internal/api/client/media/mediaupdate_test.go | 16 | ||||
| -rw-r--r-- | internal/api/client/polls/polls_test.go | 102 | ||||
| -rw-r--r-- | internal/api/client/polls/polls_vote.go | 57 | ||||
| -rw-r--r-- | internal/api/client/polls/polls_vote_test.go | 189 | ||||
| -rw-r--r-- | internal/api/client/statuses/statuscreate.go | 62 | ||||
| -rw-r--r-- | internal/api/model/poll.go | 15 | 
13 files changed, 573 insertions, 186 deletions
diff --git a/internal/api/auth/token_test.go b/internal/api/auth/token_test.go index e319c2d02..c97fce3b9 100644 --- a/internal/api/auth/token_test.go +++ b/internal/api/auth/token_test.go @@ -58,11 +58,11 @@ func (suite *TokenTestSuite) TestRetrieveClientCredentialsOK() {  	requestBody, w, err := testrig.CreateMultipartFormData(  		"", "", -		map[string]string{ -			"grant_type":    "client_credentials", -			"client_id":     testClient.ID, -			"client_secret": testClient.Secret, -			"redirect_uri":  "http://localhost:8080", +		map[string][]string{ +			"grant_type":    {"client_credentials"}, +			"client_id":     {testClient.ID}, +			"client_secret": {testClient.Secret}, +			"redirect_uri":  {"http://localhost:8080"},  		})  	if err != nil {  		panic(err) @@ -104,12 +104,12 @@ func (suite *TokenTestSuite) TestRetrieveAuthorizationCodeOK() {  	requestBody, w, err := testrig.CreateMultipartFormData(  		"", "", -		map[string]string{ -			"grant_type":    "authorization_code", -			"client_id":     testClient.ID, -			"client_secret": testClient.Secret, -			"redirect_uri":  "http://localhost:8080", -			"code":          testUserAuthorizationToken.Code, +		map[string][]string{ +			"grant_type":    {"authorization_code"}, +			"client_id":     {testClient.ID}, +			"client_secret": {testClient.Secret}, +			"redirect_uri":  {"http://localhost:8080"}, +			"code":          {testUserAuthorizationToken.Code},  		})  	if err != nil {  		panic(err) @@ -149,11 +149,11 @@ func (suite *TokenTestSuite) TestRetrieveAuthorizationCodeNoCode() {  	requestBody, w, err := testrig.CreateMultipartFormData(  		"", "", -		map[string]string{ -			"grant_type":    "authorization_code", -			"client_id":     testClient.ID, -			"client_secret": testClient.Secret, -			"redirect_uri":  "http://localhost:8080", +		map[string][]string{ +			"grant_type":    {"authorization_code"}, +			"client_id":     {testClient.ID}, +			"client_secret": {testClient.Secret}, +			"redirect_uri":  {"http://localhost:8080"},  		})  	if err != nil {  		panic(err) @@ -181,12 +181,12 @@ func (suite *TokenTestSuite) TestRetrieveAuthorizationCodeWrongGrantType() {  	requestBody, w, err := testrig.CreateMultipartFormData(  		"", "", -		map[string]string{ -			"grant_type":    "client_credentials", -			"client_id":     testClient.ID, -			"client_secret": testClient.Secret, -			"redirect_uri":  "http://localhost:8080", -			"code":          "peepeepoopoo", +		map[string][]string{ +			"grant_type":    {"client_credentials"}, +			"client_id":     {testClient.ID}, +			"client_secret": {testClient.Secret}, +			"redirect_uri":  {"http://localhost:8080"}, +			"code":          {"peepeepoopoo"},  		})  	if err != nil {  		panic(err) diff --git a/internal/api/client/accounts/accountdelete_test.go b/internal/api/client/accounts/accountdelete_test.go index d8889b680..2f5a25b4b 100644 --- a/internal/api/client/accounts/accountdelete_test.go +++ b/internal/api/client/accounts/accountdelete_test.go @@ -36,8 +36,8 @@ func (suite *AccountDeleteTestSuite) TestAccountDeletePOSTHandler() {  	// we're deleting zork  	requestBody, w, err := testrig.CreateMultipartFormData(  		"", "", -		map[string]string{ -			"password": "password", +		map[string][]string{ +			"password": {"password"},  		})  	if err != nil {  		panic(err) @@ -58,8 +58,8 @@ func (suite *AccountDeleteTestSuite) TestAccountDeletePOSTHandlerWrongPassword()  	// we're deleting zork  	requestBody, w, err := testrig.CreateMultipartFormData(  		"", "", -		map[string]string{ -			"password": "aaaaaaaaaaaaaaaaaaaaaaaaaaaa", +		map[string][]string{ +			"password": {"aaaaaaaaaaaaaaaaaaaaaaaaaaaa"},  		})  	if err != nil {  		panic(err) @@ -80,7 +80,7 @@ func (suite *AccountDeleteTestSuite) TestAccountDeletePOSTHandlerNoPassword() {  	// we're deleting zork  	requestBody, w, err := testrig.CreateMultipartFormData(  		"", "", -		map[string]string{}) +		map[string][]string{})  	if err != nil {  		panic(err)  	} diff --git a/internal/api/client/accounts/accountupdate_test.go b/internal/api/client/accounts/accountupdate_test.go index 835989037..73e33390f 100644 --- a/internal/api/client/accounts/accountupdate_test.go +++ b/internal/api/client/accounts/accountupdate_test.go @@ -38,15 +38,19 @@ type AccountUpdateTestSuite struct {  	AccountStandardTestSuite  } -func (suite *AccountUpdateTestSuite) updateAccountFromForm(data map[string]string, expectedHTTPStatus int, expectedBody string) (*apimodel.Account, error) { +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} +		if form.Has(key) { +			form[key] = append(form[key], val...) +		} else { +			form[key] = 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)  	if err != nil {  		suite.FailNow(err.Error()) @@ -55,7 +59,7 @@ func (suite *AccountUpdateTestSuite) updateAccountFromFormData(data map[string]s  	return suite.updateAccount(requestBody.Bytes(), w.FormDataContentType(), expectedHTTPStatus, expectedBody)  } -func (suite *AccountUpdateTestSuite) updateAccountFromFormDataWithFile(fieldName string, fileName string, data map[string]string, expectedHTTPStatus int, expectedBody string) (*apimodel.Account, error) { +func (suite *AccountUpdateTestSuite) updateAccountFromFormDataWithFile(fieldName string, fileName string, data map[string][]string, expectedHTTPStatus int, expectedBody string) (*apimodel.Account, error) {  	requestBody, w, err := testrig.CreateMultipartFormData(fieldName, fileName, data)  	if err != nil {  		suite.FailNow(err.Error()) @@ -116,12 +120,12 @@ func (suite *AccountUpdateTestSuite) updateAccount(  }  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", +	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, "") @@ -142,12 +146,12 @@ func (suite *AccountUpdateTestSuite) TestUpdateAccountBasicForm() {  }  func (suite *AccountUpdateTestSuite) TestUpdateAccountBasicFormData() { -	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", +	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.updateAccountFromFormData(data, http.StatusOK, "") @@ -202,8 +206,8 @@ func (suite *AccountUpdateTestSuite) TestUpdateAccountBasicJSON() {  }  func (suite *AccountUpdateTestSuite) TestUpdateAccountLockForm() { -	data := map[string]string{ -		"locked": "true", +	data := map[string][]string{ +		"locked": {"true"},  	}  	apimodelAccount, err := suite.updateAccountFromForm(data, http.StatusOK, "") @@ -215,8 +219,8 @@ func (suite *AccountUpdateTestSuite) TestUpdateAccountLockForm() {  }  func (suite *AccountUpdateTestSuite) TestUpdateAccountLockFormData() { -	data := map[string]string{ -		"locked": "true", +	data := map[string][]string{ +		"locked": {"true"},  	}  	apimodelAccount, err := suite.updateAccountFromFormData(data, http.StatusOK, "") @@ -242,8 +246,8 @@ func (suite *AccountUpdateTestSuite) TestUpdateAccountLockJSON() {  }  func (suite *AccountUpdateTestSuite) TestUpdateAccountUnlockForm() { -	data := map[string]string{ -		"locked": "false", +	data := map[string][]string{ +		"locked": {"false"},  	}  	apimodelAccount, err := suite.updateAccountFromForm(data, http.StatusOK, "") @@ -255,8 +259,8 @@ func (suite *AccountUpdateTestSuite) TestUpdateAccountUnlockForm() {  }  func (suite *AccountUpdateTestSuite) TestUpdateAccountUnlockFormData() { -	data := map[string]string{ -		"locked": "false", +	data := map[string][]string{ +		"locked": {"false"},  	}  	apimodelAccount, err := suite.updateAccountFromFormData(data, http.StatusOK, "") @@ -289,8 +293,8 @@ func (suite *AccountUpdateTestSuite) TestUpdateAccountCache() {  		suite.FailNow(err.Error())  	} -	data := map[string]string{ -		"note": "this is my new bio read it and weep", +	data := map[string][]string{ +		"note": {"this is my new bio read it and weep"},  	}  	apimodelAccount, err := suite.updateAccountFromFormData(data, http.StatusOK, "") @@ -302,8 +306,8 @@ func (suite *AccountUpdateTestSuite) TestUpdateAccountCache() {  }  func (suite *AccountUpdateTestSuite) TestUpdateAccountDiscoverableForm() { -	data := map[string]string{ -		"discoverable": "false", +	data := map[string][]string{ +		"discoverable": {"false"},  	}  	apimodelAccount, err := suite.updateAccountFromForm(data, http.StatusOK, "") @@ -320,8 +324,8 @@ func (suite *AccountUpdateTestSuite) TestUpdateAccountDiscoverableForm() {  }  func (suite *AccountUpdateTestSuite) TestUpdateAccountDiscoverableFormData() { -	data := map[string]string{ -		"discoverable": "false", +	data := map[string][]string{ +		"discoverable": {"false"},  	}  	apimodelAccount, err := suite.updateAccountFromFormData(data, http.StatusOK, "") @@ -357,10 +361,10 @@ func (suite *AccountUpdateTestSuite) TestUpdateAccountDiscoverableJSON() {  }  func (suite *AccountUpdateTestSuite) TestUpdateAccountWithImageFormData() { -	data := map[string]string{ -		"display_name": "updated zork display name!!!", -		"note":         "", -		"locked":       "true", +	data := map[string][]string{ +		"display_name": {"updated zork display name!!!"}, +		"note":         {""}, +		"locked":       {"true"},  	}  	apimodelAccount, err := suite.updateAccountFromFormDataWithFile("header", "../../../../testrig/media/test-jpeg.jpg", data, http.StatusOK, "") @@ -368,7 +372,7 @@ func (suite *AccountUpdateTestSuite) TestUpdateAccountWithImageFormData() {  		suite.FailNow(err.Error())  	} -	suite.Equal(data["display_name"], apimodelAccount.DisplayName) +	suite.Equal(data["display_name"][0], apimodelAccount.DisplayName)  	suite.True(apimodelAccount.Locked)  	suite.Empty(apimodelAccount.Note)  	suite.Empty(apimodelAccount.Source.Note) @@ -382,7 +386,7 @@ func (suite *AccountUpdateTestSuite) TestUpdateAccountWithImageFormData() {  }  func (suite *AccountUpdateTestSuite) TestUpdateAccountEmptyForm() { -	data := make(map[string]string) +	data := make(map[string][]string)  	_, err := suite.updateAccountFromForm(data, http.StatusBadRequest, `{"error":"Bad Request: empty form submitted"}`)  	if err != nil { @@ -391,7 +395,7 @@ func (suite *AccountUpdateTestSuite) TestUpdateAccountEmptyForm() {  }  func (suite *AccountUpdateTestSuite) TestUpdateAccountEmptyFormData() { -	data := make(map[string]string) +	data := make(map[string][]string)  	_, err := suite.updateAccountFromFormData(data, http.StatusBadRequest, `{"error":"Bad Request: empty form submitted"}`)  	if err != nil { @@ -400,11 +404,11 @@ 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", +	data := map[string][]string{ +		"source[privacy]":   {string(apimodel.VisibilityPrivate)}, +		"source[language]":  {"de"}, +		"source[sensitive]": {"true"}, +		"locked":            {"true"},  	}  	apimodelAccount, err := suite.updateAccountFromForm(data, http.StatusOK, "") @@ -412,18 +416,18 @@ func (suite *AccountUpdateTestSuite) TestUpdateAccountSourceForm() {  		suite.FailNow(err.Error())  	} -	suite.Equal(data["source[language]"], apimodelAccount.Source.Language) +	suite.Equal(data["source[language]"][0], apimodelAccount.Source.Language)  	suite.EqualValues(apimodel.VisibilityPrivate, apimodelAccount.Source.Privacy)  	suite.True(apimodelAccount.Source.Sensitive)  	suite.True(apimodelAccount.Locked)  }  func (suite *AccountUpdateTestSuite) TestUpdateAccountSourceFormData() { -	data := map[string]string{ -		"source[privacy]":   string(apimodel.VisibilityPrivate), -		"source[language]":  "de", -		"source[sensitive]": "true", -		"locked":            "true", +	data := map[string][]string{ +		"source[privacy]":   {string(apimodel.VisibilityPrivate)}, +		"source[language]":  {"de"}, +		"source[sensitive]": {"true"}, +		"locked":            {"true"},  	}  	apimodelAccount, err := suite.updateAccountFromFormData(data, http.StatusOK, "") @@ -431,7 +435,7 @@ func (suite *AccountUpdateTestSuite) TestUpdateAccountSourceFormData() {  		suite.FailNow(err.Error())  	} -	suite.Equal(data["source[language]"], apimodelAccount.Source.Language) +	suite.Equal(data["source[language]"][0], apimodelAccount.Source.Language)  	suite.EqualValues(apimodel.VisibilityPrivate, apimodelAccount.Source.Privacy)  	suite.True(apimodelAccount.Source.Sensitive)  	suite.True(apimodelAccount.Locked) @@ -461,8 +465,8 @@ func (suite *AccountUpdateTestSuite) TestUpdateAccountSourceJSON() {  }  func (suite *AccountUpdateTestSuite) TestUpdateAccountSourceBadContentTypeFormData() { -	data := map[string]string{ -		"source[status_content_type]": "text/markdown", +	data := map[string][]string{ +		"source[status_content_type]": {"text/markdown"},  	}  	apimodelAccount, err := suite.updateAccountFromFormData(data, http.StatusOK, "") @@ -470,19 +474,19 @@ func (suite *AccountUpdateTestSuite) TestUpdateAccountSourceBadContentTypeFormDa  		suite.FailNow(err.Error())  	} -	suite.Equal(data["source[status_content_type]"], apimodelAccount.Source.StatusContentType) +	suite.Equal(data["source[status_content_type]"][0], apimodelAccount.Source.StatusContentType)  	// Check the account in the database too.  	dbAccount, err := suite.db.GetAccountByID(context.Background(), suite.testAccounts["local_account_1"].ID)  	if err != nil {  		suite.FailNow(err.Error())  	} -	suite.Equal(data["source[status_content_type]"], dbAccount.StatusContentType) +	suite.Equal(data["source[status_content_type]"][0], dbAccount.StatusContentType)  }  func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandlerUpdateStatusContentTypeBad() { -	data := map[string]string{ -		"source[status_content_type]": "peepeepoopoo", +	data := map[string][]string{ +		"source[status_content_type]": {"peepeepoopoo"},  	}  	_, err := suite.updateAccountFromFormData(data, http.StatusBadRequest, `{"error":"Bad Request: status content type 'peepeepoopoo' was not recognized, valid options are 'text/plain', 'text/markdown'"}`) diff --git a/internal/api/client/admin/emojicreate_test.go b/internal/api/client/admin/emojicreate_test.go index b312a593a..46139df47 100644 --- a/internal/api/client/admin/emojicreate_test.go +++ b/internal/api/client/admin/emojicreate_test.go @@ -39,9 +39,9 @@ func (suite *EmojiCreateTestSuite) TestEmojiCreateNewCategory() {  	// set up the request  	requestBody, w, err := testrig.CreateMultipartFormData(  		"image", "../../../../testrig/media/rainbow-original.png", -		map[string]string{ -			"shortcode": "new_emoji", -			"category":  "Test Emojis", // this category doesn't exist yet +		map[string][]string{ +			"shortcode": {"new_emoji"}, +			"category":  {"Test Emojis"}, // this category doesn't exist yet  		})  	if err != nil {  		panic(err) @@ -112,9 +112,9 @@ func (suite *EmojiCreateTestSuite) TestEmojiCreateExistingCategory() {  	// set up the request  	requestBody, w, err := testrig.CreateMultipartFormData(  		"image", "../../../../testrig/media/rainbow-original.png", -		map[string]string{ -			"shortcode": "new_emoji", -			"category":  "cute stuff", // this category already exists +		map[string][]string{ +			"shortcode": {"new_emoji"}, +			"category":  {"cute stuff"}, // this category already exists  		})  	if err != nil {  		panic(err) @@ -185,9 +185,9 @@ func (suite *EmojiCreateTestSuite) TestEmojiCreateNoCategory() {  	// set up the request  	requestBody, w, err := testrig.CreateMultipartFormData(  		"image", "../../../../testrig/media/rainbow-original.png", -		map[string]string{ -			"shortcode": "new_emoji", -			"category":  "", +		map[string][]string{ +			"shortcode": {"new_emoji"}, +			"category":  {""},  		})  	if err != nil {  		panic(err) @@ -258,8 +258,8 @@ func (suite *EmojiCreateTestSuite) TestEmojiCreateAlreadyExists() {  	// set up the request -- use a shortcode that already exists for an emoji in the database  	requestBody, w, err := testrig.CreateMultipartFormData(  		"image", "../../../../testrig/media/rainbow-original.png", -		map[string]string{ -			"shortcode": "rainbow", +		map[string][]string{ +			"shortcode": {"rainbow"},  		})  	if err != nil {  		panic(err) diff --git a/internal/api/client/admin/emojiupdate_test.go b/internal/api/client/admin/emojiupdate_test.go index 1f0e8de13..35aeb08ed 100644 --- a/internal/api/client/admin/emojiupdate_test.go +++ b/internal/api/client/admin/emojiupdate_test.go @@ -43,9 +43,9 @@ func (suite *EmojiUpdateTestSuite) TestEmojiUpdateNewCategory() {  	// set up the request  	requestBody, w, err := testrig.CreateMultipartFormData(  		"", "", -		map[string]string{ -			"category": "New Category", // this category doesn't exist yet -			"type":     "modify", +		map[string][]string{ +			"category": {"New Category"}, // this category doesn't exist yet +			"type":     {"modify"},  		})  	if err != nil {  		panic(err) @@ -120,9 +120,9 @@ func (suite *EmojiUpdateTestSuite) TestEmojiUpdateSwitchCategory() {  	// set up the request  	requestBody, w, err := testrig.CreateMultipartFormData(  		"", "", -		map[string]string{ -			"type":     "modify", -			"category": "cute stuff", +		map[string][]string{ +			"type":     {"modify"}, +			"category": {"cute stuff"},  		})  	if err != nil {  		panic(err) @@ -197,10 +197,10 @@ func (suite *EmojiUpdateTestSuite) TestEmojiUpdateCopyRemoteToLocal() {  	// set up the request  	requestBody, w, err := testrig.CreateMultipartFormData(  		"", "", -		map[string]string{ -			"type":      "copy", -			"category":  "emojis i stole", -			"shortcode": "yell", +		map[string][]string{ +			"type":      {"copy"}, +			"category":  {"emojis i stole"}, +			"shortcode": {"yell"},  		})  	if err != nil {  		panic(err) @@ -275,8 +275,8 @@ func (suite *EmojiUpdateTestSuite) TestEmojiUpdateDisableEmoji() {  	// set up the request  	requestBody, w, err := testrig.CreateMultipartFormData(  		"", "", -		map[string]string{ -			"type": "disable", +		map[string][]string{ +			"type": {"disable"},  		})  	if err != nil {  		panic(err) @@ -316,8 +316,8 @@ func (suite *EmojiUpdateTestSuite) TestEmojiUpdateDisableLocalEmoji() {  	// set up the request  	requestBody, w, err := testrig.CreateMultipartFormData(  		"", "", -		map[string]string{ -			"type": "disable", +		map[string][]string{ +			"type": {"disable"},  		})  	if err != nil {  		panic(err) @@ -349,8 +349,8 @@ func (suite *EmojiUpdateTestSuite) TestEmojiUpdateModifyRemoteEmoji() {  	// set up the request  	requestBody, w, err := testrig.CreateMultipartFormData(  		"image", "../../../../testrig/media/kip-original.gif", -		map[string]string{ -			"type": "modify", +		map[string][]string{ +			"type": {"modify"},  		})  	if err != nil {  		panic(err) @@ -382,8 +382,8 @@ func (suite *EmojiUpdateTestSuite) TestEmojiUpdateModifyNoParams() {  	// set up the request  	requestBody, w, err := testrig.CreateMultipartFormData(  		"", "", -		map[string]string{ -			"type": "modify", +		map[string][]string{ +			"type": {"modify"},  		})  	if err != nil {  		panic(err) @@ -415,9 +415,9 @@ func (suite *EmojiUpdateTestSuite) TestEmojiUpdateCopyLocalToLocal() {  	// set up the request  	requestBody, w, err := testrig.CreateMultipartFormData(  		"", "", -		map[string]string{ -			"type":      "copy", -			"shortcode": "bottoms", +		map[string][]string{ +			"type":      {"copy"}, +			"shortcode": {"bottoms"},  		})  	if err != nil {  		panic(err) @@ -449,9 +449,9 @@ func (suite *EmojiUpdateTestSuite) TestEmojiUpdateCopyEmptyShortcode() {  	// set up the request  	requestBody, w, err := testrig.CreateMultipartFormData(  		"", "", -		map[string]string{ -			"type":      "copy", -			"shortcode": "", +		map[string][]string{ +			"type":      {"copy"}, +			"shortcode": {""},  		})  	if err != nil {  		panic(err) @@ -483,8 +483,8 @@ func (suite *EmojiUpdateTestSuite) TestEmojiUpdateCopyNoShortcode() {  	// set up the request  	requestBody, w, err := testrig.CreateMultipartFormData(  		"", "", -		map[string]string{ -			"type": "copy", +		map[string][]string{ +			"type": {"copy"},  		})  	if err != nil {  		panic(err) @@ -516,9 +516,9 @@ func (suite *EmojiUpdateTestSuite) TestEmojiUpdateCopyShortcodeAlreadyInUse() {  	// set up the request  	requestBody, w, err := testrig.CreateMultipartFormData(  		"", "", -		map[string]string{ -			"type":      "copy", -			"shortcode": "rainbow", +		map[string][]string{ +			"type":      {"copy"}, +			"shortcode": {"rainbow"},  		})  	if err != nil {  		panic(err) diff --git a/internal/api/client/instance/instancepatch_test.go b/internal/api/client/instance/instancepatch_test.go index 2af226357..2fc045855 100644 --- a/internal/api/client/instance/instancepatch_test.go +++ b/internal/api/client/instance/instancepatch_test.go @@ -36,7 +36,7 @@ type InstancePatchTestSuite struct {  	InstanceStandardTestSuite  } -func (suite *InstancePatchTestSuite) instancePatch(fieldName string, fileName string, extraFields map[string]string) (code int, body []byte) { +func (suite *InstancePatchTestSuite) instancePatch(fieldName string, fileName string, extraFields map[string][]string) (code int, body []byte) {  	requestBody, w, err := testrig.CreateMultipartFormData(fieldName, fileName, extraFields)  	if err != nil {  		suite.FailNow(err.Error()) @@ -59,10 +59,10 @@ func (suite *InstancePatchTestSuite) instancePatch(fieldName string, fileName st  }  func (suite *InstancePatchTestSuite) TestInstancePatch1() { -	code, b := suite.instancePatch("", "", map[string]string{ -		"title":            "Example Instance", -		"contact_username": "admin", -		"contact_email":    "someone@example.org", +	code, b := suite.instancePatch("", "", map[string][]string{ +		"title":            {"Example Instance"}, +		"contact_username": {"admin"}, +		"contact_email":    {"someone@example.org"},  	})  	if expectedCode := http.StatusOK; code != expectedCode { @@ -175,8 +175,8 @@ func (suite *InstancePatchTestSuite) TestInstancePatch1() {  }  func (suite *InstancePatchTestSuite) TestInstancePatch2() { -	code, b := suite.instancePatch("", "", map[string]string{ -		"title": "<p>Geoff's Instance</p>", +	code, b := suite.instancePatch("", "", map[string][]string{ +		"title": {"<p>Geoff's Instance</p>"},  	})  	if expectedCode := http.StatusOK; code != expectedCode { @@ -289,8 +289,8 @@ func (suite *InstancePatchTestSuite) TestInstancePatch2() {  }  func (suite *InstancePatchTestSuite) TestInstancePatch3() { -	code, b := suite.instancePatch("", "", map[string]string{ -		"short_description": "<p>This is some html, which is <em>allowed</em> in short descriptions.</p>", +	code, b := suite.instancePatch("", "", map[string][]string{ +		"short_description": {"<p>This is some html, which is <em>allowed</em> in short descriptions.</p>"},  	})  	if expectedCode := http.StatusOK; code != expectedCode { @@ -403,8 +403,8 @@ func (suite *InstancePatchTestSuite) TestInstancePatch3() {  }  func (suite *InstancePatchTestSuite) TestInstancePatch4() { -	code, b := suite.instancePatch("", "", map[string]string{ -		"": "", +	code, b := suite.instancePatch("", "", map[string][]string{ +		"": {""},  	})  	if expectedCode := http.StatusBadRequest; code != expectedCode { @@ -422,8 +422,8 @@ func (suite *InstancePatchTestSuite) TestInstancePatch4() {  func (suite *InstancePatchTestSuite) TestInstancePatch5() {  	requestBody, w, err := testrig.CreateMultipartFormData(  		"", "", -		map[string]string{ -			"short_description": "<p>This is some html, which is <em>allowed</em> in short descriptions.</p>", +		map[string][]string{ +			"short_description": {"<p>This is some html, which is <em>allowed</em> in short descriptions.</p>"},  		})  	if err != nil {  		panic(err) @@ -454,8 +454,8 @@ func (suite *InstancePatchTestSuite) TestInstancePatch5() {  }  func (suite *InstancePatchTestSuite) TestInstancePatch6() { -	code, b := suite.instancePatch("", "", map[string]string{ -		"contact_email": "", +	code, b := suite.instancePatch("", "", map[string][]string{ +		"contact_email": {""},  	})  	if expectedCode := http.StatusOK; code != expectedCode { @@ -568,8 +568,8 @@ func (suite *InstancePatchTestSuite) TestInstancePatch6() {  }  func (suite *InstancePatchTestSuite) TestInstancePatch7() { -	code, b := suite.instancePatch("", "", map[string]string{ -		"contact_email": "not.an.email.address", +	code, b := suite.instancePatch("", "", map[string][]string{ +		"contact_email": {"not.an.email.address"},  	})  	if expectedCode := http.StatusBadRequest; code != expectedCode { @@ -585,8 +585,8 @@ func (suite *InstancePatchTestSuite) TestInstancePatch7() {  }  func (suite *InstancePatchTestSuite) TestInstancePatch8() { -	code, b := suite.instancePatch("thumbnail", "../../../../testrig/media/peglin.gif", map[string]string{ -		"thumbnail_description": "A bouncing little green peglin.", +	code, b := suite.instancePatch("thumbnail", "../../../../testrig/media/peglin.gif", map[string][]string{ +		"thumbnail_description": {"A bouncing little green peglin."},  	})  	if expectedCode := http.StatusOK; code != expectedCode { @@ -723,8 +723,8 @@ func (suite *InstancePatchTestSuite) TestInstancePatch8() {  }`, string(instanceV2ThumbnailJson))  	// double extra special bonus: now update the image description without changing the image -	code2, b2 := suite.instancePatch("", "", map[string]string{ -		"thumbnail_description": "updating the thumbnail description without changing anything else!", +	code2, b2 := suite.instancePatch("", "", map[string][]string{ +		"thumbnail_description": {"updating the thumbnail description without changing anything else!"},  	})  	if expectedCode := http.StatusOK; code2 != expectedCode { @@ -741,8 +741,8 @@ func (suite *InstancePatchTestSuite) TestInstancePatch8() {  }  func (suite *InstancePatchTestSuite) TestInstancePatch9() { -	code, b := suite.instancePatch("", "", map[string]string{ -		"thumbnail_description": "setting a new description without having a custom image set; this should change nothing!", +	code, b := suite.instancePatch("", "", map[string][]string{ +		"thumbnail_description": {"setting a new description without having a custom image set; this should change nothing!"},  	})  	if expectedCode := http.StatusOK; code != expectedCode { diff --git a/internal/api/client/media/mediacreate_test.go b/internal/api/client/media/mediacreate_test.go index 8fcaaa06e..3f0b9dc0d 100644 --- a/internal/api/client/media/mediacreate_test.go +++ b/internal/api/client/media/mediacreate_test.go @@ -160,9 +160,9 @@ func (suite *MediaCreateTestSuite) TestMediaCreateSuccessful() {  	}  	// create the request -	buf, w, err := testrig.CreateMultipartFormData("file", "../../../../testrig/media/test-jpeg.jpg", map[string]string{ -		"description": "this is a test image -- a cool background from somewhere", -		"focus":       "-0.5,0.5", +	buf, w, err := testrig.CreateMultipartFormData("file", "../../../../testrig/media/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) @@ -245,9 +245,9 @@ func (suite *MediaCreateTestSuite) TestMediaCreateSuccessfulV2() {  	}  	// create the request -	buf, w, err := testrig.CreateMultipartFormData("file", "../../../../testrig/media/test-jpeg.jpg", map[string]string{ -		"description": "this is a test image -- a cool background from somewhere", -		"focus":       "-0.5,0.5", +	buf, w, err := testrig.CreateMultipartFormData("file", "../../../../testrig/media/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) @@ -328,9 +328,9 @@ func (suite *MediaCreateTestSuite) TestMediaCreateLongDescription() {  	description := base64.RawStdEncoding.EncodeToString(descriptionBytes)  	// create the request -	buf, w, err := testrig.CreateMultipartFormData("file", "../../../../testrig/media/test-jpeg.jpg", map[string]string{ -		"description": description, -		"focus":       "-0.5,0.5", +	buf, w, err := testrig.CreateMultipartFormData("file", "../../../../testrig/media/test-jpeg.jpg", map[string][]string{ +		"description": {description}, +		"focus":       {"-0.5,0.5"},  	})  	if err != nil {  		panic(err) @@ -369,9 +369,9 @@ func (suite *MediaCreateTestSuite) TestMediaCreateTooShortDescription() {  	ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])  	// create the request -	buf, w, err := testrig.CreateMultipartFormData("file", "../../../../testrig/media/test-jpeg.jpg", map[string]string{ -		"description": "", // provide an empty description -		"focus":       "-0.5,0.5", +	buf, w, err := testrig.CreateMultipartFormData("file", "../../../../testrig/media/test-jpeg.jpg", map[string][]string{ +		"description": {""}, // provide an empty description +		"focus":       {"-0.5,0.5"},  	})  	if err != nil {  		panic(err) diff --git a/internal/api/client/media/mediaupdate_test.go b/internal/api/client/media/mediaupdate_test.go index 423178ee7..603bde402 100644 --- a/internal/api/client/media/mediaupdate_test.go +++ b/internal/api/client/media/mediaupdate_test.go @@ -149,10 +149,10 @@ func (suite *MediaUpdateTestSuite) TestUpdateImage() {  	ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])  	// create the request -	buf, w, err := testrig.CreateMultipartFormData("", "", map[string]string{ -		"id":          toUpdate.ID, -		"description": "new description!", -		"focus":       "-0.1,0.3", +	buf, w, err := testrig.CreateMultipartFormData("", "", map[string][]string{ +		"id":          {toUpdate.ID}, +		"description": {"new description!"}, +		"focus":       {"-0.1,0.3"},  	})  	if err != nil {  		panic(err) @@ -210,10 +210,10 @@ func (suite *MediaUpdateTestSuite) TestUpdateImageShortDescription() {  	ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])  	// create the request -	buf, w, err := testrig.CreateMultipartFormData("", "", map[string]string{ -		"id":          toUpdate.ID, -		"description": "new description!", -		"focus":       "-0.1,0.3", +	buf, w, err := testrig.CreateMultipartFormData("", "", map[string][]string{ +		"id":          {toUpdate.ID}, +		"description": {"new description!"}, +		"focus":       {"-0.1,0.3"},  	})  	if err != nil {  		panic(err) diff --git a/internal/api/client/polls/polls_test.go b/internal/api/client/polls/polls_test.go new file mode 100644 index 000000000..5baa29158 --- /dev/null +++ b/internal/api/client/polls/polls_test.go @@ -0,0 +1,102 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program.  If not, see <http://www.gnu.org/licenses/>. + +package polls_test + +import ( +	"github.com/stretchr/testify/suite" +	"github.com/superseriousbusiness/gotosocial/internal/api/client/polls" +	"github.com/superseriousbusiness/gotosocial/internal/db" +	"github.com/superseriousbusiness/gotosocial/internal/email" +	"github.com/superseriousbusiness/gotosocial/internal/federation" +	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +	"github.com/superseriousbusiness/gotosocial/internal/media" +	"github.com/superseriousbusiness/gotosocial/internal/processing" +	"github.com/superseriousbusiness/gotosocial/internal/state" +	"github.com/superseriousbusiness/gotosocial/internal/storage" +	"github.com/superseriousbusiness/gotosocial/internal/typeutils" +	"github.com/superseriousbusiness/gotosocial/internal/visibility" +	"github.com/superseriousbusiness/gotosocial/testrig" +) + +type PollsStandardTestSuite struct { +	suite.Suite +	db           db.DB +	storage      *storage.Driver +	mediaManager *media.Manager +	federator    *federation.Federator +	processor    *processing.Processor +	emailSender  email.Sender +	sentEmails   map[string]string +	state        state.State + +	// standard suite models +	testTokens       map[string]*gtsmodel.Token +	testClients      map[string]*gtsmodel.Client +	testApplications map[string]*gtsmodel.Application +	testUsers        map[string]*gtsmodel.User +	testAccounts     map[string]*gtsmodel.Account +	testStatuses     map[string]*gtsmodel.Status +	testPolls        map[string]*gtsmodel.Poll + +	// module being tested +	pollsModule *polls.Module +} + +func (suite *PollsStandardTestSuite) SetupSuite() { +	suite.testTokens = testrig.NewTestTokens() +	suite.testClients = testrig.NewTestClients() +	suite.testApplications = testrig.NewTestApplications() +	suite.testUsers = testrig.NewTestUsers() +	suite.testAccounts = testrig.NewTestAccounts() +	suite.testStatuses = testrig.NewTestStatuses() +	suite.testPolls = testrig.NewTestPolls() +} + +func (suite *PollsStandardTestSuite) SetupTest() { +	suite.state.Caches.Init() +	testrig.StartWorkers(&suite.state) + +	testrig.InitTestConfig() +	testrig.InitTestLog() + +	suite.db = testrig.NewTestDB(&suite.state) +	suite.state.DB = suite.db +	suite.storage = testrig.NewInMemoryStorage() +	suite.state.Storage = suite.storage + +	testrig.StartTimelines( +		&suite.state, +		visibility.NewFilter(&suite.state), +		typeutils.NewConverter(&suite.state), +	) + +	suite.mediaManager = testrig.NewTestMediaManager(&suite.state) +	suite.federator = testrig.NewTestFederator(&suite.state, testrig.NewTestTransportController(&suite.state, testrig.NewMockHTTPClient(nil, "../../../../testrig/media")), suite.mediaManager) +	suite.sentEmails = make(map[string]string) +	suite.emailSender = testrig.NewEmailSender("../../../../web/template/", suite.sentEmails) +	suite.processor = testrig.NewTestProcessor(&suite.state, suite.federator, suite.emailSender, suite.mediaManager) +	suite.pollsModule = polls.New(suite.processor) +	testrig.StandardDBSetup(suite.db, nil) +	testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media") +} + +func (suite *PollsStandardTestSuite) TearDownTest() { +	testrig.StandardDBTeardown(suite.db) +	testrig.StandardStorageTeardown(suite.storage) +	testrig.StopWorkers(&suite.state) +} diff --git a/internal/api/client/polls/polls_vote.go b/internal/api/client/polls/polls_vote.go index 824ea08ef..e5281b3fc 100644 --- a/internal/api/client/polls/polls_vote.go +++ b/internal/api/client/polls/polls_vote.go @@ -18,7 +18,9 @@  package polls  import ( +	"fmt"  	"net/http" +	"strconv"  	"github.com/gin-gonic/gin"  	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" @@ -97,9 +99,8 @@ func (m *Module) PollVotePOSTHandler(c *gin.Context) {  		return  	} -	var form apimodel.PollVoteRequest - -	if err := c.ShouldBind(&form); err != nil { +	choices, err := bindChoices(c) +	if err != nil {  		errWithCode := gtserror.NewErrorBadRequest(err, err.Error())  		apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)  		return @@ -109,7 +110,7 @@ func (m *Module) PollVotePOSTHandler(c *gin.Context) {  		c.Request.Context(),  		authed.Account,  		pollID, -		form.Choices, +		choices,  	)  	if errWithCode != nil {  		apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) @@ -118,3 +119,51 @@ func (m *Module) PollVotePOSTHandler(c *gin.Context) {  	c.JSON(http.StatusOK, poll)  } + +func bindChoices(c *gin.Context) ([]int, error) { +	var form apimodel.PollVoteRequest +	if err := c.ShouldBind(&form); err != nil { +		return nil, err +	} + +	if form.Choices != nil { +		// Easiest option: we parsed +		// from a form successfully. +		return form.Choices, nil +	} + +	// More difficult option: we +	// parsed choices from json. +	// +	// Convert submitted choices +	// into the ints we need. +	choices := make([]int, 0, len(form.ChoicesI)) +	for _, choiceI := range form.ChoicesI { +		switch i := choiceI.(type) { + +		// JSON numbers normally +		// parse into float64. +		// +		// This is the most likely +		// option so try it first. +		case float64: +			choices = append(choices, int(i)) + +		// Fallback option for funky +		// clients (pinafore, semaphore). +		case string: +			choice, err := strconv.Atoi(i) +			if err != nil { +				return nil, err +			} + +			choices = append(choices, choice) + +		default: +			// Nothing else will do. +			return nil, fmt.Errorf("could not parse json poll choice %T to integer", choiceI) +		} +	} + +	return choices, nil +} diff --git a/internal/api/client/polls/polls_vote_test.go b/internal/api/client/polls/polls_vote_test.go new file mode 100644 index 000000000..01bd941d3 --- /dev/null +++ b/internal/api/client/polls/polls_vote_test.go @@ -0,0 +1,189 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program.  If not, see <http://www.gnu.org/licenses/>. + +package polls_test + +import ( +	"bytes" +	"encoding/json" +	"io" +	"net/http" +	"net/http/httptest" +	"strconv" +	"testing" + +	"github.com/stretchr/testify/suite" +	"github.com/superseriousbusiness/gotosocial/internal/api/client/polls" +	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" +	"github.com/superseriousbusiness/gotosocial/internal/config" +	"github.com/superseriousbusiness/gotosocial/internal/gtserror" +	"github.com/superseriousbusiness/gotosocial/internal/oauth" +	"github.com/superseriousbusiness/gotosocial/testrig" +) + +type PollCreateTestSuite struct { +	PollsStandardTestSuite +} + +func (suite *PollCreateTestSuite) voteInPoll( +	pollID string, +	contentType string, +	body io.Reader, +	expectedHTTPStatus int, +	expectedBody string, +) (*apimodel.Poll, error) { +	// instantiate recorder + test context +	recorder := httptest.NewRecorder() +	ctx, _ := testrig.CreateGinTestContext(recorder, nil) +	ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["admin_account"]) +	ctx.Set(oauth.SessionAuthorizedToken, oauth.DBTokenToToken(suite.testTokens["admin_account"])) +	ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"]) +	ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["admin_account"]) + +	// create the request +	ctx.Request = httptest.NewRequest(http.MethodPost, config.GetProtocol()+"://"+config.GetHost()+"/api/"+polls.BasePath+"/"+pollID, body) +	ctx.Request.Header.Set("accept", "application/json") +	ctx.Request.Header.Set("content-type", contentType) + +	ctx.AddParam("id", pollID) + +	// trigger the handler +	suite.pollsModule.PollVotePOSTHandler(ctx) + +	// read the response +	result := recorder.Result() +	defer result.Body.Close() + +	b, err := io.ReadAll(result.Body) +	if err != nil { +		return nil, err +	} + +	errs := gtserror.NewMultiError(2) + +	// check code + body +	if resultCode := recorder.Code; expectedHTTPStatus != resultCode { +		errs.Appendf("expected %d got %d", expectedHTTPStatus, resultCode) +	} + +	// if we got an expected body, return early +	if expectedBody != "" { +		if string(b) != expectedBody { +			errs.Appendf("expected %s got %s", expectedBody, string(b)) +		} +		return nil, errs.Combine() +	} + +	resp := &apimodel.Poll{} +	if err := json.Unmarshal(b, resp); err != nil { +		return nil, err +	} + +	return resp, nil +} + +func (suite *PollCreateTestSuite) formVoteInPoll( +	pollID string, +	choices []int, +	expectedHTTPStatus int, +	expectedBody string, +) (*apimodel.Poll, error) { +	choicesStrs := make([]string, 0, len(choices)) +	for _, choice := range choices { +		choicesStrs = append(choicesStrs, strconv.Itoa(choice)) +	} + +	body, w, err := testrig.CreateMultipartFormData("", "", map[string][]string{ +		"choices[]": choicesStrs, +	}) + +	if err != nil { +		suite.FailNow(err.Error()) +	} + +	b := body.Bytes() +	suite.T().Log(string(b)) + +	return suite.voteInPoll( +		pollID, +		w.FormDataContentType(), +		bytes.NewReader(b), +		expectedHTTPStatus, +		expectedBody, +	) +} + +func (suite *PollCreateTestSuite) jsonVoteInPoll( +	pollID string, +	choices []interface{}, +	expectedHTTPStatus int, +	expectedBody string, +) (*apimodel.Poll, error) { +	form := apimodel.PollVoteRequest{ChoicesI: choices} + +	b, err := json.Marshal(&form) +	if err != nil { +		suite.FailNow(err.Error()) +	} + +	suite.T().Log(string(b)) + +	return suite.voteInPoll( +		pollID, +		"application/json", +		bytes.NewReader(b), +		expectedHTTPStatus, +		expectedBody, +	) +} + +func (suite *PollCreateTestSuite) TestPollVoteForm() { +	targetPoll := suite.testPolls["local_account_1_status_6_poll"] + +	poll, err := suite.formVoteInPoll(targetPoll.ID, []int{2}, http.StatusOK, "") +	if err != nil { +		suite.FailNow(err.Error()) +	} + +	suite.NotEmpty(poll) +} + +func (suite *PollCreateTestSuite) TestPollVoteJSONInt() { +	targetPoll := suite.testPolls["local_account_1_status_6_poll"] + +	poll, err := suite.jsonVoteInPoll(targetPoll.ID, []interface{}{2}, http.StatusOK, "") +	if err != nil { +		suite.FailNow(err.Error()) +	} + +	suite.NotEmpty(poll) +} + +func (suite *PollCreateTestSuite) TestPollVoteJSONStr() { +	targetPoll := suite.testPolls["local_account_1_status_6_poll"] + +	poll, err := suite.jsonVoteInPoll(targetPoll.ID, []interface{}{"2"}, http.StatusOK, "") +	if err != nil { +		suite.FailNow(err.Error()) +	} + +	suite.NotEmpty(poll) +} + +func TestPollCreateTestSuite(t *testing.T) { +	suite.Run(t, &PollCreateTestSuite{}) +} diff --git a/internal/api/client/statuses/statuscreate.go b/internal/api/client/statuses/statuscreate.go index 5034e53b1..cc9b78384 100644 --- a/internal/api/client/statuses/statuscreate.go +++ b/internal/api/client/statuses/statuscreate.go @@ -21,6 +21,7 @@ import (  	"errors"  	"fmt"  	"net/http" +	"strconv"  	"github.com/gin-gonic/gin"  	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" @@ -117,7 +118,10 @@ func (m *Module) StatusCreatePOSTHandler(c *gin.Context) {  	c.JSON(http.StatusOK, apiStatus)  } -// validateNormalizeCreateStatus checks the form for disallowed combinations of attachments and overlength inputs. +// validateNormalizeCreateStatus checks the form +// for disallowed combinations of attachments and +// overlength inputs. +//  // Side effect: normalizes the post's language tag.  func validateNormalizeCreateStatus(form *apimodel.AdvancedStatusCreateForm) error {  	hasStatus := form.Status != "" @@ -134,8 +138,6 @@ func validateNormalizeCreateStatus(form *apimodel.AdvancedStatusCreateForm) erro  	maxChars := config.GetStatusesMaxChars()  	maxMediaFiles := config.GetStatusesMediaMaxFiles() -	maxPollOptions := config.GetStatusesPollMaxOptions() -	maxPollChars := config.GetStatusesPollOptionMaxChars()  	maxCwChars := config.GetStatusesCWMaxChars()  	if form.Status != "" { @@ -149,16 +151,8 @@ func validateNormalizeCreateStatus(form *apimodel.AdvancedStatusCreateForm) erro  	}  	if form.Poll != nil { -		if len(form.Poll.Options) == 0 { -			return errors.New("poll with no options") -		} -		if len(form.Poll.Options) > maxPollOptions { -			return fmt.Errorf("too many poll options provided, %d provided but limit is %d", len(form.Poll.Options), maxPollOptions) -		} -		for _, p := range form.Poll.Options { -			if length := len([]rune(p)); length > maxPollChars { -				return fmt.Errorf("poll option too long, %d characters provided but limit is %d", length, maxPollChars) -			} +		if err := validateNormalizeCreatePoll(form); err != nil { +			return err  		}  	} @@ -178,3 +172,45 @@ func validateNormalizeCreateStatus(form *apimodel.AdvancedStatusCreateForm) erro  	return nil  } + +func validateNormalizeCreatePoll(form *apimodel.AdvancedStatusCreateForm) error { +	maxPollOptions := config.GetStatusesPollMaxOptions() +	maxPollChars := config.GetStatusesPollOptionMaxChars() + +	// Normalize poll expiry if necessary. +	// If we parsed this as JSON, expires_in +	// may be either a float64 or a string. +	if ei := form.Poll.ExpiresInI; ei != nil { +		switch e := ei.(type) { +		case float64: +			form.Poll.ExpiresIn = int(e) + +		case string: +			expiresIn, err := strconv.Atoi(e) +			if err != nil { +				return fmt.Errorf("could not parse expires_in value %s as integer: %w", e, err) +			} + +			form.Poll.ExpiresIn = expiresIn + +		default: +			return fmt.Errorf("could not parse expires_in type %T as integer", ei) +		} +	} + +	if len(form.Poll.Options) == 0 { +		return errors.New("poll with no options") +	} + +	if len(form.Poll.Options) > maxPollOptions { +		return fmt.Errorf("too many poll options provided, %d provided but limit is %d", len(form.Poll.Options), maxPollOptions) +	} + +	for _, p := range form.Poll.Options { +		if length := len([]rune(p)); length > maxPollChars { +			return fmt.Errorf("poll option too long, %d characters provided but limit is %d", length, maxPollChars) +		} +	} + +	return nil +} diff --git a/internal/api/model/poll.go b/internal/api/model/poll.go index c1d2ca89e..a9842e7a9 100644 --- a/internal/api/model/poll.go +++ b/internal/api/model/poll.go @@ -80,7 +80,11 @@ type PollRequest struct {  	// Duration the poll should be open, in seconds.  	// If provided, media_ids cannot be used, and poll[options] must be provided. -	ExpiresIn int `form:"expires_in" json:"expires_in" xml:"expires_in"` +	ExpiresIn int `form:"expires_in" xml:"expires_in"` + +	// Duration the poll should be open, in seconds. +	// If provided, media_ids cannot be used, and poll[options] must be provided. +	ExpiresInI interface{} `json:"expires_in"`  	// Allow multiple choices on this poll.  	Multiple bool `form:"multiple" json:"multiple" xml:"multiple"` @@ -93,7 +97,10 @@ type PollRequest struct {  //  // swagger:ignore  type PollVoteRequest struct { -	// Choices contains poll vote choice indices. Note that form -	// uses a different key than the JSON, i.e. the '[]' suffix. -	Choices []int `form:"choices[]" json:"choices" xml:"choices"` +	// Choices contains poll vote choice indices. +	Choices []int `form:"choices[]" xml:"choices"` + +	// ChoicesI contains poll vote choice +	// indices. Can be strings or integers. +	ChoicesI []interface{} `json:"choices"`  }  | 
