diff options
Diffstat (limited to 'internal/api')
| -rw-r--r-- | internal/api/client/account/accountupdate_test.go | 6 | ||||
| -rw-r--r-- | internal/api/client/fileserver/servefile_test.go | 4 | ||||
| -rw-r--r-- | internal/api/client/media/mediacreate_test.go | 4 | ||||
| -rw-r--r-- | internal/api/client/status/statusboost_test.go | 4 | ||||
| -rw-r--r-- | internal/api/client/status/statuscreate_test.go | 4 | ||||
| -rw-r--r-- | internal/api/client/status/statusfave_test.go | 4 | ||||
| -rw-r--r-- | internal/api/client/status/statusfavedby_test.go | 4 | ||||
| -rw-r--r-- | internal/api/client/status/statusget_test.go | 4 | ||||
| -rw-r--r-- | internal/api/client/status/statusunfave_test.go | 4 | ||||
| -rw-r--r-- | internal/api/s2s/user/repliesget.go | 186 | ||||
| -rw-r--r-- | internal/api/s2s/user/repliesget_test.go | 241 | ||||
| -rw-r--r-- | internal/api/s2s/user/user.go | 10 | ||||
| -rw-r--r-- | internal/api/s2s/user/user_test.go | 16 | ||||
| -rw-r--r-- | internal/api/s2s/user/userget_test.go | 66 | 
14 files changed, 479 insertions, 78 deletions
| diff --git a/internal/api/client/account/accountupdate_test.go b/internal/api/client/account/accountupdate_test.go index 341b865ff..349429625 100644 --- a/internal/api/client/account/accountupdate_test.go +++ b/internal/api/client/account/accountupdate_test.go @@ -53,10 +53,10 @@ func (suite *AccountUpdateTestSuite) SetupTest() {  	suite.db = testrig.NewTestDB()  	suite.storage = testrig.NewTestStorage()  	suite.log = testrig.NewTestLog() -	suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil)), suite.storage) +	suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil), suite.db), suite.storage)  	suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator)  	suite.accountModule = account.New(suite.config, suite.processor, suite.log).(*account.Module) -	testrig.StandardDBSetup(suite.db) +	testrig.StandardDBSetup(suite.db, nil)  	testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media")  } @@ -80,6 +80,8 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandler()  	ctx, _ := gin.CreateTestContext(recorder)  	ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])  	ctx.Set(oauth.SessionAuthorizedToken, oauth.TokenToOauthToken(suite.testTokens["local_account_1"])) +	ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"]) +	ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])  	ctx.Request = httptest.NewRequest(http.MethodPatch, fmt.Sprintf("http://localhost:8080/%s", account.UpdateCredentialsPath), bytes.NewReader(requestBody.Bytes())) // the endpoint we're hitting  	ctx.Request.Header.Set("Content-Type", w.FormDataContentType())  	suite.accountModule.AccountUpdateCredentialsPATCHHandler(ctx) diff --git a/internal/api/client/fileserver/servefile_test.go b/internal/api/client/fileserver/servefile_test.go index cb503facb..4eec3bae5 100644 --- a/internal/api/client/fileserver/servefile_test.go +++ b/internal/api/client/fileserver/servefile_test.go @@ -78,7 +78,7 @@ func (suite *ServeFileTestSuite) SetupSuite() {  	suite.db = testrig.NewTestDB()  	suite.log = testrig.NewTestLog()  	suite.storage = testrig.NewTestStorage() -	suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil)), suite.storage) +	suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil), suite.db), suite.storage)  	suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator)  	suite.tc = testrig.NewTestTypeConverter(suite.db)  	suite.mediaHandler = testrig.NewTestMediaHandler(suite.db, suite.storage) @@ -95,7 +95,7 @@ func (suite *ServeFileTestSuite) TearDownSuite() {  }  func (suite *ServeFileTestSuite) SetupTest() { -	testrig.StandardDBSetup(suite.db) +	testrig.StandardDBSetup(suite.db, nil)  	testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media")  	suite.testTokens = testrig.NewTestTokens()  	suite.testClients = testrig.NewTestClients() diff --git a/internal/api/client/media/mediacreate_test.go b/internal/api/client/media/mediacreate_test.go index 89a77a729..a61a36324 100644 --- a/internal/api/client/media/mediacreate_test.go +++ b/internal/api/client/media/mediacreate_test.go @@ -84,7 +84,7 @@ func (suite *MediaCreateTestSuite) SetupSuite() {  	suite.tc = testrig.NewTestTypeConverter(suite.db)  	suite.mediaHandler = testrig.NewTestMediaHandler(suite.db, suite.storage)  	suite.oauthServer = testrig.NewTestOauthServer(suite.db) -	suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil)), suite.storage) +	suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil), suite.db), suite.storage)  	suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator)  	// setup module being tested @@ -98,7 +98,7 @@ func (suite *MediaCreateTestSuite) TearDownSuite() {  }  func (suite *MediaCreateTestSuite) SetupTest() { -	testrig.StandardDBSetup(suite.db) +	testrig.StandardDBSetup(suite.db, nil)  	testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media")  	suite.testTokens = testrig.NewTestTokens()  	suite.testClients = testrig.NewTestClients() diff --git a/internal/api/client/status/statusboost_test.go b/internal/api/client/status/statusboost_test.go index 9400aeddc..fbe267fac 100644 --- a/internal/api/client/status/statusboost_test.go +++ b/internal/api/client/status/statusboost_test.go @@ -52,10 +52,10 @@ func (suite *StatusBoostTestSuite) SetupTest() {  	suite.db = testrig.NewTestDB()  	suite.storage = testrig.NewTestStorage()  	suite.log = testrig.NewTestLog() -	suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil)), suite.storage) +	suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil), suite.db), suite.storage)  	suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator)  	suite.statusModule = status.New(suite.config, suite.processor, suite.log).(*status.Module) -	testrig.StandardDBSetup(suite.db) +	testrig.StandardDBSetup(suite.db, nil)  	testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media")  } diff --git a/internal/api/client/status/statuscreate_test.go b/internal/api/client/status/statuscreate_test.go index dd4a4386b..603432724 100644 --- a/internal/api/client/status/statuscreate_test.go +++ b/internal/api/client/status/statuscreate_test.go @@ -58,10 +58,10 @@ func (suite *StatusCreateTestSuite) SetupTest() {  	suite.storage = testrig.NewTestStorage()  	suite.log = testrig.NewTestLog()  	suite.tc = testrig.NewTestTypeConverter(suite.db) -	suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil)), suite.storage) +	suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil), suite.db), suite.storage)  	suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator)  	suite.statusModule = status.New(suite.config, suite.processor, suite.log).(*status.Module) -	testrig.StandardDBSetup(suite.db) +	testrig.StandardDBSetup(suite.db, nil)  	testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media")  } diff --git a/internal/api/client/status/statusfave_test.go b/internal/api/client/status/statusfave_test.go index b1cafc2fb..0f44b5e90 100644 --- a/internal/api/client/status/statusfave_test.go +++ b/internal/api/client/status/statusfave_test.go @@ -55,10 +55,10 @@ func (suite *StatusFaveTestSuite) SetupTest() {  	suite.db = testrig.NewTestDB()  	suite.storage = testrig.NewTestStorage()  	suite.log = testrig.NewTestLog() -	suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil)), suite.storage) +	suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil), suite.db), suite.storage)  	suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator)  	suite.statusModule = status.New(suite.config, suite.processor, suite.log).(*status.Module) -	testrig.StandardDBSetup(suite.db) +	testrig.StandardDBSetup(suite.db, nil)  	testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media")  } diff --git a/internal/api/client/status/statusfavedby_test.go b/internal/api/client/status/statusfavedby_test.go index b6e1591e0..22a549b30 100644 --- a/internal/api/client/status/statusfavedby_test.go +++ b/internal/api/client/status/statusfavedby_test.go @@ -55,10 +55,10 @@ func (suite *StatusFavedByTestSuite) SetupTest() {  	suite.db = testrig.NewTestDB()  	suite.storage = testrig.NewTestStorage()  	suite.log = testrig.NewTestLog() -	suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil)), suite.storage) +	suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil), suite.db), suite.storage)  	suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator)  	suite.statusModule = status.New(suite.config, suite.processor, suite.log).(*status.Module) -	testrig.StandardDBSetup(suite.db) +	testrig.StandardDBSetup(suite.db, nil)  	testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media")  } diff --git a/internal/api/client/status/statusget_test.go b/internal/api/client/status/statusget_test.go index 1bbf48a91..1c700aaa5 100644 --- a/internal/api/client/status/statusget_test.go +++ b/internal/api/client/status/statusget_test.go @@ -45,10 +45,10 @@ func (suite *StatusGetTestSuite) SetupTest() {  	suite.db = testrig.NewTestDB()  	suite.storage = testrig.NewTestStorage()  	suite.log = testrig.NewTestLog() -	suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil)), suite.storage) +	suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil), suite.db), suite.storage)  	suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator)  	suite.statusModule = status.New(suite.config, suite.processor, suite.log).(*status.Module) -	testrig.StandardDBSetup(suite.db) +	testrig.StandardDBSetup(suite.db, nil)  	testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media")  } diff --git a/internal/api/client/status/statusunfave_test.go b/internal/api/client/status/statusunfave_test.go index 36144c5ce..a5f267f4c 100644 --- a/internal/api/client/status/statusunfave_test.go +++ b/internal/api/client/status/statusunfave_test.go @@ -55,10 +55,10 @@ func (suite *StatusUnfaveTestSuite) SetupTest() {  	suite.db = testrig.NewTestDB()  	suite.storage = testrig.NewTestStorage()  	suite.log = testrig.NewTestLog() -	suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil)), suite.storage) +	suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil), suite.db), suite.storage)  	suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator)  	suite.statusModule = status.New(suite.config, suite.processor, suite.log).(*status.Module) -	testrig.StandardDBSetup(suite.db) +	testrig.StandardDBSetup(suite.db, nil)  	testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media")  } diff --git a/internal/api/s2s/user/repliesget.go b/internal/api/s2s/user/repliesget.go new file mode 100644 index 000000000..951cc428c --- /dev/null +++ b/internal/api/s2s/user/repliesget.go @@ -0,0 +1,186 @@ +package user + +import ( +	"context" +	"encoding/json" +	"fmt" +	"net/http" +	"strconv" + +	"github.com/gin-gonic/gin" +	"github.com/sirupsen/logrus" +	"github.com/superseriousbusiness/gotosocial/internal/util" +) + +// StatusRepliesGETHandler swagger:operation GET /users/{username}/statuses/{status}/replies s2sRepliesGet +// +// Get the replies collection for a status. +// +// Note that the response will be a Collection with a page as `first`, as shown below, if `page` is `false`. +// +// If `page` is `true`, then the response will be a single `CollectionPage` without the wrapping `Collection`. +// +// HTTP signature is required on the request. +// +// --- +// tags: +// - s2s/federation +// +// produces: +// - application/activity+json +// +// parameters: +// - name: username +//   type: string +//   description: Username of the account. +//   in: path +//   required: true +// - name: status +//   type: string +//   description: ID of the status. +//   in: path +//   required: true +// - name: page +//   type: boolean +//   description: Return response as a CollectionPage. +//   in: query +//   default: false +// - name: only_other_accounts +//   type: boolean +//   description: Return replies only from accounts other than the status owner. +//   in: query +//   default: false +// - name: min_id +//   type: string +//   description: Minimum ID of the next status, used for paging. +//   in: query +// +// responses: +//   '200': +//      in: body +//      schema: +//        "$ref": "#/definitions/swaggerStatusRepliesCollection" +//   '400': +//      description: bad request +//   '401': +//      description: unauthorized +//   '403': +//      description: forbidden +//   '404': +//      description: not found +func (m *Module) StatusRepliesGETHandler(c *gin.Context) { +	l := m.log.WithFields(logrus.Fields{ +		"func": "StatusRepliesGETHandler", +		"url":  c.Request.RequestURI, +	}) + +	requestedUsername := c.Param(UsernameKey) +	if requestedUsername == "" { +		c.JSON(http.StatusBadRequest, gin.H{"error": "no username specified in request"}) +		return +	} + +	requestedStatusID := c.Param(StatusIDKey) +	if requestedStatusID == "" { +		c.JSON(http.StatusBadRequest, gin.H{"error": "no status id specified in request"}) +		return +	} + +	page := false +	pageString := c.Query(PageKey) +	if pageString != "" { +		i, err := strconv.ParseBool(pageString) +		if err != nil { +			l.Debugf("error parsing page string: %s", err) +			c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse page query param"}) +			return +		} +		page = i +	} + +	onlyOtherAccounts := false +	onlyOtherAccountsString := c.Query(OnlyOtherAccountsKey) +	if onlyOtherAccountsString != "" { +		i, err := strconv.ParseBool(onlyOtherAccountsString) +		if err != nil { +			l.Debugf("error parsing only_other_accounts string: %s", err) +			c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse only_other_accounts query param"}) +			return +		} +		onlyOtherAccounts = i +	} + +	minID := "" +	minIDString := c.Query(MinIDKey) +	if minIDString != "" { +		minID = minIDString +	} + +	// make sure this actually an AP request +	format := c.NegotiateFormat(ActivityPubAcceptHeaders...) +	if format == "" { +		c.JSON(http.StatusNotAcceptable, gin.H{"error": "could not negotiate format with given Accept header(s)"}) +		return +	} +	l.Tracef("negotiated format: %s", format) + +	// transfer the signature verifier from the gin context to the request context +	ctx := c.Request.Context() +	verifier, signed := c.Get(string(util.APRequestingPublicKeyVerifier)) +	if signed { +		ctx = context.WithValue(ctx, util.APRequestingPublicKeyVerifier, verifier) +	} + +	replies, err := m.processor.GetFediStatusReplies(ctx, requestedUsername, requestedStatusID, page, onlyOtherAccounts, minID, c.Request.URL) +	if err != nil { +		l.Info(err.Error()) +		c.JSON(err.Code(), gin.H{"error": err.Safe()}) +		return +	} + +	b, mErr := json.Marshal(replies) +	if mErr != nil { +		err := fmt.Errorf("could not marshal json: %s", mErr) +		l.Error(err) +		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) +		return +	} + +	c.Data(http.StatusOK, format, b) +} + +// SwaggerStatusRepliesCollection represents a response to GET /users/{username}/statuses/{status}/replies. +// swagger:model swaggerStatusRepliesCollection +type SwaggerStatusRepliesCollection struct { +	// ActivityStreams context. +	// example: https://www.w3.org/ns/activitystreams +	Context string `json:"@context"` +	// ActivityStreams ID. +	// example: https://example.org/users/some_user/statuses/106717595988259568/replies +	ID string `json:"id"` +	// ActivityStreams type. +	// example: Collection +	Type string `json:"type"` +	// ActivityStreams first property. +	First SwaggerStatusRepliesCollectionPage `json:"first"` +} + +// SwaggerStatusRepliesCollectionPage represents one page of a collection. +// swagger:model swaggerStatusRepliesCollectionPage +type SwaggerStatusRepliesCollectionPage struct { +	// ActivityStreams ID. +	// example: https://example.org/users/some_user/statuses/106717595988259568/replies?page=true +	ID string `json:"id"` +	// ActivityStreams type. +	// example: CollectionPage +	Type string `json:"type"` +	// Link to the next page. +	// example: https://example.org/users/some_user/statuses/106717595988259568/replies?only_other_accounts=true&page=true +	Next string `json:"next"` +	// Collection this page belongs to. +	// example: https://example.org/users/some_user/statuses/106717595988259568/replies +	PartOf string `json:"partOf"` +	// Items on this page. +	// example: ["https://example.org/users/some_other_user/statuses/086417595981111564", "https://another.example.com/users/another_user/statuses/01FCN8XDV3YG7B4R42QA6YQZ9R"] +	Items []string `json:"items"` +} diff --git a/internal/api/s2s/user/repliesget_test.go b/internal/api/s2s/user/repliesget_test.go new file mode 100644 index 000000000..75edbc882 --- /dev/null +++ b/internal/api/s2s/user/repliesget_test.go @@ -0,0 +1,241 @@ +package user_test + +import ( +	"context" +	"encoding/json" +	"fmt" +	"io/ioutil" +	"net/http" +	"net/http/httptest" +	"testing" + +	"github.com/gin-gonic/gin" +	"github.com/go-fed/activity/streams" +	"github.com/go-fed/activity/streams/vocab" +	"github.com/stretchr/testify/assert" +	"github.com/stretchr/testify/suite" +	"github.com/superseriousbusiness/gotosocial/internal/api/s2s/user" +	"github.com/superseriousbusiness/gotosocial/internal/api/security" +	"github.com/superseriousbusiness/gotosocial/testrig" +) + +type RepliesGetTestSuite struct { +	UserStandardTestSuite +} + +func (suite *RepliesGetTestSuite) 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() +} + +func (suite *RepliesGetTestSuite) SetupTest() { +	suite.config = testrig.NewTestConfig() +	suite.db = testrig.NewTestDB() +	suite.tc = testrig.NewTestTypeConverter(suite.db) +	suite.storage = testrig.NewTestStorage() +	suite.log = testrig.NewTestLog() +	suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil), suite.db), suite.storage) +	suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator) +	suite.userModule = user.New(suite.config, suite.processor, suite.log).(*user.Module) +	suite.securityModule = security.New(suite.config, suite.db, suite.log).(*security.Module) +	testrig.StandardDBSetup(suite.db, suite.testAccounts) +	testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media") +} + +func (suite *RepliesGetTestSuite) TearDownTest() { +	testrig.StandardDBTeardown(suite.db) +	testrig.StandardStorageTeardown(suite.storage) +} + +func (suite *RepliesGetTestSuite) TestGetReplies() { +	// the dereference we're gonna use +	derefRequests := testrig.NewTestDereferenceRequests(suite.testAccounts) +	signedRequest := derefRequests["foss_satan_dereference_local_account_1_status_1_replies"] +	targetAccount := suite.testAccounts["local_account_1"] +	targetStatus := suite.testStatuses["local_account_1_status_1"] + +	tc := testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil), suite.db) +	federator := testrig.NewTestFederator(suite.db, tc, suite.storage) +	processor := testrig.NewTestProcessor(suite.db, suite.storage, federator) +	userModule := user.New(suite.config, processor, suite.log).(*user.Module) + +	// setup request +	recorder := httptest.NewRecorder() +	ctx, _ := gin.CreateTestContext(recorder) +	ctx.Request = httptest.NewRequest(http.MethodGet, targetStatus.URI+"/replies", nil) // the endpoint we're hitting +	ctx.Request.Header.Set("Signature", signedRequest.SignatureHeader) +	ctx.Request.Header.Set("Date", signedRequest.DateHeader) + +	// we need to pass the context through signature check first to set appropriate values on it +	suite.securityModule.SignatureCheck(ctx) + +	// 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:   user.UsernameKey, +			Value: targetAccount.Username, +		}, +		gin.Param{ +			Key:   user.StatusIDKey, +			Value: targetStatus.ID, +		}, +	} + +	// trigger the function being tested +	userModule.StatusRepliesGETHandler(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) +	assert.Equal(suite.T(), `{"@context":"https://www.w3.org/ns/activitystreams","first":{"id":"http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY/replies?page=true","next":"http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY/replies?only_other_accounts=false\u0026page=true","partOf":"http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY/replies","type":"CollectionPage"},"id":"http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY/replies","type":"Collection"}`, string(b)) + +	// should be a Collection +	m := make(map[string]interface{}) +	err = json.Unmarshal(b, &m) +	assert.NoError(suite.T(), err) + +	t, err := streams.ToType(context.Background(), m) +	assert.NoError(suite.T(), err) + +	_, ok := t.(vocab.ActivityStreamsCollection) +	assert.True(suite.T(), ok) +} + +func (suite *RepliesGetTestSuite) TestGetRepliesNext() { +	// the dereference we're gonna use +	derefRequests := testrig.NewTestDereferenceRequests(suite.testAccounts) +	signedRequest := derefRequests["foss_satan_dereference_local_account_1_status_1_replies_next"] +	targetAccount := suite.testAccounts["local_account_1"] +	targetStatus := suite.testStatuses["local_account_1_status_1"] + +	tc := testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil), suite.db) +	federator := testrig.NewTestFederator(suite.db, tc, suite.storage) +	processor := testrig.NewTestProcessor(suite.db, suite.storage, federator) +	userModule := user.New(suite.config, processor, suite.log).(*user.Module) + +	// setup request +	recorder := httptest.NewRecorder() +	ctx, _ := gin.CreateTestContext(recorder) +	ctx.Request = httptest.NewRequest(http.MethodGet, targetStatus.URI+"/replies?only_other_accounts=false&page=true", nil) // the endpoint we're hitting +	ctx.Request.Header.Set("Signature", signedRequest.SignatureHeader) +	ctx.Request.Header.Set("Date", signedRequest.DateHeader) + +	// we need to pass the context through signature check first to set appropriate values on it +	suite.securityModule.SignatureCheck(ctx) + +	// 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:   user.UsernameKey, +			Value: targetAccount.Username, +		}, +		gin.Param{ +			Key:   user.StatusIDKey, +			Value: targetStatus.ID, +		}, +	} + +	// trigger the function being tested +	userModule.StatusRepliesGETHandler(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) + +	assert.Equal(suite.T(), `{"@context":"https://www.w3.org/ns/activitystreams","id":"http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY/replies?page=true\u0026only_other_accounts=false","items":"http://localhost:8080/users/1happyturtle/statuses/01FCQSQ667XHJ9AV9T27SJJSX5","next":"http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY/replies?only_other_accounts=false\u0026page=true\u0026min_id=01FCQSQ667XHJ9AV9T27SJJSX5","partOf":"http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY/replies","type":"CollectionPage"}`, string(b)) + +	// should be a Collection +	m := make(map[string]interface{}) +	err = json.Unmarshal(b, &m) +	assert.NoError(suite.T(), err) + +	t, err := streams.ToType(context.Background(), m) +	assert.NoError(suite.T(), err) + +	page, ok := t.(vocab.ActivityStreamsCollectionPage) +	assert.True(suite.T(), ok) + +	assert.Equal(suite.T(), page.GetActivityStreamsItems().Len(), 1) +} + +func (suite *RepliesGetTestSuite) TestGetRepliesLast() { +	// the dereference we're gonna use +	derefRequests := testrig.NewTestDereferenceRequests(suite.testAccounts) +	signedRequest := derefRequests["foss_satan_dereference_local_account_1_status_1_replies_last"] +	targetAccount := suite.testAccounts["local_account_1"] +	targetStatus := suite.testStatuses["local_account_1_status_1"] + +	tc := testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil), suite.db) +	federator := testrig.NewTestFederator(suite.db, tc, suite.storage) +	processor := testrig.NewTestProcessor(suite.db, suite.storage, federator) +	userModule := user.New(suite.config, processor, suite.log).(*user.Module) + +	// setup request +	recorder := httptest.NewRecorder() +	ctx, _ := gin.CreateTestContext(recorder) +	ctx.Request = httptest.NewRequest(http.MethodGet, targetStatus.URI+"/replies?only_other_accounts=false&page=true&min_id=01FCQSQ667XHJ9AV9T27SJJSX5", nil) // the endpoint we're hitting +	ctx.Request.Header.Set("Signature", signedRequest.SignatureHeader) +	ctx.Request.Header.Set("Date", signedRequest.DateHeader) + +	// we need to pass the context through signature check first to set appropriate values on it +	suite.securityModule.SignatureCheck(ctx) + +	// 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:   user.UsernameKey, +			Value: targetAccount.Username, +		}, +		gin.Param{ +			Key:   user.StatusIDKey, +			Value: targetStatus.ID, +		}, +	} + +	// trigger the function being tested +	userModule.StatusRepliesGETHandler(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)) +	assert.Equal(suite.T(), `{"@context":"https://www.w3.org/ns/activitystreams","id":"http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY/replies?page=true\u0026only_other_accounts=false\u0026min_id=01FCQSQ667XHJ9AV9T27SJJSX5","items":[],"next":"http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY/replies?only_other_accounts=false\u0026page=true","partOf":"http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY/replies","type":"CollectionPage"}`, string(b)) + +	// should be a Collection +	m := make(map[string]interface{}) +	err = json.Unmarshal(b, &m) +	assert.NoError(suite.T(), err) + +	t, err := streams.ToType(context.Background(), m) +	assert.NoError(suite.T(), err) + +	page, ok := t.(vocab.ActivityStreamsCollectionPage) +	assert.True(suite.T(), ok) + +	assert.Equal(suite.T(), page.GetActivityStreamsItems().Len(), 0) +} + +func TestRepliesGetTestSuite(t *testing.T) { +	suite.Run(t, new(RepliesGetTestSuite)) +} diff --git a/internal/api/s2s/user/user.go b/internal/api/s2s/user/user.go index 0cb8e1e90..b5ff9a699 100644 --- a/internal/api/s2s/user/user.go +++ b/internal/api/s2s/user/user.go @@ -34,6 +34,13 @@ const (  	UsernameKey = "username"  	// StatusIDKey is for status IDs  	StatusIDKey = "status" +	// OnlyOtherAccountsKey is for filtering status responses. +	OnlyOtherAccountsKey = "only_other_accounts" +	// MinIDKey is for filtering status responses. +	MinIDKey = "min_id" +	// PageKey is for filtering status responses. +	PageKey = "page" +  	// UsersBasePath is the base path for serving information about Users eg https://example.org/users  	UsersBasePath = "/" + util.UsersPath  	// UsersBasePathWithUsername is just the users base path with the Username key in it. @@ -50,6 +57,8 @@ const (  	UsersFollowingPath = UsersBasePathWithUsername + "/" + util.FollowingPath  	// UsersStatusPath is for serving GET requests to a particular status by a user, with the given username key and status ID  	UsersStatusPath = UsersBasePathWithUsername + "/" + util.StatusesPath + "/:" + StatusIDKey +	// UsersStatusRepliesPath is for serving the replies collection of a status. +	UsersStatusRepliesPath = UsersStatusPath + "/replies"  )  // ActivityPubAcceptHeaders represents the Accept headers mentioned here: @@ -83,5 +92,6 @@ func (m *Module) Route(s router.Router) error {  	s.AttachHandler(http.MethodGet, UsersFollowingPath, m.FollowingGETHandler)  	s.AttachHandler(http.MethodGet, UsersStatusPath, m.StatusGETHandler)  	s.AttachHandler(http.MethodGet, UsersPublicKeyPath, m.PublicKeyGETHandler) +	s.AttachHandler(http.MethodGet, UsersStatusRepliesPath, m.StatusRepliesGETHandler)  	return nil  } diff --git a/internal/api/s2s/user/user_test.go b/internal/api/s2s/user/user_test.go index 91d1ea32d..71d4395eb 100644 --- a/internal/api/s2s/user/user_test.go +++ b/internal/api/s2s/user/user_test.go @@ -4,6 +4,7 @@ import (  	"github.com/sirupsen/logrus"  	"github.com/stretchr/testify/suite"  	"github.com/superseriousbusiness/gotosocial/internal/api/s2s/user" +	"github.com/superseriousbusiness/gotosocial/internal/api/security"  	"github.com/superseriousbusiness/gotosocial/internal/blob"  	"github.com/superseriousbusiness/gotosocial/internal/config"  	"github.com/superseriousbusiness/gotosocial/internal/db" @@ -18,13 +19,14 @@ import (  type UserStandardTestSuite struct {  	// standard suite interfaces  	suite.Suite -	config    *config.Config -	db        db.DB -	log       *logrus.Logger -	tc        typeutils.TypeConverter -	federator federation.Federator -	processor processing.Processor -	storage   blob.Storage +	config         *config.Config +	db             db.DB +	log            *logrus.Logger +	tc             typeutils.TypeConverter +	federator      federation.Federator +	processor      processing.Processor +	storage        blob.Storage +	securityModule *security.Module  	// standard suite models  	testTokens       map[string]*oauth.Token diff --git a/internal/api/s2s/user/userget_test.go b/internal/api/s2s/user/userget_test.go index d20148802..ab0015c57 100644 --- a/internal/api/s2s/user/userget_test.go +++ b/internal/api/s2s/user/userget_test.go @@ -1,16 +1,11 @@  package user_test  import ( -	"bytes"  	"context" -	"crypto/x509"  	"encoding/json" -	"encoding/pem" -	"fmt"  	"io/ioutil"  	"net/http"  	"net/http/httptest" -	"strings"  	"testing"  	"github.com/gin-gonic/gin" @@ -19,6 +14,7 @@ import (  	"github.com/stretchr/testify/assert"  	"github.com/stretchr/testify/suite"  	"github.com/superseriousbusiness/gotosocial/internal/api/s2s/user" +	"github.com/superseriousbusiness/gotosocial/internal/api/security"  	"github.com/superseriousbusiness/gotosocial/testrig"  ) @@ -42,10 +38,11 @@ func (suite *UserGetTestSuite) SetupTest() {  	suite.tc = testrig.NewTestTypeConverter(suite.db)  	suite.storage = testrig.NewTestStorage()  	suite.log = testrig.NewTestLog() -	suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil)), suite.storage) +	suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil), suite.db), suite.storage)  	suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator)  	suite.userModule = user.New(suite.config, suite.processor, suite.log).(*user.Module) -	testrig.StandardDBSetup(suite.db) +	suite.securityModule = security.New(suite.config, suite.db, suite.log).(*security.Module) +	testrig.StandardDBSetup(suite.db, suite.testAccounts)  	testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media")  } @@ -56,48 +53,11 @@ func (suite *UserGetTestSuite) TearDownTest() {  func (suite *UserGetTestSuite) TestGetUser() {  	// the dereference we're gonna use -	signedRequest := testrig.NewTestDereferenceRequests(suite.testAccounts)["foss_satan_dereference_zork"] - -	requestingAccount := suite.testAccounts["remote_account_1"] +	derefRequests := testrig.NewTestDereferenceRequests(suite.testAccounts) +	signedRequest := derefRequests["foss_satan_dereference_zork"]  	targetAccount := suite.testAccounts["local_account_1"] -	encodedPublicKey, err := x509.MarshalPKIXPublicKey(requestingAccount.PublicKey) -	assert.NoError(suite.T(), err) -	publicKeyBytes := pem.EncodeToMemory(&pem.Block{ -		Type:  "PUBLIC KEY", -		Bytes: encodedPublicKey, -	}) -	publicKeyString := strings.ReplaceAll(string(publicKeyBytes), "\n", "\\n") - -	// for this test we need the client to return the public key of the requester on the 'remote' instance -	responseBodyString := fmt.Sprintf(` -	{ -		"@context": [ -			"https://www.w3.org/ns/activitystreams", -			"https://w3id.org/security/v1" -		], - -		"id": "%s", -		"type": "Person", -		"preferredUsername": "%s", -		"inbox": "%s", - -		"publicKey": { -			"id": "%s", -			"owner": "%s", -			"publicKeyPem": "%s" -		} -	}`, requestingAccount.URI, requestingAccount.Username, requestingAccount.InboxURI, requestingAccount.PublicKeyURI, requestingAccount.URI, publicKeyString) - -	// create a transport controller whose client will just return the response body string we specified above -	tc := testrig.NewTestTransportController(testrig.NewMockHTTPClient(func(req *http.Request) (*http.Response, error) { -		r := ioutil.NopCloser(bytes.NewReader([]byte(responseBodyString))) -		return &http.Response{ -			StatusCode: 200, -			Body:       r, -		}, nil -	})) -	// get this transport controller embedded right in the user module we're testing +	tc := testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil), suite.db)  	federator := testrig.NewTestFederator(suite.db, tc, suite.storage)  	processor := testrig.NewTestProcessor(suite.db, suite.storage, federator)  	userModule := user.New(suite.config, processor, suite.log).(*user.Module) @@ -105,7 +65,12 @@ func (suite *UserGetTestSuite) TestGetUser() {  	// setup request  	recorder := httptest.NewRecorder()  	ctx, _ := gin.CreateTestContext(recorder) -	ctx.Request = httptest.NewRequest(http.MethodGet, fmt.Sprintf("http://localhost:8080%s", strings.Replace(user.UsersBasePathWithUsername, ":username", targetAccount.Username, 1)), nil) // the endpoint we're hitting +	ctx.Request = httptest.NewRequest(http.MethodGet, targetAccount.URI, nil) // the endpoint we're hitting +	ctx.Request.Header.Set("Signature", signedRequest.SignatureHeader) +	ctx.Request.Header.Set("Date", signedRequest.DateHeader) + +	// we need to pass the context through signature check first to set appropriate values on it +	suite.securityModule.SignatureCheck(ctx)  	// 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. @@ -116,11 +81,6 @@ func (suite *UserGetTestSuite) TestGetUser() {  		},  	} -	// we need these headers for the request to be validated -	ctx.Request.Header.Set("Signature", signedRequest.SignatureHeader) -	ctx.Request.Header.Set("Date", signedRequest.DateHeader) -	ctx.Request.Header.Set("Digest", signedRequest.DigestHeader) -  	// trigger the function being tested  	userModule.UsersGETHandler(ctx) | 
