summaryrefslogtreecommitdiff
path: root/internal/api
diff options
context:
space:
mode:
Diffstat (limited to 'internal/api')
-rw-r--r--internal/api/auth/token_test.go44
-rw-r--r--internal/api/client/accounts/accountdelete_test.go10
-rw-r--r--internal/api/client/accounts/accountupdate_test.go114
-rw-r--r--internal/api/client/admin/emojicreate_test.go22
-rw-r--r--internal/api/client/admin/emojiupdate_test.go58
-rw-r--r--internal/api/client/instance/instancepatch_test.go46
-rw-r--r--internal/api/client/media/mediacreate_test.go24
-rw-r--r--internal/api/client/media/mediaupdate_test.go16
-rw-r--r--internal/api/client/polls/polls_test.go102
-rw-r--r--internal/api/client/polls/polls_vote.go57
-rw-r--r--internal/api/client/polls/polls_vote_test.go189
-rw-r--r--internal/api/client/statuses/statuscreate.go62
-rw-r--r--internal/api/model/poll.go15
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"`
}