Dereference remote replies (#132)

* decided where to put reply dereferencing

* fiddling with dereferencing threads

* further adventures

* tidy up some stuff

* move dereferencing functionality

* a bunch of refactoring

* go fmt

* more refactoring

* bleep bloop

* docs and linting

* start implementing replies collection on gts side

* fiddling around

* allow dereferencing our replies

* lint, fmt
This commit is contained in:
Tobi Smethurst
2021-08-10 13:32:39 +02:00
committed by GitHub
parent 0386a28b5a
commit 0f2de6394a
68 changed files with 2946 additions and 1393 deletions

View File

@@ -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"`
}

View File

@@ -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))
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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)