From c7ecab9e6fb76bb10da26c803fc5838419642423 Mon Sep 17 00:00:00 2001 From: tobi <31960611+tsmethurst@users.noreply.github.com> Date: Fri, 10 Nov 2023 17:42:48 +0100 Subject: [PATCH] [chore/bugfix/horror] Allow `expires_in` and poll choices to be parsed from strings (#2346) --- internal/api/auth/token_test.go | 44 ++-- .../api/client/accounts/accountdelete_test.go | 10 +- .../api/client/accounts/accountupdate_test.go | 114 ++++++----- internal/api/client/admin/emojicreate_test.go | 22 +- internal/api/client/admin/emojiupdate_test.go | 58 +++--- .../api/client/instance/instancepatch_test.go | 46 ++--- internal/api/client/media/mediacreate_test.go | 24 +-- internal/api/client/media/mediaupdate_test.go | 16 +- internal/api/client/polls/polls_test.go | 102 ++++++++++ internal/api/client/polls/polls_vote.go | 57 +++++- internal/api/client/polls/polls_vote_test.go | 189 ++++++++++++++++++ internal/api/client/statuses/statuscreate.go | 62 ++++-- internal/api/model/poll.go | 15 +- testrig/util.go | 14 +- 14 files changed, 579 insertions(+), 194 deletions(-) create mode 100644 internal/api/client/polls/polls_test.go create mode 100644 internal/api/client/polls/polls_vote_test.go 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": "

Geoff's Instance

", + code, b := suite.instancePatch("", "", map[string][]string{ + "title": {"

Geoff's Instance

"}, }) 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": "

This is some html, which is allowed in short descriptions.

", + code, b := suite.instancePatch("", "", map[string][]string{ + "short_description": {"

This is some html, which is allowed in short descriptions.

"}, }) 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": "

This is some html, which is allowed in short descriptions.

", + map[string][]string{ + "short_description": {"

This is some html, which is allowed in short descriptions.

"}, }) 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 . + +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 . + +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"` } diff --git a/testrig/util.go b/testrig/util.go index 8ffc1b8ea..c2360dc9e 100644 --- a/testrig/util.go +++ b/testrig/util.go @@ -85,7 +85,7 @@ func StartTimelines(state *state.State, filter *visibility.Filter, converter *ty // The returned *multipart.Writer w can be used to set the content type of the request, like so: // // req.Header.Set("Content-Type", w.FormDataContentType()) -func CreateMultipartFormData(fieldName string, fileName string, extraFields map[string]string) (bytes.Buffer, *multipart.Writer, error) { +func CreateMultipartFormData(fieldName string, fileName string, extraFields map[string][]string) (bytes.Buffer, *multipart.Writer, error) { var b bytes.Buffer w := multipart.NewWriter(&b) @@ -104,13 +104,11 @@ func CreateMultipartFormData(fieldName string, fileName string, extraFields map[ } } - for k, v := range extraFields { - f, err := w.CreateFormField(k) - if err != nil { - return b, nil, err - } - if _, err := io.Copy(f, bytes.NewBufferString(v)); err != nil { - return b, nil, err + for k, vs := range extraFields { + for _, v := range vs { + if err := w.WriteField(k, v); err != nil { + return b, nil, err + } } }