gotosocial/internal/api/client/fileserver/servefile_test.go
kim 223025fc27
[security] transport.Controller{} and transport.Transport{} security and performance improvements (#564)
* cache transports in controller by privkey-generated pubkey, add retry logic to transport requests

Signed-off-by: kim <grufwub@gmail.com>

* update code comments, defer mutex unlocks

Signed-off-by: kim <grufwub@gmail.com>

* add count to 'performing request' log message

Signed-off-by: kim <grufwub@gmail.com>

* reduce repeated conversions of same url.URL object

Signed-off-by: kim <grufwub@gmail.com>

* move worker.Worker to concurrency subpackage, add WorkQueue type, limit transport http client use by WorkQueue

Signed-off-by: kim <grufwub@gmail.com>

* fix security advisories regarding max outgoing conns, max rsp body size

- implemented by a new httpclient.Client{} that wraps an underlying
  client with a queue to limit connections, and limit reader wrapping
  a response body with a configured maximum size
- update pub.HttpClient args passed around to be this new httpclient.Client{}

Signed-off-by: kim <grufwub@gmail.com>

* add httpclient tests, move ip validation to separate package + change mechanism

Signed-off-by: kim <grufwub@gmail.com>

* fix merge conflicts

Signed-off-by: kim <grufwub@gmail.com>

* use singular mutex in transport rather than separate signer mus

Signed-off-by: kim <grufwub@gmail.com>

* improved useragent string

Signed-off-by: kim <grufwub@gmail.com>

* add note regarding missing test

Signed-off-by: kim <grufwub@gmail.com>

* remove useragent field from transport (instead store in controller)

Signed-off-by: kim <grufwub@gmail.com>

* shutup linter

Signed-off-by: kim <grufwub@gmail.com>

* reset other signing headers on each loop iteration

Signed-off-by: kim <grufwub@gmail.com>

* respect request ctx during retry-backoff sleep period

Signed-off-by: kim <grufwub@gmail.com>

* use external pkg with docs explaining performance "hack"

Signed-off-by: kim <grufwub@gmail.com>

* use http package constants instead of string method literals

Signed-off-by: kim <grufwub@gmail.com>

* add license file headers

Signed-off-by: kim <grufwub@gmail.com>

* update code comment to match new func names

Signed-off-by: kim <grufwub@gmail.com>

* updates to user-agent string

Signed-off-by: kim <grufwub@gmail.com>

* update signed testrig models to fit with new transport logic (instead uses separate signer now)

Signed-off-by: kim <grufwub@gmail.com>

* fuck you linter

Signed-off-by: kim <grufwub@gmail.com>
2022-05-15 11:16:43 +02:00

218 lines
7.2 KiB
Go

/*
GoToSocial
Copyright (C) 2021-2022 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 fileserver_test
import (
"context"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"
"codeberg.org/gruf/go-store/kv"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/suite"
"github.com/superseriousbusiness/gotosocial/internal/api/client/fileserver"
"github.com/superseriousbusiness/gotosocial/internal/concurrency"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/email"
"github.com/superseriousbusiness/gotosocial/internal/federation"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/media"
"github.com/superseriousbusiness/gotosocial/internal/messages"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
"github.com/superseriousbusiness/gotosocial/internal/processing"
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
"github.com/superseriousbusiness/gotosocial/testrig"
)
type ServeFileTestSuite struct {
// standard suite interfaces
suite.Suite
db db.DB
storage *kv.KVStore
federator federation.Federator
tc typeutils.TypeConverter
processor processing.Processor
mediaManager media.Manager
oauthServer oauth.Server
emailSender email.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
// item being tested
fileServer *fileserver.FileServer
}
/*
TEST INFRASTRUCTURE
*/
func (suite *ServeFileTestSuite) SetupSuite() {
// setup standard items
testrig.InitTestConfig()
testrig.InitTestLog()
fedWorker := concurrency.NewWorkerPool[messages.FromFederator](-1, -1)
clientWorker := concurrency.NewWorkerPool[messages.FromClientAPI](-1, -1)
suite.db = testrig.NewTestDB()
suite.storage = testrig.NewTestStorage()
suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil), suite.db, fedWorker), suite.storage, testrig.NewTestMediaManager(suite.db, suite.storage), fedWorker)
suite.emailSender = testrig.NewEmailSender("../../../../web/template/", nil)
suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator, suite.emailSender, testrig.NewTestMediaManager(suite.db, suite.storage), clientWorker, fedWorker)
suite.tc = testrig.NewTestTypeConverter(suite.db)
suite.mediaManager = testrig.NewTestMediaManager(suite.db, suite.storage)
suite.oauthServer = testrig.NewTestOauthServer(suite.db)
// setup module being tested
suite.fileServer = fileserver.New(suite.processor).(*fileserver.FileServer)
}
func (suite *ServeFileTestSuite) TearDownSuite() {
if err := suite.db.Stop(context.Background()); err != nil {
logrus.Panicf("error closing db connection: %s", err)
}
}
func (suite *ServeFileTestSuite) SetupTest() {
testrig.StandardDBSetup(suite.db, nil)
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()
}
func (suite *ServeFileTestSuite) TearDownTest() {
testrig.StandardDBTeardown(suite.db)
testrig.StandardStorageTeardown(suite.storage)
}
/*
ACTUAL TESTS
*/
func (suite *ServeFileTestSuite) TestServeOriginalFileSuccessful() {
targetAttachment, ok := suite.testAttachments["admin_account_status_1_attachment_1"]
suite.True(ok)
suite.NotNil(targetAttachment)
recorder := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(recorder)
ctx.Request = httptest.NewRequest(http.MethodGet, targetAttachment.URL, nil)
ctx.Request.Header.Set("accept", "*/*")
// normally the router would populate these params from the path values,
// but because we're calling the ServeFile function directly, we need to set them manually.
ctx.Params = gin.Params{
gin.Param{
Key: fileserver.AccountIDKey,
Value: targetAttachment.AccountID,
},
gin.Param{
Key: fileserver.MediaTypeKey,
Value: string(media.TypeAttachment),
},
gin.Param{
Key: fileserver.MediaSizeKey,
Value: string(media.SizeOriginal),
},
gin.Param{
Key: fileserver.FileNameKey,
Value: fmt.Sprintf("%s.jpeg", targetAttachment.ID),
},
}
// call the function we're testing and check status code
suite.fileServer.ServeFile(ctx)
suite.EqualValues(http.StatusOK, recorder.Code)
suite.EqualValues("image/jpeg", recorder.Header().Get("content-type"))
b, err := ioutil.ReadAll(recorder.Body)
suite.NoError(err)
suite.NotNil(b)
fileInStorage, err := suite.storage.Get(targetAttachment.File.Path)
suite.NoError(err)
suite.NotNil(fileInStorage)
suite.Equal(b, fileInStorage)
}
func (suite *ServeFileTestSuite) TestServeSmallFileSuccessful() {
targetAttachment, ok := suite.testAttachments["admin_account_status_1_attachment_1"]
suite.True(ok)
suite.NotNil(targetAttachment)
recorder := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(recorder)
ctx.Request = httptest.NewRequest(http.MethodGet, targetAttachment.Thumbnail.URL, nil)
ctx.Request.Header.Set("accept", "*/*")
// normally the router would populate these params from the path values,
// but because we're calling the ServeFile function directly, we need to set them manually.
ctx.Params = gin.Params{
gin.Param{
Key: fileserver.AccountIDKey,
Value: targetAttachment.AccountID,
},
gin.Param{
Key: fileserver.MediaTypeKey,
Value: string(media.TypeAttachment),
},
gin.Param{
Key: fileserver.MediaSizeKey,
Value: string(media.SizeSmall),
},
gin.Param{
Key: fileserver.FileNameKey,
Value: fmt.Sprintf("%s.jpeg", targetAttachment.ID),
},
}
// call the function we're testing and check status code
suite.fileServer.ServeFile(ctx)
suite.EqualValues(http.StatusOK, recorder.Code)
suite.EqualValues("image/jpeg", recorder.Header().Get("content-type"))
b, err := ioutil.ReadAll(recorder.Body)
suite.NoError(err)
suite.NotNil(b)
fileInStorage, err := suite.storage.Get(targetAttachment.Thumbnail.Path)
suite.NoError(err)
suite.NotNil(fileInStorage)
suite.Equal(b, fileInStorage)
}
func TestServeFileTestSuite(t *testing.T) {
suite.Run(t, new(ServeFileTestSuite))
}