loading up the forgejo repo on tangled to test page performance
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: access ActivityPub client through interfaces to facilitate mocking in unit tests (#4853)

Was facing issues while writing unit tests for federation code. Mocks weren't catching all network calls, because was being out of scope of the mocking infra. Plus, I think we can have more granular tests.

This PR puts the client behind an interface, that can be retrieved from `ctx`. Context doesn't require initialization, as it defaults to the implementation available in-tree. It may be overridden when required (like testing).

## Mechanism

1. Get client factory from `ctx` (factory contains network and crypto parameters that are needed)
2. Initialize client with sender's keys and the receiver's public key
3. Use client as before.

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/4853
Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>
Co-authored-by: Aravinth Manivannan <realaravinth@batsense.net>
Co-committed-by: Aravinth Manivannan <realaravinth@batsense.net>

authored by

Aravinth Manivannan
Aravinth Manivannan
and committed by
Earl Warren
f9cbea3d 1ddf44ed

+140 -25
+4
.deadcode-out
··· 93 93 GetUserEmailsByNames 94 94 GetUserNamesByIDs 95 95 96 + code.gitea.io/gitea/modules/activitypub 97 + NewContext 98 + Context.APClientFactory 99 + 96 100 code.gitea.io/gitea/modules/assetfs 97 101 Bindata 98 102
+102 -16
modules/activitypub/client.go
··· 56 56 } 57 57 58 58 // Client struct 59 - type Client struct { 59 + type ClientFactory struct { 60 60 client *http.Client 61 61 algs []httpsig.Algorithm 62 62 digestAlg httpsig.DigestAlgorithm 63 63 getHeaders []string 64 64 postHeaders []string 65 - priv *rsa.PrivateKey 66 - pubID string 67 65 } 68 66 69 67 // NewClient function 70 - func NewClient(ctx context.Context, user *user_model.User, pubID string) (c *Client, err error) { 68 + func NewClientFactory() (c *ClientFactory, err error) { 71 69 if err = containsRequiredHTTPHeaders(http.MethodGet, setting.Federation.GetHeaders); err != nil { 72 70 return nil, err 73 71 } else if err = containsRequiredHTTPHeaders(http.MethodPost, setting.Federation.PostHeaders); err != nil { 74 72 return nil, err 75 73 } 76 74 75 + c = &ClientFactory{ 76 + client: &http.Client{ 77 + Transport: &http.Transport{ 78 + Proxy: proxy.Proxy(), 79 + }, 80 + Timeout: 5 * time.Second, 81 + }, 82 + algs: setting.HttpsigAlgs, 83 + digestAlg: httpsig.DigestAlgorithm(setting.Federation.DigestAlgorithm), 84 + getHeaders: setting.Federation.GetHeaders, 85 + postHeaders: setting.Federation.PostHeaders, 86 + } 87 + return c, err 88 + } 89 + 90 + type APClientFactory interface { 91 + WithKeys(ctx context.Context, user *user_model.User, pubID string) (APClient, error) 92 + } 93 + 94 + // Client struct 95 + type Client struct { 96 + client *http.Client 97 + algs []httpsig.Algorithm 98 + digestAlg httpsig.DigestAlgorithm 99 + getHeaders []string 100 + postHeaders []string 101 + priv *rsa.PrivateKey 102 + pubID string 103 + } 104 + 105 + // NewRequest function 106 + func (cf *ClientFactory) WithKeys(ctx context.Context, user *user_model.User, pubID string) (APClient, error) { 77 107 priv, err := GetPrivateKey(ctx, user) 78 108 if err != nil { 79 109 return nil, err ··· 84 114 return nil, err 85 115 } 86 116 87 - c = &Client{ 88 - client: &http.Client{ 89 - Transport: &http.Transport{ 90 - Proxy: proxy.Proxy(), 91 - }, 92 - Timeout: 5 * time.Second, 93 - }, 94 - algs: setting.HttpsigAlgs, 95 - digestAlg: httpsig.DigestAlgorithm(setting.Federation.DigestAlgorithm), 96 - getHeaders: setting.Federation.GetHeaders, 97 - postHeaders: setting.Federation.PostHeaders, 117 + c := Client{ 118 + client: cf.client, 119 + algs: cf.algs, 120 + digestAlg: cf.digestAlg, 121 + getHeaders: cf.getHeaders, 122 + postHeaders: cf.postHeaders, 98 123 priv: privParsed, 99 124 pubID: pubID, 100 125 } 101 - return c, err 126 + return &c, nil 102 127 } 103 128 104 129 // NewRequest function ··· 185 210 } 186 211 return s 187 212 } 213 + 214 + type APClient interface { 215 + newRequest(method string, b []byte, to string) (req *http.Request, err error) 216 + Post(b []byte, to string) (resp *http.Response, err error) 217 + Get(to string) (resp *http.Response, err error) 218 + GetBody(uri string) ([]byte, error) 219 + } 220 + 221 + // contextKey is a value for use with context.WithValue. 222 + type contextKey struct { 223 + name string 224 + } 225 + 226 + // clientFactoryContextKey is a context key. It is used with context.Value() to get the current Food for the context 227 + var ( 228 + clientFactoryContextKey = &contextKey{"clientFactory"} 229 + _ APClientFactory = &ClientFactory{} 230 + ) 231 + 232 + // Context represents an activitypub client factory context 233 + type Context struct { 234 + context.Context 235 + e APClientFactory 236 + } 237 + 238 + func NewContext(ctx context.Context, e APClientFactory) *Context { 239 + return &Context{ 240 + Context: ctx, 241 + e: e, 242 + } 243 + } 244 + 245 + // APClientFactory represents an activitypub client factory 246 + func (ctx *Context) APClientFactory() APClientFactory { 247 + return ctx.e 248 + } 249 + 250 + // provides APClientFactory 251 + type GetAPClient interface { 252 + GetClientFactory() APClientFactory 253 + } 254 + 255 + // GetClientFactory will get an APClientFactory from this context or returns the default implementation 256 + func GetClientFactory(ctx context.Context) (APClientFactory, error) { 257 + if e := getClientFactory(ctx); e != nil { 258 + return e, nil 259 + } 260 + return NewClientFactory() 261 + } 262 + 263 + // getClientFactory will get an APClientFactory from this context or return nil 264 + func getClientFactory(ctx context.Context) APClientFactory { 265 + if clientFactory, ok := ctx.(APClientFactory); ok { 266 + return clientFactory 267 + } 268 + clientFactoryInterface := ctx.Value(clientFactoryContextKey) 269 + if clientFactoryInterface != nil { 270 + return clientFactoryInterface.(GetAPClient).GetClientFactory() 271 + } 272 + return nil 273 + }
+10 -3
modules/activitypub/client_test.go
··· 64 64 65 65 */ 66 66 67 - func TestNewClientReturnsClient(t *testing.T) { 67 + func TestClientCtx(t *testing.T) { 68 68 require.NoError(t, unittest.PrepareTestDatabase()) 69 69 user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) 70 70 pubID := "myGpgId" 71 - c, err := NewClient(db.DefaultContext, user, pubID) 71 + cf, err := NewClientFactory() 72 + log.Debug("ClientFactory: %v\nError: %v", cf, err) 73 + require.NoError(t, err) 74 + 75 + c, err := cf.WithKeys(db.DefaultContext, user, pubID) 72 76 73 77 log.Debug("Client: %v\nError: %v", c, err) 74 78 require.NoError(t, err) 79 + _ = NewContext(db.DefaultContext, cf) 75 80 } 76 81 77 82 /* TODO: bring this test to work or delete ··· 109 114 require.NoError(t, unittest.PrepareTestDatabase()) 110 115 user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) 111 116 pubID := "https://example.com/pubID" 112 - c, err := NewClient(db.DefaultContext, user, pubID) 117 + cf, err := NewClientFactory() 118 + require.NoError(t, err) 119 + c, err := cf.WithKeys(db.DefaultContext, user, pubID) 113 120 require.NoError(t, err) 114 121 115 122 expected := "BODY"
+15 -3
services/federation/federation_service.go
··· 99 99 100 100 func CreateFederationHostFromAP(ctx context.Context, actorID fm.ActorID) (*forgefed.FederationHost, error) { 101 101 actionsUser := user.NewActionsUser() 102 - client, err := activitypub.NewClient(ctx, actionsUser, "no idea where to get key material.") 102 + clientFactory, err := activitypub.GetClientFactory(ctx) 103 + if err != nil { 104 + return nil, err 105 + } 106 + client, err := clientFactory.WithKeys(ctx, actionsUser, "no idea where to get key material.") 103 107 if err != nil { 104 108 return nil, err 105 109 } ··· 153 157 func CreateUserFromAP(ctx context.Context, personID fm.PersonID, federationHostID int64) (*user.User, *user.FederatedUser, error) { 154 158 // ToDo: Do we get a publicKeyId from server, repo or owner or repo? 155 159 actionsUser := user.NewActionsUser() 156 - client, err := activitypub.NewClient(ctx, actionsUser, "no idea where to get key material.") 160 + clientFactory, err := activitypub.GetClientFactory(ctx) 161 + if err != nil { 162 + return nil, nil, err 163 + } 164 + client, err := clientFactory.WithKeys(ctx, actionsUser, "no idea where to get key material.") 157 165 if err != nil { 158 166 return nil, nil, err 159 167 } ··· 262 270 likeActivityList = append(likeActivityList, likeActivity) 263 271 } 264 272 265 - apclient, err := activitypub.NewClient(ctx, &doer, doer.APActorID()) 273 + apclientFactory, err := activitypub.GetClientFactory(ctx) 274 + if err != nil { 275 + return err 276 + } 277 + apclient, err := apclientFactory.WithKeys(ctx, &doer, doer.APActorID()) 266 278 if err != nil { 267 279 return err 268 280 }
+3 -1
tests/integration/api_activitypub_person_test.go
··· 98 98 user1, err := user_model.GetUserByName(ctx, username1) 99 99 require.NoError(t, err) 100 100 user1url := fmt.Sprintf("%s/api/v1/activitypub/user-id/1#main-key", srv.URL) 101 - c, err := activitypub.NewClient(db.DefaultContext, user1, user1url) 101 + cf, err := activitypub.GetClientFactory(ctx) 102 + require.NoError(t, err) 103 + c, err := cf.WithKeys(db.DefaultContext, user1, user1url) 102 104 require.NoError(t, err) 103 105 user2inboxurl := fmt.Sprintf("%s/api/v1/activitypub/user-id/2/inbox", srv.URL) 104 106
+6 -2
tests/integration/api_activitypub_repository_test.go
··· 140 140 }() 141 141 actionsUser := user.NewActionsUser() 142 142 repositoryID := 2 143 - c, err := activitypub.NewClient(db.DefaultContext, actionsUser, "not used") 143 + cf, err := activitypub.GetClientFactory(db.DefaultContext) 144 + require.NoError(t, err) 145 + c, err := cf.WithKeys(db.DefaultContext, actionsUser, "not used") 144 146 require.NoError(t, err) 145 147 repoInboxURL := fmt.Sprintf( 146 148 "%s/api/v1/activitypub/repository-id/%v/inbox", ··· 232 234 }() 233 235 actionsUser := user.NewActionsUser() 234 236 repositoryID := 2 235 - c, err := activitypub.NewClient(db.DefaultContext, actionsUser, "not used") 237 + cf, err := activitypub.GetClientFactory(db.DefaultContext) 238 + require.NoError(t, err) 239 + c, err := cf.WithKeys(db.DefaultContext, actionsUser, "not used") 236 240 require.NoError(t, err) 237 241 repoInboxURL := fmt.Sprintf("%s/api/v1/activitypub/repository-id/%v/inbox", 238 242 srv.URL, repositoryID)