mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2025-10-29 18:42:26 -05:00
* start work rewriting timeline cache type
* further work rewriting timeline caching
* more work integration new timeline code
* remove old code
* add local timeline, fix up merge conflicts
* remove old use of go-bytes
* implement new timeline code into more areas of codebase, pull in latest go-mangler, go-mutexes, go-structr
* remove old timeline package, add local timeline cache
* remove references to old timeline types that needed starting up in tests
* start adding page validation
* fix test-identified timeline cache package issues
* fix up more tests, fix missing required changes, etc
* add exclusion for test.out in gitignore
* clarify some things better in code comments
* tweak cache size limits
* fix list timeline cache fetching
* further list timeline fixes
* linter, ssssssssshhhhhhhhhhhh please
* fix linter hints
* reslice the output if it's beyond length of 'lim'
* remove old timeline initialization code, bump go-structr to v0.9.4
* continued from previous commit
* improved code comments
* don't allow multiple entries for BoostOfID values to prevent repeated boosts of same boosts
* finish writing more code comments
* some variable renaming, for ease of following
* change the way we update lo,hi paging values during timeline load
* improved code comments for updated / returned lo , hi paging values
* finish writing code comments for the StatusTimeline{} type itself
* fill in more code comments
* update go-structr version to latest with changed timeline unique indexing logic
* have a local and public timeline *per user*
* rewrite calls to public / local timeline calls
* remove the zero length check, as lo, hi values might still be set
* simplify timeline cache loading, fix lo/hi returns, fix timeline invalidation side-effects missing for some federated actions
* swap the lo, hi values 🤦
* add (now) missing slice reverse of tag timeline statuses when paging ASC
* remove local / public caches (is out of scope for this work), share more timeline code
* remove unnecessary change
* again, remove more unused code
* remove unused function to appease the linter
* move boost checking to prepare function
* fix use of timeline.lastOrder, fix incorrect range functions used
* remove comments for repeat code
* remove the boost logic from prepare function
* do a maximum of 5 loads, not 10
* add repeat boost filtering logic, update go-structr, general improvements
* more code comments
* add important note
* fix timeline tests now that timelines are returned in page order
* remove unused field
* add StatusTimeline{} tests
* add more status timeline tests
* start adding preloading support
* ensure repeat boosts are marked in preloaded entries
* share a bunch of the database load code in timeline cache, don't clear timelines on relationship change
* add logic to allow dynamic clear / preloading of timelines
* comment-out unused functions, but leave in place as we might end-up using them
* fix timeline preload state check
* much improved status timeline code comments
* more code comments, don't bother inserting statuses if timeline not preloaded
* shift around some logic to make sure things aren't accidentally left set
* finish writing code comments
* remove trim-after-insert behaviour
* fix-up some comments referring to old logic
* remove unsetting of lo, hi
* fix preload repeatBoost checking logic
* don't return on status filter errors, these are usually transient
* better concurrency safety in Clear() and Done()
* fix test broken due to addition of preloader
* fix repeatBoost logic that doesn't account for already-hidden repeatBoosts
* ensure edit submodels are dropped on cache insertion
* update code-comment to expand CAS accronym
* use a plus1hULID() instead of 24h
* remove unused functions
* add note that public / local timeline requester can be nil
* fix incorrect visibility filtering of tag timeline statuses
* ensure we filter home timeline statuses on local only
* some small re-orderings to confirm query params in correct places
* fix the local only home timeline filter func
279 lines
9.7 KiB
Go
279 lines
9.7 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"
|
|
|
|
"github.com/stretchr/testify/suite"
|
|
"github.com/superseriousbusiness/gotosocial/internal/cleaner"
|
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
|
"github.com/superseriousbusiness/gotosocial/internal/email"
|
|
"github.com/superseriousbusiness/gotosocial/internal/federation"
|
|
"github.com/superseriousbusiness/gotosocial/internal/filter/interaction"
|
|
"github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
|
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
|
"github.com/superseriousbusiness/gotosocial/internal/media"
|
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
|
"github.com/superseriousbusiness/gotosocial/internal/processing"
|
|
"github.com/superseriousbusiness/gotosocial/internal/state"
|
|
"github.com/superseriousbusiness/gotosocial/internal/storage"
|
|
"github.com/superseriousbusiness/gotosocial/internal/subscriptions"
|
|
"github.com/superseriousbusiness/gotosocial/internal/transport"
|
|
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
|
|
"github.com/superseriousbusiness/gotosocial/internal/util"
|
|
"github.com/superseriousbusiness/gotosocial/internal/webpush"
|
|
"github.com/superseriousbusiness/gotosocial/testrig"
|
|
)
|
|
|
|
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),
|
|
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(context.Background(), 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
|
|
}
|
|
|
|
// Send the push notification.
|
|
sendError := suite.webPushSender.Send(ctx, notification, nil, nil)
|
|
|
|
// 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,
|
|
StatusID: "01F8MHAMCHF6Y650WCRSCP4WMY",
|
|
Read: util.Ptr(false),
|
|
}
|
|
if err := suite.db.PutNotification(context.Background(), 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 github.com/superseriousbusiness/gotosocial/internal/webpush.newSenderWith
|
|
func newSenderWith(*http.Client, *state.State, *typeutils.Converter) webpush.Sender
|