mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2025-10-31 20:42:25 -05:00
this adds another 'filter' type cache, similar to the visibility and mute caches, to cache the results of status filtering checks. for the moment this keeps all the check calls themselves within the frontend typeconversion code, but i may move this out of the typeconverter in a future PR (also removing the ErrHideStatus means of propagating a hidden status). also tweaks some of the cache invalidation hooks to not make unnecessary calls. Reviewed-on: https://codeberg.org/superseriousbusiness/gotosocial/pulls/4303 Co-authored-by: kim <grufwub@gmail.com> Co-committed-by: kim <grufwub@gmail.com>
287 lines
9.9 KiB
Go
287 lines
9.9 KiB
Go
// 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 webpush_test
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"net/http"
|
|
"testing"
|
|
"time"
|
|
|
|
// for go:linkname
|
|
_ "unsafe"
|
|
|
|
"code.superseriousbusiness.org/gotosocial/internal/cleaner"
|
|
"code.superseriousbusiness.org/gotosocial/internal/db"
|
|
"code.superseriousbusiness.org/gotosocial/internal/email"
|
|
"code.superseriousbusiness.org/gotosocial/internal/federation"
|
|
"code.superseriousbusiness.org/gotosocial/internal/filter/interaction"
|
|
"code.superseriousbusiness.org/gotosocial/internal/filter/mutes"
|
|
"code.superseriousbusiness.org/gotosocial/internal/filter/visibility"
|
|
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
|
|
"code.superseriousbusiness.org/gotosocial/internal/media"
|
|
"code.superseriousbusiness.org/gotosocial/internal/oauth"
|
|
"code.superseriousbusiness.org/gotosocial/internal/processing"
|
|
"code.superseriousbusiness.org/gotosocial/internal/state"
|
|
"code.superseriousbusiness.org/gotosocial/internal/storage"
|
|
"code.superseriousbusiness.org/gotosocial/internal/subscriptions"
|
|
"code.superseriousbusiness.org/gotosocial/internal/transport"
|
|
"code.superseriousbusiness.org/gotosocial/internal/typeutils"
|
|
"code.superseriousbusiness.org/gotosocial/internal/util"
|
|
"code.superseriousbusiness.org/gotosocial/internal/webpush"
|
|
"code.superseriousbusiness.org/gotosocial/testrig"
|
|
"github.com/stretchr/testify/suite"
|
|
)
|
|
|
|
type RealSenderStandardTestSuite struct {
|
|
suite.Suite
|
|
db db.DB
|
|
storage *storage.Driver
|
|
state state.State
|
|
mediaManager *media.Manager
|
|
typeconverter *typeutils.Converter
|
|
httpClient *testrig.MockHTTPClient
|
|
transportController transport.Controller
|
|
federator *federation.Federator
|
|
oauthServer oauth.Server
|
|
emailSender email.Sender
|
|
webPushSender webpush.Sender
|
|
|
|
// standard suite models
|
|
testAccounts map[string]*gtsmodel.Account
|
|
testNotifications map[string]*gtsmodel.Notification
|
|
testWebPushSubscriptions map[string]*gtsmodel.WebPushSubscription
|
|
|
|
processor *processing.Processor
|
|
|
|
webPushHttpClientDo func(request *http.Request) (*http.Response, error)
|
|
}
|
|
|
|
func (suite *RealSenderStandardTestSuite) SetupSuite() {
|
|
suite.testAccounts = testrig.NewTestAccounts()
|
|
suite.testNotifications = testrig.NewTestNotifications()
|
|
suite.testWebPushSubscriptions = testrig.NewTestWebPushSubscriptions()
|
|
}
|
|
|
|
func (suite *RealSenderStandardTestSuite) SetupTest() {
|
|
suite.state.Caches.Init()
|
|
|
|
testrig.InitTestConfig()
|
|
testrig.InitTestLog()
|
|
|
|
suite.db = testrig.NewTestDB(&suite.state)
|
|
suite.state.DB = suite.db
|
|
suite.storage = testrig.NewInMemoryStorage()
|
|
suite.state.Storage = suite.storage
|
|
suite.typeconverter = typeutils.NewConverter(&suite.state)
|
|
|
|
suite.httpClient = testrig.NewMockHTTPClient(nil, "../../testrig/media")
|
|
suite.httpClient.TestRemotePeople = testrig.NewTestFediPeople()
|
|
suite.httpClient.TestRemoteStatuses = testrig.NewTestFediStatuses()
|
|
|
|
suite.transportController = testrig.NewTestTransportController(&suite.state, suite.httpClient)
|
|
suite.mediaManager = testrig.NewTestMediaManager(&suite.state)
|
|
suite.federator = testrig.NewTestFederator(&suite.state, suite.transportController, suite.mediaManager)
|
|
suite.oauthServer = testrig.NewTestOauthServer(&suite.state)
|
|
suite.emailSender = testrig.NewEmailSender("../../web/template/", nil)
|
|
|
|
suite.webPushSender = newSenderWith(
|
|
&http.Client{
|
|
Transport: suite,
|
|
},
|
|
&suite.state,
|
|
suite.typeconverter,
|
|
)
|
|
|
|
suite.processor = processing.NewProcessor(
|
|
cleaner.New(&suite.state),
|
|
subscriptions.New(
|
|
&suite.state,
|
|
suite.transportController,
|
|
suite.typeconverter,
|
|
),
|
|
suite.typeconverter,
|
|
suite.federator,
|
|
suite.oauthServer,
|
|
suite.mediaManager,
|
|
&suite.state,
|
|
suite.emailSender,
|
|
suite.webPushSender,
|
|
visibility.NewFilter(&suite.state),
|
|
mutes.NewFilter(&suite.state),
|
|
interaction.NewFilter(&suite.state),
|
|
)
|
|
testrig.StartWorkers(&suite.state, suite.processor.Workers())
|
|
|
|
testrig.StandardDBSetup(suite.db, suite.testAccounts)
|
|
testrig.StandardStorageSetup(suite.storage, "../../testrig/media")
|
|
}
|
|
|
|
func (suite *RealSenderStandardTestSuite) TearDownTest() {
|
|
testrig.StandardDBTeardown(suite.db)
|
|
testrig.StandardStorageTeardown(suite.storage)
|
|
testrig.StopWorkers(&suite.state)
|
|
suite.webPushHttpClientDo = nil
|
|
}
|
|
|
|
// RoundTrip implements http.RoundTripper with a closure stored in the test suite.
|
|
func (suite *RealSenderStandardTestSuite) RoundTrip(request *http.Request) (*http.Response, error) {
|
|
return suite.webPushHttpClientDo(request)
|
|
}
|
|
|
|
// notifyingReadCloser is a zero-length io.ReadCloser that can tell us when it's been closed,
|
|
// indicating the simulated Web Push server response has been sent, received, read, and closed.
|
|
type notifyingReadCloser struct {
|
|
bodyClosed chan struct{}
|
|
}
|
|
|
|
func (rc *notifyingReadCloser) Read(_ []byte) (n int, err error) {
|
|
return 0, io.EOF
|
|
}
|
|
|
|
func (rc *notifyingReadCloser) Close() error {
|
|
rc.bodyClosed <- struct{}{}
|
|
close(rc.bodyClosed)
|
|
return nil
|
|
}
|
|
|
|
// Simulate sending a push notification with the suite's fake web client.
|
|
func (suite *RealSenderStandardTestSuite) simulatePushNotification(
|
|
notificationID string,
|
|
statusCode int,
|
|
expectSend bool,
|
|
expectDeletedSubscription bool,
|
|
) error {
|
|
// Don't let the test run forever if the push notification was not sent for some reason.
|
|
ctx, cancel := context.WithTimeout(suite.T().Context(), 3*time.Second)
|
|
defer cancel()
|
|
|
|
notification, err := suite.state.DB.GetNotificationByID(ctx, notificationID)
|
|
if !suite.NoError(err) {
|
|
suite.FailNow("Couldn't fetch notification to send")
|
|
}
|
|
|
|
rc := ¬ifyingReadCloser{
|
|
bodyClosed: make(chan struct{}, 1),
|
|
}
|
|
|
|
// Simulate a response from the Web Push server.
|
|
suite.webPushHttpClientDo = func(request *http.Request) (*http.Response, error) {
|
|
return &http.Response{
|
|
Status: http.StatusText(statusCode),
|
|
StatusCode: statusCode,
|
|
Body: rc,
|
|
}, nil
|
|
}
|
|
|
|
apiNotif, err := suite.typeconverter.NotificationToAPINotification(ctx, notification, false)
|
|
suite.NoError(err)
|
|
|
|
// Send the push notification.
|
|
sendError := suite.webPushSender.Send(ctx,
|
|
notification,
|
|
apiNotif,
|
|
)
|
|
|
|
// Wait for it to be sent or for the context to time out.
|
|
bodyClosed := false
|
|
contextExpired := false
|
|
select {
|
|
case <-rc.bodyClosed:
|
|
bodyClosed = true
|
|
case <-ctx.Done():
|
|
contextExpired = true
|
|
}
|
|
|
|
// In some cases we expect the notification *not* to be sent.
|
|
if !expectSend {
|
|
suite.False(bodyClosed)
|
|
suite.True(contextExpired)
|
|
return nil
|
|
}
|
|
|
|
suite.True(bodyClosed)
|
|
suite.False(contextExpired)
|
|
|
|
// Look for the associated Web Push subscription. Some server responses should delete it.
|
|
subscription, err := suite.state.DB.GetWebPushSubscriptionByTokenID(
|
|
ctx,
|
|
suite.testWebPushSubscriptions["local_account_1_token_1"].TokenID,
|
|
)
|
|
if expectDeletedSubscription {
|
|
suite.ErrorIs(err, db.ErrNoEntries)
|
|
} else {
|
|
suite.NotNil(subscription)
|
|
}
|
|
|
|
return sendError
|
|
}
|
|
|
|
// Test a successful response to sending a push notification.
|
|
func (suite *RealSenderStandardTestSuite) TestSendSuccess() {
|
|
notificationID := suite.testNotifications["local_account_1_like"].ID
|
|
suite.NoError(suite.simulatePushNotification(notificationID, http.StatusOK, true, false))
|
|
}
|
|
|
|
// Test a rate-limiting response to sending a push notification.
|
|
// This should not delete the subscription.
|
|
func (suite *RealSenderStandardTestSuite) TestRateLimited() {
|
|
notificationID := suite.testNotifications["local_account_1_like"].ID
|
|
suite.NoError(suite.simulatePushNotification(notificationID, http.StatusTooManyRequests, true, false))
|
|
}
|
|
|
|
// Test a non-special-cased client error response to sending a push notification.
|
|
// This should delete the subscription.
|
|
func (suite *RealSenderStandardTestSuite) TestClientError() {
|
|
notificationID := suite.testNotifications["local_account_1_like"].ID
|
|
suite.NoError(suite.simulatePushNotification(notificationID, http.StatusBadRequest, true, true))
|
|
}
|
|
|
|
// Test a server error response to sending a push notification.
|
|
// This should not delete the subscription.
|
|
func (suite *RealSenderStandardTestSuite) TestServerError() {
|
|
notificationID := suite.testNotifications["local_account_1_like"].ID
|
|
suite.NoError(suite.simulatePushNotification(notificationID, http.StatusInternalServerError, true, false))
|
|
}
|
|
|
|
// Don't send a push notification if it doesn't match policy.
|
|
func (suite *RealSenderStandardTestSuite) TestSendPolicyMismatch() {
|
|
// Setup: create a new notification from an account that the subscribed account doesn't follow.
|
|
notification := >smodel.Notification{
|
|
ID: "01JJZ2Y9Z8E1XKT90EHZ5KZBDW",
|
|
NotificationType: gtsmodel.NotificationFavourite,
|
|
TargetAccountID: suite.testAccounts["local_account_1"].ID,
|
|
OriginAccountID: suite.testAccounts["remote_account_1"].ID,
|
|
StatusOrEditID: "01F8MHAMCHF6Y650WCRSCP4WMY",
|
|
Read: util.Ptr(false),
|
|
}
|
|
if err := suite.db.PutNotification(suite.T().Context(), notification); !suite.NoError(err) {
|
|
suite.FailNow(err.Error())
|
|
return
|
|
}
|
|
|
|
suite.NoError(suite.simulatePushNotification(notification.ID, 0, false, false))
|
|
}
|
|
|
|
func TestRealSenderStandardTestSuite(t *testing.T) {
|
|
suite.Run(t, &RealSenderStandardTestSuite{})
|
|
}
|
|
|
|
//go:linkname newSenderWith code.superseriousbusiness.org/gotosocial/internal/webpush.newSenderWith
|
|
func newSenderWith(*http.Client, *state.State, *typeutils.Converter) webpush.Sender
|