639b428cf4
Expose the repository flags feature over the API, so the flags can be managed by a site administrator without using the web API. Signed-off-by: Gergely Nagy <forgejo@gergo.csillger.hu> (cherry picked from commitbac9f0225d
) (cherry picked from commite7f5c1ba14
) (cherry picked from commit95d9fe19cf
) (cherry picked from commit7fc51991e4
)
391 lines
13 KiB
Go
391 lines
13 KiB
Go
// 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"
|
|
"slices"
|
|
"testing"
|
|
|
|
auth_model "code.gitea.io/gitea/models/auth"
|
|
"code.gitea.io/gitea/models/db"
|
|
repo_model "code.gitea.io/gitea/models/repo"
|
|
"code.gitea.io/gitea/models/unittest"
|
|
user_model "code.gitea.io/gitea/models/user"
|
|
"code.gitea.io/gitea/modules/setting"
|
|
api "code.gitea.io/gitea/modules/structs"
|
|
"code.gitea.io/gitea/modules/test"
|
|
"code.gitea.io/gitea/routers"
|
|
"code.gitea.io/gitea/tests"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
func TestRepositoryFlagsUIDisabled(t *testing.T) {
|
|
defer tests.PrepareTestEnv(t)()
|
|
defer test.MockVariableValue(&setting.Repository.EnableFlags, false)()
|
|
defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())()
|
|
|
|
admin := unittest.AssertExistsAndLoadBean(t, &user_model.User{IsAdmin: true})
|
|
session := loginUser(t, admin.Name)
|
|
|
|
// With the repo flags feature disabled, the /flags route is 404
|
|
req := NewRequest(t, "GET", "/user2/repo1/flags")
|
|
session.MakeRequest(t, req, http.StatusNotFound)
|
|
|
|
// With the repo flags feature disabled, the "Modify flags" tab does not
|
|
// appear for instance admins
|
|
req = NewRequest(t, "GET", "/user2/repo1")
|
|
resp := session.MakeRequest(t, req, http.StatusOK)
|
|
doc := NewHTMLParser(t, resp.Body)
|
|
flagsLinkCount := doc.Find(fmt.Sprintf(`a[href="%s/flags"]`, "/user2/repo1")).Length()
|
|
assert.Equal(t, 0, flagsLinkCount)
|
|
}
|
|
|
|
func TestRepositoryFlagsAPI(t *testing.T) {
|
|
defer tests.PrepareTestEnv(t)()
|
|
defer test.MockVariableValue(&setting.Repository.EnableFlags, true)()
|
|
defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())()
|
|
|
|
// *************
|
|
// ** Helpers **
|
|
// *************
|
|
|
|
adminUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{IsAdmin: true}).Name
|
|
normalUserBean := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
|
assert.False(t, normalUserBean.IsAdmin)
|
|
normalUser := normalUserBean.Name
|
|
|
|
assertAccess := func(t *testing.T, user, method, uri string, expectedStatus int) {
|
|
session := loginUser(t, user)
|
|
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeReadAdmin)
|
|
|
|
req := NewRequestf(t, method, "/api/v1/repos/user2/repo1/flags%s", uri).AddTokenAuth(token)
|
|
MakeRequest(t, req, expectedStatus)
|
|
}
|
|
|
|
// ***********
|
|
// ** Tests **
|
|
// ***********
|
|
|
|
t.Run("API access", func(t *testing.T) {
|
|
t.Run("as admin", func(t *testing.T) {
|
|
defer tests.PrintCurrentTest(t)()
|
|
|
|
assertAccess(t, adminUser, "GET", "", http.StatusOK)
|
|
})
|
|
|
|
t.Run("as normal user", func(t *testing.T) {
|
|
defer tests.PrintCurrentTest(t)()
|
|
|
|
assertAccess(t, normalUser, "GET", "", http.StatusForbidden)
|
|
})
|
|
})
|
|
|
|
t.Run("token scopes", func(t *testing.T) {
|
|
defer tests.PrintCurrentTest(t)()
|
|
|
|
// Trying to access the API with a token that lacks permissions, will
|
|
// fail, even if the token owner is an instance admin.
|
|
session := loginUser(t, adminUser)
|
|
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
|
|
|
req := NewRequest(t, "GET", "/api/v1/repos/user2/repo1/flags").AddTokenAuth(token)
|
|
MakeRequest(t, req, http.StatusForbidden)
|
|
})
|
|
|
|
t.Run("setting.Repository.EnableFlags is respected", func(t *testing.T) {
|
|
defer tests.PrintCurrentTest(t)()
|
|
defer test.MockVariableValue(&setting.Repository.EnableFlags, false)()
|
|
defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())()
|
|
|
|
t.Run("as admin", func(t *testing.T) {
|
|
defer tests.PrintCurrentTest(t)()
|
|
|
|
assertAccess(t, adminUser, "GET", "", http.StatusNotFound)
|
|
})
|
|
|
|
t.Run("as normal user", func(t *testing.T) {
|
|
defer tests.PrintCurrentTest(t)()
|
|
|
|
assertAccess(t, normalUser, "GET", "", http.StatusNotFound)
|
|
})
|
|
})
|
|
|
|
t.Run("API functionality", func(t *testing.T) {
|
|
defer tests.PrintCurrentTest(t)()
|
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
|
|
defer func() {
|
|
repo.ReplaceAllFlags(db.DefaultContext, []string{})
|
|
}()
|
|
|
|
baseURLFmtStr := "/api/v1/repos/user5/repo4/flags%s"
|
|
|
|
session := loginUser(t, adminUser)
|
|
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteAdmin)
|
|
|
|
// Listing flags
|
|
req := NewRequestf(t, "GET", baseURLFmtStr, "").AddTokenAuth(token)
|
|
resp := MakeRequest(t, req, http.StatusOK)
|
|
var flags []string
|
|
DecodeJSON(t, resp, &flags)
|
|
assert.Empty(t, flags)
|
|
|
|
// Replacing all tags works, twice in a row
|
|
for i := 0; i < 2; i++ {
|
|
req = NewRequestWithJSON(t, "PUT", fmt.Sprintf(baseURLFmtStr, ""), &api.ReplaceFlagsOption{
|
|
Flags: []string{"flag-1", "flag-2", "flag-3"},
|
|
}).AddTokenAuth(token)
|
|
MakeRequest(t, req, http.StatusNoContent)
|
|
}
|
|
|
|
// The list now includes all three flags
|
|
req = NewRequestf(t, "GET", baseURLFmtStr, "").AddTokenAuth(token)
|
|
resp = MakeRequest(t, req, http.StatusOK)
|
|
DecodeJSON(t, resp, &flags)
|
|
assert.Len(t, flags, 3)
|
|
for _, flag := range []string{"flag-1", "flag-2", "flag-3"} {
|
|
assert.True(t, slices.Contains(flags, flag))
|
|
}
|
|
|
|
// Check a flag that is on the repo
|
|
req = NewRequestf(t, "GET", baseURLFmtStr, "/flag-1").AddTokenAuth(token)
|
|
MakeRequest(t, req, http.StatusNoContent)
|
|
|
|
// Check a flag that isn't on the repo
|
|
req = NewRequestf(t, "GET", baseURLFmtStr, "/no-such-flag").AddTokenAuth(token)
|
|
MakeRequest(t, req, http.StatusNotFound)
|
|
|
|
// We can add the same flag twice
|
|
for i := 0; i < 2; i++ {
|
|
req = NewRequestf(t, "PUT", baseURLFmtStr, "/brand-new-flag").AddTokenAuth(token)
|
|
MakeRequest(t, req, http.StatusNoContent)
|
|
}
|
|
|
|
// The new flag is there
|
|
req = NewRequestf(t, "GET", baseURLFmtStr, "/brand-new-flag").AddTokenAuth(token)
|
|
MakeRequest(t, req, http.StatusNoContent)
|
|
|
|
// We can delete a flag, twice
|
|
for i := 0; i < 2; i++ {
|
|
req = NewRequestf(t, "DELETE", baseURLFmtStr, "/flag-3").AddTokenAuth(token)
|
|
MakeRequest(t, req, http.StatusNoContent)
|
|
}
|
|
|
|
// We can delete a flag that wasn't there
|
|
req = NewRequestf(t, "DELETE", baseURLFmtStr, "/no-such-flag").AddTokenAuth(token)
|
|
MakeRequest(t, req, http.StatusNoContent)
|
|
|
|
// We can delete all of the flags in one go, too
|
|
req = NewRequestf(t, "DELETE", baseURLFmtStr, "").AddTokenAuth(token)
|
|
MakeRequest(t, req, http.StatusNoContent)
|
|
|
|
// ..once all flags are deleted, none are listed, either
|
|
req = NewRequestf(t, "GET", baseURLFmtStr, "").AddTokenAuth(token)
|
|
resp = MakeRequest(t, req, http.StatusOK)
|
|
DecodeJSON(t, resp, &flags)
|
|
assert.Empty(t, flags)
|
|
})
|
|
}
|
|
|
|
func TestRepositoryFlagsUI(t *testing.T) {
|
|
defer tests.PrepareTestEnv(t)()
|
|
defer test.MockVariableValue(&setting.Repository.EnableFlags, true)()
|
|
defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())()
|
|
|
|
// *******************
|
|
// ** Preparations **
|
|
// *******************
|
|
flaggedRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
|
unflaggedRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
|
|
|
|
// **************
|
|
// ** Helpers **
|
|
// **************
|
|
|
|
adminUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{IsAdmin: true}).Name
|
|
flaggedOwner := "user2"
|
|
flaggedRepoURLStr := "/user2/repo1"
|
|
unflaggedOwner := "user5"
|
|
unflaggedRepoURLStr := "/user5/repo4"
|
|
otherUser := "user4"
|
|
|
|
ensureFlags := func(repo *repo_model.Repository, flags []string) func() {
|
|
repo.ReplaceAllFlags(db.DefaultContext, flags)
|
|
|
|
return func() {
|
|
repo.ReplaceAllFlags(db.DefaultContext, flags)
|
|
}
|
|
}
|
|
|
|
// Tests:
|
|
// - Presence of the link
|
|
// - Number of flags listed in the admin-only message box
|
|
// - Whether there's a link to /user/repo/flags
|
|
// - Whether /user/repo/flags is OK or Forbidden
|
|
assertFlagAccessAndCount := func(t *testing.T, user, repoURL string, hasAccess bool, expectedFlagCount int) {
|
|
t.Helper()
|
|
|
|
var expectedLinkCount int
|
|
var expectedStatus int
|
|
if hasAccess {
|
|
expectedLinkCount = 1
|
|
expectedStatus = http.StatusOK
|
|
} else {
|
|
expectedLinkCount = 0
|
|
if user != "" {
|
|
expectedStatus = http.StatusForbidden
|
|
} else {
|
|
expectedStatus = http.StatusSeeOther
|
|
}
|
|
}
|
|
|
|
var resp *httptest.ResponseRecorder
|
|
var session *TestSession
|
|
req := NewRequest(t, "GET", repoURL)
|
|
if user != "" {
|
|
session = loginUser(t, user)
|
|
resp = session.MakeRequest(t, req, http.StatusOK)
|
|
} else {
|
|
resp = MakeRequest(t, req, http.StatusOK)
|
|
}
|
|
doc := NewHTMLParser(t, resp.Body)
|
|
|
|
flagsLinkCount := doc.Find(fmt.Sprintf(`a[href="%s/flags"]`, repoURL)).Length()
|
|
assert.Equal(t, expectedLinkCount, flagsLinkCount)
|
|
|
|
flagCount := doc.Find(".ui.info.message .ui.label").Length()
|
|
assert.Equal(t, expectedFlagCount, flagCount)
|
|
|
|
req = NewRequest(t, "GET", fmt.Sprintf("%s/flags", repoURL))
|
|
if user != "" {
|
|
session.MakeRequest(t, req, expectedStatus)
|
|
} else {
|
|
MakeRequest(t, req, expectedStatus)
|
|
}
|
|
}
|
|
|
|
// Ensures that given a repo owner and a repo:
|
|
// - An instance admin has access to flags, and sees the list on the repo home
|
|
// - A repo admin does not have access to either, and does not see the list
|
|
// - A passer by has no access to either, and does not see the list
|
|
runTests := func(t *testing.T, ownerUser, repoURL string, expectedFlagCount int) {
|
|
t.Run("as instance admin", func(t *testing.T) {
|
|
defer tests.PrintCurrentTest(t)()
|
|
|
|
assertFlagAccessAndCount(t, adminUser, repoURL, true, expectedFlagCount)
|
|
})
|
|
t.Run("as owner", func(t *testing.T) {
|
|
defer tests.PrintCurrentTest(t)()
|
|
|
|
assertFlagAccessAndCount(t, ownerUser, repoURL, false, 0)
|
|
})
|
|
t.Run("as other user", func(t *testing.T) {
|
|
defer tests.PrintCurrentTest(t)()
|
|
|
|
assertFlagAccessAndCount(t, otherUser, repoURL, false, 0)
|
|
})
|
|
t.Run("as non-logged in user", func(t *testing.T) {
|
|
defer tests.PrintCurrentTest(t)()
|
|
|
|
assertFlagAccessAndCount(t, "", repoURL, false, 0)
|
|
})
|
|
}
|
|
|
|
// **************************
|
|
// ** The tests themselves **
|
|
// **************************
|
|
t.Run("unflagged repo", func(t *testing.T) {
|
|
defer tests.PrintCurrentTest(t)()
|
|
defer ensureFlags(unflaggedRepo, []string{})()
|
|
|
|
runTests(t, unflaggedOwner, unflaggedRepoURLStr, 0)
|
|
})
|
|
|
|
t.Run("flagged repo", func(t *testing.T) {
|
|
defer tests.PrintCurrentTest(t)()
|
|
defer ensureFlags(flaggedRepo, []string{"test-flag"})()
|
|
|
|
runTests(t, flaggedOwner, flaggedRepoURLStr, 1)
|
|
})
|
|
|
|
t.Run("modifying flags", func(t *testing.T) {
|
|
defer tests.PrintCurrentTest(t)()
|
|
|
|
session := loginUser(t, adminUser)
|
|
flaggedRepoManageURL := fmt.Sprintf("%s/flags", flaggedRepoURLStr)
|
|
unflaggedRepoManageURL := fmt.Sprintf("%s/flags", unflaggedRepoURLStr)
|
|
|
|
assertUIFlagStates := func(t *testing.T, url string, flagStates map[string]bool) {
|
|
t.Helper()
|
|
|
|
req := NewRequest(t, "GET", url)
|
|
resp := session.MakeRequest(t, req, http.StatusOK)
|
|
|
|
doc := NewHTMLParser(t, resp.Body)
|
|
flagBoxes := doc.Find(`input[name="flags"]`)
|
|
assert.Equal(t, len(flagStates), flagBoxes.Length())
|
|
|
|
for name, state := range flagStates {
|
|
_, checked := doc.Find(fmt.Sprintf(`input[value="%s"]`, name)).Attr("checked")
|
|
assert.Equal(t, state, checked)
|
|
}
|
|
}
|
|
|
|
t.Run("flag presence on the UI", func(t *testing.T) {
|
|
defer tests.PrintCurrentTest(t)()
|
|
defer ensureFlags(flaggedRepo, []string{"test-flag"})()
|
|
|
|
assertUIFlagStates(t, flaggedRepoManageURL, map[string]bool{"test-flag": true})
|
|
})
|
|
|
|
t.Run("setting.Repository.SettableFlags is respected", func(t *testing.T) {
|
|
defer tests.PrintCurrentTest(t)()
|
|
defer test.MockVariableValue(&setting.Repository.SettableFlags, []string{"featured", "no-license"})()
|
|
defer ensureFlags(flaggedRepo, []string{"test-flag"})()
|
|
|
|
assertUIFlagStates(t, flaggedRepoManageURL, map[string]bool{
|
|
"test-flag": true,
|
|
"featured": false,
|
|
"no-license": false,
|
|
})
|
|
})
|
|
|
|
t.Run("removing flags", func(t *testing.T) {
|
|
defer tests.PrintCurrentTest(t)()
|
|
defer ensureFlags(flaggedRepo, []string{"test-flag"})()
|
|
|
|
flagged := flaggedRepo.IsFlagged(db.DefaultContext)
|
|
assert.True(t, flagged)
|
|
|
|
req := NewRequestWithValues(t, "POST", flaggedRepoManageURL, map[string]string{
|
|
"_csrf": GetCSRF(t, session, flaggedRepoManageURL),
|
|
})
|
|
session.MakeRequest(t, req, http.StatusSeeOther)
|
|
|
|
flagged = flaggedRepo.IsFlagged(db.DefaultContext)
|
|
assert.False(t, flagged)
|
|
|
|
assertUIFlagStates(t, flaggedRepoManageURL, map[string]bool{})
|
|
})
|
|
|
|
t.Run("adding flags", func(t *testing.T) {
|
|
defer tests.PrintCurrentTest(t)()
|
|
defer ensureFlags(unflaggedRepo, []string{})()
|
|
|
|
flagged := unflaggedRepo.IsFlagged(db.DefaultContext)
|
|
assert.False(t, flagged)
|
|
|
|
req := NewRequestWithValues(t, "POST", unflaggedRepoManageURL, map[string]string{
|
|
"_csrf": GetCSRF(t, session, unflaggedRepoManageURL),
|
|
"flags": "test-flag",
|
|
})
|
|
session.MakeRequest(t, req, http.StatusSeeOther)
|
|
|
|
assertUIFlagStates(t, unflaggedRepoManageURL, map[string]bool{"test-flag": true})
|
|
})
|
|
})
|
|
}
|