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 }