mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2026-05-12 22:10:25 +00:00
feat: Follow remote users; feed tab (#10380)
This is hopefully the final part of PR #4767, rebased and squashed. More thorough federation tests are at https://code.forgejo.org/forgejo/end-to-end/pulls/1276 but the mock has been extended to hopefully cover a good chunk as well. Co-authored-by: Gergely Nagy <forgejo@gergo.csillger.hu> Co-authored-by: Michael Jerger <michael.jerger@meissa-gmbh.de> Co-authored-by: zam <mirco.zachmann@meissa.de> Co-authored-by: Panagiotis "Ivory" Vasilopoulos <git@n0toose.net> Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/10380 Reviewed-by: Gusted <gusted@noreply.codeberg.org> Co-authored-by: famfo <famfo@famfo.xyz> Co-committed-by: famfo <famfo@famfo.xyz>
This commit is contained in:
parent
9de142eb7f
commit
fd28fd896b
26 changed files with 599 additions and 84 deletions
|
|
@ -10,6 +10,7 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"forgejo.org/models/db"
|
||||
"forgejo.org/models/unittest"
|
||||
user_model "forgejo.org/models/user"
|
||||
"forgejo.org/modules/activitypub"
|
||||
|
|
@ -26,7 +27,6 @@ import (
|
|||
// Flow of this test is documented at: https://codeberg.org/forgejo-contrib/federation/src/branch/main/doc/user-activity-following.md
|
||||
func TestActivityPubPersonInboxFollow(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.Federation.Enabled, true)()
|
||||
defer test.MockVariableValue(&setting.Federation.SignatureEnforced, false)()
|
||||
defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())()
|
||||
|
||||
federation.Init()
|
||||
|
|
@ -40,6 +40,7 @@ func TestActivityPubPersonInboxFollow(t *testing.T) {
|
|||
|
||||
distantURL := federatedSrv.URL
|
||||
distantUser15URL := fmt.Sprintf("%s/api/v1/activitypub/user-id/15", distantURL)
|
||||
distantUser15AliasURL := fmt.Sprintf("%s/api/v1/activitypub/user-id/alias15", distantURL)
|
||||
|
||||
localUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
localUser2URL := localUrl.JoinPath("/api/v1/activitypub/user-id/2").String()
|
||||
|
|
@ -52,14 +53,16 @@ func TestActivityPubPersonInboxFollow(t *testing.T) {
|
|||
`{"type":"Follow",`+
|
||||
`"actor":"%s",`+
|
||||
`"object":"%s"}`,
|
||||
distantUser15URL,
|
||||
distantUser15AliasURL,
|
||||
localUser2URL,
|
||||
)
|
||||
|
||||
cf, err := activitypub.NewClientFactoryWithTimeout(60 * time.Second)
|
||||
require.NoError(t, err)
|
||||
c, err := cf.WithKeysDirect(ctx, mock.ApActor.PrivKey,
|
||||
mock.ApActor.KeyID(federatedSrv.URL))
|
||||
|
||||
c, err := cf.WithKeysDirect(ctx, mock.ApActor.PrivKey, mock.ApActor.KeyID(federatedSrv.URL))
|
||||
require.NoError(t, err)
|
||||
|
||||
resp, err := c.Post(followActivity, localUser2Inbox)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusAccepted, resp.StatusCode)
|
||||
|
|
@ -94,9 +97,12 @@ func TestActivityPubPersonInboxFollow(t *testing.T) {
|
|||
distantUser15URL,
|
||||
localUser2URL,
|
||||
)
|
||||
|
||||
c, err = cf.WithKeysDirect(ctx, mock.ApActor.PrivKey,
|
||||
mock.ApActor.KeyID(federatedSrv.URL))
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
resp, err = c.Post(undoFollowActivity, localUser2Inbox)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusNoContent, resp.StatusCode)
|
||||
|
|
@ -110,3 +116,44 @@ func TestActivityPubPersonInboxFollow(t *testing.T) {
|
|||
)
|
||||
})
|
||||
}
|
||||
|
||||
func TestActivityPubFollowRefollow(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.Federation.Enabled, true)()
|
||||
defer test.MockVariableValue(&setting.Federation.SignatureEnforced, false)()
|
||||
defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())()
|
||||
|
||||
require.NoError(t, federation.Init())
|
||||
|
||||
mock := test.NewFederationServerMock()
|
||||
federatedSrv := mock.DistantServer(t)
|
||||
defer federatedSrv.Close()
|
||||
|
||||
onApplicationRun(t, func(t *testing.T, localUrl *url.URL) {
|
||||
defer test.MockVariableValue(&setting.AppURL, localUrl.String())()
|
||||
|
||||
distantURL := federatedSrv.URL
|
||||
distantUser15AliasURL := fmt.Sprintf("%s/api/v1/activitypub/user-id/alias15", distantURL)
|
||||
|
||||
localUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
localUser2URL := localUrl.JoinPath("/api/v1/activitypub/user-id/2")
|
||||
localUser2Inbox := localUrl.JoinPath("/api/v1/activitypub/user-id/2/inbox")
|
||||
|
||||
ctx := t.Context()
|
||||
|
||||
var follow user_model.FederatedUserFollower
|
||||
has, err := db.GetEngine(ctx).Get(&follow)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, has)
|
||||
|
||||
require.NoError(t, mock.FollowActorUnsigned(federatedSrv.URL, 15, *localUser2URL, *localUser2Inbox))
|
||||
|
||||
has, err = db.GetEngine(ctx).Get(&follow)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, has)
|
||||
assert.Equal(t, int64(2), follow.FollowedUserID)
|
||||
|
||||
apiCtx, _ := contexttest.MockAPIContext(t, localUser2Inbox.String())
|
||||
err = federation.FollowRemoteActor(apiCtx, localUser, distantUser15AliasURL)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"forgejo.org/models/activities"
|
||||
auth_model "forgejo.org/models/auth"
|
||||
"forgejo.org/models/unittest"
|
||||
user_model "forgejo.org/models/user"
|
||||
|
|
@ -26,9 +27,80 @@ import (
|
|||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestActivityPubPersonInboxNoteFromDistant(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.Federation.Enabled, true)()
|
||||
defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())()
|
||||
|
||||
federation.Init()
|
||||
|
||||
mock := test.NewFederationServerMock()
|
||||
federatedSrv := mock.DistantServer(t)
|
||||
defer federatedSrv.Close()
|
||||
|
||||
onApplicationRun(t, func(t *testing.T, localUrl *url.URL) {
|
||||
defer test.MockVariableValue(&setting.AppURL, localUrl.String())()
|
||||
|
||||
distantURL := federatedSrv.URL
|
||||
distantUser15URL := fmt.Sprintf("%s/api/v1/activitypub/user-id/15", distantURL)
|
||||
|
||||
localUser2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
localUser2URL := localUrl.JoinPath("/api/v1/activitypub/user-id/2").String()
|
||||
localUser2Inbox := localUrl.JoinPath("/api/v1/activitypub/user-id/2/inbox").String()
|
||||
localSession2 := loginUser(t, localUser2.LoginName)
|
||||
localSecssion2Token := getTokenForLoggedInUser(t, localSession2, auth_model.AccessTokenScopeWriteUser)
|
||||
|
||||
// view own empty feed on web UI
|
||||
feedPage := NewHTMLParser(t, localSession2.MakeRequest(t, NewRequest(t, "GET", "/user2?tab=feed"), http.StatusOK).Body)
|
||||
feedPage.AssertElement(t, "#empty-ap-feed", true)
|
||||
|
||||
// follow (local follows distant)
|
||||
req := NewRequestWithJSON(t, "POST",
|
||||
"/api/v1/user/activitypub/follow",
|
||||
&structs.APRemoteFollowOption{
|
||||
Target: distantUser15URL,
|
||||
}).
|
||||
AddTokenAuth(localSecssion2Token)
|
||||
MakeRequest(t, req, http.StatusNoContent)
|
||||
|
||||
// send note (distant -> local)
|
||||
distantNoteURL := fmt.Sprintf("%s/api/v1/activitypub/note/104", distantURL)
|
||||
|
||||
userActivity := fmt.Appendf(
|
||||
[]byte{},
|
||||
`{"type":"Create",`+
|
||||
`"actor":"%s",`+
|
||||
`"to": ["https://www.w3.org/ns/activitystreams#Public"],`+
|
||||
`"cc": ["%s"],`+
|
||||
`"object": {"type":"Note","content":"The Content!",`+
|
||||
`"url":"%s"}}`,
|
||||
distantUser15URL,
|
||||
localUser2URL,
|
||||
distantNoteURL,
|
||||
)
|
||||
|
||||
ctx, _ := contexttest.MockAPIContext(t, localUser2Inbox)
|
||||
cf, err := activitypub.NewClientFactoryWithTimeout(60 * time.Second)
|
||||
require.NoError(t, err)
|
||||
|
||||
c, err := cf.WithKeysDirect(ctx, mock.ApActor.PrivKey, mock.ApActor.KeyID(federatedSrv.URL))
|
||||
require.NoError(t, err)
|
||||
|
||||
resp, err := c.Post(userActivity, localUser2Inbox)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, http.StatusNoContent, resp.StatusCode)
|
||||
|
||||
// check whether user activity exists in local instance
|
||||
unittest.AssertExistsAndLoadBean(t, &activities.FederatedUserActivity{NoteURL: distantNoteURL})
|
||||
|
||||
// view own non-empty feed on web UI
|
||||
feedPage = NewHTMLParser(t, localSession2.MakeRequest(t, NewRequest(t, "GET", "/user2?tab=feed"), http.StatusOK).Body)
|
||||
feedPage.AssertElement(t, "#empty-ap-feed", false)
|
||||
})
|
||||
}
|
||||
|
||||
func TestActivityPubPersonInboxNoteToDistant(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.Federation.Enabled, true)()
|
||||
defer test.MockVariableValue(&setting.Federation.SignatureEnforced, false)()
|
||||
defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())()
|
||||
|
||||
federation.Init()
|
||||
|
|
@ -60,14 +132,17 @@ func TestActivityPubPersonInboxNoteToDistant(t *testing.T) {
|
|||
distantUser15URL,
|
||||
localUser2URL,
|
||||
)
|
||||
|
||||
ctx, _ := contexttest.MockAPIContext(t, localUser2Inbox)
|
||||
cf, err := activitypub.NewClientFactoryWithTimeout(60 * time.Second)
|
||||
require.NoError(t, err)
|
||||
c, err := cf.WithKeysDirect(ctx, mock.ApActor.PrivKey,
|
||||
mock.ApActor.KeyID(federatedSrv.URL))
|
||||
|
||||
c, err := cf.WithKeysDirect(ctx, mock.ApActor.PrivKey, mock.ApActor.KeyID(federatedSrv.URL))
|
||||
require.NoError(t, err)
|
||||
|
||||
resp, err := c.Post(followActivity, localUser2Inbox)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, http.StatusAccepted, resp.StatusCode)
|
||||
|
||||
// local action which triggers a user activity
|
||||
|
|
@ -87,9 +162,11 @@ func TestActivityPubPersonInboxNoteToDistant(t *testing.T) {
|
|||
// distant request activity & activity note
|
||||
localUser2ActivityNote := fmt.Sprintf("%v/activities/1", localUser2URL)
|
||||
localUser2Activity := fmt.Sprintf("%v/activities/1/activity", localUser2URL)
|
||||
|
||||
resp, err = c.Get(localUser2ActivityNote)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
resp, err = c.Get(localUser2Activity)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
|
|
|||
55
tests/integration/api_user_follow_federation_test.go
Normal file
55
tests/integration/api_user_follow_federation_test.go
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
// Copyright 2025 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
auth_model "forgejo.org/models/auth"
|
||||
"forgejo.org/models/forgefed"
|
||||
"forgejo.org/models/unittest"
|
||||
"forgejo.org/models/user"
|
||||
"forgejo.org/modules/setting"
|
||||
"forgejo.org/modules/structs"
|
||||
"forgejo.org/modules/test"
|
||||
"forgejo.org/routers"
|
||||
"forgejo.org/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestActivityPubFollowFederated(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
defer test.MockVariableValue(&setting.Federation.Enabled, true)()
|
||||
defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())()
|
||||
|
||||
mock := test.NewFederationServerMock()
|
||||
federatedSrv := mock.DistantServer(t)
|
||||
defer federatedSrv.Close()
|
||||
|
||||
localUser10Name := "user10"
|
||||
localSession10 := loginUser(t, localUser10Name)
|
||||
localSecssion10Token := getTokenForLoggedInUser(t, localSession10, auth_model.AccessTokenScopeWriteUser)
|
||||
|
||||
distantURL := federatedSrv.URL
|
||||
distantUser15URL := fmt.Sprintf("%s/api/v1/activitypub/user-id/15", distantURL)
|
||||
|
||||
// local user follow distant
|
||||
req := NewRequestWithJSON(t, "POST",
|
||||
"/api/v1/user/activitypub/follow",
|
||||
&structs.APRemoteFollowOption{
|
||||
Target: distantUser15URL,
|
||||
}).
|
||||
AddTokenAuth(localSecssion10Token)
|
||||
MakeRequest(t, req, http.StatusNoContent)
|
||||
|
||||
// check: federated actors now exist local
|
||||
federationHost := unittest.AssertExistsAndLoadBean(t, &forgefed.FederationHost{HostFqdn: "127.0.0.1"})
|
||||
unittest.AssertExistsAndLoadBean(t, &user.FederatedUser{ExternalID: "15", FederationHostID: federationHost.ID})
|
||||
|
||||
// check: follow request arrived at distant
|
||||
assert.Contains(t, mock.LastPost, "\"object\":\"http://DISTANT_FEDERATION_HOST/api/v1/activitypub/user-id/15\"")
|
||||
}
|
||||
53
tests/integration/federation_home_template_test.go
Normal file
53
tests/integration/federation_home_template_test.go
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
// Copyright 2026 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"forgejo.org/modules/setting"
|
||||
"forgejo.org/modules/test"
|
||||
"forgejo.org/routers"
|
||||
"forgejo.org/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
|
||||
func getLinks(t *testing.T, url string) []*html.Node {
|
||||
req := NewRequest(t, "GET", url)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
links := htmlDoc.doc.Find("link[type=\"application/activity+json\"]").Nodes
|
||||
|
||||
return links
|
||||
}
|
||||
|
||||
func TestFederationBaseHead(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())()
|
||||
|
||||
t.Run("Federation disabled", func(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.Federation.Enabled, false)()
|
||||
|
||||
links := getLinks(t, "/user1")
|
||||
assert.Empty(t, links)
|
||||
})
|
||||
|
||||
t.Run("Federation enabled", func(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.Federation.Enabled, true)()
|
||||
|
||||
links := getLinks(t, "/user1")
|
||||
assert.Len(t, links, 1)
|
||||
})
|
||||
|
||||
t.Run("Organization", func(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.Federation.Enabled, true)()
|
||||
|
||||
links := getLinks(t, "/org3")
|
||||
assert.Empty(t, links)
|
||||
})
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue