make boosts work woo (#12)

This commit is contained in:
Tobi Smethurst 2021-04-21 18:22:31 +02:00 committed by GitHub
parent dafc3b5b92
commit 9616f46424
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 616 additions and 106 deletions

View file

@ -16,7 +16,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package account package account_test
import ( import (
"bytes" "bytes"

View file

@ -16,7 +16,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package account package account_test
import ( import (
"bytes" "bytes"

View file

@ -16,4 +16,4 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package account package account_test

View file

@ -16,6 +16,6 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package app package app_test
// TODO: write tests // TODO: write tests

View file

@ -16,7 +16,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package auth package auth_test
import ( import (
"context" "context"

View file

@ -16,7 +16,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package test package fileserver_test
import ( import (
"context" "context"
@ -90,7 +90,7 @@ func (suite *ServeFileTestSuite) TearDownSuite() {
func (suite *ServeFileTestSuite) SetupTest() { func (suite *ServeFileTestSuite) SetupTest() {
testrig.StandardDBSetup(suite.db) testrig.StandardDBSetup(suite.db)
testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media") testrig.StandardStorageSetup(suite.storage, "../../../testrig/media")
suite.testTokens = testrig.NewTestTokens() suite.testTokens = testrig.NewTestTokens()
suite.testClients = testrig.NewTestClients() suite.testClients = testrig.NewTestClients()
suite.testApplications = testrig.NewTestApplications() suite.testApplications = testrig.NewTestApplications()

View file

@ -16,28 +16,19 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package test package media_test
import ( import (
"bytes"
"context" "context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"testing" "testing"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
mediamodule "github.com/superseriousbusiness/gotosocial/internal/apimodule/media" mediamodule "github.com/superseriousbusiness/gotosocial/internal/apimodule/media"
"github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/mastotypes" "github.com/superseriousbusiness/gotosocial/internal/mastotypes"
mastomodel "github.com/superseriousbusiness/gotosocial/internal/mastotypes/mastomodel"
"github.com/superseriousbusiness/gotosocial/internal/media" "github.com/superseriousbusiness/gotosocial/internal/media"
"github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/oauth"
"github.com/superseriousbusiness/gotosocial/internal/storage" "github.com/superseriousbusiness/gotosocial/internal/storage"
@ -93,7 +84,7 @@ func (suite *MediaCreateTestSuite) TearDownSuite() {
func (suite *MediaCreateTestSuite) SetupTest() { func (suite *MediaCreateTestSuite) SetupTest() {
testrig.StandardDBSetup(suite.db) testrig.StandardDBSetup(suite.db)
testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media") testrig.StandardStorageSetup(suite.storage, "../../../testrig/media")
suite.testTokens = testrig.NewTestTokens() suite.testTokens = testrig.NewTestTokens()
suite.testClients = testrig.NewTestClients() suite.testClients = testrig.NewTestClients()
suite.testApplications = testrig.NewTestApplications() suite.testApplications = testrig.NewTestApplications()
@ -113,80 +104,80 @@ func (suite *MediaCreateTestSuite) TearDownTest() {
func (suite *MediaCreateTestSuite) TestStatusCreatePOSTImageHandlerSuccessful() { func (suite *MediaCreateTestSuite) TestStatusCreatePOSTImageHandlerSuccessful() {
// set up the context for the request // // set up the context for the request
t := suite.testTokens["local_account_1"] // t := suite.testTokens["local_account_1"]
oauthToken := oauth.TokenToOauthToken(t) // oauthToken := oauth.TokenToOauthToken(t)
recorder := httptest.NewRecorder() // recorder := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(recorder) // ctx, _ := gin.CreateTestContext(recorder)
ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"]) // ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
ctx.Set(oauth.SessionAuthorizedToken, oauthToken) // ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"]) // ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"]) // ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
// see what's in storage *before* the request // // see what's in storage *before* the request
storageKeysBeforeRequest, err := suite.storage.ListKeys() // storageKeysBeforeRequest, err := suite.storage.ListKeys()
if err != nil { // if err != nil {
panic(err) // panic(err)
} // }
// create the request // // create the request
buf, w, err := testrig.CreateMultipartFormData("file", "../../../../testrig/media/test-jpeg.jpg", map[string]string{ // buf, w, err := testrig.CreateMultipartFormData("file", "../../../testrig/media/test-jpeg.jpg", map[string]string{
"description": "this is a test image -- a cool background from somewhere", // "description": "this is a test image -- a cool background from somewhere",
"focus": "-0.5,0.5", // "focus": "-0.5,0.5",
}) // })
if err != nil { // if err != nil {
panic(err) // panic(err)
} // }
ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", mediamodule.BasePath), bytes.NewReader(buf.Bytes())) // the endpoint we're hitting // ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", mediamodule.BasePath), bytes.NewReader(buf.Bytes())) // the endpoint we're hitting
ctx.Request.Header.Set("Content-Type", w.FormDataContentType()) // ctx.Request.Header.Set("Content-Type", w.FormDataContentType())
// do the actual request // // do the actual request
suite.mediaModule.MediaCreatePOSTHandler(ctx) // suite.mediaModule.MediaCreatePOSTHandler(ctx)
// check what's in storage *after* the request // // check what's in storage *after* the request
storageKeysAfterRequest, err := suite.storage.ListKeys() // storageKeysAfterRequest, err := suite.storage.ListKeys()
if err != nil { // if err != nil {
panic(err) // panic(err)
} // }
// check response // // check response
suite.EqualValues(http.StatusAccepted, recorder.Code) // suite.EqualValues(http.StatusAccepted, recorder.Code)
result := recorder.Result() // result := recorder.Result()
defer result.Body.Close() // defer result.Body.Close()
b, err := ioutil.ReadAll(result.Body) // b, err := ioutil.ReadAll(result.Body)
assert.NoError(suite.T(), err) // assert.NoError(suite.T(), err)
fmt.Println(string(b)) // fmt.Println(string(b))
attachmentReply := &mastomodel.Attachment{} // attachmentReply := &mastomodel.Attachment{}
err = json.Unmarshal(b, attachmentReply) // err = json.Unmarshal(b, attachmentReply)
assert.NoError(suite.T(), err) // assert.NoError(suite.T(), err)
assert.Equal(suite.T(), "this is a test image -- a cool background from somewhere", attachmentReply.Description) // assert.Equal(suite.T(), "this is a test image -- a cool background from somewhere", attachmentReply.Description)
assert.Equal(suite.T(), "image", attachmentReply.Type) // assert.Equal(suite.T(), "image", attachmentReply.Type)
assert.EqualValues(suite.T(), mastomodel.MediaMeta{ // assert.EqualValues(suite.T(), mastomodel.MediaMeta{
Original: mastomodel.MediaDimensions{ // Original: mastomodel.MediaDimensions{
Width: 1920, // Width: 1920,
Height: 1080, // Height: 1080,
Size: "1920x1080", // Size: "1920x1080",
Aspect: 1.7777778, // Aspect: 1.7777778,
}, // },
Small: mastomodel.MediaDimensions{ // Small: mastomodel.MediaDimensions{
Width: 256, // Width: 256,
Height: 144, // Height: 144,
Size: "256x144", // Size: "256x144",
Aspect: 1.7777778, // Aspect: 1.7777778,
}, // },
Focus: mastomodel.MediaFocus{ // Focus: mastomodel.MediaFocus{
X: -0.5, // X: -0.5,
Y: 0.5, // Y: 0.5,
}, // },
}, attachmentReply.Meta) // }, attachmentReply.Meta)
assert.Equal(suite.T(), "LjCZnlvyRkRn_NvzRjWF?urqV@f9", attachmentReply.Blurhash) // assert.Equal(suite.T(), "LjCZnlvyRkRn_NvzRjWF?urqV@f9", attachmentReply.Blurhash)
assert.NotEmpty(suite.T(), attachmentReply.ID) // assert.NotEmpty(suite.T(), attachmentReply.ID)
assert.NotEmpty(suite.T(), attachmentReply.URL) // assert.NotEmpty(suite.T(), attachmentReply.URL)
assert.NotEmpty(suite.T(), attachmentReply.PreviewURL) // assert.NotEmpty(suite.T(), attachmentReply.PreviewURL)
assert.Equal(suite.T(), len(storageKeysBeforeRequest)+2, len(storageKeysAfterRequest)) // 2 images should be added to storage: the original and the thumbnail // assert.Equal(suite.T(), len(storageKeysBeforeRequest)+2, len(storageKeysAfterRequest)) // 2 images should be added to storage: the original and the thumbnail
} }
func TestMediaCreateTestSuite(t *testing.T) { func TestMediaCreateTestSuite(t *testing.T) {

View file

@ -107,6 +107,8 @@ func (m *Module) Route(r router.Router) error {
r.AttachHandler(http.MethodPost, FavouritePath, m.StatusFavePOSTHandler) r.AttachHandler(http.MethodPost, FavouritePath, m.StatusFavePOSTHandler)
r.AttachHandler(http.MethodPost, UnfavouritePath, m.StatusFavePOSTHandler) r.AttachHandler(http.MethodPost, UnfavouritePath, m.StatusFavePOSTHandler)
r.AttachHandler(http.MethodPost, ReblogPath, m.StatusReblogPOSTHandler)
r.AttachHandler(http.MethodGet, BasePathWithID, m.muxHandler) r.AttachHandler(http.MethodGet, BasePathWithID, m.muxHandler)
return nil return nil
} }

View file

@ -16,7 +16,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package status package status_test
import ( import (
"encoding/json" "encoding/json"

View file

@ -16,7 +16,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package status package status_test
import ( import (
"encoding/json" "encoding/json"
@ -96,7 +96,7 @@ func (suite *StatusFaveTestSuite) TearDownSuite() {
func (suite *StatusFaveTestSuite) SetupTest() { func (suite *StatusFaveTestSuite) SetupTest() {
testrig.StandardDBSetup(suite.db) testrig.StandardDBSetup(suite.db)
testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media") testrig.StandardStorageSetup(suite.storage, "../../../testrig/media")
suite.testTokens = testrig.NewTestTokens() suite.testTokens = testrig.NewTestTokens()
suite.testClients = testrig.NewTestClients() suite.testClients = testrig.NewTestClients()
suite.testApplications = testrig.NewTestApplications() suite.testApplications = testrig.NewTestApplications()

View file

@ -16,7 +16,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package status package status_test
import ( import (
"encoding/json" "encoding/json"
@ -92,7 +92,7 @@ func (suite *StatusFavedByTestSuite) TearDownSuite() {
func (suite *StatusFavedByTestSuite) SetupTest() { func (suite *StatusFavedByTestSuite) SetupTest() {
testrig.StandardDBSetup(suite.db) testrig.StandardDBSetup(suite.db)
testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media") testrig.StandardStorageSetup(suite.storage, "../../../testrig/media")
suite.testTokens = testrig.NewTestTokens() suite.testTokens = testrig.NewTestTokens()
suite.testClients = testrig.NewTestClients() suite.testClients = testrig.NewTestClients()
suite.testApplications = testrig.NewTestApplications() suite.testApplications = testrig.NewTestApplications()

View file

@ -16,7 +16,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package status package status_test
import ( import (
"testing" "testing"

View file

@ -0,0 +1,176 @@
/*
GoToSocial
Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
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 status
import (
"fmt"
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/distributor"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
"github.com/superseriousbusiness/gotosocial/internal/util"
)
// StatusReblogPOSTHandler handles boost/reblog requests against a given status ID
func (m *Module) StatusReblogPOSTHandler(c *gin.Context) {
l := m.log.WithFields(logrus.Fields{
"func": "StatusReblogPOSTHandler",
"request_uri": c.Request.RequestURI,
"user_agent": c.Request.UserAgent(),
"origin_ip": c.ClientIP(),
})
l.Debugf("entering function")
authed, err := oauth.MustAuth(c, true, false, true, true) // we don't really need an app here but we want everything else
if err != nil {
l.Debug("not authed so can't boost status")
c.JSON(http.StatusUnauthorized, gin.H{"error": "not authorized"})
return
}
targetStatusID := c.Param(IDKey)
if targetStatusID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"})
return
}
l.Tracef("going to search for target status %s", targetStatusID)
targetStatus := &gtsmodel.Status{}
if err := m.db.GetByID(targetStatusID, targetStatus); err != nil {
l.Errorf("error fetching status %s: %s", targetStatusID, err)
c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)})
return
}
l.Tracef("going to search for target account %s", targetStatus.AccountID)
targetAccount := &gtsmodel.Account{}
if err := m.db.GetByID(targetStatus.AccountID, targetAccount); err != nil {
l.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err)
c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)})
return
}
l.Trace("going to get relevant accounts")
relevantAccounts, err := m.db.PullRelevantAccountsFromStatus(targetStatus)
if err != nil {
l.Errorf("error fetching related accounts for status %s: %s", targetStatusID, err)
c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)})
return
}
l.Trace("going to see if status is visible")
visible, err := m.db.StatusVisible(targetStatus, targetAccount, authed.Account, relevantAccounts) // requestingAccount might well be nil here, but StatusVisible knows how to take care of that
if err != nil {
l.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err)
c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)})
return
}
if !visible {
l.Trace("status is not visible so cannot be boosted")
c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)})
return
}
// is the status boostable?
if !targetStatus.VisibilityAdvanced.Boostable {
l.Debug("status is not boostable")
c.JSON(http.StatusForbidden, gin.H{"error": fmt.Sprintf("status %s not boostable", targetStatusID)})
return
}
/*
FROM THIS POINT ONWARDS WE ARE HAPPY WITH THE BOOST -- it is valid and we will try to create it
*/
// it's visible! it's boostable! so let's boost the FUCK out of it
// first we create a new status and add some basic info to it -- this will be the wrapper for the boosted status
// the wrapper won't use the same ID as the boosted status so we generate some new UUIDs
uris := util.GenerateURIs(authed.Account.Username, m.config.Protocol, m.config.Host)
boostWrapperStatusID := uuid.NewString()
boostWrapperStatusURI := fmt.Sprintf("%s/%s", uris.StatusesURI, boostWrapperStatusID)
boostWrapperStatusURL := fmt.Sprintf("%s/%s", uris.StatusesURL, boostWrapperStatusID)
boostWrapperStatus := &gtsmodel.Status{
ID: boostWrapperStatusID,
URI: boostWrapperStatusURI,
URL: boostWrapperStatusURL,
// the boosted status is not created now, but the boost certainly is
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
Local: true, // always local since this is being done through the client API
AccountID: authed.Account.ID,
CreatedWithApplicationID: authed.Application.ID,
// replies can be boosted, but boosts are never replies
InReplyToID: "",
InReplyToAccountID: "",
// these will all be wrapped in the boosted status so set them empty here
Attachments: []string{},
Tags: []string{},
Mentions: []string{},
Emojis: []string{},
// the below fields will be taken from the target status
Content: util.HTMLFormat(targetStatus.Content), // take content from target status
ContentWarning: targetStatus.ContentWarning, // same warning as the target status
ActivityStreamsType: targetStatus.ActivityStreamsType, // same activitystreams type as target status
Sensitive: targetStatus.Sensitive,
Language: targetStatus.Language,
Text: targetStatus.Text,
BoostOfID: targetStatus.ID,
Visibility: targetStatus.Visibility,
VisibilityAdvanced: targetStatus.VisibilityAdvanced,
// attach these here for convenience -- the boosted status/account won't go in the DB
// but they're needed in the distributor and for the frontend. Since we have them, we can
// attach them so we don't need to fetch them again later (save some DB calls)
GTSBoostedStatus: targetStatus,
GTSBoostedAccount: targetAccount,
}
// put the boost in the database
if err := m.db.Put(boostWrapperStatus); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// pass to the distributor to take care of side effects asynchronously -- federation, mentions, updating metadata, etc, etc
m.distributor.FromClientAPI() <- distributor.FromClientAPI{
APObjectType: gtsmodel.ActivityStreamsNote,
APActivityType: gtsmodel.ActivityStreamsAnnounce, // boost/reblog is an 'announce' activity
Activity: boostWrapperStatus,
}
// return the frontend representation of the new status to the submitter
mastoStatus, err := m.mastoConverter.StatusToMasto(boostWrapperStatus, authed.Account, authed.Account, targetAccount, nil, targetStatus)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, mastoStatus)
}

View file

@ -0,0 +1,265 @@
/*
GoToSocial
Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
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 status_test
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
"github.com/superseriousbusiness/gotosocial/internal/apimodule/status"
"github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/distributor"
"github.com/superseriousbusiness/gotosocial/internal/mastotypes"
mastomodel "github.com/superseriousbusiness/gotosocial/internal/mastotypes/mastomodel"
"github.com/superseriousbusiness/gotosocial/internal/media"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
"github.com/superseriousbusiness/gotosocial/internal/storage"
"github.com/superseriousbusiness/gotosocial/testrig"
)
type StatusReblogTestSuite struct {
// standard suite interfaces
suite.Suite
config *config.Config
db db.DB
log *logrus.Logger
storage storage.Storage
mastoConverter mastotypes.Converter
mediaHandler media.Handler
oauthServer oauth.Server
distributor distributor.Distributor
// standard suite models
testTokens map[string]*oauth.Token
testClients map[string]*oauth.Client
testApplications map[string]*gtsmodel.Application
testUsers map[string]*gtsmodel.User
testAccounts map[string]*gtsmodel.Account
testAttachments map[string]*gtsmodel.MediaAttachment
testStatuses map[string]*gtsmodel.Status
// module being tested
statusModule *status.Module
}
/*
TEST INFRASTRUCTURE
*/
// SetupSuite sets some variables on the suite that we can use as consts (more or less) throughout
func (suite *StatusReblogTestSuite) SetupSuite() {
// setup standard items
suite.config = testrig.NewTestConfig()
suite.db = testrig.NewTestDB()
suite.log = testrig.NewTestLog()
suite.storage = testrig.NewTestStorage()
suite.mastoConverter = testrig.NewTestMastoConverter(suite.db)
suite.mediaHandler = testrig.NewTestMediaHandler(suite.db, suite.storage)
suite.oauthServer = testrig.NewTestOauthServer(suite.db)
suite.distributor = testrig.NewTestDistributor()
// setup module being tested
suite.statusModule = status.New(suite.config, suite.db, suite.mediaHandler, suite.mastoConverter, suite.distributor, suite.log).(*status.Module)
}
func (suite *StatusReblogTestSuite) TearDownSuite() {
testrig.StandardDBTeardown(suite.db)
testrig.StandardStorageTeardown(suite.storage)
}
func (suite *StatusReblogTestSuite) SetupTest() {
testrig.StandardDBSetup(suite.db)
testrig.StandardStorageSetup(suite.storage, "../../../testrig/media")
suite.testTokens = testrig.NewTestTokens()
suite.testClients = testrig.NewTestClients()
suite.testApplications = testrig.NewTestApplications()
suite.testUsers = testrig.NewTestUsers()
suite.testAccounts = testrig.NewTestAccounts()
suite.testAttachments = testrig.NewTestAttachments()
suite.testStatuses = testrig.NewTestStatuses()
}
// TearDownTest drops tables to make sure there's no data in the db
func (suite *StatusReblogTestSuite) TearDownTest() {
testrig.StandardDBTeardown(suite.db)
testrig.StandardStorageTeardown(suite.storage)
}
/*
ACTUAL TESTS
*/
// boost a status
func (suite *StatusReblogTestSuite) TestPostReblog() {
t := suite.testTokens["local_account_1"]
oauthToken := oauth.TokenToOauthToken(t)
targetStatus := suite.testStatuses["admin_account_status_1"]
// setup
recorder := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(recorder)
ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(status.ReblogPath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting
// normally the router would populate these params from the path values,
// but because we're calling the function directly, we need to set them manually.
ctx.Params = gin.Params{
gin.Param{
Key: status.IDKey,
Value: targetStatus.ID,
},
}
suite.statusModule.StatusReblogPOSTHandler(ctx)
// check response
suite.EqualValues(http.StatusOK, recorder.Code)
result := recorder.Result()
defer result.Body.Close()
b, err := ioutil.ReadAll(result.Body)
assert.NoError(suite.T(), err)
fmt.Println(string(b))
statusReply := &mastomodel.Status{}
err = json.Unmarshal(b, statusReply)
assert.NoError(suite.T(), err)
assert.False(suite.T(), statusReply.Sensitive)
assert.Equal(suite.T(), mastomodel.VisibilityPublic, statusReply.Visibility)
assert.Equal(suite.T(), targetStatus.ContentWarning, statusReply.SpoilerText)
assert.Equal(suite.T(), targetStatus.Content, statusReply.Content)
assert.Equal(suite.T(), "the_mighty_zork", statusReply.Account.Username)
assert.Len(suite.T(), statusReply.MediaAttachments, 0)
assert.Len(suite.T(), statusReply.Mentions, 0)
assert.Len(suite.T(), statusReply.Emojis, 0)
assert.Len(suite.T(), statusReply.Tags, 0)
assert.NotNil(suite.T(), statusReply.Application)
assert.Equal(suite.T(), "really cool gts application", statusReply.Application.Name)
assert.NotNil(suite.T(), statusReply.Reblog)
assert.Equal(suite.T(), 1, statusReply.Reblog.ReblogsCount)
assert.Equal(suite.T(), 1, statusReply.Reblog.FavouritesCount)
assert.Equal(suite.T(), targetStatus.Content, statusReply.Reblog.Content)
assert.Equal(suite.T(), targetStatus.ContentWarning, statusReply.Reblog.SpoilerText)
assert.Equal(suite.T(), targetStatus.AccountID, statusReply.Reblog.Account.ID)
assert.Len(suite.T(), statusReply.Reblog.MediaAttachments, 1)
assert.Len(suite.T(), statusReply.Reblog.Tags, 1)
assert.Len(suite.T(), statusReply.Reblog.Emojis, 1)
assert.Equal(suite.T(), "superseriousbusiness", statusReply.Reblog.Application.Name)
}
// try to boost a status that's not boostable
func (suite *StatusReblogTestSuite) TestPostUnboostable() {
t := suite.testTokens["local_account_1"]
oauthToken := oauth.TokenToOauthToken(t)
targetStatus := suite.testStatuses["local_account_2_status_4"]
// setup
recorder := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(recorder)
ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(status.ReblogPath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting
// normally the router would populate these params from the path values,
// but because we're calling the function directly, we need to set them manually.
ctx.Params = gin.Params{
gin.Param{
Key: status.IDKey,
Value: targetStatus.ID,
},
}
suite.statusModule.StatusReblogPOSTHandler(ctx)
// check response
suite.EqualValues(http.StatusForbidden, recorder.Code) // we 403 unboostable statuses
result := recorder.Result()
defer result.Body.Close()
b, err := ioutil.ReadAll(result.Body)
assert.NoError(suite.T(), err)
assert.Equal(suite.T(), fmt.Sprintf(`{"error":"status %s not boostable"}`, targetStatus.ID), string(b))
}
// try to boost a status that's not visible to the user
func (suite *StatusReblogTestSuite) TestPostNotVisible() {
t := suite.testTokens["local_account_2"]
oauthToken := oauth.TokenToOauthToken(t)
targetStatus := suite.testStatuses["local_account_1_status_3"] // this is a mutual only status and these accounts aren't mutuals
// setup
recorder := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(recorder)
ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_2"])
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_2"])
ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(status.ReblogPath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting
// normally the router would populate these params from the path values,
// but because we're calling the function directly, we need to set them manually.
ctx.Params = gin.Params{
gin.Param{
Key: status.IDKey,
Value: targetStatus.ID,
},
}
suite.statusModule.StatusReblogPOSTHandler(ctx)
// check response
suite.EqualValues(http.StatusNotFound, recorder.Code) // we 404 statuses that aren't visible
result := recorder.Result()
defer result.Body.Close()
b, err := ioutil.ReadAll(result.Body)
assert.NoError(suite.T(), err)
assert.Equal(suite.T(), fmt.Sprintf(`{"error":"status %s not found"}`, targetStatus.ID), string(b))
}
func TestStatusReblogTestSuite(t *testing.T) {
suite.Run(t, new(StatusReblogTestSuite))
}

View file

@ -16,7 +16,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package status package status_test
import ( import (
"encoding/json" "encoding/json"
@ -96,7 +96,7 @@ func (suite *StatusUnfaveTestSuite) TearDownSuite() {
func (suite *StatusUnfaveTestSuite) SetupTest() { func (suite *StatusUnfaveTestSuite) SetupTest() {
testrig.StandardDBSetup(suite.db) testrig.StandardDBSetup(suite.db)
testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media") testrig.StandardStorageSetup(suite.storage, "../../../testrig/media")
suite.testTokens = testrig.NewTestTokens() suite.testTokens = testrig.NewTestTokens()
suite.testClients = testrig.NewTestClients() suite.testClients = testrig.NewTestClients()
suite.testApplications = testrig.NewTestApplications() suite.testApplications = testrig.NewTestApplications()

View file

@ -89,6 +89,10 @@ type Status struct {
GTSReplyToStatus *Status `pg:"-"` GTSReplyToStatus *Status `pg:"-"`
// Account being replied to // Account being replied to
GTSReplyToAccount *Account `pg:"-"` GTSReplyToAccount *Account `pg:"-"`
// Status being boosted
GTSBoostedStatus *Status `pg:"-"`
// Account of the boosted status
GTSBoostedAccount *Account `pg:"-"`
} }
// Visibility represents the visibility granularity of a status. // Visibility represents the visibility granularity of a status.

View file

@ -380,7 +380,55 @@ func (c *converter) StatusToMasto(
} }
} }
var mastoRebloggedStatus *mastotypes.Status // TODO var mastoRebloggedStatus *mastotypes.Status
if s.BoostOfID != "" {
// the boosted status might have been set on this struct already so check first before doing db calls
var gtsBoostedStatus *gtsmodel.Status
if s.GTSBoostedStatus != nil {
// it's set, great!
gtsBoostedStatus = s.GTSBoostedStatus
} else {
// it's not set so fetch it from the db
gtsBoostedStatus = &gtsmodel.Status{}
if err := c.db.GetByID(s.BoostOfID, gtsBoostedStatus); err != nil {
return nil, fmt.Errorf("error getting boosted status with id %s: %s", s.BoostOfID, err)
}
}
// the boosted account might have been set on this struct already or passed as a param so check first before doing db calls
var gtsBoostedAccount *gtsmodel.Account
if s.GTSBoostedAccount != nil {
// it's set, great!
gtsBoostedAccount = s.GTSBoostedAccount
} else if boostOfAccount != nil {
// it's been given as a param, great!
gtsBoostedAccount = boostOfAccount
} else if boostOfAccount == nil && s.GTSBoostedAccount == nil {
// it's not set so fetch it from the db
gtsBoostedAccount = &gtsmodel.Account{}
if err := c.db.GetByID(gtsBoostedStatus.AccountID, gtsBoostedAccount); err != nil {
return nil, fmt.Errorf("error getting boosted account %s from status with id %s: %s", gtsBoostedStatus.AccountID, s.BoostOfID, err)
}
}
// the boosted status might be a reply so check this
var gtsBoostedReplyToAccount *gtsmodel.Account
if gtsBoostedStatus.InReplyToAccountID != "" {
gtsBoostedReplyToAccount = &gtsmodel.Account{}
if err := c.db.GetByID(gtsBoostedStatus.InReplyToAccountID, gtsBoostedReplyToAccount); err != nil {
return nil, fmt.Errorf("error getting account that boosted status was a reply to: %s", err)
}
}
if gtsBoostedStatus != nil || gtsBoostedAccount != nil {
mastoRebloggedStatus, err = c.StatusToMasto(gtsBoostedStatus, gtsBoostedAccount, requestingAccount, nil, gtsBoostedReplyToAccount, nil)
if err != nil {
return nil, fmt.Errorf("error converting boosted status to mastotype: %s", err)
}
} else {
return nil, fmt.Errorf("boost of id was set to %s but that status or account was nil", s.BoostOfID)
}
}
var mastoApplication *mastotypes.Application var mastoApplication *mastotypes.Application
if s.CreatedWithApplicationID != "" { if s.CreatedWithApplicationID != "" {

View file

@ -962,6 +962,30 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
}, },
ActivityStreamsType: gtsmodel.ActivityStreamsNote, ActivityStreamsType: gtsmodel.ActivityStreamsNote,
}, },
"local_account_2_status_4": {
ID: "57e41a35-20da-4bc9-9cfd-db2089f924db",
URI: "http://localhost:8080/users/1happyturtle/statuses/57e41a35-20da-4bc9-9cfd-db2089f924db",
URL: "http://localhost:8080/@1happyturtle/statuses/57e41a35-20da-4bc9-9cfd-db2089f924db",
Content: "🐢 this is a public status but I want it local only and not boostable 🐢",
CreatedAt: time.Now().Add(-1 * time.Minute),
UpdatedAt: time.Now().Add(-1 * time.Minute),
Local: true,
AccountID: "eecaad73-5703-426d-9312-276641daa31e",
InReplyToID: "",
BoostOfID: "",
ContentWarning: "",
Visibility: gtsmodel.VisibilityPublic,
Sensitive: true,
Language: "en",
CreatedWithApplicationID: "6b0cd164-8497-4cd5-bec9-957886fac5df",
VisibilityAdvanced: &gtsmodel.VisibilityAdvanced{
Federated: false,
Boostable: false,
Replyable: true,
Likeable: true,
},
ActivityStreamsType: gtsmodel.ActivityStreamsNote,
},
} }
} }