2023-03-12 16:00:57 +01:00
|
|
|
// 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/>.
|
2021-08-29 12:03:08 +02:00
|
|
|
|
|
|
|
|
package dereferencing_test
|
|
|
|
|
|
|
|
|
|
import (
|
2024-02-14 11:13:38 +00:00
|
|
|
"fmt"
|
2021-08-29 12:03:08 +02:00
|
|
|
"testing"
|
2024-12-05 13:35:07 +00:00
|
|
|
"time"
|
2021-08-29 12:03:08 +02:00
|
|
|
|
2025-04-25 15:15:36 +02:00
|
|
|
"code.superseriousbusiness.org/activity/streams"
|
2025-04-26 15:34:10 +02:00
|
|
|
"code.superseriousbusiness.org/gotosocial/internal/ap"
|
|
|
|
|
"code.superseriousbusiness.org/gotosocial/internal/db"
|
|
|
|
|
"code.superseriousbusiness.org/gotosocial/internal/federation/dereferencing"
|
|
|
|
|
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
|
|
|
|
|
"code.superseriousbusiness.org/gotosocial/internal/util"
|
|
|
|
|
"code.superseriousbusiness.org/gotosocial/testrig"
|
2021-08-29 12:03:08 +02:00
|
|
|
"github.com/stretchr/testify/suite"
|
|
|
|
|
)
|
|
|
|
|
|
2024-12-05 13:35:07 +00:00
|
|
|
// instantFreshness is the shortest possible freshness window.
|
|
|
|
|
var instantFreshness = util.Ptr(dereferencing.FreshnessWindow(0))
|
|
|
|
|
|
2021-08-29 12:03:08 +02:00
|
|
|
type StatusTestSuite struct {
|
|
|
|
|
DereferencerStandardTestSuite
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (suite *StatusTestSuite) TestDereferenceSimpleStatus() {
|
|
|
|
|
fetchingAccount := suite.testAccounts["local_account_1"]
|
|
|
|
|
|
|
|
|
|
statusURL := testrig.URLMustParse("https://unknown-instance.com/users/brand_new_person/statuses/01FE4NTHKWW7THT67EF10EB839")
|
2025-05-22 12:26:11 +02:00
|
|
|
status, _, err := suite.dereferencer.GetStatusByURI(suite.T().Context(), fetchingAccount.Username, statusURL)
|
2021-08-29 12:03:08 +02:00
|
|
|
suite.NoError(err)
|
|
|
|
|
suite.NotNil(status)
|
|
|
|
|
|
|
|
|
|
// status values should be set
|
|
|
|
|
suite.Equal("https://unknown-instance.com/users/brand_new_person/statuses/01FE4NTHKWW7THT67EF10EB839", status.URI)
|
|
|
|
|
suite.Equal("https://unknown-instance.com/users/@brand_new_person/01FE4NTHKWW7THT67EF10EB839", status.URL)
|
|
|
|
|
suite.Equal("Hello world!", status.Content)
|
|
|
|
|
suite.Equal("https://unknown-instance.com/users/brand_new_person", status.AccountURI)
|
2022-08-15 12:35:05 +02:00
|
|
|
suite.False(*status.Local)
|
2021-08-29 12:03:08 +02:00
|
|
|
suite.Empty(status.ContentWarning)
|
|
|
|
|
suite.Equal(gtsmodel.VisibilityPublic, status.Visibility)
|
2021-08-31 15:59:12 +02:00
|
|
|
suite.Equal(ap.ObjectNote, status.ActivityStreamsType)
|
2021-08-29 12:03:08 +02:00
|
|
|
|
|
|
|
|
// status should be in the database
|
2025-05-22 12:26:11 +02:00
|
|
|
dbStatus, err := suite.db.GetStatusByURI(suite.T().Context(), status.URI)
|
2021-08-29 12:03:08 +02:00
|
|
|
suite.NoError(err)
|
|
|
|
|
suite.Equal(status.ID, dbStatus.ID)
|
2022-08-15 12:35:05 +02:00
|
|
|
suite.True(*dbStatus.Federated)
|
2021-08-29 12:03:08 +02:00
|
|
|
|
|
|
|
|
// account should be in the database now too
|
2025-05-22 12:26:11 +02:00
|
|
|
account, err := suite.db.GetAccountByURI(suite.T().Context(), status.AccountURI)
|
2021-08-29 12:03:08 +02:00
|
|
|
suite.NoError(err)
|
|
|
|
|
suite.NotNil(account)
|
2022-08-15 12:35:05 +02:00
|
|
|
suite.True(*account.Discoverable)
|
2021-08-29 12:03:08 +02:00
|
|
|
suite.Equal("https://unknown-instance.com/users/brand_new_person", account.URI)
|
|
|
|
|
suite.Equal("hey I'm a new person, your instance hasn't seen me yet uwu", account.Note)
|
|
|
|
|
suite.Equal("Geoff Brando New Personson", account.DisplayName)
|
|
|
|
|
suite.Equal("brand_new_person", account.Username)
|
|
|
|
|
suite.NotNil(account.PublicKey)
|
|
|
|
|
suite.Nil(account.PrivateKey)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (suite *StatusTestSuite) TestDereferenceStatusWithMention() {
|
|
|
|
|
fetchingAccount := suite.testAccounts["local_account_1"]
|
|
|
|
|
|
|
|
|
|
statusURL := testrig.URLMustParse("https://unknown-instance.com/users/brand_new_person/statuses/01FE5Y30E3W4P7TRE0R98KAYQV")
|
2025-05-22 12:26:11 +02:00
|
|
|
status, _, err := suite.dereferencer.GetStatusByURI(suite.T().Context(), fetchingAccount.Username, statusURL)
|
2021-08-29 12:03:08 +02:00
|
|
|
suite.NoError(err)
|
|
|
|
|
suite.NotNil(status)
|
|
|
|
|
|
|
|
|
|
// status values should be set
|
|
|
|
|
suite.Equal("https://unknown-instance.com/users/brand_new_person/statuses/01FE5Y30E3W4P7TRE0R98KAYQV", status.URI)
|
|
|
|
|
suite.Equal("https://unknown-instance.com/users/@brand_new_person/01FE5Y30E3W4P7TRE0R98KAYQV", status.URL)
|
|
|
|
|
suite.Equal("Hey @the_mighty_zork@localhost:8080 how's it going?", status.Content)
|
|
|
|
|
suite.Equal("https://unknown-instance.com/users/brand_new_person", status.AccountURI)
|
2022-08-15 12:35:05 +02:00
|
|
|
suite.False(*status.Local)
|
2021-08-29 12:03:08 +02:00
|
|
|
suite.Empty(status.ContentWarning)
|
|
|
|
|
suite.Equal(gtsmodel.VisibilityPublic, status.Visibility)
|
2021-08-31 15:59:12 +02:00
|
|
|
suite.Equal(ap.ObjectNote, status.ActivityStreamsType)
|
2021-08-29 12:03:08 +02:00
|
|
|
|
|
|
|
|
// status should be in the database
|
2025-05-22 12:26:11 +02:00
|
|
|
dbStatus, err := suite.db.GetStatusByURI(suite.T().Context(), status.URI)
|
2021-08-29 12:03:08 +02:00
|
|
|
suite.NoError(err)
|
|
|
|
|
suite.Equal(status.ID, dbStatus.ID)
|
2022-08-15 12:35:05 +02:00
|
|
|
suite.True(*dbStatus.Federated)
|
2021-08-29 12:03:08 +02:00
|
|
|
|
|
|
|
|
// account should be in the database now too
|
2025-05-22 12:26:11 +02:00
|
|
|
account, err := suite.db.GetAccountByURI(suite.T().Context(), status.AccountURI)
|
2021-08-29 12:03:08 +02:00
|
|
|
suite.NoError(err)
|
|
|
|
|
suite.NotNil(account)
|
2022-08-15 12:35:05 +02:00
|
|
|
suite.True(*account.Discoverable)
|
2021-08-29 12:03:08 +02:00
|
|
|
suite.Equal("https://unknown-instance.com/users/brand_new_person", account.URI)
|
|
|
|
|
suite.Equal("hey I'm a new person, your instance hasn't seen me yet uwu", account.Note)
|
|
|
|
|
suite.Equal("Geoff Brando New Personson", account.DisplayName)
|
|
|
|
|
suite.Equal("brand_new_person", account.Username)
|
|
|
|
|
suite.NotNil(account.PublicKey)
|
|
|
|
|
suite.Nil(account.PrivateKey)
|
|
|
|
|
|
|
|
|
|
// we should have a mention in the database
|
|
|
|
|
m := >smodel.Mention{}
|
2025-05-22 12:26:11 +02:00
|
|
|
err = suite.db.GetWhere(suite.T().Context(), []db.Where{{Key: "status_id", Value: status.ID}}, m)
|
2021-08-29 12:03:08 +02:00
|
|
|
suite.NoError(err)
|
|
|
|
|
suite.NotNil(m)
|
|
|
|
|
suite.Equal(status.ID, m.StatusID)
|
|
|
|
|
suite.Equal(account.ID, m.OriginAccountID)
|
|
|
|
|
suite.Equal(fetchingAccount.ID, m.TargetAccountID)
|
|
|
|
|
suite.Equal(account.URI, m.OriginAccountURI)
|
2022-08-15 12:35:05 +02:00
|
|
|
suite.False(*m.Silent)
|
2021-08-29 12:03:08 +02:00
|
|
|
}
|
|
|
|
|
|
2023-07-31 15:47:35 +02:00
|
|
|
func (suite *StatusTestSuite) TestDereferenceStatusWithTag() {
|
|
|
|
|
fetchingAccount := suite.testAccounts["local_account_1"]
|
|
|
|
|
|
|
|
|
|
statusURL := testrig.URLMustParse("https://unknown-instance.com/users/brand_new_person/statuses/01H641QSRS3TCXSVC10X4GPKW7")
|
2025-05-22 12:26:11 +02:00
|
|
|
status, _, err := suite.dereferencer.GetStatusByURI(suite.T().Context(), fetchingAccount.Username, statusURL)
|
2023-07-31 15:47:35 +02:00
|
|
|
suite.NoError(err)
|
|
|
|
|
suite.NotNil(status)
|
|
|
|
|
|
|
|
|
|
// status values should be set
|
|
|
|
|
suite.Equal("https://unknown-instance.com/users/brand_new_person/statuses/01H641QSRS3TCXSVC10X4GPKW7", status.URI)
|
|
|
|
|
suite.Equal("https://unknown-instance.com/users/@brand_new_person/01H641QSRS3TCXSVC10X4GPKW7", status.URL)
|
[feature] Support new model of interaction flow for forward compat with v0.21.0 (#4394)
~~Still WIP!~~
This PR allows v0.20.0 of GtS to be forward-compatible with the interaction request / authorization flow that will fully replace the current flow in v0.21.0.
Basically, this means we need to recognize LikeRequest, ReplyRequest, and AnnounceRequest, and in response to those requests, deliver either a Reject or an Accept, with the latter pointing towards a LikeAuthorization, ReplyAuthorization, or AnnounceAuthorization, respectively. This can then be used by the remote instance to prove to third parties that the interaction has been accepted by the interactee. These Authorization types need to be dereferencable to third parties, so we need to serve them.
As well as recognizing the above "polite" interaction request types, we also need to still serve appropriate responses to "impolite" interaction request types, where an instance that's unaware of interaction policies tries to interact with a post by sending a reply, like, or boost directly, without wrapping it in a WhateverRequest type.
Doesn't fully close https://codeberg.org/superseriousbusiness/gotosocial/issues/4026 but gets damn near (just gotta update the federating with GtS documentation).
Migrations tested on both Postgres and SQLite.
Co-authored-by: kim <grufwub@gmail.com>
Reviewed-on: https://codeberg.org/superseriousbusiness/gotosocial/pulls/4394
Co-authored-by: tobi <tobi.smethurst@protonmail.com>
Co-committed-by: tobi <tobi.smethurst@protonmail.com>
2025-09-14 15:37:35 +02:00
|
|
|
suite.Equal("<p><span class=\"h-card\"><a href=\"http://fossbros-anonymous.io/@foss_satan\" class=\"u-url mention\" rel=\"nofollow noreferrer noopener\" target=\"_blank\">@<span>foss_satan</span></a></span>Babe are you okay, you've hardly touched your <a href=\"https://unknown-instance.com/tags/piss\" class=\"mention hashtag\" rel=\"tag nofollow noreferrer noopener\" target=\"_blank\">#<span>piss</span></a></p>", status.Content)
|
2023-07-31 15:47:35 +02:00
|
|
|
suite.Equal("https://unknown-instance.com/users/brand_new_person", status.AccountURI)
|
|
|
|
|
suite.False(*status.Local)
|
|
|
|
|
suite.Empty(status.ContentWarning)
|
|
|
|
|
suite.Equal(gtsmodel.VisibilityPublic, status.Visibility)
|
|
|
|
|
suite.Equal(ap.ObjectNote, status.ActivityStreamsType)
|
|
|
|
|
|
|
|
|
|
// Ensure tags set + ID'd.
|
|
|
|
|
suite.Len(status.Tags, 1)
|
|
|
|
|
suite.Len(status.TagIDs, 1)
|
|
|
|
|
|
|
|
|
|
// status should be in the database
|
2025-05-22 12:26:11 +02:00
|
|
|
dbStatus, err := suite.db.GetStatusByURI(suite.T().Context(), status.URI)
|
2023-07-31 15:47:35 +02:00
|
|
|
suite.NoError(err)
|
|
|
|
|
suite.Equal(status.ID, dbStatus.ID)
|
|
|
|
|
suite.True(*dbStatus.Federated)
|
|
|
|
|
|
|
|
|
|
// account should be in the database now too
|
2025-05-22 12:26:11 +02:00
|
|
|
account, err := suite.db.GetAccountByURI(suite.T().Context(), status.AccountURI)
|
2023-07-31 15:47:35 +02:00
|
|
|
suite.NoError(err)
|
|
|
|
|
suite.NotNil(account)
|
|
|
|
|
suite.True(*account.Discoverable)
|
|
|
|
|
suite.Equal("https://unknown-instance.com/users/brand_new_person", account.URI)
|
|
|
|
|
suite.Equal("hey I'm a new person, your instance hasn't seen me yet uwu", account.Note)
|
|
|
|
|
suite.Equal("Geoff Brando New Personson", account.DisplayName)
|
|
|
|
|
suite.Equal("brand_new_person", account.Username)
|
|
|
|
|
suite.NotNil(account.PublicKey)
|
|
|
|
|
suite.Nil(account.PrivateKey)
|
|
|
|
|
|
|
|
|
|
// we should have a tag in the database
|
|
|
|
|
t := >smodel.Tag{}
|
2025-05-22 12:26:11 +02:00
|
|
|
err = suite.db.GetWhere(suite.T().Context(), []db.Where{{Key: "name", Value: "piss"}}, t)
|
2023-07-31 15:47:35 +02:00
|
|
|
suite.NoError(err)
|
|
|
|
|
suite.NotNil(t)
|
|
|
|
|
}
|
|
|
|
|
|
2022-03-21 19:46:51 +01:00
|
|
|
func (suite *StatusTestSuite) TestDereferenceStatusWithImageAndNoContent() {
|
|
|
|
|
fetchingAccount := suite.testAccounts["local_account_1"]
|
|
|
|
|
|
|
|
|
|
statusURL := testrig.URLMustParse("https://turnip.farm/users/turniplover6969/statuses/70c53e54-3146-42d5-a630-83c8b6c7c042")
|
2025-05-22 12:26:11 +02:00
|
|
|
status, _, err := suite.dereferencer.GetStatusByURI(suite.T().Context(), fetchingAccount.Username, statusURL)
|
2022-03-21 19:46:51 +01:00
|
|
|
suite.NoError(err)
|
|
|
|
|
suite.NotNil(status)
|
|
|
|
|
|
|
|
|
|
// status values should be set
|
|
|
|
|
suite.Equal("https://turnip.farm/users/turniplover6969/statuses/70c53e54-3146-42d5-a630-83c8b6c7c042", status.URI)
|
|
|
|
|
suite.Equal("https://turnip.farm/@turniplover6969/70c53e54-3146-42d5-a630-83c8b6c7c042", status.URL)
|
|
|
|
|
suite.Equal("", status.Content)
|
|
|
|
|
suite.Equal("https://turnip.farm/users/turniplover6969", status.AccountURI)
|
2022-08-15 12:35:05 +02:00
|
|
|
suite.False(*status.Local)
|
2022-03-21 19:46:51 +01:00
|
|
|
suite.Empty(status.ContentWarning)
|
|
|
|
|
suite.Equal(gtsmodel.VisibilityPublic, status.Visibility)
|
|
|
|
|
suite.Equal(ap.ObjectNote, status.ActivityStreamsType)
|
|
|
|
|
|
|
|
|
|
// status should be in the database
|
2025-05-22 12:26:11 +02:00
|
|
|
dbStatus, err := suite.db.GetStatusByURI(suite.T().Context(), status.URI)
|
2022-03-21 19:46:51 +01:00
|
|
|
suite.NoError(err)
|
|
|
|
|
suite.Equal(status.ID, dbStatus.ID)
|
2022-08-15 12:35:05 +02:00
|
|
|
suite.True(*dbStatus.Federated)
|
2022-03-21 19:46:51 +01:00
|
|
|
|
|
|
|
|
// account should be in the database now too
|
2025-05-22 12:26:11 +02:00
|
|
|
account, err := suite.db.GetAccountByURI(suite.T().Context(), status.AccountURI)
|
2022-03-21 19:46:51 +01:00
|
|
|
suite.NoError(err)
|
|
|
|
|
suite.NotNil(account)
|
2022-08-15 12:35:05 +02:00
|
|
|
suite.True(*account.Discoverable)
|
2022-03-21 19:46:51 +01:00
|
|
|
suite.Equal("https://turnip.farm/users/turniplover6969", account.URI)
|
|
|
|
|
suite.Equal("I just think they're neat", account.Note)
|
|
|
|
|
suite.Equal("Turnip Lover 6969", account.DisplayName)
|
|
|
|
|
suite.Equal("turniplover6969", account.Username)
|
|
|
|
|
suite.NotNil(account.PublicKey)
|
|
|
|
|
suite.Nil(account.PrivateKey)
|
|
|
|
|
|
|
|
|
|
// we should have an attachment in the database
|
|
|
|
|
a := >smodel.MediaAttachment{}
|
2025-05-22 12:26:11 +02:00
|
|
|
err = suite.db.GetWhere(suite.T().Context(), []db.Where{{Key: "status_id", Value: status.ID}}, a)
|
2022-03-21 19:46:51 +01:00
|
|
|
suite.NoError(err)
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-14 11:13:38 +00:00
|
|
|
func (suite *StatusTestSuite) TestDereferenceStatusWithNonMatchingURI() {
|
|
|
|
|
fetchingAccount := suite.testAccounts["local_account_1"]
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
remoteURI = "https://turnip.farm/users/turniplover6969/statuses/70c53e54-3146-42d5-a630-83c8b6c7c042"
|
|
|
|
|
remoteAltURI = "https://turnip.farm/users/turniphater420/statuses/70c53e54-3146-42d5-a630-83c8b6c7c042"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Create a copy of this remote account at alternative URI.
|
|
|
|
|
remoteStatus := suite.client.TestRemoteStatuses[remoteURI]
|
|
|
|
|
suite.client.TestRemoteStatuses[remoteAltURI] = remoteStatus
|
|
|
|
|
|
|
|
|
|
// Attempt to fetch account at alternative URI, it should fail!
|
|
|
|
|
fetchedStatus, _, err := suite.dereferencer.GetStatusByURI(
|
2025-05-22 12:26:11 +02:00
|
|
|
suite.T().Context(),
|
2024-02-14 11:13:38 +00:00
|
|
|
fetchingAccount.Username,
|
|
|
|
|
testrig.URLMustParse(remoteAltURI),
|
|
|
|
|
)
|
|
|
|
|
suite.Equal(err.Error(), fmt.Sprintf("enrichStatus: dereferenced status uri %s does not match %s", remoteURI, remoteAltURI))
|
|
|
|
|
suite.Nil(fetchedStatus)
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-05 13:35:07 +00:00
|
|
|
func (suite *StatusTestSuite) TestDereferencerRefreshStatusUpdated() {
|
2025-10-03 15:50:57 +02:00
|
|
|
ctx := suite.T().Context()
|
2024-12-05 13:35:07 +00:00
|
|
|
|
|
|
|
|
// The local account we will be fetching statuses as.
|
|
|
|
|
fetchingAccount := suite.testAccounts["local_account_1"]
|
|
|
|
|
|
|
|
|
|
// The test status in question that we will be dereferencing from "remote".
|
|
|
|
|
testURIStr := "https://unknown-instance.com/users/brand_new_person/statuses/01FE4NTHKWW7THT67EF10EB839"
|
|
|
|
|
testURI := testrig.URLMustParse(testURIStr)
|
|
|
|
|
testStatusable := suite.client.TestRemoteStatuses[testURIStr]
|
|
|
|
|
|
|
|
|
|
// Fetch the remote status first to load it into instance.
|
|
|
|
|
testStatus, statusable, err := suite.dereferencer.GetStatusByURI(ctx,
|
|
|
|
|
fetchingAccount.Username,
|
|
|
|
|
testURI,
|
|
|
|
|
)
|
|
|
|
|
suite.NotNil(statusable)
|
|
|
|
|
suite.NoError(err)
|
|
|
|
|
|
|
|
|
|
// Run through multiple possible edits.
|
|
|
|
|
for _, testCase := range []struct {
|
|
|
|
|
editedContent string
|
|
|
|
|
editedContentWarning string
|
|
|
|
|
editedLanguage string
|
|
|
|
|
editedSensitive bool
|
|
|
|
|
editedAttachmentIDs []string
|
|
|
|
|
editedPollOptions []string
|
|
|
|
|
editedPollVotes []int
|
|
|
|
|
editedAt time.Time
|
|
|
|
|
}{
|
|
|
|
|
{
|
|
|
|
|
editedContent: "updated status content!",
|
|
|
|
|
editedContentWarning: "CW: edited status content",
|
|
|
|
|
editedLanguage: testStatus.Language, // no change
|
|
|
|
|
editedSensitive: *testStatus.Sensitive, // no change
|
|
|
|
|
editedAttachmentIDs: testStatus.AttachmentIDs, // no change
|
|
|
|
|
editedPollOptions: getPollOptions(testStatus), // no change
|
|
|
|
|
editedPollVotes: getPollVotes(testStatus), // no change
|
|
|
|
|
editedAt: time.Now(),
|
|
|
|
|
},
|
|
|
|
|
} {
|
|
|
|
|
// Take a snapshot of current
|
|
|
|
|
// state of the test status.
|
|
|
|
|
testStatus = copyStatus(testStatus)
|
|
|
|
|
|
|
|
|
|
// Edit the "remote" statusable obj.
|
|
|
|
|
suite.editStatusable(testStatusable,
|
|
|
|
|
testCase.editedContent,
|
|
|
|
|
testCase.editedContentWarning,
|
|
|
|
|
testCase.editedLanguage,
|
|
|
|
|
testCase.editedSensitive,
|
|
|
|
|
testCase.editedAttachmentIDs,
|
|
|
|
|
testCase.editedPollOptions,
|
|
|
|
|
testCase.editedPollVotes,
|
|
|
|
|
testCase.editedAt,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Refresh with a given statusable to updated to edited copy.
|
|
|
|
|
latest, statusable, err := suite.dereferencer.RefreshStatus(ctx,
|
|
|
|
|
fetchingAccount.Username,
|
|
|
|
|
testStatus,
|
|
|
|
|
nil, // NOTE: can provide testStatusable here to test as being received (not deref'd)
|
|
|
|
|
instantFreshness,
|
|
|
|
|
)
|
|
|
|
|
suite.NotNil(statusable)
|
|
|
|
|
suite.NoError(err)
|
|
|
|
|
|
|
|
|
|
// verify updated status details.
|
|
|
|
|
suite.verifyEditedStatusUpdate(
|
|
|
|
|
|
|
|
|
|
// the original status
|
|
|
|
|
// before any changes.
|
|
|
|
|
testStatus,
|
|
|
|
|
|
|
|
|
|
// latest status
|
|
|
|
|
// being tested.
|
|
|
|
|
latest,
|
|
|
|
|
|
|
|
|
|
// expected current state.
|
|
|
|
|
>smodel.StatusEdit{
|
|
|
|
|
Content: testCase.editedContent,
|
|
|
|
|
ContentWarning: testCase.editedContentWarning,
|
|
|
|
|
Language: testCase.editedLanguage,
|
|
|
|
|
Sensitive: &testCase.editedSensitive,
|
|
|
|
|
AttachmentIDs: testCase.editedAttachmentIDs,
|
|
|
|
|
PollOptions: testCase.editedPollOptions,
|
|
|
|
|
PollVotes: testCase.editedPollVotes,
|
|
|
|
|
// createdAt never changes
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// expected historic edit.
|
|
|
|
|
>smodel.StatusEdit{
|
|
|
|
|
Content: testStatus.Content,
|
|
|
|
|
ContentWarning: testStatus.ContentWarning,
|
|
|
|
|
Language: testStatus.Language,
|
|
|
|
|
Sensitive: testStatus.Sensitive,
|
|
|
|
|
AttachmentIDs: testStatus.AttachmentIDs,
|
|
|
|
|
PollOptions: getPollOptions(testStatus),
|
|
|
|
|
PollVotes: getPollVotes(testStatus),
|
2025-01-08 10:29:23 +00:00
|
|
|
CreatedAt: testStatus.UpdatedAt(),
|
2024-12-05 13:35:07 +00:00
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-03 15:50:57 +02:00
|
|
|
func (suite *StatusTestSuite) TestDereferencerRefreshStatusRace() {
|
|
|
|
|
ctx := suite.T().Context()
|
|
|
|
|
|
|
|
|
|
// The local account we will be fetching statuses as.
|
|
|
|
|
fetchingAccount := suite.testAccounts["local_account_1"]
|
|
|
|
|
|
|
|
|
|
// The test status in question that we will be dereferencing from "remote".
|
|
|
|
|
testURIStr := "https://unknown-instance.com/users/brand_new_person/statuses/01FE4NTHKWW7THT67EF10EB839"
|
|
|
|
|
testURI := testrig.URLMustParse(testURIStr)
|
|
|
|
|
testStatusable := suite.client.TestRemoteStatuses[testURIStr]
|
|
|
|
|
|
|
|
|
|
// Fetch the remote status first to load it into instance.
|
|
|
|
|
testStatus, statusable, err := suite.dereferencer.GetStatusByURI(ctx,
|
|
|
|
|
fetchingAccount.Username,
|
|
|
|
|
testURI,
|
|
|
|
|
)
|
|
|
|
|
suite.NotNil(statusable)
|
|
|
|
|
suite.NoError(err)
|
|
|
|
|
|
|
|
|
|
// Take a snapshot of current
|
|
|
|
|
// state of the test status.
|
|
|
|
|
beforeEdit := copyStatus(testStatus)
|
|
|
|
|
|
|
|
|
|
// Edit the "remote" statusable obj.
|
|
|
|
|
suite.editStatusable(testStatusable,
|
|
|
|
|
"updated status content!",
|
|
|
|
|
"CW: edited status content",
|
|
|
|
|
beforeEdit.Language, // no change
|
|
|
|
|
*beforeEdit.Sensitive, // no change
|
|
|
|
|
beforeEdit.AttachmentIDs, // no change
|
|
|
|
|
getPollOptions(beforeEdit), // no change
|
|
|
|
|
getPollVotes(beforeEdit), // no change
|
|
|
|
|
time.Now(),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Refresh with a given statusable to updated to edited copy.
|
|
|
|
|
afterEdit, statusable, err := suite.dereferencer.RefreshStatus(ctx,
|
|
|
|
|
fetchingAccount.Username,
|
|
|
|
|
testStatus,
|
|
|
|
|
testStatusable,
|
|
|
|
|
instantFreshness,
|
|
|
|
|
)
|
|
|
|
|
suite.NotNil(statusable)
|
|
|
|
|
suite.NoError(err)
|
|
|
|
|
|
|
|
|
|
// verify updated status details.
|
|
|
|
|
suite.verifyEditedStatusUpdate(
|
|
|
|
|
|
|
|
|
|
// the original status
|
|
|
|
|
// before any changes.
|
|
|
|
|
beforeEdit,
|
|
|
|
|
|
|
|
|
|
// latest status
|
|
|
|
|
// being tested.
|
|
|
|
|
afterEdit,
|
|
|
|
|
|
|
|
|
|
// expected current state.
|
|
|
|
|
>smodel.StatusEdit{
|
|
|
|
|
Content: "updated status content!",
|
|
|
|
|
ContentWarning: "CW: edited status content",
|
|
|
|
|
Language: beforeEdit.Language,
|
|
|
|
|
Sensitive: beforeEdit.Sensitive,
|
|
|
|
|
AttachmentIDs: beforeEdit.AttachmentIDs,
|
|
|
|
|
PollOptions: getPollOptions(beforeEdit),
|
|
|
|
|
PollVotes: getPollVotes(beforeEdit),
|
|
|
|
|
// createdAt never changes
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// expected historic edit.
|
|
|
|
|
>smodel.StatusEdit{
|
|
|
|
|
Content: beforeEdit.Content,
|
|
|
|
|
ContentWarning: beforeEdit.ContentWarning,
|
|
|
|
|
Language: beforeEdit.Language,
|
|
|
|
|
Sensitive: beforeEdit.Sensitive,
|
|
|
|
|
AttachmentIDs: beforeEdit.AttachmentIDs,
|
|
|
|
|
PollOptions: getPollOptions(beforeEdit),
|
|
|
|
|
PollVotes: getPollVotes(beforeEdit),
|
|
|
|
|
CreatedAt: beforeEdit.UpdatedAt(),
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Now make another attempt to refresh, using the old copy of the
|
|
|
|
|
// status. This should still successfully update based on our passed
|
|
|
|
|
// freshness window, but it *should* refetch the provided status to
|
|
|
|
|
// check for race shenanigans and realize that no edit has occurred.
|
|
|
|
|
afterBodge, statusable, err := suite.dereferencer.RefreshStatus(ctx,
|
|
|
|
|
fetchingAccount.Username,
|
|
|
|
|
beforeEdit,
|
|
|
|
|
testStatusable,
|
|
|
|
|
instantFreshness,
|
|
|
|
|
)
|
|
|
|
|
suite.NotNil(statusable)
|
|
|
|
|
suite.NoError(err)
|
|
|
|
|
|
|
|
|
|
// Check that no further edit occurred on status.
|
|
|
|
|
suite.Equal(afterEdit.EditIDs, afterBodge.EditIDs)
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-05 13:35:07 +00:00
|
|
|
// editStatusable updates the given statusable attributes.
|
|
|
|
|
// note that this acts on the original object, no copying.
|
|
|
|
|
func (suite *StatusTestSuite) editStatusable(
|
|
|
|
|
statusable ap.Statusable,
|
|
|
|
|
content string,
|
|
|
|
|
contentWarning string,
|
|
|
|
|
language string,
|
|
|
|
|
sensitive bool,
|
|
|
|
|
attachmentIDs []string, // TODO: this will require some thinking as to how ...
|
|
|
|
|
pollOptions []string, // TODO: this will require changing statusable type to question
|
|
|
|
|
pollVotes []int, // TODO: this will require changing statusable type to question
|
|
|
|
|
editedAt time.Time,
|
|
|
|
|
) {
|
|
|
|
|
// simply reset all mentions / emojis / tags
|
|
|
|
|
statusable.SetActivityStreamsTag(nil)
|
|
|
|
|
|
|
|
|
|
// Update the statusable content property + language (if set).
|
|
|
|
|
contentProp := streams.NewActivityStreamsContentProperty()
|
|
|
|
|
statusable.SetActivityStreamsContent(contentProp)
|
|
|
|
|
contentProp.AppendXMLSchemaString(content)
|
|
|
|
|
if language != "" {
|
|
|
|
|
contentProp.AppendRDFLangString(map[string]string{
|
|
|
|
|
language: content,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update the statusable content-warning property.
|
|
|
|
|
summaryProp := streams.NewActivityStreamsSummaryProperty()
|
|
|
|
|
statusable.SetActivityStreamsSummary(summaryProp)
|
|
|
|
|
summaryProp.AppendXMLSchemaString(contentWarning)
|
|
|
|
|
|
|
|
|
|
// Update the statusable sensitive property.
|
|
|
|
|
sensitiveProp := streams.NewActivityStreamsSensitiveProperty()
|
|
|
|
|
statusable.SetActivityStreamsSensitive(sensitiveProp)
|
|
|
|
|
sensitiveProp.AppendXMLSchemaBoolean(sensitive)
|
|
|
|
|
|
|
|
|
|
// Update the statusable updated property.
|
|
|
|
|
ap.SetUpdated(statusable, editedAt)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// verifyEditedStatusUpdate verifies that a given status has
|
|
|
|
|
// the expected number of historic edits, the 'current' status
|
|
|
|
|
// attributes (encapsulated as an edit for minimized no. args),
|
|
|
|
|
// and the last given 'historic' status edit attributes.
|
|
|
|
|
func (suite *StatusTestSuite) verifyEditedStatusUpdate(
|
|
|
|
|
testStatus *gtsmodel.Status, // the original model
|
|
|
|
|
status *gtsmodel.Status, // the status to check
|
|
|
|
|
current *gtsmodel.StatusEdit, // expected current state
|
|
|
|
|
historic *gtsmodel.StatusEdit, // historic edit we expect to have
|
|
|
|
|
) {
|
|
|
|
|
// don't use this func
|
|
|
|
|
// name in error msgs.
|
|
|
|
|
suite.T().Helper()
|
|
|
|
|
|
|
|
|
|
// Check we have expected number of edits.
|
|
|
|
|
previousEdits := len(testStatus.Edits)
|
|
|
|
|
suite.Len(status.Edits, previousEdits+1)
|
|
|
|
|
suite.Len(status.EditIDs, previousEdits+1)
|
|
|
|
|
|
|
|
|
|
// Check current state of status.
|
|
|
|
|
suite.Equal(current.Content, status.Content)
|
|
|
|
|
suite.Equal(current.ContentWarning, status.ContentWarning)
|
|
|
|
|
suite.Equal(current.Language, status.Language)
|
|
|
|
|
suite.Equal(*current.Sensitive, *status.Sensitive)
|
|
|
|
|
suite.Equal(current.AttachmentIDs, status.AttachmentIDs)
|
|
|
|
|
suite.Equal(current.PollOptions, getPollOptions(status))
|
|
|
|
|
suite.Equal(current.PollVotes, getPollVotes(status))
|
|
|
|
|
|
|
|
|
|
// Check the latest historic edit matches expected.
|
|
|
|
|
latestEdit := status.Edits[len(status.Edits)-1]
|
|
|
|
|
suite.Equal(historic.Content, latestEdit.Content)
|
|
|
|
|
suite.Equal(historic.ContentWarning, latestEdit.ContentWarning)
|
|
|
|
|
suite.Equal(historic.Language, latestEdit.Language)
|
|
|
|
|
suite.Equal(*historic.Sensitive, *latestEdit.Sensitive)
|
|
|
|
|
suite.Equal(historic.AttachmentIDs, latestEdit.AttachmentIDs)
|
|
|
|
|
suite.Equal(historic.PollOptions, latestEdit.PollOptions)
|
|
|
|
|
suite.Equal(historic.PollVotes, latestEdit.PollVotes)
|
|
|
|
|
suite.Equal(historic.CreatedAt, latestEdit.CreatedAt)
|
|
|
|
|
|
|
|
|
|
// The status creation date should never change.
|
|
|
|
|
suite.Equal(testStatus.CreatedAt, status.CreatedAt)
|
|
|
|
|
}
|
|
|
|
|
|
2021-08-29 12:03:08 +02:00
|
|
|
func TestStatusTestSuite(t *testing.T) {
|
|
|
|
|
suite.Run(t, new(StatusTestSuite))
|
|
|
|
|
}
|
2024-12-05 13:35:07 +00:00
|
|
|
|
|
|
|
|
// copyStatus returns a copy of the given status model (not including sub-structs).
|
|
|
|
|
func copyStatus(status *gtsmodel.Status) *gtsmodel.Status {
|
|
|
|
|
copy := new(gtsmodel.Status)
|
|
|
|
|
*copy = *status
|
|
|
|
|
return copy
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// getPollOptions extracts poll option strings from status (if poll is set).
|
|
|
|
|
func getPollOptions(status *gtsmodel.Status) []string {
|
|
|
|
|
if status.Poll != nil {
|
|
|
|
|
return status.Poll.Options
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// getPollVotes extracts poll vote counts from status (if poll is set).
|
|
|
|
|
func getPollVotes(status *gtsmodel.Status) []int {
|
|
|
|
|
if status.Poll != nil {
|
|
|
|
|
return status.Poll.Votes
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|