forgejo/tests/integration/repo_fork_test.go
Gusted a4642af51a
Some checks are pending
/ release (push) Waiting to run
testing-integration / test-unit (push) Waiting to run
testing-integration / test-sqlite (push) Waiting to run
testing-integration / test-mariadb (v10.6) (push) Waiting to run
testing-integration / test-mariadb (v11.8) (push) Waiting to run
testing / backend-checks (push) Waiting to run
testing / frontend-checks (push) Waiting to run
testing / test-unit (push) Blocked by required conditions
testing / test-e2e (push) Blocked by required conditions
testing / test-remote-cacher (redis) (push) Blocked by required conditions
testing / test-remote-cacher (valkey) (push) Blocked by required conditions
testing / test-remote-cacher (garnet) (push) Blocked by required conditions
testing / test-remote-cacher (redict) (push) Blocked by required conditions
testing / test-mysql (push) Blocked by required conditions
testing / test-pgsql (push) Blocked by required conditions
testing / test-sqlite (push) Blocked by required conditions
testing / security-check (push) Blocked by required conditions
feat: replace cross origin protection (#9830)
Replace the anti-CSRF token with a [cross origin protection by Go](https://go.dev/doc/go1.25#nethttppkgnethttp) that uses a stateless way of verifying if a request was cross origin or not. This allows is to remove al lot of code and replace it with a few lines of code and we no longer have to hand roll this protection. The new protection uses indicators by the browser itself that indicate if the request is cross-origin, thus we no longer have to take care of ensuring the generated CSRF token is passed back to the server any request by the the browser will have send this indicator.

Resolves forgejo/forgejo#3538

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/9830
Reviewed-by: oliverpool <oliverpool@noreply.codeberg.org>
Reviewed-by: Mathieu Fenniak <mfenniak@noreply.codeberg.org>
Co-authored-by: Gusted <postmaster@gusted.xyz>
Co-committed-by: Gusted <postmaster@gusted.xyz>
2025-10-29 22:43:22 +01:00

274 lines
9.5 KiB
Go

// Copyright 2017 The Gitea Authors. All rights reserved.
// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"forgejo.org/models/db"
repo_model "forgejo.org/models/repo"
"forgejo.org/models/unittest"
user_model "forgejo.org/models/user"
"forgejo.org/modules/setting"
"forgejo.org/modules/structs"
"forgejo.org/modules/test"
"forgejo.org/routers"
repo_service "forgejo.org/services/repository"
"forgejo.org/tests"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func testRepoFork(t *testing.T, session *TestSession, ownerName, repoName, forkOwnerName, forkRepoName string) *httptest.ResponseRecorder {
t.Helper()
forkOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: forkOwnerName})
// Step0: check the existence of the to-fork repo
req := NewRequestf(t, "GET", "/%s/%s", forkOwnerName, forkRepoName)
session.MakeRequest(t, req, http.StatusNotFound)
// Step1: visit the /fork page
forkURL := fmt.Sprintf("/%s/%s/fork", ownerName, repoName)
req = NewRequest(t, "GET", forkURL)
resp := session.MakeRequest(t, req, http.StatusOK)
// Step2: fill the form of the forking
htmlDoc := NewHTMLParser(t, resp.Body)
link, exists := htmlDoc.doc.Find(fmt.Sprintf("form.ui.form[action=\"%s\"]", forkURL)).Attr("action")
assert.True(t, exists, "The template has changed")
_, exists = htmlDoc.doc.Find(fmt.Sprintf(".owner.dropdown .item[data-value=\"%d\"]", forkOwner.ID)).Attr("data-value")
assert.True(t, exists, "Fork owner %q is not present in select box", forkOwnerName)
req = NewRequestWithValues(t, "POST", link, map[string]string{
"uid": fmt.Sprintf("%d", forkOwner.ID),
"repo_name": forkRepoName,
})
session.MakeRequest(t, req, http.StatusSeeOther)
// Step3: check the existence of the forked repo
req = NewRequestf(t, "GET", "/%s/%s", forkOwnerName, forkRepoName)
resp = session.MakeRequest(t, req, http.StatusOK)
return resp
}
func testRepoForkLegacyRedirect(t *testing.T, session *TestSession, ownerName, repoName string) {
t.Helper()
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: ownerName})
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: owner.ID, Name: repoName})
// Visit the /repo/fork/:id url
req := NewRequestf(t, "GET", "/repo/fork/%d", repo.ID)
resp := session.MakeRequest(t, req, http.StatusMovedPermanently)
assert.Equal(t, repo.Link()+"/fork", resp.Header().Get("Location"))
}
func TestRepoFork(t *testing.T) {
onApplicationRun(t, func(t *testing.T, u *url.URL) {
user5 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user5"})
session := loginUser(t, user5.Name)
t.Run("by name", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
defer func() {
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: user5.ID, Name: "repo1"})
repo_service.DeleteRepository(db.DefaultContext, user5, repo, false)
}()
testRepoFork(t, session, "user2", "repo1", "user5", "repo1")
})
t.Run("legacy redirect", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
testRepoForkLegacyRedirect(t, session, "user2", "repo1")
t.Run("private 404", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
// Make sure the repo we try to fork is private
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 31, IsPrivate: true})
// user5 does not have access to user2/repo20
req := NewRequestf(t, "GET", "/repo/fork/%d", repo.ID) // user2/repo20
session.MakeRequest(t, req, http.StatusNotFound)
})
t.Run("authenticated private redirect", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
// Make sure the repo we try to fork is private
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 31, IsPrivate: true})
// user1 has access to user2/repo20
session := loginUser(t, "user1")
req := NewRequestf(t, "GET", "/repo/fork/%d", repo.ID) // user2/repo20
session.MakeRequest(t, req, http.StatusMovedPermanently)
})
t.Run("no code unit", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
// Make sure the repo we try to fork is private.
// We're also choosing user15/big_test_private_2, because it has the Code unit disabled.
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 20, IsPrivate: true})
// user1, even though an admin, can't fork a repo without a code unit.
session := loginUser(t, "user1")
req := NewRequestf(t, "GET", "/repo/fork/%d", repo.ID) // user15/big_test_private_2
session.MakeRequest(t, req, http.StatusNotFound)
})
})
t.Run("fork button", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequest(t, "GET", "/user2/repo1/issues")
resp := MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
forksLink := htmlDoc.Find("a[href$='/forks']")
assert.Equal(t, 1, forksLink.Length())
href, _ := forksLink.Attr("href")
assert.Equal(t, "/user2/repo1/forks", href)
assert.Equal(t, "0", strings.TrimSpace(forksLink.Text()))
ariaLabel, _ := forksLink.Attr("aria-label")
assert.Equal(t, "0 forks", ariaLabel)
t.Run("no fork button on empty repo", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
// Create an empty repository
repo, err := repo_service.CreateRepository(db.DefaultContext, user5, user5, repo_service.CreateRepoOptions{
Name: "empty-repo",
AutoInit: false,
})
defer func() {
repo_service.DeleteRepository(db.DefaultContext, user5, repo, false)
}()
require.NoError(t, err)
assert.NotEmpty(t, repo)
// Load the repository home view
req := NewRequest(t, "GET", repo.HTMLURL())
resp := session.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
// On an empty repo, the fork button is not present
htmlDoc.AssertElement(t, ".basic.button[href*='/fork']", false)
})
})
t.Run("DISABLE_FORKS", func(t *testing.T) {
defer test.MockVariableValue(&setting.Repository.DisableForks, true)()
defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())()
t.Run("fork button not present", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
// The "Fork" button should not appear on the repo home
req := NewRequest(t, "GET", "/user2/repo1")
resp := MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
htmlDoc.AssertElement(t, "[href=/user2/repo1/fork]", false)
})
t.Run("forking by URL", func(t *testing.T) {
t.Run("by name", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
// Forking by URL should be Not Found
req := NewRequest(t, "GET", "/user2/repo1/fork")
session.MakeRequest(t, req, http.StatusNotFound)
})
t.Run("by legacy URL", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
// Forking by legacy URL should be Not Found
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) // user2/repo1
req := NewRequestf(t, "GET", "/repo/fork/%d", repo.ID)
session.MakeRequest(t, req, http.StatusNotFound)
})
})
t.Run("fork listing", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
// Listing the forks should be Not Found, too
req := NewRequest(t, "GET", "/user2/repo1/forks")
MakeRequest(t, req, http.StatusNotFound)
})
})
})
}
func TestRepoForkToOrg(t *testing.T) {
onApplicationRun(t, func(t *testing.T, u *url.URL) {
session := loginUser(t, "user2")
org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "org3"})
t.Run("by name", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
defer func() {
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: org3.ID, Name: "repo1"})
repo_service.DeleteRepository(db.DefaultContext, org3, repo, false)
}()
testRepoFork(t, session, "user2", "repo1", "org3", "repo1")
// Check that no more forking is allowed as user2 owns repository
// and org3 organization that owner user2 is also now has forked this repository
req := NewRequest(t, "GET", "/user2/repo1")
resp := session.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
_, exists := htmlDoc.doc.Find("a.ui.button[href^=\"/fork\"]").Attr("href")
assert.False(t, exists, "Forking should not be allowed anymore")
})
t.Run("legacy redirect", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
testRepoForkLegacyRedirect(t, session, "user2", "repo1")
})
})
}
func TestForkListPrivateRepo(t *testing.T) {
forkItemSelector := ".tw-flex.tw-items-center.tw-py-2"
onApplicationRun(t, func(t *testing.T, u *url.URL) {
session := loginUser(t, "user5")
org23 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 23, Visibility: structs.VisibleTypePrivate})
testRepoFork(t, session, "user2", "repo1", org23.Name, "repo1")
t.Run("Anonymous", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequest(t, "GET", "/user2/repo1/forks")
resp := MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
htmlDoc.AssertElement(t, forkItemSelector, false)
})
t.Run("Logged in", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequest(t, "GET", "/user2/repo1/forks")
resp := session.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
htmlDoc.AssertElement(t, forkItemSelector, true)
})
})
}