mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-10-30 23:12:25 -05:00 
			
		
		
		
	[feature] Federate local account deletion (#431)
* add account delete to API * model account delete request * add AccountDeleteLocal * federate local account deletes * add DeleteLocal * update transport (controller) to allow shortcuts * delete logic + testing * update swagger docs * more tests + fixes
This commit is contained in:
		
					parent
					
						
							
								e63b653199
							
						
					
				
			
			
				commit
				
					
						532c4cc697
					
				
			
		
					 15 changed files with 541 additions and 16 deletions
				
			
		|  | @ -1798,7 +1798,7 @@ info: | ||||||
|     name: AGPL3 |     name: AGPL3 | ||||||
|     url: https://www.gnu.org/licenses/agpl-3.0.en.html |     url: https://www.gnu.org/licenses/agpl-3.0.en.html | ||||||
|   title: GoToSocial |   title: GoToSocial | ||||||
|   version: 0.0.1 |   version: REPLACE_ME | ||||||
| paths: | paths: | ||||||
|   /.well-known/nodeinfo: |   /.well-known/nodeinfo: | ||||||
|     get: |     get: | ||||||
|  | @ -2191,6 +2191,31 @@ paths: | ||||||
|       summary: Unfollow account with id. |       summary: Unfollow account with id. | ||||||
|       tags: |       tags: | ||||||
|       - accounts |       - accounts | ||||||
|  |   /api/v1/accounts/delete: | ||||||
|  |     post: | ||||||
|  |       consumes: | ||||||
|  |       - multipart/form-data | ||||||
|  |       operationId: accountDelete | ||||||
|  |       parameters: | ||||||
|  |       - description: Password of the account user, for confirmation. | ||||||
|  |         in: formData | ||||||
|  |         name: password | ||||||
|  |         required: true | ||||||
|  |         type: string | ||||||
|  |       responses: | ||||||
|  |         "202": | ||||||
|  |           description: The account deletion has been accepted and the account will | ||||||
|  |             be deleted. | ||||||
|  |         "400": | ||||||
|  |           description: bad request | ||||||
|  |         "401": | ||||||
|  |           description: unauthorized | ||||||
|  |       security: | ||||||
|  |       - OAuth2 Bearer: | ||||||
|  |         - write:accounts | ||||||
|  |       summary: Delete your account. | ||||||
|  |       tags: | ||||||
|  |       - accounts | ||||||
|   /api/v1/accounts/relationships: |   /api/v1/accounts/relationships: | ||||||
|     get: |     get: | ||||||
|       operationId: accountRelationships |       operationId: accountRelationships | ||||||
|  | @ -2341,6 +2366,8 @@ paths: | ||||||
|           description: bad request |           description: bad request | ||||||
|         "403": |         "403": | ||||||
|           description: forbidden |           description: forbidden | ||||||
|  |         "409": | ||||||
|  |           description: conflict -- domain/shortcode combo for emoji already exists | ||||||
|       security: |       security: | ||||||
|       - OAuth2 Bearer: |       - OAuth2 Bearer: | ||||||
|         - admin |         - admin | ||||||
|  |  | ||||||
|  | @ -71,6 +71,8 @@ const ( | ||||||
| 	BlockPath = BasePathWithID + "/block" | 	BlockPath = BasePathWithID + "/block" | ||||||
| 	// UnblockPath is for removing a block of an account | 	// UnblockPath is for removing a block of an account | ||||||
| 	UnblockPath = BasePathWithID + "/unblock" | 	UnblockPath = BasePathWithID + "/unblock" | ||||||
|  | 	// DeleteAccountPath is for deleting one's account via the API | ||||||
|  | 	DeleteAccountPath = BasePath + "/delete" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // Module implements the ClientAPIModule interface for account-related actions | // Module implements the ClientAPIModule interface for account-related actions | ||||||
|  | @ -90,6 +92,9 @@ func (m *Module) Route(r router.Router) error { | ||||||
| 	// create account | 	// create account | ||||||
| 	r.AttachHandler(http.MethodPost, BasePath, m.AccountCreatePOSTHandler) | 	r.AttachHandler(http.MethodPost, BasePath, m.AccountCreatePOSTHandler) | ||||||
| 
 | 
 | ||||||
|  | 	// delete account | ||||||
|  | 	r.AttachHandler(http.MethodPost, DeleteAccountPath, m.AccountDeletePOSTHandler) | ||||||
|  | 
 | ||||||
| 	// get account | 	// get account | ||||||
| 	r.AttachHandler(http.MethodGet, BasePathWithID, m.muxHandler) | 	r.AttachHandler(http.MethodGet, BasePathWithID, m.muxHandler) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -19,7 +19,6 @@ import ( | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/media" | 	"github.com/superseriousbusiness/gotosocial/internal/media" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/processing" | 	"github.com/superseriousbusiness/gotosocial/internal/processing" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/typeutils" |  | ||||||
| 	"github.com/superseriousbusiness/gotosocial/testrig" | 	"github.com/superseriousbusiness/gotosocial/testrig" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -27,7 +26,6 @@ type AccountStandardTestSuite struct { | ||||||
| 	// standard suite interfaces | 	// standard suite interfaces | ||||||
| 	suite.Suite | 	suite.Suite | ||||||
| 	db           db.DB | 	db           db.DB | ||||||
| 	tc           typeutils.TypeConverter |  | ||||||
| 	storage      *kv.KVStore | 	storage      *kv.KVStore | ||||||
| 	mediaManager media.Manager | 	mediaManager media.Manager | ||||||
| 	federator    federation.Federator | 	federator    federation.Federator | ||||||
|  |  | ||||||
							
								
								
									
										90
									
								
								internal/api/client/account/accountdelete.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								internal/api/client/account/accountdelete.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,90 @@ | ||||||
|  | /* | ||||||
|  |    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 account | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"net/http" | ||||||
|  | 
 | ||||||
|  | 	"github.com/sirupsen/logrus" | ||||||
|  | 
 | ||||||
|  | 	"github.com/gin-gonic/gin" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/api/model" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // AccountDeletePOSTHandler swagger:operation POST /api/v1/accounts/delete accountDelete | ||||||
|  | // | ||||||
|  | // Delete your account. | ||||||
|  | // | ||||||
|  | // --- | ||||||
|  | // tags: | ||||||
|  | // - accounts | ||||||
|  | // | ||||||
|  | // consumes: | ||||||
|  | // - multipart/form-data | ||||||
|  | // | ||||||
|  | // parameters: | ||||||
|  | // - name: password | ||||||
|  | //   in: formData | ||||||
|  | //   description: Password of the account user, for confirmation. | ||||||
|  | //   type: string | ||||||
|  | //   required: true | ||||||
|  | // | ||||||
|  | // security: | ||||||
|  | // - OAuth2 Bearer: | ||||||
|  | //   - write:accounts | ||||||
|  | // | ||||||
|  | // responses: | ||||||
|  | //   '202': | ||||||
|  | //     description: "The account deletion has been accepted and the account will be deleted." | ||||||
|  | //   '400': | ||||||
|  | //      description: bad request | ||||||
|  | //   '401': | ||||||
|  | //      description: unauthorized | ||||||
|  | func (m *Module) AccountDeletePOSTHandler(c *gin.Context) { | ||||||
|  | 	l := logrus.WithField("func", "AccountDeletePOSTHandler") | ||||||
|  | 	authed, err := oauth.Authed(c, true, true, true, true) | ||||||
|  | 	if err != nil { | ||||||
|  | 		l.Debugf("couldn't auth: %s", err) | ||||||
|  | 		c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	l.Tracef("retrieved account %+v", authed.Account.ID) | ||||||
|  | 
 | ||||||
|  | 	form := &model.AccountDeleteRequest{} | ||||||
|  | 	if err := c.ShouldBind(&form); err != nil { | ||||||
|  | 		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if form.Password == "" { | ||||||
|  | 		c.JSON(http.StatusBadRequest, gin.H{"error": "no password provided in account delete request"}) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	form.DeleteOriginID = authed.Account.ID | ||||||
|  | 
 | ||||||
|  | 	if errWithCode := m.processor.AccountDeleteLocal(c.Request.Context(), authed, form); errWithCode != nil { | ||||||
|  | 		l.Debugf("could not delete account: %s", errWithCode.Error()) | ||||||
|  | 		c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	c.JSON(http.StatusAccepted, gin.H{"message": "accepted"}) | ||||||
|  | } | ||||||
							
								
								
									
										101
									
								
								internal/api/client/account/accountdelete_test.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								internal/api/client/account/accountdelete_test.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,101 @@ | ||||||
|  | /* | ||||||
|  |    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 account_test | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"net/http" | ||||||
|  | 	"net/http/httptest" | ||||||
|  | 	"testing" | ||||||
|  | 
 | ||||||
|  | 	"github.com/stretchr/testify/suite" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/api/client/account" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/testrig" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | type AccountDeleteTestSuite struct { | ||||||
|  | 	AccountStandardTestSuite | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (suite *AccountDeleteTestSuite) TestAccountDeletePOSTHandler() { | ||||||
|  | 	// set up the request | ||||||
|  | 	// we're deleting zork | ||||||
|  | 	requestBody, w, err := testrig.CreateMultipartFormData( | ||||||
|  | 		"", "", | ||||||
|  | 		map[string]string{ | ||||||
|  | 			"password": "password", | ||||||
|  | 		}) | ||||||
|  | 	if err != nil { | ||||||
|  | 		panic(err) | ||||||
|  | 	} | ||||||
|  | 	bodyBytes := requestBody.Bytes() | ||||||
|  | 	recorder := httptest.NewRecorder() | ||||||
|  | 	ctx := suite.newContext(recorder, http.MethodPost, bodyBytes, account.DeleteAccountPath, w.FormDataContentType()) | ||||||
|  | 
 | ||||||
|  | 	// call the handler | ||||||
|  | 	suite.accountModule.AccountDeletePOSTHandler(ctx) | ||||||
|  | 
 | ||||||
|  | 	// 1. we should have Accepted because our request was valid | ||||||
|  | 	suite.Equal(http.StatusAccepted, recorder.Code) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (suite *AccountDeleteTestSuite) TestAccountDeletePOSTHandlerWrongPassword() { | ||||||
|  | 	// set up the request | ||||||
|  | 	// we're deleting zork | ||||||
|  | 	requestBody, w, err := testrig.CreateMultipartFormData( | ||||||
|  | 		"", "", | ||||||
|  | 		map[string]string{ | ||||||
|  | 			"password": "aaaaaaaaaaaaaaaaaaaaaaaaaaaa", | ||||||
|  | 		}) | ||||||
|  | 	if err != nil { | ||||||
|  | 		panic(err) | ||||||
|  | 	} | ||||||
|  | 	bodyBytes := requestBody.Bytes() | ||||||
|  | 	recorder := httptest.NewRecorder() | ||||||
|  | 	ctx := suite.newContext(recorder, http.MethodPost, bodyBytes, account.DeleteAccountPath, w.FormDataContentType()) | ||||||
|  | 
 | ||||||
|  | 	// call the handler | ||||||
|  | 	suite.accountModule.AccountDeletePOSTHandler(ctx) | ||||||
|  | 
 | ||||||
|  | 	// 1. we should have Forbidden because we supplied the wrong password | ||||||
|  | 	suite.Equal(http.StatusForbidden, recorder.Code) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (suite *AccountDeleteTestSuite) TestAccountDeletePOSTHandlerNoPassword() { | ||||||
|  | 	// set up the request | ||||||
|  | 	// we're deleting zork | ||||||
|  | 	requestBody, w, err := testrig.CreateMultipartFormData( | ||||||
|  | 		"", "", | ||||||
|  | 		map[string]string{}) | ||||||
|  | 	if err != nil { | ||||||
|  | 		panic(err) | ||||||
|  | 	} | ||||||
|  | 	bodyBytes := requestBody.Bytes() | ||||||
|  | 	recorder := httptest.NewRecorder() | ||||||
|  | 	ctx := suite.newContext(recorder, http.MethodPost, bodyBytes, account.DeleteAccountPath, w.FormDataContentType()) | ||||||
|  | 
 | ||||||
|  | 	// call the handler | ||||||
|  | 	suite.accountModule.AccountDeletePOSTHandler(ctx) | ||||||
|  | 
 | ||||||
|  | 	// 1. we should have StatusBadRequest because our request was invalid | ||||||
|  | 	suite.Equal(http.StatusBadRequest, recorder.Code) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestAccountDeleteTestSuite(t *testing.T) { | ||||||
|  | 	suite.Run(t, new(AccountDeleteTestSuite)) | ||||||
|  | } | ||||||
|  | @ -187,3 +187,14 @@ type AccountFollowRequest struct { | ||||||
| 	// Notify when this account posts. | 	// Notify when this account posts. | ||||||
| 	Notify *bool `form:"notify" json:"notify" xml:"notify"` | 	Notify *bool `form:"notify" json:"notify" xml:"notify"` | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | // AccountDeleteRequest models a request to delete an account. | ||||||
|  | // | ||||||
|  | // swagger:ignore | ||||||
|  | type AccountDeleteRequest struct { | ||||||
|  | 	// Password of the account's user, for confirmation. | ||||||
|  | 	Password string `form:"password" json:"password" xml:"password"` | ||||||
|  | 	// The origin of the delete account request. | ||||||
|  | 	// Can be the ID of the account owner, or the ID of an admin account. | ||||||
|  | 	DeleteOriginID string `form:"-" json:"-" xml:"-"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -25,13 +25,15 @@ import ( | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"net/http/httptest" | 	"net/http/httptest" | ||||||
| 	"testing" | 	"testing" | ||||||
|  | 	"time" | ||||||
| 
 | 
 | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/stretchr/testify/assert" |  | ||||||
| 	"github.com/stretchr/testify/suite" | 	"github.com/stretchr/testify/suite" | ||||||
| 	"github.com/superseriousbusiness/activity/streams" | 	"github.com/superseriousbusiness/activity/streams" | ||||||
| 	"github.com/superseriousbusiness/activity/streams/vocab" | 	"github.com/superseriousbusiness/activity/streams/vocab" | ||||||
|  | 	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/api/s2s/user" | 	"github.com/superseriousbusiness/gotosocial/internal/api/s2s/user" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/testrig" | 	"github.com/superseriousbusiness/gotosocial/testrig" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -80,24 +82,101 @@ func (suite *UserGetTestSuite) TestGetUser() { | ||||||
| 	result := recorder.Result() | 	result := recorder.Result() | ||||||
| 	defer result.Body.Close() | 	defer result.Body.Close() | ||||||
| 	b, err := ioutil.ReadAll(result.Body) | 	b, err := ioutil.ReadAll(result.Body) | ||||||
| 	assert.NoError(suite.T(), err) | 	suite.NoError(err) | ||||||
| 
 | 
 | ||||||
| 	// should be a Person | 	// should be a Person | ||||||
| 	m := make(map[string]interface{}) | 	m := make(map[string]interface{}) | ||||||
| 	err = json.Unmarshal(b, &m) | 	err = json.Unmarshal(b, &m) | ||||||
| 	assert.NoError(suite.T(), err) | 	suite.NoError(err) | ||||||
| 
 | 
 | ||||||
| 	t, err := streams.ToType(context.Background(), m) | 	t, err := streams.ToType(context.Background(), m) | ||||||
| 	assert.NoError(suite.T(), err) | 	suite.NoError(err) | ||||||
| 
 | 
 | ||||||
| 	person, ok := t.(vocab.ActivityStreamsPerson) | 	person, ok := t.(vocab.ActivityStreamsPerson) | ||||||
| 	assert.True(suite.T(), ok) | 	suite.True(ok) | ||||||
| 
 | 
 | ||||||
| 	// convert person to account | 	// convert person to account | ||||||
| 	// since this account is already known, we should get a pretty full model of it from the conversion | 	// since this account is already known, we should get a pretty full model of it from the conversion | ||||||
| 	a, err := suite.tc.ASRepresentationToAccount(context.Background(), person, false) | 	a, err := suite.tc.ASRepresentationToAccount(context.Background(), person, false) | ||||||
| 	assert.NoError(suite.T(), err) | 	suite.NoError(err) | ||||||
| 	assert.EqualValues(suite.T(), targetAccount.Username, a.Username) | 	suite.EqualValues(targetAccount.Username, a.Username) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // TestGetUserPublicKeyDeleted checks whether the public key of a deleted account can still be dereferenced. | ||||||
|  | // This is needed by remote instances for authenticating delete requests and stuff like that. | ||||||
|  | func (suite *UserGetTestSuite) TestGetUserPublicKeyDeleted() { | ||||||
|  | 	targetAccount := suite.testAccounts["local_account_1"] | ||||||
|  | 
 | ||||||
|  | 	// first delete the account, as though zork had deleted himself | ||||||
|  | 	authed := &oauth.Auth{ | ||||||
|  | 		Application: suite.testApplications["local_account_1"], | ||||||
|  | 		User:        suite.testUsers["local_account_1"], | ||||||
|  | 		Account:     suite.testAccounts["local_account_1"], | ||||||
|  | 	} | ||||||
|  | 	suite.processor.AccountDeleteLocal(context.Background(), authed, &apimodel.AccountDeleteRequest{ | ||||||
|  | 		Password:       "password", | ||||||
|  | 		DeleteOriginID: targetAccount.ID, | ||||||
|  | 	}) | ||||||
|  | 
 | ||||||
|  | 	// now wait just a sec for it to go through.... | ||||||
|  | 	time.Sleep(1 * time.Second) | ||||||
|  | 
 | ||||||
|  | 	// the dereference we're gonna use | ||||||
|  | 	derefRequests := testrig.NewTestDereferenceRequests(suite.testAccounts) | ||||||
|  | 	signedRequest := derefRequests["foss_satan_dereference_zork_public_key"] | ||||||
|  | 
 | ||||||
|  | 	tc := testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil), suite.db) | ||||||
|  | 	federator := testrig.NewTestFederator(suite.db, tc, suite.storage, suite.mediaManager) | ||||||
|  | 	emailSender := testrig.NewEmailSender("../../../../web/template/", nil) | ||||||
|  | 	processor := testrig.NewTestProcessor(suite.db, suite.storage, federator, emailSender, suite.mediaManager) | ||||||
|  | 	userModule := user.New(processor).(*user.Module) | ||||||
|  | 
 | ||||||
|  | 	// setup request | ||||||
|  | 	recorder := httptest.NewRecorder() | ||||||
|  | 	ctx, _ := gin.CreateTestContext(recorder) | ||||||
|  | 	ctx.Request = httptest.NewRequest(http.MethodGet, targetAccount.PublicKeyURI, nil) // the endpoint we're hitting | ||||||
|  | 	ctx.Request.Header.Set("accept", "application/activity+json") | ||||||
|  | 	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, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// trigger the function being tested | ||||||
|  | 	userModule.UsersGETHandler(ctx) | ||||||
|  | 
 | ||||||
|  | 	// check response | ||||||
|  | 	suite.EqualValues(http.StatusOK, recorder.Code) | ||||||
|  | 
 | ||||||
|  | 	result := recorder.Result() | ||||||
|  | 	defer result.Body.Close() | ||||||
|  | 	b, err := ioutil.ReadAll(result.Body) | ||||||
|  | 	suite.NoError(err) | ||||||
|  | 
 | ||||||
|  | 	// should be a Person | ||||||
|  | 	m := make(map[string]interface{}) | ||||||
|  | 	err = json.Unmarshal(b, &m) | ||||||
|  | 	suite.NoError(err) | ||||||
|  | 
 | ||||||
|  | 	t, err := streams.ToType(context.Background(), m) | ||||||
|  | 	suite.NoError(err) | ||||||
|  | 
 | ||||||
|  | 	person, ok := t.(vocab.ActivityStreamsPerson) | ||||||
|  | 	suite.True(ok) | ||||||
|  | 
 | ||||||
|  | 	// convert person to account | ||||||
|  | 	a, err := suite.tc.ASRepresentationToAccount(context.Background(), person, false) | ||||||
|  | 	suite.NoError(err) | ||||||
|  | 	suite.EqualValues(targetAccount.Username, a.Username) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func TestUserGetTestSuite(t *testing.T) { | func TestUserGetTestSuite(t *testing.T) { | ||||||
|  |  | ||||||
|  | @ -30,6 +30,10 @@ func (p *processor) AccountCreate(ctx context.Context, authed *oauth.Auth, form | ||||||
| 	return p.accountProcessor.Create(ctx, authed.Token, authed.Application, form) | 	return p.accountProcessor.Create(ctx, authed.Token, authed.Application, form) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func (p *processor) AccountDeleteLocal(ctx context.Context, authed *oauth.Auth, form *apimodel.AccountDeleteRequest) gtserror.WithCode { | ||||||
|  | 	return p.accountProcessor.DeleteLocal(ctx, authed.Account, form) | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func (p *processor) AccountGet(ctx context.Context, authed *oauth.Auth, targetAccountID string) (*apimodel.Account, error) { | func (p *processor) AccountGet(ctx context.Context, authed *oauth.Auth, targetAccountID string) (*apimodel.Account, error) { | ||||||
| 	return p.accountProcessor.Get(ctx, authed.Account, targetAccountID) | 	return p.accountProcessor.Get(ctx, authed.Account, targetAccountID) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -42,7 +42,10 @@ type Processor interface { | ||||||
| 	Create(ctx context.Context, applicationToken oauth2.TokenInfo, application *gtsmodel.Application, form *apimodel.AccountCreateRequest) (*apimodel.Token, error) | 	Create(ctx context.Context, applicationToken oauth2.TokenInfo, application *gtsmodel.Application, form *apimodel.AccountCreateRequest) (*apimodel.Token, error) | ||||||
| 	// Delete deletes an account, and all of that account's statuses, media, follows, notifications, etc etc etc. | 	// Delete deletes an account, and all of that account's statuses, media, follows, notifications, etc etc etc. | ||||||
| 	// The origin passed here should be either the ID of the account doing the delete (can be itself), or the ID of a domain block. | 	// The origin passed here should be either the ID of the account doing the delete (can be itself), or the ID of a domain block. | ||||||
| 	Delete(ctx context.Context, account *gtsmodel.Account, origin string) error | 	Delete(ctx context.Context, account *gtsmodel.Account, origin string) gtserror.WithCode | ||||||
|  | 	// DeleteLocal is like delete, but specifically for deletion of local accounts rather than federated ones. | ||||||
|  | 	// Unlike Delete, it will propagate the deletion out across the federating API to other instances. | ||||||
|  | 	DeleteLocal(ctx context.Context, account *gtsmodel.Account, form *apimodel.AccountDeleteRequest) gtserror.WithCode | ||||||
| 	// Get processes the given request for account information. | 	// Get processes the given request for account information. | ||||||
| 	Get(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Account, error) | 	Get(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Account, error) | ||||||
| 	// Update processes the update of an account with the given form | 	// Update processes the update of an account with the given form | ||||||
|  |  | ||||||
|  | @ -20,13 +20,17 @@ package account | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"context" | 	"context" | ||||||
|  | 	"errors" | ||||||
| 	"time" | 	"time" | ||||||
| 
 | 
 | ||||||
| 	"github.com/sirupsen/logrus" | 	"github.com/sirupsen/logrus" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/ap" | 	"github.com/superseriousbusiness/gotosocial/internal/ap" | ||||||
|  | 	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db" | 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/messages" | 	"github.com/superseriousbusiness/gotosocial/internal/messages" | ||||||
|  | 	"golang.org/x/crypto/bcrypt" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // Delete handles the complete deletion of an account. | // Delete handles the complete deletion of an account. | ||||||
|  | @ -50,7 +54,7 @@ import ( | ||||||
| // 16. Delete account's user | // 16. Delete account's user | ||||||
| // 17. Delete account's timeline | // 17. Delete account's timeline | ||||||
| // 18. Delete account itself | // 18. Delete account itself | ||||||
| func (p *processor) Delete(ctx context.Context, account *gtsmodel.Account, origin string) error { | func (p *processor) Delete(ctx context.Context, account *gtsmodel.Account, origin string) gtserror.WithCode { | ||||||
| 	fields := logrus.Fields{ | 	fields := logrus.Fields{ | ||||||
| 		"func":     "Delete", | 		"func":     "Delete", | ||||||
| 		"username": account.Username, | 		"username": account.Username, | ||||||
|  | @ -256,7 +260,7 @@ selectStatusesLoop: | ||||||
| 	// 16. Delete account's user | 	// 16. Delete account's user | ||||||
| 	l.Debug("deleting account user") | 	l.Debug("deleting account user") | ||||||
| 	if err := p.db.DeleteWhere(ctx, []db.Where{{Key: "account_id", Value: account.ID}}, >smodel.User{}); err != nil { | 	if err := p.db.DeleteWhere(ctx, []db.Where{{Key: "account_id", Value: account.ID}}, >smodel.User{}); err != nil { | ||||||
| 		return err | 		return gtserror.NewErrorInternalError(err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// 17. Delete account's timeline | 	// 17. Delete account's timeline | ||||||
|  | @ -282,9 +286,52 @@ selectStatusesLoop: | ||||||
| 
 | 
 | ||||||
| 	account, err := p.db.UpdateAccount(ctx, account) | 	account, err := p.db.UpdateAccount(ctx, account) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return gtserror.NewErrorInternalError(err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	l.Infof("deleted account with username %s from domain %s", account.Username, account.Domain) | 	l.Infof("deleted account with username %s from domain %s", account.Username, account.Domain) | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | func (p *processor) DeleteLocal(ctx context.Context, account *gtsmodel.Account, form *apimodel.AccountDeleteRequest) gtserror.WithCode { | ||||||
|  | 	fromClientAPIMessage := messages.FromClientAPI{ | ||||||
|  | 		APObjectType:   ap.ActorPerson, | ||||||
|  | 		APActivityType: ap.ActivityDelete, | ||||||
|  | 		TargetAccount:  account, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if form.DeleteOriginID == account.ID { | ||||||
|  | 		// the account owner themself has requested deletion via the API, get their user from the db | ||||||
|  | 		user := >smodel.User{} | ||||||
|  | 		if err := p.db.GetWhere(ctx, []db.Where{{Key: "account_id", Value: account.ID}}, user); err != nil { | ||||||
|  | 			return gtserror.NewErrorInternalError(err) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// now check that the password they supplied is correct | ||||||
|  | 		// make sure a password is actually set and bail if not | ||||||
|  | 		if user.EncryptedPassword == "" { | ||||||
|  | 			return gtserror.NewErrorForbidden(errors.New("user password was not set")) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// compare the provided password with the encrypted one from the db, bail if they don't match | ||||||
|  | 		if err := bcrypt.CompareHashAndPassword([]byte(user.EncryptedPassword), []byte(form.Password)); err != nil { | ||||||
|  | 			return gtserror.NewErrorForbidden(errors.New("invalid password")) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		fromClientAPIMessage.OriginAccount = account | ||||||
|  | 	} else { | ||||||
|  | 		// the delete has been requested by some other account, grab it; | ||||||
|  | 		// if we've reached this point we know it has permission already | ||||||
|  | 		requestingAccount, err := p.db.GetAccountByID(ctx, form.DeleteOriginID) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return gtserror.NewErrorInternalError(err) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		fromClientAPIMessage.OriginAccount = requestingAccount | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// put the delete in the processor queue to handle the rest of it asynchronously | ||||||
|  | 	p.fromClientAPI <- fromClientAPIMessage | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
							
								
								
									
										90
									
								
								internal/processing/account_test.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								internal/processing/account_test.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,90 @@ | ||||||
|  | /* | ||||||
|  |    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 processing_test | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"fmt" | ||||||
|  | 	"testing" | ||||||
|  | 	"time" | ||||||
|  | 
 | ||||||
|  | 	"github.com/stretchr/testify/suite" | ||||||
|  | 	"github.com/superseriousbusiness/activity/pub" | ||||||
|  | 	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | type AccountTestSuite struct { | ||||||
|  | 	ProcessingStandardTestSuite | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (suite *AccountTestSuite) TestAccountDeleteLocal() { | ||||||
|  | 	ctx := context.Background() | ||||||
|  | 	deletingAccount := suite.testAccounts["local_account_1"] | ||||||
|  | 	followingAccount := suite.testAccounts["remote_account_1"] | ||||||
|  | 
 | ||||||
|  | 	// make the following account follow the deleting account so that a delete message will be sent to it via the federating API | ||||||
|  | 	follow := >smodel.Follow{ | ||||||
|  | 		ID:              "01FJ1S8DX3STJJ6CEYPMZ1M0R3", | ||||||
|  | 		CreatedAt:       time.Now(), | ||||||
|  | 		UpdatedAt:       time.Now(), | ||||||
|  | 		URI:             fmt.Sprintf("%s/follow/01FJ1S8DX3STJJ6CEYPMZ1M0R3", followingAccount.URI), | ||||||
|  | 		AccountID:       followingAccount.ID, | ||||||
|  | 		TargetAccountID: deletingAccount.ID, | ||||||
|  | 	} | ||||||
|  | 	err := suite.db.Put(ctx, follow) | ||||||
|  | 	suite.NoError(err) | ||||||
|  | 
 | ||||||
|  | 	errWithCode := suite.processor.AccountDeleteLocal(ctx, suite.testAutheds["local_account_1"], &apimodel.AccountDeleteRequest{ | ||||||
|  | 		Password:       "password", | ||||||
|  | 		DeleteOriginID: deletingAccount.ID, | ||||||
|  | 	}) | ||||||
|  | 	suite.NoError(errWithCode) | ||||||
|  | 	time.Sleep(1 * time.Second) // wait a sec for the delete to process | ||||||
|  | 
 | ||||||
|  | 	// the delete should be federated outwards to the following account's inbox | ||||||
|  | 	sent, ok := suite.sentHTTPRequests[followingAccount.InboxURI] | ||||||
|  | 	suite.True(ok) | ||||||
|  | 	delete := &struct { | ||||||
|  | 		Actor  string `json:"actor"` | ||||||
|  | 		ID     string `json:"id"` | ||||||
|  | 		Object string `json:"object"` | ||||||
|  | 		To     string `json:"to"` | ||||||
|  | 		CC     string `json:"cc"` | ||||||
|  | 		Type   string `json:"type"` | ||||||
|  | 	}{} | ||||||
|  | 	err = json.Unmarshal(sent, delete) | ||||||
|  | 	suite.NoError(err) | ||||||
|  | 
 | ||||||
|  | 	suite.Equal(deletingAccount.URI, delete.Actor) | ||||||
|  | 	suite.Equal(deletingAccount.URI, delete.Object) | ||||||
|  | 	suite.Equal(deletingAccount.FollowersURI, delete.To) | ||||||
|  | 	suite.Equal(pub.PublicActivityPubIRI, delete.CC) | ||||||
|  | 	suite.Equal("Delete", delete.Type) | ||||||
|  | 
 | ||||||
|  | 	// the deleted account should be deleted | ||||||
|  | 	dbAccount, err := suite.db.GetAccountByID(ctx, deletingAccount.ID) | ||||||
|  | 	suite.NoError(err) | ||||||
|  | 	suite.WithinDuration(dbAccount.SuspendedAt, time.Now(), 30*time.Second) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestAccountTestSuite(t *testing.T) { | ||||||
|  | 	suite.Run(t, &AccountTestSuite{}) | ||||||
|  | } | ||||||
|  | @ -24,6 +24,7 @@ import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"net/url" | 	"net/url" | ||||||
| 
 | 
 | ||||||
|  | 	"github.com/superseriousbusiness/activity/pub" | ||||||
| 	"github.com/superseriousbusiness/activity/streams" | 	"github.com/superseriousbusiness/activity/streams" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/ap" | 	"github.com/superseriousbusiness/gotosocial/internal/ap" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db" | 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||||
|  | @ -320,11 +321,69 @@ func (p *processor) processDeleteAccountFromClientAPI(ctx context.Context, clien | ||||||
| 		// origin is whichever account caused this message | 		// origin is whichever account caused this message | ||||||
| 		origin = clientMsg.OriginAccount.ID | 		origin = clientMsg.OriginAccount.ID | ||||||
| 	} | 	} | ||||||
|  | 
 | ||||||
|  | 	if err := p.federateAccountDelete(ctx, clientMsg.TargetAccount); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	return p.accountProcessor.Delete(ctx, clientMsg.TargetAccount, origin) | 	return p.accountProcessor.Delete(ctx, clientMsg.TargetAccount, origin) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // TODO: move all the below functions into federation.Federator | // TODO: move all the below functions into federation.Federator | ||||||
| 
 | 
 | ||||||
|  | func (p *processor) federateAccountDelete(ctx context.Context, account *gtsmodel.Account) error { | ||||||
|  | 	// do nothing if this isn't our account | ||||||
|  | 	if account.Domain != "" { | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	outboxIRI, err := url.Parse(account.OutboxURI) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("federateAccountDelete: error parsing outboxURI %s: %s", account.OutboxURI, err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	actorIRI, err := url.Parse(account.URI) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("federateAccountDelete: error parsing actorIRI %s: %s", account.URI, err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	followersIRI, err := url.Parse(account.FollowersURI) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("federateAccountDelete: error parsing followersIRI %s: %s", account.FollowersURI, err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	publicIRI, err := url.Parse(pub.PublicActivityPubIRI) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("federateAccountDelete: error parsing url %s: %s", pub.PublicActivityPubIRI, err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// create a delete and set the appropriate actor on it | ||||||
|  | 	delete := streams.NewActivityStreamsDelete() | ||||||
|  | 
 | ||||||
|  | 	// set the actor for the delete; no matter who deleted it we should use the account owner for this | ||||||
|  | 	deleteActor := streams.NewActivityStreamsActorProperty() | ||||||
|  | 	deleteActor.AppendIRI(actorIRI) | ||||||
|  | 	delete.SetActivityStreamsActor(deleteActor) | ||||||
|  | 
 | ||||||
|  | 	// Set the account IRI as the 'object' property. | ||||||
|  | 	deleteObject := streams.NewActivityStreamsObjectProperty() | ||||||
|  | 	deleteObject.AppendIRI(actorIRI) | ||||||
|  | 	delete.SetActivityStreamsObject(deleteObject) | ||||||
|  | 
 | ||||||
|  | 	// send to followers... | ||||||
|  | 	deleteTo := streams.NewActivityStreamsToProperty() | ||||||
|  | 	deleteTo.AppendIRI(followersIRI) | ||||||
|  | 	delete.SetActivityStreamsTo(deleteTo) | ||||||
|  | 
 | ||||||
|  | 	// ... and CC to public | ||||||
|  | 	deleteCC := streams.NewActivityStreamsCcProperty() | ||||||
|  | 	deleteCC.AppendIRI(publicIRI) | ||||||
|  | 	delete.SetActivityStreamsCc(deleteCC) | ||||||
|  | 
 | ||||||
|  | 	_, err = p.federator.FederatingActor().Send(ctx, outboxIRI, delete) | ||||||
|  | 	return err | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func (p *processor) federateStatus(ctx context.Context, status *gtsmodel.Status) error { | func (p *processor) federateStatus(ctx context.Context, status *gtsmodel.Status) error { | ||||||
| 	// do nothing if the status shouldn't be federated | 	// do nothing if the status shouldn't be federated | ||||||
| 	if !status.Federated { | 	if !status.Federated { | ||||||
|  |  | ||||||
|  | @ -73,6 +73,8 @@ type Processor interface { | ||||||
| 
 | 
 | ||||||
| 	// AccountCreate processes the given form for creating a new account, returning an oauth token for that account if successful. | 	// AccountCreate processes the given form for creating a new account, returning an oauth token for that account if successful. | ||||||
| 	AccountCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.AccountCreateRequest) (*apimodel.Token, error) | 	AccountCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.AccountCreateRequest) (*apimodel.Token, error) | ||||||
|  | 	// AccountDeleteLocal processes the delete of a LOCAL account using the given form. | ||||||
|  | 	AccountDeleteLocal(ctx context.Context, authed *oauth.Auth, form *apimodel.AccountDeleteRequest) gtserror.WithCode | ||||||
| 	// AccountGet processes the given request for account information. | 	// AccountGet processes the given request for account information. | ||||||
| 	AccountGet(ctx context.Context, authed *oauth.Auth, targetAccountID string) (*apimodel.Account, error) | 	AccountGet(ctx context.Context, authed *oauth.Auth, targetAccountID string) (*apimodel.Account, error) | ||||||
| 	// AccountUpdate processes the update of an account with the given form | 	// AccountUpdate processes the update of an account with the given form | ||||||
|  |  | ||||||
|  | @ -95,8 +95,8 @@ func (suite *ProcessingStandardTestSuite) SetupSuite() { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (suite *ProcessingStandardTestSuite) SetupTest() { | func (suite *ProcessingStandardTestSuite) SetupTest() { | ||||||
| 	testrig.InitTestLog() |  | ||||||
| 	testrig.InitTestConfig() | 	testrig.InitTestConfig() | ||||||
|  | 	testrig.InitTestLog() | ||||||
| 
 | 
 | ||||||
| 	suite.db = testrig.NewTestDB() | 	suite.db = testrig.NewTestDB() | ||||||
| 	suite.testActivities = testrig.NewTestActivities(suite.testAccounts) | 	suite.testActivities = testrig.NewTestActivities(suite.testAccounts) | ||||||
|  |  | ||||||
|  | @ -377,7 +377,7 @@ func NewTestAccounts() map[string]*gtsmodel.Account { | ||||||
| 			AlsoKnownAs:             "", | 			AlsoKnownAs:             "", | ||||||
| 			PrivateKey:              &rsa.PrivateKey{}, | 			PrivateKey:              &rsa.PrivateKey{}, | ||||||
| 			PublicKey:               &rsa.PublicKey{}, | 			PublicKey:               &rsa.PublicKey{}, | ||||||
| 			PublicKeyURI:            "http://localhost:8080/users/the_mighty_zork#main-key", | 			PublicKeyURI:            "http://localhost:8080/users/the_mighty_zork/main-key", | ||||||
| 			SensitizedAt:            time.Time{}, | 			SensitizedAt:            time.Time{}, | ||||||
| 			SilencedAt:              time.Time{}, | 			SilencedAt:              time.Time{}, | ||||||
| 			SuspendedAt:             time.Time{}, | 			SuspendedAt:             time.Time{}, | ||||||
|  | @ -1656,6 +1656,14 @@ func NewTestDereferenceRequests(accounts map[string]*gtsmodel.Account) map[strin | ||||||
| 		DateHeader:      date, | 		DateHeader:      date, | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	target = URLMustParse(accounts["local_account_1"].PublicKeyURI) | ||||||
|  | 	sig, digest, date = GetSignatureForDereference(accounts["remote_account_1"].PublicKeyURI, accounts["remote_account_1"].PrivateKey, target) | ||||||
|  | 	fossSatanDereferenceZorkPublicKey := ActivityWithSignature{ | ||||||
|  | 		SignatureHeader: sig, | ||||||
|  | 		DigestHeader:    digest, | ||||||
|  | 		DateHeader:      date, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	target = URLMustParse(statuses["local_account_1_status_1"].URI + "/replies") | 	target = URLMustParse(statuses["local_account_1_status_1"].URI + "/replies") | ||||||
| 	sig, digest, date = GetSignatureForDereference(accounts["remote_account_1"].PublicKeyURI, accounts["remote_account_1"].PrivateKey, target) | 	sig, digest, date = GetSignatureForDereference(accounts["remote_account_1"].PublicKeyURI, accounts["remote_account_1"].PrivateKey, target) | ||||||
| 	fossSatanDereferenceLocalAccount1Status1Replies := ActivityWithSignature{ | 	fossSatanDereferenceLocalAccount1Status1Replies := ActivityWithSignature{ | ||||||
|  | @ -1706,6 +1714,7 @@ func NewTestDereferenceRequests(accounts map[string]*gtsmodel.Account) map[strin | ||||||
| 
 | 
 | ||||||
| 	return map[string]ActivityWithSignature{ | 	return map[string]ActivityWithSignature{ | ||||||
| 		"foss_satan_dereference_zork":                                  fossSatanDereferenceZork, | 		"foss_satan_dereference_zork":                                  fossSatanDereferenceZork, | ||||||
|  | 		"foss_satan_dereference_zork_public_key":                       fossSatanDereferenceZorkPublicKey, | ||||||
| 		"foss_satan_dereference_local_account_1_status_1_replies":      fossSatanDereferenceLocalAccount1Status1Replies, | 		"foss_satan_dereference_local_account_1_status_1_replies":      fossSatanDereferenceLocalAccount1Status1Replies, | ||||||
| 		"foss_satan_dereference_local_account_1_status_1_replies_next": fossSatanDereferenceLocalAccount1Status1RepliesNext, | 		"foss_satan_dereference_local_account_1_status_1_replies_next": fossSatanDereferenceLocalAccount1Status1RepliesNext, | ||||||
| 		"foss_satan_dereference_local_account_1_status_1_replies_last": fossSatanDereferenceLocalAccount1Status1RepliesLast, | 		"foss_satan_dereference_local_account_1_status_1_replies_last": fossSatanDereferenceLocalAccount1Status1RepliesLast, | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue