fromfederator_test.go (20429B)
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 processing_test 19 20 import ( 21 "context" 22 "encoding/json" 23 "fmt" 24 "testing" 25 "time" 26 27 "github.com/stretchr/testify/suite" 28 "github.com/superseriousbusiness/gotosocial/internal/ap" 29 apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" 30 "github.com/superseriousbusiness/gotosocial/internal/db" 31 "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" 32 "github.com/superseriousbusiness/gotosocial/internal/id" 33 "github.com/superseriousbusiness/gotosocial/internal/messages" 34 "github.com/superseriousbusiness/gotosocial/internal/stream" 35 "github.com/superseriousbusiness/gotosocial/testrig" 36 ) 37 38 type FromFederatorTestSuite struct { 39 ProcessingStandardTestSuite 40 } 41 42 // remote_account_1 boosts the first status of local_account_1 43 func (suite *FromFederatorTestSuite) TestProcessFederationAnnounce() { 44 boostedStatus := suite.testStatuses["local_account_1_status_1"] 45 boostingAccount := suite.testAccounts["remote_account_1"] 46 announceStatus := >smodel.Status{} 47 announceStatus.URI = "https://example.org/some-announce-uri" 48 announceStatus.BoostOf = >smodel.Status{ 49 URI: boostedStatus.URI, 50 } 51 announceStatus.CreatedAt = time.Now() 52 announceStatus.UpdatedAt = time.Now() 53 announceStatus.AccountID = boostingAccount.ID 54 announceStatus.AccountURI = boostingAccount.URI 55 announceStatus.Account = boostingAccount 56 announceStatus.Visibility = boostedStatus.Visibility 57 58 err := suite.processor.ProcessFromFederator(context.Background(), messages.FromFederator{ 59 APObjectType: ap.ActivityAnnounce, 60 APActivityType: ap.ActivityCreate, 61 GTSModel: announceStatus, 62 ReceivingAccount: suite.testAccounts["local_account_1"], 63 }) 64 suite.NoError(err) 65 66 // side effects should be triggered 67 // 1. status should have an ID, and be in the database 68 suite.NotEmpty(announceStatus.ID) 69 _, err = suite.db.GetStatusByID(context.Background(), announceStatus.ID) 70 suite.NoError(err) 71 72 // 2. a notification should exist for the announce 73 where := []db.Where{ 74 { 75 Key: "status_id", 76 Value: announceStatus.ID, 77 }, 78 } 79 notif := >smodel.Notification{} 80 err = suite.db.GetWhere(context.Background(), where, notif) 81 suite.NoError(err) 82 suite.Equal(gtsmodel.NotificationReblog, notif.NotificationType) 83 suite.Equal(boostedStatus.AccountID, notif.TargetAccountID) 84 suite.Equal(announceStatus.AccountID, notif.OriginAccountID) 85 suite.Equal(announceStatus.ID, notif.StatusID) 86 suite.False(*notif.Read) 87 } 88 89 func (suite *FromFederatorTestSuite) TestProcessReplyMention() { 90 repliedAccount := suite.testAccounts["local_account_1"] 91 repliedStatus := suite.testStatuses["local_account_1_status_1"] 92 replyingAccount := suite.testAccounts["remote_account_1"] 93 94 replyingStatus := >smodel.Status{ 95 CreatedAt: time.Now(), 96 UpdatedAt: time.Now(), 97 URI: "http://fossbros-anonymous.io/users/foss_satan/statuses/106221634728637552", 98 URL: "http://fossbros-anonymous.io/@foss_satan/106221634728637552", 99 Content: `<p><span class="h-card"><a href="http://localhost:8080/@the_mighty_zork" class="u-url mention">@<span>the_mighty_zork</span></a></span> nice there it is:</p><p><a href="http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY/activity" rel="nofollow noopener noreferrer" target="_blank"><span class="invisible">https://</span><span class="ellipsis">social.pixie.town/users/f0x/st</span><span class="invisible">atuses/106221628567855262/activity</span></a></p>`, 100 Mentions: []*gtsmodel.Mention{ 101 { 102 TargetAccountURI: repliedAccount.URI, 103 NameString: "@the_mighty_zork@localhost:8080", 104 }, 105 }, 106 AccountID: replyingAccount.ID, 107 AccountURI: replyingAccount.URI, 108 InReplyToID: repliedStatus.ID, 109 InReplyToURI: repliedStatus.URI, 110 InReplyToAccountID: repliedAccount.ID, 111 Visibility: gtsmodel.VisibilityUnlocked, 112 ActivityStreamsType: ap.ObjectNote, 113 Federated: testrig.TrueBool(), 114 Boostable: testrig.TrueBool(), 115 Replyable: testrig.TrueBool(), 116 Likeable: testrig.FalseBool(), 117 } 118 119 wssStream, errWithCode := suite.processor.Stream().Open(context.Background(), repliedAccount, stream.TimelineHome) 120 suite.NoError(errWithCode) 121 122 // id the status based on the time it was created 123 statusID, err := id.NewULIDFromTime(replyingStatus.CreatedAt) 124 suite.NoError(err) 125 replyingStatus.ID = statusID 126 127 err = suite.db.PutStatus(context.Background(), replyingStatus) 128 suite.NoError(err) 129 130 err = suite.processor.ProcessFromFederator(context.Background(), messages.FromFederator{ 131 APObjectType: ap.ObjectNote, 132 APActivityType: ap.ActivityCreate, 133 GTSModel: replyingStatus, 134 ReceivingAccount: suite.testAccounts["local_account_1"], 135 }) 136 suite.NoError(err) 137 138 // side effects should be triggered 139 // 1. status should be in the database 140 suite.NotEmpty(replyingStatus.ID) 141 _, err = suite.db.GetStatusByID(context.Background(), replyingStatus.ID) 142 suite.NoError(err) 143 144 // 2. a notification should exist for the mention 145 var notif gtsmodel.Notification 146 err = suite.db.GetWhere(context.Background(), []db.Where{ 147 {Key: "status_id", Value: replyingStatus.ID}, 148 }, ¬if) 149 suite.NoError(err) 150 suite.Equal(gtsmodel.NotificationMention, notif.NotificationType) 151 suite.Equal(replyingStatus.InReplyToAccountID, notif.TargetAccountID) 152 suite.Equal(replyingStatus.AccountID, notif.OriginAccountID) 153 suite.Equal(replyingStatus.ID, notif.StatusID) 154 suite.False(*notif.Read) 155 156 // the notification should be streamed 157 var msg *stream.Message 158 select { 159 case msg = <-wssStream.Messages: 160 // fine 161 case <-time.After(5 * time.Second): 162 suite.FailNow("no message from wssStream") 163 } 164 165 suite.Equal(stream.EventTypeNotification, msg.Event) 166 suite.NotEmpty(msg.Payload) 167 suite.EqualValues([]string{stream.TimelineHome}, msg.Stream) 168 notifStreamed := &apimodel.Notification{} 169 err = json.Unmarshal([]byte(msg.Payload), notifStreamed) 170 suite.NoError(err) 171 suite.Equal("mention", notifStreamed.Type) 172 suite.Equal(replyingAccount.ID, notifStreamed.Account.ID) 173 } 174 175 func (suite *FromFederatorTestSuite) TestProcessFave() { 176 favedAccount := suite.testAccounts["local_account_1"] 177 favedStatus := suite.testStatuses["local_account_1_status_1"] 178 favingAccount := suite.testAccounts["remote_account_1"] 179 180 wssStream, errWithCode := suite.processor.Stream().Open(context.Background(), favedAccount, stream.TimelineNotifications) 181 suite.NoError(errWithCode) 182 183 fave := >smodel.StatusFave{ 184 ID: "01FGKJPXFTVQPG9YSSZ95ADS7Q", 185 CreatedAt: time.Now(), 186 UpdatedAt: time.Now(), 187 AccountID: favingAccount.ID, 188 Account: favingAccount, 189 TargetAccountID: favedAccount.ID, 190 TargetAccount: favedAccount, 191 StatusID: favedStatus.ID, 192 Status: favedStatus, 193 URI: favingAccount.URI + "/faves/aaaaaaaaaaaa", 194 } 195 196 err := suite.db.Put(context.Background(), fave) 197 suite.NoError(err) 198 199 err = suite.processor.ProcessFromFederator(context.Background(), messages.FromFederator{ 200 APObjectType: ap.ActivityLike, 201 APActivityType: ap.ActivityCreate, 202 GTSModel: fave, 203 ReceivingAccount: favedAccount, 204 }) 205 suite.NoError(err) 206 207 // side effects should be triggered 208 // 1. a notification should exist for the fave 209 where := []db.Where{ 210 { 211 Key: "status_id", 212 Value: favedStatus.ID, 213 }, 214 { 215 Key: "origin_account_id", 216 Value: favingAccount.ID, 217 }, 218 } 219 220 notif := >smodel.Notification{} 221 err = suite.db.GetWhere(context.Background(), where, notif) 222 suite.NoError(err) 223 suite.Equal(gtsmodel.NotificationFave, notif.NotificationType) 224 suite.Equal(fave.TargetAccountID, notif.TargetAccountID) 225 suite.Equal(fave.AccountID, notif.OriginAccountID) 226 suite.Equal(fave.StatusID, notif.StatusID) 227 suite.False(*notif.Read) 228 229 // 2. a notification should be streamed 230 var msg *stream.Message 231 select { 232 case msg = <-wssStream.Messages: 233 // fine 234 case <-time.After(5 * time.Second): 235 suite.FailNow("no message from wssStream") 236 } 237 suite.Equal(stream.EventTypeNotification, msg.Event) 238 suite.NotEmpty(msg.Payload) 239 suite.EqualValues([]string{stream.TimelineNotifications}, msg.Stream) 240 } 241 242 // TestProcessFaveWithDifferentReceivingAccount ensures that when an account receives a fave that's for 243 // another account in their AP inbox, a notification isn't streamed to the receiving account. 244 // 245 // This tests for an issue we were seeing where Misskey sends out faves to inboxes of people that don't own 246 // the fave, but just follow the actor who received the fave. 247 func (suite *FromFederatorTestSuite) TestProcessFaveWithDifferentReceivingAccount() { 248 receivingAccount := suite.testAccounts["local_account_2"] 249 favedAccount := suite.testAccounts["local_account_1"] 250 favedStatus := suite.testStatuses["local_account_1_status_1"] 251 favingAccount := suite.testAccounts["remote_account_1"] 252 253 wssStream, errWithCode := suite.processor.Stream().Open(context.Background(), receivingAccount, stream.TimelineHome) 254 suite.NoError(errWithCode) 255 256 fave := >smodel.StatusFave{ 257 ID: "01FGKJPXFTVQPG9YSSZ95ADS7Q", 258 CreatedAt: time.Now(), 259 UpdatedAt: time.Now(), 260 AccountID: favingAccount.ID, 261 Account: favingAccount, 262 TargetAccountID: favedAccount.ID, 263 TargetAccount: favedAccount, 264 StatusID: favedStatus.ID, 265 Status: favedStatus, 266 URI: favingAccount.URI + "/faves/aaaaaaaaaaaa", 267 } 268 269 err := suite.db.Put(context.Background(), fave) 270 suite.NoError(err) 271 272 err = suite.processor.ProcessFromFederator(context.Background(), messages.FromFederator{ 273 APObjectType: ap.ActivityLike, 274 APActivityType: ap.ActivityCreate, 275 GTSModel: fave, 276 ReceivingAccount: receivingAccount, 277 }) 278 suite.NoError(err) 279 280 // side effects should be triggered 281 // 1. a notification should exist for the fave 282 where := []db.Where{ 283 { 284 Key: "status_id", 285 Value: favedStatus.ID, 286 }, 287 { 288 Key: "origin_account_id", 289 Value: favingAccount.ID, 290 }, 291 } 292 293 notif := >smodel.Notification{} 294 err = suite.db.GetWhere(context.Background(), where, notif) 295 suite.NoError(err) 296 suite.Equal(gtsmodel.NotificationFave, notif.NotificationType) 297 suite.Equal(fave.TargetAccountID, notif.TargetAccountID) 298 suite.Equal(fave.AccountID, notif.OriginAccountID) 299 suite.Equal(fave.StatusID, notif.StatusID) 300 suite.False(*notif.Read) 301 302 // 2. no notification should be streamed to the account that received the fave message, because they weren't the target 303 suite.Empty(wssStream.Messages) 304 } 305 306 func (suite *FromFederatorTestSuite) TestProcessAccountDelete() { 307 ctx := context.Background() 308 309 deletedAccount := suite.testAccounts["remote_account_1"] 310 receivingAccount := suite.testAccounts["local_account_1"] 311 312 // before doing the delete.... 313 // make local_account_1 and remote_account_1 into mufos 314 zorkFollowSatan := >smodel.Follow{ 315 ID: "01FGRY72ASHBSET64353DPHK9T", 316 CreatedAt: time.Now().Add(-1 * time.Hour), 317 UpdatedAt: time.Now().Add(-1 * time.Hour), 318 AccountID: deletedAccount.ID, 319 TargetAccountID: receivingAccount.ID, 320 ShowReblogs: testrig.TrueBool(), 321 URI: fmt.Sprintf("%s/follows/01FGRY72ASHBSET64353DPHK9T", deletedAccount.URI), 322 Notify: testrig.FalseBool(), 323 } 324 err := suite.db.Put(ctx, zorkFollowSatan) 325 suite.NoError(err) 326 327 satanFollowZork := >smodel.Follow{ 328 ID: "01FGRYAVAWWPP926J175QGM0WV", 329 CreatedAt: time.Now().Add(-1 * time.Hour), 330 UpdatedAt: time.Now().Add(-1 * time.Hour), 331 AccountID: receivingAccount.ID, 332 TargetAccountID: deletedAccount.ID, 333 ShowReblogs: testrig.TrueBool(), 334 URI: fmt.Sprintf("%s/follows/01FGRYAVAWWPP926J175QGM0WV", receivingAccount.URI), 335 Notify: testrig.FalseBool(), 336 } 337 err = suite.db.Put(ctx, satanFollowZork) 338 suite.NoError(err) 339 340 // now they are mufos! 341 err = suite.processor.ProcessFromFederator(ctx, messages.FromFederator{ 342 APObjectType: ap.ObjectProfile, 343 APActivityType: ap.ActivityDelete, 344 GTSModel: deletedAccount, 345 ReceivingAccount: receivingAccount, 346 }) 347 suite.NoError(err) 348 349 // local account 2 blocked foss_satan, that block should be gone now 350 testBlock := suite.testBlocks["local_account_2_block_remote_account_1"] 351 dbBlock := >smodel.Block{} 352 err = suite.db.GetByID(ctx, testBlock.ID, dbBlock) 353 suite.ErrorIs(err, db.ErrNoEntries) 354 355 // the mufos should be gone now too 356 satanFollowsZork, err := suite.db.IsFollowing(ctx, deletedAccount.ID, receivingAccount.ID) 357 suite.NoError(err) 358 suite.False(satanFollowsZork) 359 zorkFollowsSatan, err := suite.db.IsFollowing(ctx, receivingAccount.ID, deletedAccount.ID) 360 suite.NoError(err) 361 suite.False(zorkFollowsSatan) 362 363 // no statuses from foss satan should be left in the database 364 if !testrig.WaitFor(func() bool { 365 s, err := suite.db.GetAccountStatuses(ctx, deletedAccount.ID, 0, false, false, "", "", false, false) 366 return s == nil && err == db.ErrNoEntries 367 }) { 368 suite.FailNow("timeout waiting for statuses to be deleted") 369 } 370 371 dbAccount, err := suite.db.GetAccountByID(ctx, deletedAccount.ID) 372 suite.NoError(err) 373 374 suite.Empty(dbAccount.Note) 375 suite.Empty(dbAccount.DisplayName) 376 suite.Empty(dbAccount.AvatarMediaAttachmentID) 377 suite.Empty(dbAccount.AvatarRemoteURL) 378 suite.Empty(dbAccount.HeaderMediaAttachmentID) 379 suite.Empty(dbAccount.HeaderRemoteURL) 380 suite.Empty(dbAccount.Reason) 381 suite.Empty(dbAccount.Fields) 382 suite.True(*dbAccount.HideCollections) 383 suite.False(*dbAccount.Discoverable) 384 suite.WithinDuration(time.Now(), dbAccount.SuspendedAt, 30*time.Second) 385 suite.Equal(dbAccount.ID, dbAccount.SuspensionOrigin) 386 } 387 388 func (suite *FromFederatorTestSuite) TestProcessFollowRequestLocked() { 389 ctx := context.Background() 390 391 originAccount := suite.testAccounts["remote_account_1"] 392 393 // target is a locked account 394 targetAccount := suite.testAccounts["local_account_2"] 395 396 wssStream, errWithCode := suite.processor.Stream().Open(context.Background(), targetAccount, stream.TimelineHome) 397 suite.NoError(errWithCode) 398 399 // put the follow request in the database as though it had passed through the federating db already 400 satanFollowRequestTurtle := >smodel.FollowRequest{ 401 ID: "01FGRYAVAWWPP926J175QGM0WV", 402 CreatedAt: time.Now(), 403 UpdatedAt: time.Now(), 404 AccountID: originAccount.ID, 405 Account: originAccount, 406 TargetAccountID: targetAccount.ID, 407 TargetAccount: targetAccount, 408 ShowReblogs: testrig.TrueBool(), 409 URI: fmt.Sprintf("%s/follows/01FGRYAVAWWPP926J175QGM0WV", originAccount.URI), 410 Notify: testrig.FalseBool(), 411 } 412 413 err := suite.db.Put(ctx, satanFollowRequestTurtle) 414 suite.NoError(err) 415 416 err = suite.processor.ProcessFromFederator(ctx, messages.FromFederator{ 417 APObjectType: ap.ActivityFollow, 418 APActivityType: ap.ActivityCreate, 419 GTSModel: satanFollowRequestTurtle, 420 ReceivingAccount: targetAccount, 421 }) 422 suite.NoError(err) 423 424 // a notification should be streamed 425 var msg *stream.Message 426 select { 427 case msg = <-wssStream.Messages: 428 // fine 429 case <-time.After(5 * time.Second): 430 suite.FailNow("no message from wssStream") 431 } 432 suite.Equal(stream.EventTypeNotification, msg.Event) 433 suite.NotEmpty(msg.Payload) 434 suite.EqualValues([]string{stream.TimelineHome}, msg.Stream) 435 notif := &apimodel.Notification{} 436 err = json.Unmarshal([]byte(msg.Payload), notif) 437 suite.NoError(err) 438 suite.Equal("follow_request", notif.Type) 439 suite.Equal(originAccount.ID, notif.Account.ID) 440 441 // no messages should have been sent out, since we didn't need to federate an accept 442 suite.Empty(suite.httpClient.SentMessages) 443 } 444 445 func (suite *FromFederatorTestSuite) TestProcessFollowRequestUnlocked() { 446 ctx := context.Background() 447 448 originAccount := suite.testAccounts["remote_account_1"] 449 450 // target is an unlocked account 451 targetAccount := suite.testAccounts["local_account_1"] 452 453 wssStream, errWithCode := suite.processor.Stream().Open(context.Background(), targetAccount, stream.TimelineHome) 454 suite.NoError(errWithCode) 455 456 // put the follow request in the database as though it had passed through the federating db already 457 satanFollowRequestTurtle := >smodel.FollowRequest{ 458 ID: "01FGRYAVAWWPP926J175QGM0WV", 459 CreatedAt: time.Now(), 460 UpdatedAt: time.Now(), 461 AccountID: originAccount.ID, 462 Account: originAccount, 463 TargetAccountID: targetAccount.ID, 464 TargetAccount: targetAccount, 465 ShowReblogs: testrig.TrueBool(), 466 URI: fmt.Sprintf("%s/follows/01FGRYAVAWWPP926J175QGM0WV", originAccount.URI), 467 Notify: testrig.FalseBool(), 468 } 469 470 err := suite.db.Put(ctx, satanFollowRequestTurtle) 471 suite.NoError(err) 472 473 err = suite.processor.ProcessFromFederator(ctx, messages.FromFederator{ 474 APObjectType: ap.ActivityFollow, 475 APActivityType: ap.ActivityCreate, 476 GTSModel: satanFollowRequestTurtle, 477 ReceivingAccount: targetAccount, 478 }) 479 suite.NoError(err) 480 481 // an accept message should be sent to satan's inbox 482 var sent [][]byte 483 if !testrig.WaitFor(func() bool { 484 sentI, ok := suite.httpClient.SentMessages.Load(*originAccount.SharedInboxURI) 485 if ok { 486 sent, ok = sentI.([][]byte) 487 if !ok { 488 panic("SentMessages entry was not []byte") 489 } 490 return true 491 } 492 return false 493 }) { 494 suite.FailNow("timed out waiting for message") 495 } 496 497 accept := &struct { 498 Actor string `json:"actor"` 499 ID string `json:"id"` 500 Object struct { 501 Actor string `json:"actor"` 502 ID string `json:"id"` 503 Object string `json:"object"` 504 To string `json:"to"` 505 Type string `json:"type"` 506 } 507 To string `json:"to"` 508 Type string `json:"type"` 509 }{} 510 err = json.Unmarshal(sent[0], accept) 511 suite.NoError(err) 512 513 suite.Equal(targetAccount.URI, accept.Actor) 514 suite.Equal(originAccount.URI, accept.Object.Actor) 515 suite.Equal(satanFollowRequestTurtle.URI, accept.Object.ID) 516 suite.Equal(targetAccount.URI, accept.Object.Object) 517 suite.Equal(targetAccount.URI, accept.Object.To) 518 suite.Equal("Follow", accept.Object.Type) 519 suite.Equal(originAccount.URI, accept.To) 520 suite.Equal("Accept", accept.Type) 521 522 // a notification should be streamed 523 var msg *stream.Message 524 select { 525 case msg = <-wssStream.Messages: 526 // fine 527 case <-time.After(5 * time.Second): 528 suite.FailNow("no message from wssStream") 529 } 530 suite.Equal(stream.EventTypeNotification, msg.Event) 531 suite.NotEmpty(msg.Payload) 532 suite.EqualValues([]string{stream.TimelineHome}, msg.Stream) 533 notif := &apimodel.Notification{} 534 err = json.Unmarshal([]byte(msg.Payload), notif) 535 suite.NoError(err) 536 suite.Equal("follow", notif.Type) 537 suite.Equal(originAccount.ID, notif.Account.ID) 538 } 539 540 // TestCreateStatusFromIRI checks if a forwarded status can be dereferenced by the processor. 541 func (suite *FromFederatorTestSuite) TestCreateStatusFromIRI() { 542 ctx := context.Background() 543 544 receivingAccount := suite.testAccounts["local_account_1"] 545 statusCreator := suite.testAccounts["remote_account_2"] 546 547 err := suite.processor.ProcessFromFederator(ctx, messages.FromFederator{ 548 APObjectType: ap.ObjectNote, 549 APActivityType: ap.ActivityCreate, 550 GTSModel: nil, // gtsmodel is nil because this is a forwarded status -- we want to dereference it using the iri 551 ReceivingAccount: receivingAccount, 552 APIri: testrig.URLMustParse("http://example.org/users/Some_User/statuses/afaba698-5740-4e32-a702-af61aa543bc1"), 553 }) 554 suite.NoError(err) 555 556 // status should now be in the database, attributed to remote_account_2 557 s, err := suite.db.GetStatusByURI(context.Background(), "http://example.org/users/Some_User/statuses/afaba698-5740-4e32-a702-af61aa543bc1") 558 suite.NoError(err) 559 suite.Equal(statusCreator.URI, s.AccountURI) 560 } 561 562 func TestFromFederatorTestSuite(t *testing.T) { 563 suite.Run(t, &FromFederatorTestSuite{}) 564 }