inboxpost_test.go (19562B)
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 users_test 19 20 import ( 21 "bytes" 22 "context" 23 "encoding/json" 24 "errors" 25 "fmt" 26 "io" 27 "net/http" 28 "net/http/httptest" 29 "testing" 30 "time" 31 32 "github.com/gin-gonic/gin" 33 "github.com/stretchr/testify/suite" 34 "github.com/superseriousbusiness/activity/pub" 35 "github.com/superseriousbusiness/activity/streams" 36 "github.com/superseriousbusiness/activity/streams/vocab" 37 "github.com/superseriousbusiness/gotosocial/internal/ap" 38 "github.com/superseriousbusiness/gotosocial/internal/api/activitypub/users" 39 "github.com/superseriousbusiness/gotosocial/internal/db" 40 "github.com/superseriousbusiness/gotosocial/internal/gtserror" 41 "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" 42 "github.com/superseriousbusiness/gotosocial/internal/id" 43 "github.com/superseriousbusiness/gotosocial/testrig" 44 ) 45 46 type InboxPostTestSuite struct { 47 UserStandardTestSuite 48 } 49 50 func (suite *InboxPostTestSuite) inboxPost( 51 activity pub.Activity, 52 requestingAccount *gtsmodel.Account, 53 targetAccount *gtsmodel.Account, 54 expectedHTTPStatus int, 55 expectedBody string, 56 middlewares ...func(*gin.Context), 57 ) { 58 var ( 59 recorder = httptest.NewRecorder() 60 ctx, _ = testrig.CreateGinTestContext(recorder, nil) 61 ) 62 63 // Prepare the requst body bytes. 64 bodyI, err := ap.Serialize(activity) 65 if err != nil { 66 suite.FailNow(err.Error()) 67 } 68 69 b, err := json.MarshalIndent(bodyI, "", " ") 70 if err != nil { 71 suite.FailNow(err.Error()) 72 } 73 suite.T().Logf("prepared POST body:\n%s", string(b)) 74 75 // Prepare signature headers for this Activity. 76 signature, digestHeader, dateHeader := testrig.GetSignatureForActivity( 77 activity, 78 requestingAccount.PublicKeyURI, 79 requestingAccount.PrivateKey, 80 testrig.URLMustParse(targetAccount.InboxURI), 81 ) 82 83 // Put the request together. 84 ctx.AddParam(users.UsernameKey, targetAccount.Username) 85 ctx.Request = httptest.NewRequest(http.MethodPost, targetAccount.InboxURI, bytes.NewReader(b)) 86 ctx.Request.Header.Set("Signature", signature) 87 ctx.Request.Header.Set("Date", dateHeader) 88 ctx.Request.Header.Set("Digest", digestHeader) 89 ctx.Request.Header.Set("Content-Type", "application/activity+json") 90 91 // Pass the context through provided middlewares. 92 for _, middleware := range middlewares { 93 middleware(ctx) 94 } 95 96 // Trigger the function being tested. 97 suite.userModule.InboxPOSTHandler(ctx) 98 99 // Read the result. 100 result := recorder.Result() 101 defer result.Body.Close() 102 103 b, err = io.ReadAll(result.Body) 104 if err != nil { 105 suite.FailNow(err.Error()) 106 } 107 108 errs := gtserror.MultiError{} 109 110 // Check expected code + body. 111 if resultCode := recorder.Code; expectedHTTPStatus != resultCode { 112 errs = append(errs, fmt.Sprintf("expected %d got %d", expectedHTTPStatus, resultCode)) 113 } 114 115 // If we got an expected body, return early. 116 if expectedBody != "" && string(b) != expectedBody { 117 errs = append(errs, fmt.Sprintf("expected %s got %s", expectedBody, string(b))) 118 } 119 120 if err := errs.Combine(); err != nil { 121 suite.FailNow("", "%v (body %s)", err, string(b)) 122 } 123 } 124 125 func (suite *InboxPostTestSuite) newBlock(blockID string, blockingAccount *gtsmodel.Account, blockedAccount *gtsmodel.Account) vocab.ActivityStreamsBlock { 126 block := streams.NewActivityStreamsBlock() 127 128 // set the actor property to the block-ing account's URI 129 actorProp := streams.NewActivityStreamsActorProperty() 130 actorIRI := testrig.URLMustParse(blockingAccount.URI) 131 actorProp.AppendIRI(actorIRI) 132 block.SetActivityStreamsActor(actorProp) 133 134 // set the ID property to the blocks's URI 135 idProp := streams.NewJSONLDIdProperty() 136 idProp.Set(testrig.URLMustParse(blockID)) 137 block.SetJSONLDId(idProp) 138 139 // set the object property to the target account's URI 140 objectProp := streams.NewActivityStreamsObjectProperty() 141 targetIRI := testrig.URLMustParse(blockedAccount.URI) 142 objectProp.AppendIRI(targetIRI) 143 block.SetActivityStreamsObject(objectProp) 144 145 // set the TO property to the target account's IRI 146 toProp := streams.NewActivityStreamsToProperty() 147 toIRI := testrig.URLMustParse(blockedAccount.URI) 148 toProp.AppendIRI(toIRI) 149 block.SetActivityStreamsTo(toProp) 150 151 return block 152 } 153 154 func (suite *InboxPostTestSuite) newUndo( 155 originalActivity pub.Activity, 156 objectF func() vocab.ActivityStreamsObjectProperty, 157 to string, 158 undoIRI string, 159 ) vocab.ActivityStreamsUndo { 160 undo := streams.NewActivityStreamsUndo() 161 162 // Set the appropriate actor. 163 undo.SetActivityStreamsActor(originalActivity.GetActivityStreamsActor()) 164 165 // Set the original activity uri as the 'object' property. 166 undo.SetActivityStreamsObject(objectF()) 167 168 // Set the To of the undo as the target of the activity. 169 undoTo := streams.NewActivityStreamsToProperty() 170 undoTo.AppendIRI(testrig.URLMustParse(to)) 171 undo.SetActivityStreamsTo(undoTo) 172 173 // Set the ID property to the undo's URI. 174 undoID := streams.NewJSONLDIdProperty() 175 undoID.SetIRI(testrig.URLMustParse(undoIRI)) 176 undo.SetJSONLDId(undoID) 177 178 return undo 179 } 180 181 func (suite *InboxPostTestSuite) newUpdatePerson(person vocab.ActivityStreamsPerson, cc string, updateIRI string) vocab.ActivityStreamsUpdate { 182 // create an update 183 update := streams.NewActivityStreamsUpdate() 184 185 // set the appropriate actor on it 186 updateActor := streams.NewActivityStreamsActorProperty() 187 updateActor.AppendIRI(person.GetJSONLDId().Get()) 188 update.SetActivityStreamsActor(updateActor) 189 190 // Set the person as the 'object' property. 191 updateObject := streams.NewActivityStreamsObjectProperty() 192 updateObject.AppendActivityStreamsPerson(person) 193 update.SetActivityStreamsObject(updateObject) 194 195 // Set the To of the update as public 196 updateTo := streams.NewActivityStreamsToProperty() 197 updateTo.AppendIRI(testrig.URLMustParse(pub.PublicActivityPubIRI)) 198 update.SetActivityStreamsTo(updateTo) 199 200 // set the cc of the update to the receivingAccount 201 updateCC := streams.NewActivityStreamsCcProperty() 202 updateCC.AppendIRI(testrig.URLMustParse(cc)) 203 update.SetActivityStreamsCc(updateCC) 204 205 // set some random-ass ID for the activity 206 updateID := streams.NewJSONLDIdProperty() 207 updateID.SetIRI(testrig.URLMustParse(updateIRI)) 208 update.SetJSONLDId(updateID) 209 210 return update 211 } 212 213 func (suite *InboxPostTestSuite) newDelete(actorIRI string, objectIRI string, deleteIRI string) vocab.ActivityStreamsDelete { 214 // create a delete 215 delete := streams.NewActivityStreamsDelete() 216 217 // set the appropriate actor on it 218 deleteActor := streams.NewActivityStreamsActorProperty() 219 deleteActor.AppendIRI(testrig.URLMustParse(actorIRI)) 220 delete.SetActivityStreamsActor(deleteActor) 221 222 // Set 'object' property. 223 deleteObject := streams.NewActivityStreamsObjectProperty() 224 deleteObject.AppendIRI(testrig.URLMustParse(objectIRI)) 225 delete.SetActivityStreamsObject(deleteObject) 226 227 // Set the To of the delete as public 228 deleteTo := streams.NewActivityStreamsToProperty() 229 deleteTo.AppendIRI(testrig.URLMustParse(pub.PublicActivityPubIRI)) 230 delete.SetActivityStreamsTo(deleteTo) 231 232 // set some random-ass ID for the activity 233 deleteID := streams.NewJSONLDIdProperty() 234 deleteID.SetIRI(testrig.URLMustParse(deleteIRI)) 235 delete.SetJSONLDId(deleteID) 236 237 return delete 238 } 239 240 // TestPostBlock verifies that a remote account can block one of 241 // our instance users. 242 func (suite *InboxPostTestSuite) TestPostBlock() { 243 var ( 244 requestingAccount = suite.testAccounts["remote_account_1"] 245 targetAccount = suite.testAccounts["local_account_1"] 246 activityID = requestingAccount.URI + "/some-new-activity/01FG9C441MCTW3R2W117V2PQK3" 247 ) 248 249 block := suite.newBlock(activityID, requestingAccount, targetAccount) 250 251 // Block. 252 suite.inboxPost( 253 block, 254 requestingAccount, 255 targetAccount, 256 http.StatusAccepted, 257 `{"status":"Accepted"}`, 258 suite.signatureCheck, 259 ) 260 261 // Ensure block created in the database. 262 var ( 263 dbBlock *gtsmodel.Block 264 err error 265 ) 266 267 if !testrig.WaitFor(func() bool { 268 dbBlock, err = suite.db.GetBlock(context.Background(), requestingAccount.ID, targetAccount.ID) 269 return err == nil && dbBlock != nil 270 }) { 271 suite.FailNow("timed out waiting for block to be created") 272 } 273 } 274 275 // TestPostUnblock verifies that a remote account who blocks 276 // one of our instance users should be able to undo that block. 277 func (suite *InboxPostTestSuite) TestPostUnblock() { 278 var ( 279 ctx = context.Background() 280 requestingAccount = suite.testAccounts["remote_account_1"] 281 targetAccount = suite.testAccounts["local_account_1"] 282 blockID = "http://fossbros-anonymous.io/blocks/01H1462TPRTVG2RTQCTSQ7N6Q0" 283 undoID = "http://fossbros-anonymous.io/some-activity/01H1463RDQNG5H98F29BXYHW6B" 284 ) 285 286 // Put a block in the database so we have something to undo. 287 block := >smodel.Block{ 288 ID: id.NewULID(), 289 URI: blockID, 290 AccountID: requestingAccount.ID, 291 TargetAccountID: targetAccount.ID, 292 } 293 if err := suite.db.PutBlock(ctx, block); err != nil { 294 suite.FailNow(err.Error()) 295 } 296 297 // Create the undo from the AS model block. 298 asBlock, err := suite.tc.BlockToAS(ctx, block) 299 if err != nil { 300 suite.FailNow(err.Error()) 301 } 302 303 undo := suite.newUndo(asBlock, func() vocab.ActivityStreamsObjectProperty { 304 // Append the whole block as Object. 305 op := streams.NewActivityStreamsObjectProperty() 306 op.AppendActivityStreamsBlock(asBlock) 307 return op 308 }, targetAccount.URI, undoID) 309 310 // Undo. 311 suite.inboxPost( 312 undo, 313 requestingAccount, 314 targetAccount, 315 http.StatusAccepted, 316 `{"status":"Accepted"}`, 317 suite.signatureCheck, 318 ) 319 320 // Ensure block removed from the database. 321 if !testrig.WaitFor(func() bool { 322 _, err := suite.db.GetBlockByID(ctx, block.ID) 323 return errors.Is(err, db.ErrNoEntries) 324 }) { 325 suite.FailNow("timed out waiting for block to be removed") 326 } 327 } 328 329 func (suite *InboxPostTestSuite) TestPostUpdate() { 330 var ( 331 requestingAccount = new(gtsmodel.Account) 332 targetAccount = suite.testAccounts["local_account_1"] 333 activityID = "http://fossbros-anonymous.io/72cc96a3-f742-4daf-b9f5-3407667260c5" 334 updatedDisplayName = "updated display name!" 335 ) 336 337 // Copy the requesting account, since we'll be changing it. 338 *requestingAccount = *suite.testAccounts["remote_account_1"] 339 340 // Update the account's display name. 341 requestingAccount.DisplayName = updatedDisplayName 342 343 // Add an emoji to the account; because we're serializing this 344 // remote account from our own instance, we need to cheat a bit 345 // to get the emoji to work properly, just for this test. 346 testEmoji := >smodel.Emoji{} 347 *testEmoji = *testrig.NewTestEmojis()["yell"] 348 testEmoji.ImageURL = testEmoji.ImageRemoteURL // <- here's the cheat 349 requestingAccount.Emojis = []*gtsmodel.Emoji{testEmoji} 350 351 // Create an update from the account. 352 asAccount, err := suite.tc.AccountToAS(context.Background(), requestingAccount) 353 if err != nil { 354 suite.FailNow(err.Error()) 355 } 356 update := suite.newUpdatePerson(asAccount, targetAccount.URI, activityID) 357 358 // Update. 359 suite.inboxPost( 360 update, 361 requestingAccount, 362 targetAccount, 363 http.StatusAccepted, 364 `{"status":"Accepted"}`, 365 suite.signatureCheck, 366 ) 367 368 // account should be changed in the database now 369 var dbUpdatedAccount *gtsmodel.Account 370 371 if !testrig.WaitFor(func() bool { 372 // displayName should be updated 373 dbUpdatedAccount, _ = suite.db.GetAccountByID(context.Background(), requestingAccount.ID) 374 return dbUpdatedAccount.DisplayName == updatedDisplayName 375 }) { 376 suite.FailNow("timed out waiting for account update") 377 } 378 379 // emojis should be updated 380 suite.Contains(dbUpdatedAccount.EmojiIDs, testEmoji.ID) 381 382 // account should be freshly fetched 383 suite.WithinDuration(time.Now(), dbUpdatedAccount.FetchedAt, 10*time.Second) 384 385 // everything else should be the same as it was before 386 suite.EqualValues(requestingAccount.Username, dbUpdatedAccount.Username) 387 suite.EqualValues(requestingAccount.Domain, dbUpdatedAccount.Domain) 388 suite.EqualValues(requestingAccount.AvatarMediaAttachmentID, dbUpdatedAccount.AvatarMediaAttachmentID) 389 suite.EqualValues(requestingAccount.AvatarMediaAttachment, dbUpdatedAccount.AvatarMediaAttachment) 390 suite.EqualValues(requestingAccount.AvatarRemoteURL, dbUpdatedAccount.AvatarRemoteURL) 391 suite.EqualValues(requestingAccount.HeaderMediaAttachmentID, dbUpdatedAccount.HeaderMediaAttachmentID) 392 suite.EqualValues(requestingAccount.HeaderMediaAttachment, dbUpdatedAccount.HeaderMediaAttachment) 393 suite.EqualValues(requestingAccount.HeaderRemoteURL, dbUpdatedAccount.HeaderRemoteURL) 394 suite.EqualValues(requestingAccount.Note, dbUpdatedAccount.Note) 395 suite.EqualValues(requestingAccount.Memorial, dbUpdatedAccount.Memorial) 396 suite.EqualValues(requestingAccount.AlsoKnownAs, dbUpdatedAccount.AlsoKnownAs) 397 suite.EqualValues(requestingAccount.MovedToAccountID, dbUpdatedAccount.MovedToAccountID) 398 suite.EqualValues(requestingAccount.Bot, dbUpdatedAccount.Bot) 399 suite.EqualValues(requestingAccount.Reason, dbUpdatedAccount.Reason) 400 suite.EqualValues(requestingAccount.Locked, dbUpdatedAccount.Locked) 401 suite.EqualValues(requestingAccount.Discoverable, dbUpdatedAccount.Discoverable) 402 suite.EqualValues(requestingAccount.Privacy, dbUpdatedAccount.Privacy) 403 suite.EqualValues(requestingAccount.Sensitive, dbUpdatedAccount.Sensitive) 404 suite.EqualValues(requestingAccount.Language, dbUpdatedAccount.Language) 405 suite.EqualValues(requestingAccount.URI, dbUpdatedAccount.URI) 406 suite.EqualValues(requestingAccount.URL, dbUpdatedAccount.URL) 407 suite.EqualValues(requestingAccount.InboxURI, dbUpdatedAccount.InboxURI) 408 suite.EqualValues(requestingAccount.OutboxURI, dbUpdatedAccount.OutboxURI) 409 suite.EqualValues(requestingAccount.FollowingURI, dbUpdatedAccount.FollowingURI) 410 suite.EqualValues(requestingAccount.FollowersURI, dbUpdatedAccount.FollowersURI) 411 suite.EqualValues(requestingAccount.FeaturedCollectionURI, dbUpdatedAccount.FeaturedCollectionURI) 412 suite.EqualValues(requestingAccount.ActorType, dbUpdatedAccount.ActorType) 413 suite.EqualValues(requestingAccount.PublicKey, dbUpdatedAccount.PublicKey) 414 suite.EqualValues(requestingAccount.PublicKeyURI, dbUpdatedAccount.PublicKeyURI) 415 suite.EqualValues(requestingAccount.SensitizedAt, dbUpdatedAccount.SensitizedAt) 416 suite.EqualValues(requestingAccount.SilencedAt, dbUpdatedAccount.SilencedAt) 417 suite.EqualValues(requestingAccount.SuspendedAt, dbUpdatedAccount.SuspendedAt) 418 suite.EqualValues(requestingAccount.HideCollections, dbUpdatedAccount.HideCollections) 419 suite.EqualValues(requestingAccount.SuspensionOrigin, dbUpdatedAccount.SuspensionOrigin) 420 } 421 422 func (suite *InboxPostTestSuite) TestPostDelete() { 423 var ( 424 ctx = context.Background() 425 requestingAccount = suite.testAccounts["remote_account_1"] 426 targetAccount = suite.testAccounts["local_account_1"] 427 activityID = requestingAccount.URI + "/some-new-activity/01FG9C441MCTW3R2W117V2PQK3" 428 ) 429 430 delete := suite.newDelete(requestingAccount.URI, requestingAccount.URI, activityID) 431 432 // Delete. 433 suite.inboxPost( 434 delete, 435 requestingAccount, 436 targetAccount, 437 http.StatusAccepted, 438 `{"status":"Accepted"}`, 439 suite.signatureCheck, 440 ) 441 442 if !testrig.WaitFor(func() bool { 443 // local account 2 blocked foss_satan, that block should be gone now 444 testBlock := suite.testBlocks["local_account_2_block_remote_account_1"] 445 _, err := suite.db.GetBlockByID(ctx, testBlock.ID) 446 return suite.ErrorIs(err, db.ErrNoEntries) 447 }) { 448 suite.FailNow("timed out waiting for block to be removed") 449 } 450 451 if !testrig.WaitFor(func() bool { 452 // no statuses from foss satan should be left in the database 453 dbStatuses, err := suite.db.GetAccountStatuses(ctx, requestingAccount.ID, 0, false, false, "", "", false, false) 454 return len(dbStatuses) == 0 && errors.Is(err, db.ErrNoEntries) 455 }) { 456 suite.FailNow("timed out waiting for statuses to be removed") 457 } 458 459 // Account should be stubbified. 460 dbAccount, err := suite.db.GetAccountByID(ctx, requestingAccount.ID) 461 suite.NoError(err) 462 suite.Empty(dbAccount.Note) 463 suite.Empty(dbAccount.DisplayName) 464 suite.Empty(dbAccount.AvatarMediaAttachmentID) 465 suite.Empty(dbAccount.AvatarRemoteURL) 466 suite.Empty(dbAccount.HeaderMediaAttachmentID) 467 suite.Empty(dbAccount.HeaderRemoteURL) 468 suite.Empty(dbAccount.Reason) 469 suite.Empty(dbAccount.Fields) 470 suite.True(*dbAccount.HideCollections) 471 suite.False(*dbAccount.Discoverable) 472 suite.WithinDuration(time.Now(), dbAccount.SuspendedAt, 30*time.Second) 473 suite.Equal(dbAccount.ID, dbAccount.SuspensionOrigin) 474 } 475 476 func (suite *InboxPostTestSuite) TestPostEmptyCreate() { 477 var ( 478 requestingAccount = suite.testAccounts["remote_account_1"] 479 targetAccount = suite.testAccounts["local_account_1"] 480 ) 481 482 // Post a create with no object. 483 create := streams.NewActivityStreamsCreate() 484 485 suite.inboxPost( 486 create, 487 requestingAccount, 488 targetAccount, 489 http.StatusBadRequest, 490 `{"error":"Bad Request: incoming Activity Create did not have required id property set"}`, 491 suite.signatureCheck, 492 ) 493 } 494 495 func (suite *InboxPostTestSuite) TestPostFromBlockedAccount() { 496 var ( 497 requestingAccount = suite.testAccounts["remote_account_1"] 498 targetAccount = suite.testAccounts["local_account_2"] 499 activityID = requestingAccount.URI + "/some-new-activity/01FG9C441MCTW3R2W117V2PQK3" 500 ) 501 502 person, err := suite.tc.AccountToAS(context.Background(), requestingAccount) 503 if err != nil { 504 suite.FailNow(err.Error()) 505 } 506 507 // Post an update from foss satan to turtle, who blocks him. 508 update := suite.newUpdatePerson(person, targetAccount.URI, activityID) 509 510 suite.inboxPost( 511 update, 512 requestingAccount, 513 targetAccount, 514 http.StatusForbidden, 515 `{"error":"Forbidden"}`, 516 suite.signatureCheck, 517 ) 518 } 519 520 func (suite *InboxPostTestSuite) TestPostFromBlockedAccountToOtherAccount() { 521 var ( 522 requestingAccount = suite.testAccounts["remote_account_1"] 523 targetAccount = suite.testAccounts["local_account_1"] 524 activity = suite.testActivities["reply_to_turtle_for_turtle"] 525 statusURI = "http://fossbros-anonymous.io/users/foss_satan/statuses/2f1195a6-5cb0-4475-adf5-92ab9a0147fe" 526 ) 527 528 // Post an reply to turtle to ZORK from remote account. 529 // Turtle blocks the remote account but is only tangentially 530 // related to this POST request. The response will indicate 531 // accepted but the post won't actually be processed. 532 suite.inboxPost( 533 activity.Activity, 534 requestingAccount, 535 targetAccount, 536 http.StatusAccepted, 537 `{"status":"Accepted"}`, 538 suite.signatureCheck, 539 ) 540 541 _, err := suite.state.DB.GetStatusByURI(context.Background(), statusURI) 542 suite.ErrorIs(err, db.ErrNoEntries) 543 } 544 545 func (suite *InboxPostTestSuite) TestPostUnauthorized() { 546 var ( 547 requestingAccount = suite.testAccounts["remote_account_1"] 548 targetAccount = suite.testAccounts["local_account_1"] 549 ) 550 551 // Post an empty create. 552 create := streams.NewActivityStreamsCreate() 553 554 suite.inboxPost( 555 create, 556 requestingAccount, 557 targetAccount, 558 http.StatusUnauthorized, 559 `{"error":"Unauthorized"}`, 560 // Omit signature check middleware. 561 ) 562 } 563 564 func TestInboxPostTestSuite(t *testing.T) { 565 suite.Run(t, &InboxPostTestSuite{}) 566 }