diff --git a/modules/test/distant_federation_server_mock.go b/modules/test/distant_federation_server_mock.go new file mode 100644 index 0000000000..fd68c88a40 --- /dev/null +++ b/modules/test/distant_federation_server_mock.go @@ -0,0 +1,117 @@ +// Copyright 2025 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package test + +import ( + "fmt" + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +type FederationServerMockPerson struct { + ID int64 + Name string + PubKey string +} +type FederationServerMockRepository struct { + ID int64 +} +type FederationServerMock struct { + Persons []FederationServerMockPerson + Repositories []FederationServerMockRepository + LastPost string +} + +func NewFederationServerMockPerson(id int64, name string) FederationServerMockPerson { + return FederationServerMockPerson{ + ID: id, + Name: name, + PubKey: `"-----BEGIN PUBLIC KEY-----\nMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEA18H5s7N6ItZUAh9tneII\nIuZdTTa3cZlLa/9ejWAHTkcp3WLW+/zbsumlMrWYfBy2/yTm56qasWt38iY4D6ul\n` + + `CPiwhAqX3REvVq8tM79a2CEqZn9ka6vuXoDgBg/sBf/BUWqf7orkjUXwk/U0Egjf\nk5jcurF4vqf1u+rlAHH37dvSBaDjNj6Qnj4OP12bjfaY/yvs7+jue/eNXFHjzN4E\n` + + `T2H4B/yeKTJ4UuAwTlLaNbZJul2baLlHelJPAsxiYaziVuV5P+IGWckY6RSerRaZ\nAkc4mmGGtjAyfN9aewe+lNVfwS7ElFx546PlLgdQgjmeSwLX8FWxbPE5A/PmaXCs\n` + + `nx+nou+3dD7NluULLtdd7K+2x02trObKXCAzmi5/Dc+yKTzpFqEz+hLNCz7TImP/\ncK//NV9Q+X67J9O27baH9R9ZF4zMw8rv2Pg0WLSw1z7lLXwlgIsDapeMCsrxkVO4\n` + + `LXX5AQ1xQNtlssnVoUBqBrvZsX2jUUKUocvZqMGuE4hfAgMBAAE=\n-----END PUBLIC KEY-----\n"`, + } +} + +func NewFederationServerMockRepository(id int64) FederationServerMockRepository { + return FederationServerMockRepository{ + ID: id, + } +} + +func (p FederationServerMockPerson) marshal(host string) string { + return fmt.Sprintf(`{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1"],`+ + `"id":"http://%[1]v/api/activitypub/user-id/%[2]v",`+ + `"type":"Person",`+ + `"icon":{"type":"Image","mediaType":"image/png","url":"http://%[1]v/avatars/1bb05d9a5f6675ed0272af9ea193063c"},`+ + `"url":"http://%[1]v/%[2]v",`+ + `"inbox":"http://%[1]v/api/activitypub/user-id/%[2]v/inbox",`+ + `"outbox":"http://%[1]v/api/activitypub/user-id/%[2]v/outbox",`+ + `"preferredUsername":"%[3]v",`+ + `"publicKey":{"id":"http://%[1]v/api/activitypub/user-id/%[2]v#main-key",`+ + `"owner":"http://%[1]v/api/activitypub/user-id/%[2]v",`+ + `"publicKeyPem":%[4]v}}`, host, p.ID, p.Name, p.PubKey) +} + +func NewFederationServerMock() *FederationServerMock { + return &FederationServerMock{ + Persons: []FederationServerMockPerson{ + NewFederationServerMockPerson(15, "stargoose1"), + NewFederationServerMockPerson(30, "stargoose2"), + }, + Repositories: []FederationServerMockRepository{ + NewFederationServerMockRepository(1), + }, + LastPost: "", + } +} + +func (mock *FederationServerMock) DistantServer(t *testing.T) *httptest.Server { + federatedRoutes := http.NewServeMux() + federatedRoutes.HandleFunc("/.well-known/nodeinfo", + func(res http.ResponseWriter, req *http.Request) { + // curl -H "Accept: application/json" https://federated-repo.prod.meissa.de/.well-known/nodeinfo + // TODO: as soon as content-type will become important: content-type: application/json;charset=utf-8 + fmt.Fprintf(res, `{"links":[{"href":"http://%s/api/v1/nodeinfo","rel":"http://nodeinfo.diaspora.software/ns/schema/2.1"}]}`, req.Host) + }) + federatedRoutes.HandleFunc("/api/v1/nodeinfo", + func(res http.ResponseWriter, req *http.Request) { + // curl -H "Accept: application/json" https://federated-repo.prod.meissa.de/api/v1/nodeinfo + fmt.Fprint(res, `{"version":"2.1","software":{"name":"forgejo","version":"1.20.0+dev-3183-g976d79044",`+ + `"repository":"https://codeberg.org/forgejo/forgejo.git","homepage":"https://forgejo.org/"},`+ + `"protocols":["activitypub"],"services":{"inbound":[],"outbound":["rss2.0"]},`+ + `"openRegistrations":true,"usage":{"users":{"total":14,"activeHalfyear":2}},"metadata":{}}`) + }) + for _, person := range mock.Persons { + federatedRoutes.HandleFunc(fmt.Sprintf("/api/v1/activitypub/user-id/%v", person.ID), + func(res http.ResponseWriter, req *http.Request) { + // curl -H "Accept: application/json" https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/2 + fmt.Fprint(res, person.marshal(req.Host)) + }) + } + for _, repository := range mock.Repositories { + federatedRoutes.HandleFunc(fmt.Sprintf("/api/v1/activitypub/repository-id/%v/inbox/", repository.ID), + func(res http.ResponseWriter, req *http.Request) { + if req.Method != "POST" { + t.Errorf("POST expected at: %q", req.URL.EscapedPath()) + } + buf := new(strings.Builder) + _, err := io.Copy(buf, req.Body) + if err != nil { + t.Errorf("Error reading body: %q", err) + } + mock.LastPost = buf.String() + }) + } + federatedRoutes.HandleFunc("/", + func(res http.ResponseWriter, req *http.Request) { + t.Errorf("Unhandled request: %q", req.URL.EscapedPath()) + }) + federatedSrv := httptest.NewServer(federatedRoutes) + return federatedSrv +} diff --git a/tests/integration/api_activitypub_repository_test.go b/tests/integration/api_activitypub_repository_test.go index 2ca7d42e2a..a86e32b278 100644 --- a/tests/integration/api_activitypub_repository_test.go +++ b/tests/integration/api_activitypub_repository_test.go @@ -1,4 +1,4 @@ -// Copyright 2024 The Forgejo Authors. All rights reserved. +// Copyright 2024, 2025 The Forgejo Authors. All rights reserved. // SPDX-License-Identifier: MIT package integration @@ -6,7 +6,6 @@ package integration import ( "fmt" "net/http" - "net/http/httptest" "net/url" "testing" "time" @@ -58,56 +57,8 @@ func TestActivityPubRepositoryInboxValid(t *testing.T) { defer test.MockVariableValue(&setting.Federation.Enabled, true)() defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())() - federatedRoutes := http.NewServeMux() - federatedRoutes.HandleFunc("/.well-known/nodeinfo", - func(res http.ResponseWriter, req *http.Request) { - // curl -H "Accept: application/json" https://federated-repo.prod.meissa.de/.well-known/nodeinfo - // TODO: as soon as content-type will become important: content-type: application/json;charset=utf-8 - fmt.Fprintf(res, `{"links":[{"href":"http://%s/api/v1/nodeinfo","rel":"http://nodeinfo.diaspora.software/ns/schema/2.1"}]}`, req.Host) - }) - federatedRoutes.HandleFunc("/api/v1/nodeinfo", - func(res http.ResponseWriter, req *http.Request) { - // curl -H "Accept: application/json" https://federated-repo.prod.meissa.de/api/v1/nodeinfo - fmt.Fprint(res, `{"version":"2.1","software":{"name":"forgejo","version":"1.20.0+dev-3183-g976d79044",`+ - `"repository":"https://codeberg.org/forgejo/forgejo.git","homepage":"https://forgejo.org/"},`+ - `"protocols":["activitypub"],"services":{"inbound":[],"outbound":["rss2.0"]},`+ - `"openRegistrations":true,"usage":{"users":{"total":14,"activeHalfyear":2}},"metadata":{}}`) - }) - federatedRoutes.HandleFunc("/api/v1/activitypub/user-id/15", - func(res http.ResponseWriter, req *http.Request) { - // curl -H "Accept: application/json" https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/2 - fmt.Fprint(res, `{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1"],`+ - `"id":"https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/15","type":"Person",`+ - `"icon":{"type":"Image","mediaType":"image/png","url":"https://federated-repo.prod.meissa.de/avatars/1bb05d9a5f6675ed0272af9ea193063c"},`+ - `"url":"https://federated-repo.prod.meissa.de/stargoose1","inbox":"https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/15/inbox",`+ - `"outbox":"https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/15/outbox","preferredUsername":"stargoose1",`+ - `"publicKey":{"id":"https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/15#main-key","owner":"https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/15",`+ - `"publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEA18H5s7N6ItZUAh9tneII\nIuZdTTa3cZlLa/9ejWAHTkcp3WLW+/zbsumlMrWYfBy2/yTm56qasWt38iY4D6ul\n`+ - `CPiwhAqX3REvVq8tM79a2CEqZn9ka6vuXoDgBg/sBf/BUWqf7orkjUXwk/U0Egjf\nk5jcurF4vqf1u+rlAHH37dvSBaDjNj6Qnj4OP12bjfaY/yvs7+jue/eNXFHjzN4E\n`+ - `T2H4B/yeKTJ4UuAwTlLaNbZJul2baLlHelJPAsxiYaziVuV5P+IGWckY6RSerRaZ\nAkc4mmGGtjAyfN9aewe+lNVfwS7ElFx546PlLgdQgjmeSwLX8FWxbPE5A/PmaXCs\n`+ - `nx+nou+3dD7NluULLtdd7K+2x02trObKXCAzmi5/Dc+yKTzpFqEz+hLNCz7TImP/\ncK//NV9Q+X67J9O27baH9R9ZF4zMw8rv2Pg0WLSw1z7lLXwlgIsDapeMCsrxkVO4\n`+ - `LXX5AQ1xQNtlssnVoUBqBrvZsX2jUUKUocvZqMGuE4hfAgMBAAE=\n-----END PUBLIC KEY-----\n"}}`) - }) - federatedRoutes.HandleFunc("/api/v1/activitypub/user-id/30", - func(res http.ResponseWriter, req *http.Request) { - // curl -H "Accept: application/json" https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/3 - fmt.Fprint(res, `{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1"],`+ - `"id":"https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/30","type":"Person",`+ - `"icon":{"type":"Image","mediaType":"image/png","url":"https://federated-repo.prod.meissa.de/avatars/9c03f03d1c1f13f21976a22489326fe1"},`+ - `"url":"https://federated-repo.prod.meissa.de/stargoose2","inbox":"https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/30/inbox",`+ - `"outbox":"https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/30/outbox","preferredUsername":"stargoose2",`+ - `"publicKey":{"id":"https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/30#main-key","owner":"https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/30",`+ - `"publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAyv5NytsfqpWXSrwuk8a3\n0W1zE13QJioXb/e3opgN2CfKZkdm3hb+4+mGKoU/rCqegnL9/AO0Aw+R8fCHXx44\n`+ - `iNkdVpdY8Dzq+tQ9IetPWbyVIBvSzGgvpqfS05JuVPsy8cBX9wByODjr5kq7k1/v\nY1G7E3uh0a/XJc+mZutwGC3gPgR93NSrqsvTPN4wdhCCu9uj02S8OBoKuSYaPkU+\n`+ - `tZ4CEDpnclAOw/eNiH4x2irMvVtruEgtlTA5K2I4YJrmtGLidus47FCyc8/zEKUh\nAeiD8KWDvqsQgOhUwcQgRxAnYVCoMD9cnE+WFFRHTuQecNlmdNFs3Cr0yKcWjDde\n`+ - `trvnehW7LfPveGb0tHRHPuVAJpncTOidUR5h/7pqMyvKHzuAHWomm9rEaGUxd/7a\nL1CFjAf39+QIEgu0Anj8mIc7CTiz+DQhDz+0jBOsQ0iDXc5GeBz7X9Xv4Jp966nq\n`+ - `MUR0GQGXvfZQN9IqMO+WoUVy10Ddhns1EWGlA0x4fecnAgMBAAE=\n-----END PUBLIC KEY-----\n"}}`) - }) - federatedRoutes.HandleFunc("/", - func(res http.ResponseWriter, req *http.Request) { - t.Errorf("Unhandled request: %q", req.URL.EscapedPath()) - }) - federatedSrv := httptest.NewServer(federatedRoutes) + mock := test.NewFederationServerMock() + federatedSrv := mock.DistantServer(t) defer federatedSrv.Close() onGiteaRun(t, func(t *testing.T, u *url.URL) { diff --git a/tests/integration/repo_settings_test.go b/tests/integration/repo_settings_test.go index ff138532f9..2c4ffba24d 100644 --- a/tests/integration/repo_settings_test.go +++ b/tests/integration/repo_settings_test.go @@ -5,9 +5,7 @@ package integration import ( "fmt" - "io" "net/http" - "net/http/httptest" "strings" "testing" @@ -21,6 +19,7 @@ import ( fm "code.gitea.io/gitea/modules/forgefed" "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/validation" gitea_context "code.gitea.io/gitea/services/context" repo_service "code.gitea.io/gitea/services/repository" @@ -278,59 +277,8 @@ func TestRepoFollowing(t *testing.T) { setting.Federation.Enabled = false }() - federatedRoutes := http.NewServeMux() - federatedRoutes.HandleFunc("/.well-known/nodeinfo", - func(res http.ResponseWriter, req *http.Request) { - // curl -H "Accept: application/json" https://federated-repo.prod.meissa.de/.well-known/nodeinfo - responseBody := fmt.Sprintf(`{"links":[{"href":"http://%s/api/v1/nodeinfo","rel":"http://nodeinfo.diaspora.software/ns/schema/2.1"}]}`, req.Host) - t.Logf("response: %s", responseBody) - // TODO: as soon as content-type will become important: content-type: application/json;charset=utf-8 - fmt.Fprint(res, responseBody) - }) - federatedRoutes.HandleFunc("/api/v1/nodeinfo", - func(res http.ResponseWriter, req *http.Request) { - // curl -H "Accept: application/json" https://federated-repo.prod.meissa.de/api/v1/nodeinfo - responseBody := fmt.Sprintf(`{"version":"2.1","software":{"name":"forgejo","version":"1.20.0+dev-3183-g976d79044",` + - `"repository":"https://codeberg.org/forgejo/forgejo.git","homepage":"https://forgejo.org/"},` + - `"protocols":["activitypub"],"services":{"inbound":[],"outbound":["rss2.0"]},` + - `"openRegistrations":true,"usage":{"users":{"total":14,"activeHalfyear":2}},"metadata":{}}`) - fmt.Fprint(res, responseBody) - }) - repo1InboxReceivedLike := false - federatedRoutes.HandleFunc("/api/v1/activitypub/repository-id/1/inbox/", - func(res http.ResponseWriter, req *http.Request) { - if req.Method != "POST" { - t.Errorf("Unhandled request: %q", req.URL.EscapedPath()) - } - buf := new(strings.Builder) - _, err := io.Copy(buf, req.Body) - if err != nil { - t.Errorf("Error reading body: %q", err) - } - like := fm.ForgeLike{} - err = like.UnmarshalJSON([]byte(buf.String())) - if err != nil { - t.Errorf("Error unmarshalling ForgeLike: %q", err) - } - if isValid, err := validation.IsValid(like); !isValid { - t.Errorf("ForgeLike is not valid: %q", err) - } - - activityType := like.Type - object := like.Object.GetLink().String() - isLikeType := activityType == "Like" - isCorrectObject := strings.HasSuffix(object, "/api/v1/activitypub/repository-id/1") - if !isLikeType || !isCorrectObject { - t.Errorf("Activity is not a like for this repo") - } - - repo1InboxReceivedLike = true - }) - federatedRoutes.HandleFunc("/", - func(res http.ResponseWriter, req *http.Request) { - t.Errorf("Unhandled request: %q", req.URL.EscapedPath()) - }) - federatedSrv := httptest.NewServer(federatedRoutes) + mock := test.NewFederationServerMock() + federatedSrv := mock.DistantServer(t) defer federatedSrv.Close() user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) @@ -363,8 +311,24 @@ func TestRepoFollowing(t *testing.T) { req := NewRequestWithValues(t, "POST", link, map[string]string{ "_csrf": GetCSRF(t, session, repoLink), }) - assert.False(t, repo1InboxReceivedLike) + session.MakeRequest(t, req, http.StatusOK) - assert.True(t, repo1InboxReceivedLike) + + // Verify distant server received a like activity + like := fm.ForgeLike{} + err := like.UnmarshalJSON([]byte(mock.LastPost)) + if err != nil { + t.Errorf("Error unmarshalling ForgeLike: %q", err) + } + if isValid, err := validation.IsValid(like); !isValid { + t.Errorf("ForgeLike is not valid: %q", err) + } + activityType := like.Type + object := like.Object.GetLink().String() + isLikeType := activityType == "Like" + isCorrectObject := strings.HasSuffix(object, "/api/v1/activitypub/repository-id/1") + if !isLikeType || !isCorrectObject { + t.Errorf("Activity is not a like for this repo") + } }) }