gotosocial/internal/webpush/realsender_test.go

270 lines
9.3 KiB
Go
Raw Normal View History

[feature] Push notifications (#3587) * Update push subscription API model to be Mastodon 4.0 compatible * Add webpush-go dependency # Conflicts: # go.sum * Single-row table for storing instance's VAPID key pair * Generate VAPID key pair during startup * Add VAPID public key to instance info API * Return VAPID public key when registering an app * Store Web Push subscriptions in DB * Add Web Push sender (similar to email sender) * Add no-op push senders to most processor tests * Test Web Push notifications from workers * Delete Web Push subscriptions when account is deleted * Implement push subscription API * Linter fixes * Update Swagger * Fix enum to int migration * Fix GetVAPIDKeyPair * Create web push subscriptions table with indexes * Log Web Push server error messages * Send instance URL as Web Push JWT subject * Accept any 2xx code as a success * Fix malformed VAPID sub claim * Use packed notification flags * Remove unused date columns * Add notification type for update notifications Not used yet * Make GetVAPIDKeyPair idempotent and remove PutVAPIDKeyPair * Post-rebase fixes * go mod tidy * Special-case 400 errors other than 408/429 Most client errors should remove the subscription. * Improve titles, trim body to reasonable length * Disallow cleartext HTTP for Web Push servers * Fix lint * Remove redundant index on unique column Also removes redundant unique and notnull tags on ID column since these are implied by pk * Make realsender.go more readable * Use Tobi's style for wrapping errors * Restore treating all 5xx codes as temporary problems * Always load target account settings * Stub `policy` and `standard` * webpush.Sender: take type converter as ctor param * Move webpush.MockSender and noopSender into testrig
2025-01-24 00:47:30 +00: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/>.
package webpush_test
import (
"context"
"io"
"net/http"
"testing"
"time"
// for go:linkname
_ "unsafe"
[feature] Push notifications (#3587) * Update push subscription API model to be Mastodon 4.0 compatible * Add webpush-go dependency # Conflicts: # go.sum * Single-row table for storing instance's VAPID key pair * Generate VAPID key pair during startup * Add VAPID public key to instance info API * Return VAPID public key when registering an app * Store Web Push subscriptions in DB * Add Web Push sender (similar to email sender) * Add no-op push senders to most processor tests * Test Web Push notifications from workers * Delete Web Push subscriptions when account is deleted * Implement push subscription API * Linter fixes * Update Swagger * Fix enum to int migration * Fix GetVAPIDKeyPair * Create web push subscriptions table with indexes * Log Web Push server error messages * Send instance URL as Web Push JWT subject * Accept any 2xx code as a success * Fix malformed VAPID sub claim * Use packed notification flags * Remove unused date columns * Add notification type for update notifications Not used yet * Make GetVAPIDKeyPair idempotent and remove PutVAPIDKeyPair * Post-rebase fixes * go mod tidy * Special-case 400 errors other than 408/429 Most client errors should remove the subscription. * Improve titles, trim body to reasonable length * Disallow cleartext HTTP for Web Push servers * Fix lint * Remove redundant index on unique column Also removes redundant unique and notnull tags on ID column since these are implied by pk * Make realsender.go more readable * Use Tobi's style for wrapping errors * Restore treating all 5xx codes as temporary problems * Always load target account settings * Stub `policy` and `standard` * webpush.Sender: take type converter as ctor param * Move webpush.MockSender and noopSender into testrig
2025-01-24 00:47:30 +00:00
"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/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
testTokens map[string]*gtsmodel.Token
testClients map[string]*gtsmodel.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
testTags map[string]*gtsmodel.Tag
testMentions map[string]*gtsmodel.Mention
testEmojis map[string]*gtsmodel.Emoji
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.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()
suite.testTags = testrig.NewTestTags()
suite.testMentions = testrig.NewTestMentions()
suite.testEmojis = testrig.NewTestEmojis()
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)
testrig.StartTimelines(
&suite.state,
visibility.NewFilter(&suite.state),
suite.typeconverter,
)
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.db)
suite.emailSender = testrig.NewEmailSender("../../web/template/", nil)
suite.webPushSender = newSenderWith(
[feature] Push notifications (#3587) * Update push subscription API model to be Mastodon 4.0 compatible * Add webpush-go dependency # Conflicts: # go.sum * Single-row table for storing instance's VAPID key pair * Generate VAPID key pair during startup * Add VAPID public key to instance info API * Return VAPID public key when registering an app * Store Web Push subscriptions in DB * Add Web Push sender (similar to email sender) * Add no-op push senders to most processor tests * Test Web Push notifications from workers * Delete Web Push subscriptions when account is deleted * Implement push subscription API * Linter fixes * Update Swagger * Fix enum to int migration * Fix GetVAPIDKeyPair * Create web push subscriptions table with indexes * Log Web Push server error messages * Send instance URL as Web Push JWT subject * Accept any 2xx code as a success * Fix malformed VAPID sub claim * Use packed notification flags * Remove unused date columns * Add notification type for update notifications Not used yet * Make GetVAPIDKeyPair idempotent and remove PutVAPIDKeyPair * Post-rebase fixes * go mod tidy * Special-case 400 errors other than 408/429 Most client errors should remove the subscription. * Improve titles, trim body to reasonable length * Disallow cleartext HTTP for Web Push servers * Fix lint * Remove redundant index on unique column Also removes redundant unique and notnull tags on ID column since these are implied by pk * Make realsender.go more readable * Use Tobi's style for wrapping errors * Restore treating all 5xx codes as temporary problems * Always load target account settings * Stub `policy` and `standard` * webpush.Sender: take type converter as ctor param * Move webpush.MockSender and noopSender into testrig
2025-01-24 00:47:30 +00:00
&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(
statusCode int,
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(), 5*time.Second)
defer cancel()
notification, err := suite.state.DB.GetNotificationByID(ctx, suite.testNotifications["local_account_1_like"].ID)
if !suite.NoError(err) {
suite.FailNow("Couldn't fetch notification to send")
}
rc := &notifyingReadCloser{
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
}
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() {
suite.NoError(suite.simulatePushNotification(http.StatusOK, false))
}
// Test a rate-limiting response to sending a push notification.
// This should not delete the subscription.
func (suite *RealSenderStandardTestSuite) TestRateLimited() {
suite.NoError(suite.simulatePushNotification(http.StatusTooManyRequests, false))
}
// Test a non-special-cased client error response to sending a push notification.
// This should delete the subscription.
func (suite *RealSenderStandardTestSuite) TestClientError() {
suite.NoError(suite.simulatePushNotification(http.StatusBadRequest, true))
}
// Test a server error response to sending a push notification.
// This should not delete the subscription.
func (suite *RealSenderStandardTestSuite) TestServerError() {
suite.NoError(suite.simulatePushNotification(http.StatusInternalServerError, 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