diff options
author | 2023-08-09 19:14:33 +0200 | |
---|---|---|
committer | 2023-08-09 19:14:33 +0200 | |
commit | 9770d54237bea828cab7e50aec7dff452c203138 (patch) | |
tree | 59c444a02e81925bab47d3656a489a8c7087d530 /internal/processing/workers/fromfediapi_test.go | |
parent | [bugfix] Fix incorrect per-loop variable capture (#2092) (diff) | |
download | gotosocial-9770d54237bea828cab7e50aec7dff452c203138.tar.xz |
[feature] List replies policy, refactor async workers (#2087)
* Add/update some DB functions.
* move async workers into subprocessor
* rename FromFederator -> FromFediAPI
* update home timeline check to include check for current status first before moving to parent status
* change streamMap to pointer to mollify linter
* update followtoas func signature
* fix merge
* remove errant debug log
* don't use separate errs.Combine() check to wrap errs
* wrap parts of workers functionality in sub-structs
* populate report using new db funcs
* embed federator (tiny bit tidier)
* flesh out error msg, add continue(!)
* fix other error messages to be more specific
* better, nicer
* give parseURI util function a bit more util
* missing headers
* use pointers for subprocessors
Diffstat (limited to 'internal/processing/workers/fromfediapi_test.go')
-rw-r--r-- | internal/processing/workers/fromfediapi_test.go | 565 |
1 files changed, 565 insertions, 0 deletions
diff --git a/internal/processing/workers/fromfediapi_test.go b/internal/processing/workers/fromfediapi_test.go new file mode 100644 index 000000000..f8e3941fc --- /dev/null +++ b/internal/processing/workers/fromfediapi_test.go @@ -0,0 +1,565 @@ +// 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 <http://www.gnu.org/licenses/>. + +package workers_test + +import ( + "context" + "encoding/json" + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/suite" + "github.com/superseriousbusiness/gotosocial/internal/ap" + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/id" + "github.com/superseriousbusiness/gotosocial/internal/messages" + "github.com/superseriousbusiness/gotosocial/internal/stream" + "github.com/superseriousbusiness/gotosocial/internal/util" + "github.com/superseriousbusiness/gotosocial/testrig" +) + +type FromFediAPITestSuite struct { + WorkersTestSuite +} + +// remote_account_1 boosts the first status of local_account_1 +func (suite *FromFediAPITestSuite) TestProcessFederationAnnounce() { + boostedStatus := suite.testStatuses["local_account_1_status_1"] + boostingAccount := suite.testAccounts["remote_account_1"] + announceStatus := >smodel.Status{} + announceStatus.URI = "https://example.org/some-announce-uri" + announceStatus.BoostOf = >smodel.Status{ + URI: boostedStatus.URI, + } + announceStatus.CreatedAt = time.Now() + announceStatus.UpdatedAt = time.Now() + announceStatus.AccountID = boostingAccount.ID + announceStatus.AccountURI = boostingAccount.URI + announceStatus.Account = boostingAccount + announceStatus.Visibility = boostedStatus.Visibility + + err := suite.processor.Workers().ProcessFromFediAPI(context.Background(), messages.FromFediAPI{ + APObjectType: ap.ActivityAnnounce, + APActivityType: ap.ActivityCreate, + GTSModel: announceStatus, + ReceivingAccount: suite.testAccounts["local_account_1"], + }) + suite.NoError(err) + + // side effects should be triggered + // 1. status should have an ID, and be in the database + suite.NotEmpty(announceStatus.ID) + _, err = suite.db.GetStatusByID(context.Background(), announceStatus.ID) + suite.NoError(err) + + // 2. a notification should exist for the announce + where := []db.Where{ + { + Key: "status_id", + Value: announceStatus.ID, + }, + } + notif := >smodel.Notification{} + err = suite.db.GetWhere(context.Background(), where, notif) + suite.NoError(err) + suite.Equal(gtsmodel.NotificationReblog, notif.NotificationType) + suite.Equal(boostedStatus.AccountID, notif.TargetAccountID) + suite.Equal(announceStatus.AccountID, notif.OriginAccountID) + suite.Equal(announceStatus.ID, notif.StatusID) + suite.False(*notif.Read) +} + +func (suite *FromFediAPITestSuite) TestProcessReplyMention() { + repliedAccount := suite.testAccounts["local_account_1"] + repliedStatus := suite.testStatuses["local_account_1_status_1"] + replyingAccount := suite.testAccounts["remote_account_1"] + + replyingStatus := >smodel.Status{ + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + URI: "http://fossbros-anonymous.io/users/foss_satan/statuses/106221634728637552", + URL: "http://fossbros-anonymous.io/@foss_satan/106221634728637552", + 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>`, + Mentions: []*gtsmodel.Mention{ + { + TargetAccountURI: repliedAccount.URI, + NameString: "@the_mighty_zork@localhost:8080", + }, + }, + AccountID: replyingAccount.ID, + AccountURI: replyingAccount.URI, + InReplyToID: repliedStatus.ID, + InReplyToURI: repliedStatus.URI, + InReplyToAccountID: repliedAccount.ID, + Visibility: gtsmodel.VisibilityUnlocked, + ActivityStreamsType: ap.ObjectNote, + Federated: util.Ptr(true), + Boostable: util.Ptr(true), + Replyable: util.Ptr(true), + Likeable: util.Ptr(false), + } + + wssStream, errWithCode := suite.processor.Stream().Open(context.Background(), repliedAccount, stream.TimelineHome) + suite.NoError(errWithCode) + + // id the status based on the time it was created + statusID, err := id.NewULIDFromTime(replyingStatus.CreatedAt) + suite.NoError(err) + replyingStatus.ID = statusID + + err = suite.db.PutStatus(context.Background(), replyingStatus) + suite.NoError(err) + + err = suite.processor.Workers().ProcessFromFediAPI(context.Background(), messages.FromFediAPI{ + APObjectType: ap.ObjectNote, + APActivityType: ap.ActivityCreate, + GTSModel: replyingStatus, + ReceivingAccount: suite.testAccounts["local_account_1"], + }) + suite.NoError(err) + + // side effects should be triggered + // 1. status should be in the database + suite.NotEmpty(replyingStatus.ID) + _, err = suite.db.GetStatusByID(context.Background(), replyingStatus.ID) + suite.NoError(err) + + // 2. a notification should exist for the mention + var notif gtsmodel.Notification + err = suite.db.GetWhere(context.Background(), []db.Where{ + {Key: "status_id", Value: replyingStatus.ID}, + }, ¬if) + suite.NoError(err) + suite.Equal(gtsmodel.NotificationMention, notif.NotificationType) + suite.Equal(replyingStatus.InReplyToAccountID, notif.TargetAccountID) + suite.Equal(replyingStatus.AccountID, notif.OriginAccountID) + suite.Equal(replyingStatus.ID, notif.StatusID) + suite.False(*notif.Read) + + // the notification should be streamed + var msg *stream.Message + select { + case msg = <-wssStream.Messages: + // fine + case <-time.After(5 * time.Second): + suite.FailNow("no message from wssStream") + } + + suite.Equal(stream.EventTypeNotification, msg.Event) + suite.NotEmpty(msg.Payload) + suite.EqualValues([]string{stream.TimelineHome}, msg.Stream) + notifStreamed := &apimodel.Notification{} + err = json.Unmarshal([]byte(msg.Payload), notifStreamed) + suite.NoError(err) + suite.Equal("mention", notifStreamed.Type) + suite.Equal(replyingAccount.ID, notifStreamed.Account.ID) +} + +func (suite *FromFediAPITestSuite) TestProcessFave() { + favedAccount := suite.testAccounts["local_account_1"] + favedStatus := suite.testStatuses["local_account_1_status_1"] + favingAccount := suite.testAccounts["remote_account_1"] + + wssStream, errWithCode := suite.processor.Stream().Open(context.Background(), favedAccount, stream.TimelineNotifications) + suite.NoError(errWithCode) + + fave := >smodel.StatusFave{ + ID: "01FGKJPXFTVQPG9YSSZ95ADS7Q", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + AccountID: favingAccount.ID, + Account: favingAccount, + TargetAccountID: favedAccount.ID, + TargetAccount: favedAccount, + StatusID: favedStatus.ID, + Status: favedStatus, + URI: favingAccount.URI + "/faves/aaaaaaaaaaaa", + } + + err := suite.db.Put(context.Background(), fave) + suite.NoError(err) + + err = suite.processor.Workers().ProcessFromFediAPI(context.Background(), messages.FromFediAPI{ + APObjectType: ap.ActivityLike, + APActivityType: ap.ActivityCreate, + GTSModel: fave, + ReceivingAccount: favedAccount, + }) + suite.NoError(err) + + // side effects should be triggered + // 1. a notification should exist for the fave + where := []db.Where{ + { + Key: "status_id", + Value: favedStatus.ID, + }, + { + Key: "origin_account_id", + Value: favingAccount.ID, + }, + } + + notif := >smodel.Notification{} + err = suite.db.GetWhere(context.Background(), where, notif) + suite.NoError(err) + suite.Equal(gtsmodel.NotificationFave, notif.NotificationType) + suite.Equal(fave.TargetAccountID, notif.TargetAccountID) + suite.Equal(fave.AccountID, notif.OriginAccountID) + suite.Equal(fave.StatusID, notif.StatusID) + suite.False(*notif.Read) + + // 2. a notification should be streamed + var msg *stream.Message + select { + case msg = <-wssStream.Messages: + // fine + case <-time.After(5 * time.Second): + suite.FailNow("no message from wssStream") + } + suite.Equal(stream.EventTypeNotification, msg.Event) + suite.NotEmpty(msg.Payload) + suite.EqualValues([]string{stream.TimelineNotifications}, msg.Stream) +} + +// TestProcessFaveWithDifferentReceivingAccount ensures that when an account receives a fave that's for +// another account in their AP inbox, a notification isn't streamed to the receiving account. +// +// This tests for an issue we were seeing where Misskey sends out faves to inboxes of people that don't own +// the fave, but just follow the actor who received the fave. +func (suite *FromFediAPITestSuite) TestProcessFaveWithDifferentReceivingAccount() { + receivingAccount := suite.testAccounts["local_account_2"] + favedAccount := suite.testAccounts["local_account_1"] + favedStatus := suite.testStatuses["local_account_1_status_1"] + favingAccount := suite.testAccounts["remote_account_1"] + + wssStream, errWithCode := suite.processor.Stream().Open(context.Background(), receivingAccount, stream.TimelineHome) + suite.NoError(errWithCode) + + fave := >smodel.StatusFave{ + ID: "01FGKJPXFTVQPG9YSSZ95ADS7Q", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + AccountID: favingAccount.ID, + Account: favingAccount, + TargetAccountID: favedAccount.ID, + TargetAccount: favedAccount, + StatusID: favedStatus.ID, + Status: favedStatus, + URI: favingAccount.URI + "/faves/aaaaaaaaaaaa", + } + + err := suite.db.Put(context.Background(), fave) + suite.NoError(err) + + err = suite.processor.Workers().ProcessFromFediAPI(context.Background(), messages.FromFediAPI{ + APObjectType: ap.ActivityLike, + APActivityType: ap.ActivityCreate, + GTSModel: fave, + ReceivingAccount: receivingAccount, + }) + suite.NoError(err) + + // side effects should be triggered + // 1. a notification should exist for the fave + where := []db.Where{ + { + Key: "status_id", + Value: favedStatus.ID, + }, + { + Key: "origin_account_id", + Value: favingAccount.ID, + }, + } + + notif := >smodel.Notification{} + err = suite.db.GetWhere(context.Background(), where, notif) + suite.NoError(err) + suite.Equal(gtsmodel.NotificationFave, notif.NotificationType) + suite.Equal(fave.TargetAccountID, notif.TargetAccountID) + suite.Equal(fave.AccountID, notif.OriginAccountID) + suite.Equal(fave.StatusID, notif.StatusID) + suite.False(*notif.Read) + + // 2. no notification should be streamed to the account that received the fave message, because they weren't the target + suite.Empty(wssStream.Messages) +} + +func (suite *FromFediAPITestSuite) TestProcessAccountDelete() { + ctx := context.Background() + + deletedAccount := suite.testAccounts["remote_account_1"] + receivingAccount := suite.testAccounts["local_account_1"] + + // before doing the delete.... + // make local_account_1 and remote_account_1 into mufos + zorkFollowSatan := >smodel.Follow{ + ID: "01FGRY72ASHBSET64353DPHK9T", + CreatedAt: time.Now().Add(-1 * time.Hour), + UpdatedAt: time.Now().Add(-1 * time.Hour), + AccountID: deletedAccount.ID, + TargetAccountID: receivingAccount.ID, + ShowReblogs: util.Ptr(true), + URI: fmt.Sprintf("%s/follows/01FGRY72ASHBSET64353DPHK9T", deletedAccount.URI), + Notify: util.Ptr(false), + } + err := suite.db.Put(ctx, zorkFollowSatan) + suite.NoError(err) + + satanFollowZork := >smodel.Follow{ + ID: "01FGRYAVAWWPP926J175QGM0WV", + CreatedAt: time.Now().Add(-1 * time.Hour), + UpdatedAt: time.Now().Add(-1 * time.Hour), + AccountID: receivingAccount.ID, + TargetAccountID: deletedAccount.ID, + ShowReblogs: util.Ptr(true), + URI: fmt.Sprintf("%s/follows/01FGRYAVAWWPP926J175QGM0WV", receivingAccount.URI), + Notify: util.Ptr(false), + } + err = suite.db.Put(ctx, satanFollowZork) + suite.NoError(err) + + // now they are mufos! + err = suite.processor.Workers().ProcessFromFediAPI(ctx, messages.FromFediAPI{ + APObjectType: ap.ObjectProfile, + APActivityType: ap.ActivityDelete, + GTSModel: deletedAccount, + ReceivingAccount: receivingAccount, + }) + suite.NoError(err) + + // local account 2 blocked foss_satan, that block should be gone now + testBlock := suite.testBlocks["local_account_2_block_remote_account_1"] + dbBlock := >smodel.Block{} + err = suite.db.GetByID(ctx, testBlock.ID, dbBlock) + suite.ErrorIs(err, db.ErrNoEntries) + + // the mufos should be gone now too + satanFollowsZork, err := suite.db.IsFollowing(ctx, deletedAccount.ID, receivingAccount.ID) + suite.NoError(err) + suite.False(satanFollowsZork) + zorkFollowsSatan, err := suite.db.IsFollowing(ctx, receivingAccount.ID, deletedAccount.ID) + suite.NoError(err) + suite.False(zorkFollowsSatan) + + // no statuses from foss satan should be left in the database + if !testrig.WaitFor(func() bool { + s, err := suite.db.GetAccountStatuses(ctx, deletedAccount.ID, 0, false, false, "", "", false, false) + return s == nil && err == db.ErrNoEntries + }) { + suite.FailNow("timeout waiting for statuses to be deleted") + } + + dbAccount, err := suite.db.GetAccountByID(ctx, deletedAccount.ID) + suite.NoError(err) + + suite.Empty(dbAccount.Note) + suite.Empty(dbAccount.DisplayName) + suite.Empty(dbAccount.AvatarMediaAttachmentID) + suite.Empty(dbAccount.AvatarRemoteURL) + suite.Empty(dbAccount.HeaderMediaAttachmentID) + suite.Empty(dbAccount.HeaderRemoteURL) + suite.Empty(dbAccount.Reason) + suite.Empty(dbAccount.Fields) + suite.True(*dbAccount.HideCollections) + suite.False(*dbAccount.Discoverable) + suite.WithinDuration(time.Now(), dbAccount.SuspendedAt, 30*time.Second) + suite.Equal(dbAccount.ID, dbAccount.SuspensionOrigin) +} + +func (suite *FromFediAPITestSuite) TestProcessFollowRequestLocked() { + ctx := context.Background() + + originAccount := suite.testAccounts["remote_account_1"] + + // target is a locked account + targetAccount := suite.testAccounts["local_account_2"] + + wssStream, errWithCode := suite.processor.Stream().Open(context.Background(), targetAccount, stream.TimelineHome) + suite.NoError(errWithCode) + + // put the follow request in the database as though it had passed through the federating db already + satanFollowRequestTurtle := >smodel.FollowRequest{ + ID: "01FGRYAVAWWPP926J175QGM0WV", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + AccountID: originAccount.ID, + Account: originAccount, + TargetAccountID: targetAccount.ID, + TargetAccount: targetAccount, + ShowReblogs: util.Ptr(true), + URI: fmt.Sprintf("%s/follows/01FGRYAVAWWPP926J175QGM0WV", originAccount.URI), + Notify: util.Ptr(false), + } + + err := suite.db.Put(ctx, satanFollowRequestTurtle) + suite.NoError(err) + + err = suite.processor.Workers().ProcessFromFediAPI(ctx, messages.FromFediAPI{ + APObjectType: ap.ActivityFollow, + APActivityType: ap.ActivityCreate, + GTSModel: satanFollowRequestTurtle, + ReceivingAccount: targetAccount, + }) + suite.NoError(err) + + // a notification should be streamed + var msg *stream.Message + select { + case msg = <-wssStream.Messages: + // fine + case <-time.After(5 * time.Second): + suite.FailNow("no message from wssStream") + } + suite.Equal(stream.EventTypeNotification, msg.Event) + suite.NotEmpty(msg.Payload) + suite.EqualValues([]string{stream.TimelineHome}, msg.Stream) + notif := &apimodel.Notification{} + err = json.Unmarshal([]byte(msg.Payload), notif) + suite.NoError(err) + suite.Equal("follow_request", notif.Type) + suite.Equal(originAccount.ID, notif.Account.ID) + + // no messages should have been sent out, since we didn't need to federate an accept + suite.Empty(suite.httpClient.SentMessages) +} + +func (suite *FromFediAPITestSuite) TestProcessFollowRequestUnlocked() { + ctx := context.Background() + + originAccount := suite.testAccounts["remote_account_1"] + + // target is an unlocked account + targetAccount := suite.testAccounts["local_account_1"] + + wssStream, errWithCode := suite.processor.Stream().Open(context.Background(), targetAccount, stream.TimelineHome) + suite.NoError(errWithCode) + + // put the follow request in the database as though it had passed through the federating db already + satanFollowRequestTurtle := >smodel.FollowRequest{ + ID: "01FGRYAVAWWPP926J175QGM0WV", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + AccountID: originAccount.ID, + Account: originAccount, + TargetAccountID: targetAccount.ID, + TargetAccount: targetAccount, + ShowReblogs: util.Ptr(true), + URI: fmt.Sprintf("%s/follows/01FGRYAVAWWPP926J175QGM0WV", originAccount.URI), + Notify: util.Ptr(false), + } + + err := suite.db.Put(ctx, satanFollowRequestTurtle) + suite.NoError(err) + + err = suite.processor.Workers().ProcessFromFediAPI(ctx, messages.FromFediAPI{ + APObjectType: ap.ActivityFollow, + APActivityType: ap.ActivityCreate, + GTSModel: satanFollowRequestTurtle, + ReceivingAccount: targetAccount, + }) + suite.NoError(err) + + // an accept message should be sent to satan's inbox + var sent [][]byte + if !testrig.WaitFor(func() bool { + sentI, ok := suite.httpClient.SentMessages.Load(*originAccount.SharedInboxURI) + if ok { + sent, ok = sentI.([][]byte) + if !ok { + panic("SentMessages entry was not []byte") + } + return true + } + return false + }) { + suite.FailNow("timed out waiting for message") + } + + accept := &struct { + Actor string `json:"actor"` + ID string `json:"id"` + Object struct { + Actor string `json:"actor"` + ID string `json:"id"` + Object string `json:"object"` + To string `json:"to"` + Type string `json:"type"` + } + To string `json:"to"` + Type string `json:"type"` + }{} + err = json.Unmarshal(sent[0], accept) + suite.NoError(err) + + suite.Equal(targetAccount.URI, accept.Actor) + suite.Equal(originAccount.URI, accept.Object.Actor) + suite.Equal(satanFollowRequestTurtle.URI, accept.Object.ID) + suite.Equal(targetAccount.URI, accept.Object.Object) + suite.Equal(targetAccount.URI, accept.Object.To) + suite.Equal("Follow", accept.Object.Type) + suite.Equal(originAccount.URI, accept.To) + suite.Equal("Accept", accept.Type) + + // a notification should be streamed + var msg *stream.Message + select { + case msg = <-wssStream.Messages: + // fine + case <-time.After(5 * time.Second): + suite.FailNow("no message from wssStream") + } + suite.Equal(stream.EventTypeNotification, msg.Event) + suite.NotEmpty(msg.Payload) + suite.EqualValues([]string{stream.TimelineHome}, msg.Stream) + notif := &apimodel.Notification{} + err = json.Unmarshal([]byte(msg.Payload), notif) + suite.NoError(err) + suite.Equal("follow", notif.Type) + suite.Equal(originAccount.ID, notif.Account.ID) +} + +// TestCreateStatusFromIRI checks if a forwarded status can be dereferenced by the processor. +func (suite *FromFediAPITestSuite) TestCreateStatusFromIRI() { + ctx := context.Background() + + receivingAccount := suite.testAccounts["local_account_1"] + statusCreator := suite.testAccounts["remote_account_2"] + + err := suite.processor.Workers().ProcessFromFediAPI(ctx, messages.FromFediAPI{ + APObjectType: ap.ObjectNote, + APActivityType: ap.ActivityCreate, + GTSModel: nil, // gtsmodel is nil because this is a forwarded status -- we want to dereference it using the iri + ReceivingAccount: receivingAccount, + APIri: testrig.URLMustParse("http://example.org/users/Some_User/statuses/afaba698-5740-4e32-a702-af61aa543bc1"), + }) + suite.NoError(err) + + // status should now be in the database, attributed to remote_account_2 + s, err := suite.db.GetStatusByURI(context.Background(), "http://example.org/users/Some_User/statuses/afaba698-5740-4e32-a702-af61aa543bc1") + suite.NoError(err) + suite.Equal(statusCreator.URI, s.AccountURI) +} + +func TestFromFederatorTestSuite(t *testing.T) { + suite.Run(t, &FromFediAPITestSuite{}) +} |