diff options
Diffstat (limited to 'tests/integration/repo_flags_test.go')
-rw-r--r-- | tests/integration/repo_flags_test.go | 391 |
1 files changed, 391 insertions, 0 deletions
diff --git a/tests/integration/repo_flags_test.go b/tests/integration/repo_flags_test.go new file mode 100644 index 00000000..8b64776a --- /dev/null +++ b/tests/integration/repo_flags_test.go @@ -0,0 +1,391 @@ +// 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}) + }) + }) +} |