gtsocial-umbx

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

accountupdate_test.go (15668B)


      1 // GoToSocial
      2 // Copyright (C) GoToSocial Authors admin@gotosocial.org
      3 // SPDX-License-Identifier: AGPL-3.0-or-later
      4 //
      5 // This program is free software: you can redistribute it and/or modify
      6 // it under the terms of the GNU Affero General Public License as published by
      7 // the Free Software Foundation, either version 3 of the License, or
      8 // (at your option) any later version.
      9 //
     10 // This program is distributed in the hope that it will be useful,
     11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
     12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     13 // GNU Affero General Public License for more details.
     14 //
     15 // You should have received a copy of the GNU Affero General Public License
     16 // along with this program.  If not, see <http://www.gnu.org/licenses/>.
     17 
     18 package accounts_test
     19 
     20 import (
     21 	"context"
     22 	"encoding/json"
     23 	"fmt"
     24 	"io"
     25 	"net/http"
     26 	"net/http/httptest"
     27 	"net/url"
     28 	"testing"
     29 
     30 	"github.com/stretchr/testify/suite"
     31 	"github.com/superseriousbusiness/gotosocial/internal/api/client/accounts"
     32 	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
     33 	"github.com/superseriousbusiness/gotosocial/internal/gtserror"
     34 	"github.com/superseriousbusiness/gotosocial/testrig"
     35 )
     36 
     37 type AccountUpdateTestSuite struct {
     38 	AccountStandardTestSuite
     39 }
     40 
     41 func (suite *AccountUpdateTestSuite) updateAccountFromForm(data map[string]string, expectedHTTPStatus int, expectedBody string) (*apimodel.Account, error) {
     42 	form := url.Values{}
     43 	for key, val := range data {
     44 		form[key] = []string{val}
     45 	}
     46 	return suite.updateAccount([]byte(form.Encode()), "application/x-www-form-urlencoded", expectedHTTPStatus, expectedBody)
     47 }
     48 
     49 func (suite *AccountUpdateTestSuite) updateAccountFromFormData(data map[string]string, expectedHTTPStatus int, expectedBody string) (*apimodel.Account, error) {
     50 	requestBody, w, err := testrig.CreateMultipartFormData("", "", data)
     51 	if err != nil {
     52 		suite.FailNow(err.Error())
     53 	}
     54 
     55 	return suite.updateAccount(requestBody.Bytes(), w.FormDataContentType(), expectedHTTPStatus, expectedBody)
     56 }
     57 
     58 func (suite *AccountUpdateTestSuite) updateAccountFromFormDataWithFile(fieldName string, fileName string, data map[string]string, expectedHTTPStatus int, expectedBody string) (*apimodel.Account, error) {
     59 	requestBody, w, err := testrig.CreateMultipartFormData(fieldName, fileName, data)
     60 	if err != nil {
     61 		suite.FailNow(err.Error())
     62 	}
     63 
     64 	return suite.updateAccount(requestBody.Bytes(), w.FormDataContentType(), expectedHTTPStatus, expectedBody)
     65 }
     66 
     67 func (suite *AccountUpdateTestSuite) updateAccountFromJSON(data string, expectedHTTPStatus int, expectedBody string) (*apimodel.Account, error) {
     68 	return suite.updateAccount([]byte(data), "application/json", expectedHTTPStatus, expectedBody)
     69 }
     70 
     71 func (suite *AccountUpdateTestSuite) updateAccount(
     72 	bodyBytes []byte,
     73 	contentType string,
     74 	expectedHTTPStatus int,
     75 	expectedBody string,
     76 ) (*apimodel.Account, error) {
     77 	// Initialize http test context.
     78 	recorder := httptest.NewRecorder()
     79 	ctx := suite.newContext(recorder, http.MethodPatch, bodyBytes, accounts.UpdatePath, contentType)
     80 
     81 	// Trigger the handler.
     82 	suite.accountsModule.AccountUpdateCredentialsPATCHHandler(ctx)
     83 
     84 	// Read the result.
     85 	result := recorder.Result()
     86 	defer result.Body.Close()
     87 
     88 	b, err := io.ReadAll(result.Body)
     89 	if err != nil {
     90 		return nil, err
     91 	}
     92 
     93 	errs := gtserror.MultiError{}
     94 
     95 	// Check expected code + body.
     96 	if resultCode := recorder.Code; expectedHTTPStatus != resultCode {
     97 		errs = append(errs, fmt.Sprintf("expected %d got %d", expectedHTTPStatus, resultCode))
     98 	}
     99 
    100 	// If we got an expected body, return early.
    101 	if expectedBody != "" && string(b) != expectedBody {
    102 		errs = append(errs, fmt.Sprintf("expected %s got %s", expectedBody, string(b)))
    103 	}
    104 
    105 	if err := errs.Combine(); err != nil {
    106 		return nil, fmt.Errorf("%v (body %s)", err, string(b))
    107 	}
    108 
    109 	// Return account response.
    110 	resp := &apimodel.Account{}
    111 	if err := json.Unmarshal(b, resp); err != nil {
    112 		return nil, err
    113 	}
    114 
    115 	return resp, nil
    116 }
    117 
    118 func (suite *AccountUpdateTestSuite) TestUpdateAccountBasicForm() {
    119 	data := map[string]string{
    120 		"note":                        "this is my new bio read it and weep",
    121 		"fields_attributes[0][name]":  "pronouns",
    122 		"fields_attributes[0][value]": "they/them",
    123 		"fields_attributes[1][name]":  "Website",
    124 		"fields_attributes[1][value]": "https://example.com",
    125 	}
    126 
    127 	apimodelAccount, err := suite.updateAccountFromForm(data, http.StatusOK, "")
    128 	if err != nil {
    129 		suite.FailNow(err.Error())
    130 	}
    131 
    132 	suite.Equal("<p>this is my new bio read it and weep</p>", apimodelAccount.Note)
    133 	suite.Equal("this is my new bio read it and weep", apimodelAccount.Source.Note)
    134 
    135 	if l := len(apimodelAccount.Fields); l != 2 {
    136 		suite.FailNow("", "expected %d fields, got %d", 2, l)
    137 	}
    138 	suite.Equal(`pronouns`, apimodelAccount.Fields[0].Name)
    139 	suite.Equal(`they/them`, apimodelAccount.Fields[0].Value)
    140 	suite.Equal(`Website`, apimodelAccount.Fields[1].Name)
    141 	suite.Equal(`<a href="https://example.com" rel="nofollow noreferrer noopener" target="_blank">https://example.com</a>`, apimodelAccount.Fields[1].Value)
    142 }
    143 
    144 func (suite *AccountUpdateTestSuite) TestUpdateAccountBasicFormData() {
    145 	data := map[string]string{
    146 		"note":                        "this is my new bio read it and weep",
    147 		"fields_attributes[0][name]":  "pronouns",
    148 		"fields_attributes[0][value]": "they/them",
    149 		"fields_attributes[1][name]":  "Website",
    150 		"fields_attributes[1][value]": "https://example.com",
    151 	}
    152 
    153 	apimodelAccount, err := suite.updateAccountFromFormData(data, http.StatusOK, "")
    154 	if err != nil {
    155 		suite.FailNow(err.Error())
    156 	}
    157 
    158 	suite.Equal("<p>this is my new bio read it and weep</p>", apimodelAccount.Note)
    159 	suite.Equal("this is my new bio read it and weep", apimodelAccount.Source.Note)
    160 
    161 	if l := len(apimodelAccount.Fields); l != 2 {
    162 		suite.FailNow("", "expected %d fields, got %d", 2, l)
    163 	}
    164 	suite.Equal(`pronouns`, apimodelAccount.Fields[0].Name)
    165 	suite.Equal(`they/them`, apimodelAccount.Fields[0].Value)
    166 	suite.Equal(`Website`, apimodelAccount.Fields[1].Name)
    167 	suite.Equal(`<a href="https://example.com" rel="nofollow noreferrer noopener" target="_blank">https://example.com</a>`, apimodelAccount.Fields[1].Value)
    168 }
    169 
    170 func (suite *AccountUpdateTestSuite) TestUpdateAccountBasicJSON() {
    171 	data := `
    172 {
    173   "note": "this is my new bio read it and weep",
    174   "fields_attributes": {
    175     "0": {
    176       "name": "pronouns",
    177       "value": "they/them"
    178     },
    179     "1": {
    180       "name": "Website",
    181       "value": "https://example.com"
    182     }
    183   }
    184 }
    185 `
    186 
    187 	apimodelAccount, err := suite.updateAccountFromJSON(data, http.StatusOK, "")
    188 	if err != nil {
    189 		suite.FailNow(err.Error())
    190 	}
    191 
    192 	suite.Equal("<p>this is my new bio read it and weep</p>", apimodelAccount.Note)
    193 	suite.Equal("this is my new bio read it and weep", apimodelAccount.Source.Note)
    194 
    195 	if l := len(apimodelAccount.Fields); l != 2 {
    196 		suite.FailNow("", "expected %d fields, got %d", 2, l)
    197 	}
    198 	suite.Equal(`pronouns`, apimodelAccount.Fields[0].Name)
    199 	suite.Equal(`they/them`, apimodelAccount.Fields[0].Value)
    200 	suite.Equal(`Website`, apimodelAccount.Fields[1].Name)
    201 	suite.Equal(`<a href="https://example.com" rel="nofollow noreferrer noopener" target="_blank">https://example.com</a>`, apimodelAccount.Fields[1].Value)
    202 }
    203 
    204 func (suite *AccountUpdateTestSuite) TestUpdateAccountLockForm() {
    205 	data := map[string]string{
    206 		"locked": "true",
    207 	}
    208 
    209 	apimodelAccount, err := suite.updateAccountFromForm(data, http.StatusOK, "")
    210 	if err != nil {
    211 		suite.FailNow(err.Error())
    212 	}
    213 
    214 	suite.True(apimodelAccount.Locked)
    215 }
    216 
    217 func (suite *AccountUpdateTestSuite) TestUpdateAccountLockFormData() {
    218 	data := map[string]string{
    219 		"locked": "true",
    220 	}
    221 
    222 	apimodelAccount, err := suite.updateAccountFromFormData(data, http.StatusOK, "")
    223 	if err != nil {
    224 		suite.FailNow(err.Error())
    225 	}
    226 
    227 	suite.True(apimodelAccount.Locked)
    228 }
    229 
    230 func (suite *AccountUpdateTestSuite) TestUpdateAccountLockJSON() {
    231 	data := `
    232 {
    233   "locked": true
    234 }`
    235 
    236 	apimodelAccount, err := suite.updateAccountFromJSON(data, http.StatusOK, "")
    237 	if err != nil {
    238 		suite.FailNow(err.Error())
    239 	}
    240 
    241 	suite.True(apimodelAccount.Locked)
    242 }
    243 
    244 func (suite *AccountUpdateTestSuite) TestUpdateAccountUnlockForm() {
    245 	data := map[string]string{
    246 		"locked": "false",
    247 	}
    248 
    249 	apimodelAccount, err := suite.updateAccountFromForm(data, http.StatusOK, "")
    250 	if err != nil {
    251 		suite.FailNow(err.Error())
    252 	}
    253 
    254 	suite.False(apimodelAccount.Locked)
    255 }
    256 
    257 func (suite *AccountUpdateTestSuite) TestUpdateAccountUnlockFormData() {
    258 	data := map[string]string{
    259 		"locked": "false",
    260 	}
    261 
    262 	apimodelAccount, err := suite.updateAccountFromFormData(data, http.StatusOK, "")
    263 	if err != nil {
    264 		suite.FailNow(err.Error())
    265 	}
    266 
    267 	suite.False(apimodelAccount.Locked)
    268 }
    269 
    270 func (suite *AccountUpdateTestSuite) TestUpdateAccountUnlockJSON() {
    271 	data := `
    272 {
    273   "locked": false
    274 }`
    275 
    276 	apimodelAccount, err := suite.updateAccountFromJSON(data, http.StatusOK, "")
    277 	if err != nil {
    278 		suite.FailNow(err.Error())
    279 	}
    280 
    281 	suite.False(apimodelAccount.Locked)
    282 }
    283 
    284 func (suite *AccountUpdateTestSuite) TestUpdateAccountCache() {
    285 	// Get the account first to make sure it's in the database
    286 	// cache. When the account is updated via the PATCH handler,
    287 	// it should invalidate the cache and return the new version.
    288 	if _, err := suite.db.GetAccountByID(context.Background(), suite.testAccounts["local_account_1"].ID); err != nil {
    289 		suite.FailNow(err.Error())
    290 	}
    291 
    292 	data := map[string]string{
    293 		"note": "this is my new bio read it and weep",
    294 	}
    295 
    296 	apimodelAccount, err := suite.updateAccountFromFormData(data, http.StatusOK, "")
    297 	if err != nil {
    298 		suite.FailNow(err.Error())
    299 	}
    300 
    301 	suite.Equal("<p>this is my new bio read it and weep</p>", apimodelAccount.Note)
    302 }
    303 
    304 func (suite *AccountUpdateTestSuite) TestUpdateAccountDiscoverableForm() {
    305 	data := map[string]string{
    306 		"discoverable": "false",
    307 	}
    308 
    309 	apimodelAccount, err := suite.updateAccountFromForm(data, http.StatusOK, "")
    310 	if err != nil {
    311 		suite.FailNow(err.Error())
    312 	}
    313 
    314 	suite.False(apimodelAccount.Discoverable)
    315 
    316 	// Check the account in the database too.
    317 	dbZork, err := suite.db.GetAccountByID(context.Background(), apimodelAccount.ID)
    318 	suite.NoError(err)
    319 	suite.False(*dbZork.Discoverable)
    320 }
    321 
    322 func (suite *AccountUpdateTestSuite) TestUpdateAccountDiscoverableFormData() {
    323 	data := map[string]string{
    324 		"discoverable": "false",
    325 	}
    326 
    327 	apimodelAccount, err := suite.updateAccountFromFormData(data, http.StatusOK, "")
    328 	if err != nil {
    329 		suite.FailNow(err.Error())
    330 	}
    331 
    332 	suite.False(apimodelAccount.Discoverable)
    333 
    334 	// Check the account in the database too.
    335 	dbZork, err := suite.db.GetAccountByID(context.Background(), apimodelAccount.ID)
    336 	suite.NoError(err)
    337 	suite.False(*dbZork.Discoverable)
    338 }
    339 
    340 func (suite *AccountUpdateTestSuite) TestUpdateAccountDiscoverableJSON() {
    341 	data := `
    342 {
    343   "discoverable": false
    344 }`
    345 
    346 	apimodelAccount, err := suite.updateAccountFromJSON(data, http.StatusOK, "")
    347 	if err != nil {
    348 		suite.FailNow(err.Error())
    349 	}
    350 
    351 	suite.False(apimodelAccount.Discoverable)
    352 
    353 	// Check the account in the database too.
    354 	dbZork, err := suite.db.GetAccountByID(context.Background(), apimodelAccount.ID)
    355 	suite.NoError(err)
    356 	suite.False(*dbZork.Discoverable)
    357 }
    358 
    359 func (suite *AccountUpdateTestSuite) TestUpdateAccountWithImageFormData() {
    360 	data := map[string]string{
    361 		"display_name": "updated zork display name!!!",
    362 		"note":         "",
    363 		"locked":       "true",
    364 	}
    365 
    366 	apimodelAccount, err := suite.updateAccountFromFormDataWithFile("header", "../../../../testrig/media/test-jpeg.jpg", data, http.StatusOK, "")
    367 	if err != nil {
    368 		suite.FailNow(err.Error())
    369 	}
    370 
    371 	suite.Equal(data["display_name"], apimodelAccount.DisplayName)
    372 	suite.True(apimodelAccount.Locked)
    373 	suite.Empty(apimodelAccount.Note)
    374 	suite.Empty(apimodelAccount.Source.Note)
    375 	suite.NotEmpty(apimodelAccount.Header)
    376 	suite.NotEmpty(apimodelAccount.HeaderStatic)
    377 
    378 	// Can't predict IDs generated for new media
    379 	// so just ensure it's different than before.
    380 	suite.NotEqual("http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/original/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg", apimodelAccount.Header)
    381 	suite.NotEqual("http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg", apimodelAccount.HeaderStatic)
    382 }
    383 
    384 func (suite *AccountUpdateTestSuite) TestUpdateAccountEmptyForm() {
    385 	data := make(map[string]string)
    386 
    387 	_, err := suite.updateAccountFromForm(data, http.StatusBadRequest, `{"error":"Bad Request: empty form submitted"}`)
    388 	if err != nil {
    389 		suite.FailNow(err.Error())
    390 	}
    391 }
    392 
    393 func (suite *AccountUpdateTestSuite) TestUpdateAccountEmptyFormData() {
    394 	data := make(map[string]string)
    395 
    396 	_, err := suite.updateAccountFromFormData(data, http.StatusBadRequest, `{"error":"Bad Request: empty form submitted"}`)
    397 	if err != nil {
    398 		suite.FailNow(err.Error())
    399 	}
    400 }
    401 
    402 func (suite *AccountUpdateTestSuite) TestUpdateAccountSourceForm() {
    403 	data := map[string]string{
    404 		"source[privacy]":   string(apimodel.VisibilityPrivate),
    405 		"source[language]":  "de",
    406 		"source[sensitive]": "true",
    407 		"locked":            "true",
    408 	}
    409 
    410 	apimodelAccount, err := suite.updateAccountFromForm(data, http.StatusOK, "")
    411 	if err != nil {
    412 		suite.FailNow(err.Error())
    413 	}
    414 
    415 	suite.Equal(data["source[language]"], apimodelAccount.Source.Language)
    416 	suite.EqualValues(apimodel.VisibilityPrivate, apimodelAccount.Source.Privacy)
    417 	suite.True(apimodelAccount.Source.Sensitive)
    418 	suite.True(apimodelAccount.Locked)
    419 }
    420 
    421 func (suite *AccountUpdateTestSuite) TestUpdateAccountSourceFormData() {
    422 	data := map[string]string{
    423 		"source[privacy]":   string(apimodel.VisibilityPrivate),
    424 		"source[language]":  "de",
    425 		"source[sensitive]": "true",
    426 		"locked":            "true",
    427 	}
    428 
    429 	apimodelAccount, err := suite.updateAccountFromFormData(data, http.StatusOK, "")
    430 	if err != nil {
    431 		suite.FailNow(err.Error())
    432 	}
    433 
    434 	suite.Equal(data["source[language]"], apimodelAccount.Source.Language)
    435 	suite.EqualValues(apimodel.VisibilityPrivate, apimodelAccount.Source.Privacy)
    436 	suite.True(apimodelAccount.Source.Sensitive)
    437 	suite.True(apimodelAccount.Locked)
    438 }
    439 
    440 func (suite *AccountUpdateTestSuite) TestUpdateAccountSourceJSON() {
    441 	data := `
    442 {
    443   "source": {
    444     "privacy": "private",
    445     "language": "de",
    446     "sensitive": true
    447   },
    448   "locked": true
    449 }
    450 `
    451 
    452 	apimodelAccount, err := suite.updateAccountFromJSON(data, http.StatusOK, "")
    453 	if err != nil {
    454 		suite.FailNow(err.Error())
    455 	}
    456 
    457 	suite.Equal("de", apimodelAccount.Source.Language)
    458 	suite.EqualValues(apimodel.VisibilityPrivate, apimodelAccount.Source.Privacy)
    459 	suite.True(apimodelAccount.Source.Sensitive)
    460 	suite.True(apimodelAccount.Locked)
    461 }
    462 
    463 func (suite *AccountUpdateTestSuite) TestUpdateAccountSourceBadContentTypeFormData() {
    464 	data := map[string]string{
    465 		"source[status_content_type]": "text/markdown",
    466 	}
    467 
    468 	apimodelAccount, err := suite.updateAccountFromFormData(data, http.StatusOK, "")
    469 	if err != nil {
    470 		suite.FailNow(err.Error())
    471 	}
    472 
    473 	suite.Equal(data["source[status_content_type]"], apimodelAccount.Source.StatusContentType)
    474 
    475 	// Check the account in the database too.
    476 	dbAccount, err := suite.db.GetAccountByID(context.Background(), suite.testAccounts["local_account_1"].ID)
    477 	if err != nil {
    478 		suite.FailNow(err.Error())
    479 	}
    480 	suite.Equal(data["source[status_content_type]"], dbAccount.StatusContentType)
    481 }
    482 
    483 func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandlerUpdateStatusContentTypeBad() {
    484 	data := map[string]string{
    485 		"source[status_content_type]": "peepeepoopoo",
    486 	}
    487 
    488 	_, err := suite.updateAccountFromFormData(data, http.StatusBadRequest, `{"error":"Bad Request: status content type 'peepeepoopoo' was not recognized, valid options are 'text/plain', 'text/markdown'"}`)
    489 	if err != nil {
    490 		suite.FailNow(err.Error())
    491 	}
    492 }
    493 
    494 func TestAccountUpdateTestSuite(t *testing.T) {
    495 	suite.Run(t, new(AccountUpdateTestSuite))
    496 }