loading up the forgejo repo on tangled to test page performance
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

[FEAT] Repository flags

This implements "repository flags", a way for instance administrators to
assign custom flags to repositories. The idea is that custom templates
can look at these flags, and display banners based on them, Forgejo does
not provide anything built on top of it, just the foundation. The
feature is optional, and disabled by default. To enable it, set
`[repository].ENABLE_FLAGS = true`.

On the UI side, instance administrators will see a new "Manage flags"
tab on repositories, and a list of enabled tags (if any) on the
repository home page. The "Manage flags" page allows them to remove
existing flags, or add any new ones that are listed in
`[repository].SETTABLE_FLAGS`.

The model does not enforce that only the `SETTABLE_FLAGS` are present.
If the setting is changed, old flags may remain present in the database,
and anything that uses them, will still work. The repository flag
management page will allow an instance administrator to remove them, but
not set them, once removed.

Signed-off-by: Gergely Nagy <forgejo@gergo.csillger.hu>
(cherry picked from commit ba735ce2228f8dd7ca105e94b9baa1be058ebe37)
(cherry picked from commit f09f6e029b4fb2714b86cd32dc19255078ecc0ee)
(cherry picked from commit 2f8b0414892f6099f519bda63a9e0fbc8ba6cfc7)
(cherry picked from commit d3186ee5f41fac896c7d2341402fcd39dd250bf1)

authored by

Gergely Nagy and committed by
Earl Warren
36f7c162 9809f96a

+604
+2
models/forgejo_migrations/migrate.go
··· 46 46 NewMigration("create the forgejo_auth_token table", forgejo_v1_20.CreateAuthorizationTokenTable), 47 47 // v3 -> v4 48 48 NewMigration("Add default_permissions to repo_unit", forgejo_v1_22.AddDefaultPermissionsToRepoUnit), 49 + // v4 -> v5 50 + NewMigration("create the forgejo_repo_flag table", forgejo_v1_22.CreateRepoFlagTable), 49 51 } 50 52 51 53 // GetCurrentDBVersion returns the current Forgejo database version.
+22
models/forgejo_migrations/v1_22/v5.go
··· 1 + // Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved. 2 + // SPDX-License-Identifier: MIT 3 + 4 + package v1_22 //nolint 5 + 6 + import ( 7 + "xorm.io/xorm" 8 + ) 9 + 10 + type RepoFlag struct { 11 + ID int64 `xorm:"pk autoincr"` 12 + RepoID int64 `xorm:"UNIQUE(s) INDEX"` 13 + Name string `xorm:"UNIQUE(s) INDEX"` 14 + } 15 + 16 + func (RepoFlag) TableName() string { 17 + return "forgejo_repo_flag" 18 + } 19 + 20 + func CreateRepoFlagTable(x *xorm.Engine) error { 21 + return x.Sync(new(RepoFlag)) 22 + }
+102
models/repo/repo_flags.go
··· 1 + // Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved. 2 + // SPDX-License-Identifier: MIT 3 + 4 + package repo 5 + 6 + import ( 7 + "context" 8 + 9 + "code.gitea.io/gitea/models/db" 10 + 11 + "xorm.io/builder" 12 + ) 13 + 14 + // RepoFlag represents a single flag against a repository 15 + type RepoFlag struct { //revive:disable-line:exported 16 + ID int64 `xorm:"pk autoincr"` 17 + RepoID int64 `xorm:"UNIQUE(s) INDEX"` 18 + Name string `xorm:"UNIQUE(s) INDEX"` 19 + } 20 + 21 + func init() { 22 + db.RegisterModel(new(RepoFlag)) 23 + } 24 + 25 + // TableName provides the real table name 26 + func (RepoFlag) TableName() string { 27 + return "forgejo_repo_flag" 28 + } 29 + 30 + // ListFlags returns the array of flags on the repo. 31 + func (repo *Repository) ListFlags(ctx context.Context) ([]RepoFlag, error) { 32 + var flags []RepoFlag 33 + err := db.GetEngine(ctx).Table(&RepoFlag{}).Where("repo_id = ?", repo.ID).Find(&flags) 34 + if err != nil { 35 + return nil, err 36 + } 37 + return flags, nil 38 + } 39 + 40 + // IsFlagged returns whether a repo has any flags or not 41 + func (repo *Repository) IsFlagged(ctx context.Context) bool { 42 + has, _ := db.Exist[RepoFlag](ctx, builder.Eq{"repo_id": repo.ID}) 43 + return has 44 + } 45 + 46 + // GetFlag returns a single RepoFlag based on its name 47 + func (repo *Repository) GetFlag(ctx context.Context, flagName string) (bool, *RepoFlag, error) { 48 + flag, has, err := db.Get[RepoFlag](ctx, builder.Eq{"repo_id": repo.ID, "name": flagName}) 49 + if err != nil { 50 + return false, nil, err 51 + } 52 + return has, flag, nil 53 + } 54 + 55 + // HasFlag returns true if a repo has a given flag, false otherwise 56 + func (repo *Repository) HasFlag(ctx context.Context, flagName string) bool { 57 + has, _ := db.Exist[RepoFlag](ctx, builder.Eq{"repo_id": repo.ID, "name": flagName}) 58 + return has 59 + } 60 + 61 + // AddFlag adds a new flag to the repo 62 + func (repo *Repository) AddFlag(ctx context.Context, flagName string) error { 63 + return db.Insert(ctx, RepoFlag{ 64 + RepoID: repo.ID, 65 + Name: flagName, 66 + }) 67 + } 68 + 69 + // DeleteFlag removes a flag from the repo 70 + func (repo *Repository) DeleteFlag(ctx context.Context, flagName string) (int64, error) { 71 + return db.DeleteByBean(ctx, &RepoFlag{RepoID: repo.ID, Name: flagName}) 72 + } 73 + 74 + // ReplaceAllFlags replaces all flags of a repo with a new set 75 + func (repo *Repository) ReplaceAllFlags(ctx context.Context, flagNames []string) error { 76 + ctx, committer, err := db.TxContext(ctx) 77 + if err != nil { 78 + return err 79 + } 80 + defer committer.Close() 81 + 82 + if err := db.DeleteBeans(ctx, &RepoFlag{RepoID: repo.ID}); err != nil { 83 + return err 84 + } 85 + 86 + if len(flagNames) == 0 { 87 + return committer.Commit() 88 + } 89 + 90 + var flags []RepoFlag 91 + for _, name := range flagNames { 92 + flags = append(flags, RepoFlag{ 93 + RepoID: repo.ID, 94 + Name: name, 95 + }) 96 + } 97 + if err := db.Insert(ctx, &flags); err != nil { 98 + return err 99 + } 100 + 101 + return committer.Commit() 102 + }
+114
models/repo/repo_flags_test.go
··· 1 + // Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved. 2 + // SPDX-License-Identifier: MIT 3 + 4 + package repo_test 5 + 6 + import ( 7 + "testing" 8 + 9 + "code.gitea.io/gitea/models/db" 10 + repo_model "code.gitea.io/gitea/models/repo" 11 + "code.gitea.io/gitea/models/unittest" 12 + 13 + "github.com/stretchr/testify/assert" 14 + ) 15 + 16 + func TestRepositoryFlags(t *testing.T) { 17 + assert.NoError(t, unittest.PrepareTestDatabase()) 18 + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 10}) 19 + 20 + // ******************** 21 + // ** NEGATIVE TESTS ** 22 + // ******************** 23 + 24 + // Unless we add flags, the repo has none 25 + flags, err := repo.ListFlags(db.DefaultContext) 26 + assert.NoError(t, err) 27 + assert.Empty(t, flags) 28 + 29 + // If the repo has no flags, it is not flagged 30 + flagged := repo.IsFlagged(db.DefaultContext) 31 + assert.False(t, flagged) 32 + 33 + // Trying to find a flag when there is none 34 + has := repo.HasFlag(db.DefaultContext, "foo") 35 + assert.False(t, has) 36 + 37 + // Trying to retrieve a non-existent flag indicates not found 38 + has, _, err = repo.GetFlag(db.DefaultContext, "foo") 39 + assert.NoError(t, err) 40 + assert.False(t, has) 41 + 42 + // Deleting a non-existent flag fails 43 + deleted, err := repo.DeleteFlag(db.DefaultContext, "no-such-flag") 44 + assert.NoError(t, err) 45 + assert.Equal(t, int64(0), deleted) 46 + 47 + // ******************** 48 + // ** POSITIVE TESTS ** 49 + // ******************** 50 + 51 + // Adding a flag works 52 + err = repo.AddFlag(db.DefaultContext, "foo") 53 + assert.NoError(t, err) 54 + 55 + // Adding it again fails 56 + err = repo.AddFlag(db.DefaultContext, "foo") 57 + assert.Error(t, err) 58 + 59 + // Listing flags includes the one we added 60 + flags, err = repo.ListFlags(db.DefaultContext) 61 + assert.NoError(t, err) 62 + assert.Len(t, flags, 1) 63 + assert.Equal(t, "foo", flags[0].Name) 64 + 65 + // With a flag added, the repo is flagged 66 + flagged = repo.IsFlagged(db.DefaultContext) 67 + assert.True(t, flagged) 68 + 69 + // The flag can be found 70 + has = repo.HasFlag(db.DefaultContext, "foo") 71 + assert.True(t, has) 72 + 73 + // Added flag can be retrieved 74 + _, flag, err := repo.GetFlag(db.DefaultContext, "foo") 75 + assert.NoError(t, err) 76 + assert.Equal(t, "foo", flag.Name) 77 + 78 + // Deleting a flag works 79 + deleted, err = repo.DeleteFlag(db.DefaultContext, "foo") 80 + assert.NoError(t, err) 81 + assert.Equal(t, int64(1), deleted) 82 + 83 + // The list is now empty 84 + flags, err = repo.ListFlags(db.DefaultContext) 85 + assert.NoError(t, err) 86 + assert.Empty(t, flags) 87 + 88 + // Replacing an empty list works 89 + err = repo.ReplaceAllFlags(db.DefaultContext, []string{"bar"}) 90 + assert.NoError(t, err) 91 + 92 + // The repo is now flagged with "bar" 93 + has = repo.HasFlag(db.DefaultContext, "bar") 94 + assert.True(t, has) 95 + 96 + // Replacing a tag set with another works 97 + err = repo.ReplaceAllFlags(db.DefaultContext, []string{"baz", "quux"}) 98 + assert.NoError(t, err) 99 + 100 + // The repo now has two tags 101 + flags, err = repo.ListFlags(db.DefaultContext) 102 + assert.NoError(t, err) 103 + assert.Len(t, flags, 2) 104 + assert.Equal(t, "baz", flags[0].Name) 105 + assert.Equal(t, "quux", flags[1].Name) 106 + 107 + // Replacing flags with an empty set deletes all flags 108 + err = repo.ReplaceAllFlags(db.DefaultContext, []string{}) 109 + assert.NoError(t, err) 110 + 111 + // The repo is now unflagged 112 + flagged = repo.IsFlagged(db.DefaultContext) 113 + assert.False(t, flagged) 114 + }
+7
modules/setting/repository.go
··· 113 113 Wiki []string 114 114 DefaultTrustModel string 115 115 } `ini:"repository.signing"` 116 + 117 + SettableFlags []string 118 + EnableFlags bool 116 119 }{ 117 120 DetectedCharsetsOrder: []string{ 118 121 "UTF-8", ··· 270 273 Wiki: []string{"never"}, 271 274 DefaultTrustModel: "collaborator", 272 275 }, 276 + 277 + EnableFlags: false, 273 278 } 274 279 RepoRootPath string 275 280 ScriptType = "bash" ··· 372 377 log.Error("Unrecognised repository download or clone method: %s", method) 373 378 } 374 379 } 380 + 381 + Repository.EnableFlags = sec.Key("ENABLE_FLAGS").MustBool() 375 382 }
+3
modules/templates/helper.go
··· 96 96 "AppDomain": func() string { // documented in mail-templates.md 97 97 return setting.Domain 98 98 }, 99 + "RepoFlagsEnabled": func() bool { 100 + return setting.Repository.EnableFlags 101 + }, 99 102 "AssetVersion": func() string { 100 103 return setting.AssetVersion 101 104 },
+49
routers/web/repo/flags/manage.go
··· 1 + // Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved. 2 + // SPDX-License-Identifier: MIT 3 + 4 + package flags 5 + 6 + import ( 7 + "net/http" 8 + 9 + "code.gitea.io/gitea/modules/base" 10 + "code.gitea.io/gitea/modules/context" 11 + "code.gitea.io/gitea/modules/log" 12 + "code.gitea.io/gitea/modules/setting" 13 + ) 14 + 15 + const ( 16 + tplRepoFlags base.TplName = "repo/flags" 17 + ) 18 + 19 + func Manage(ctx *context.Context) { 20 + ctx.Data["IsRepoFlagsPage"] = true 21 + ctx.Data["Title"] = ctx.Tr("repo.admin.manage_flags") 22 + 23 + flags := map[string]bool{} 24 + for _, f := range setting.Repository.SettableFlags { 25 + flags[f] = false 26 + } 27 + repoFlags, _ := ctx.Repo.Repository.ListFlags(ctx) 28 + for _, f := range repoFlags { 29 + flags[f.Name] = true 30 + } 31 + 32 + ctx.Data["Flags"] = flags 33 + 34 + ctx.HTML(http.StatusOK, tplRepoFlags) 35 + } 36 + 37 + func ManagePost(ctx *context.Context) { 38 + newFlags := ctx.FormStrings("flags") 39 + 40 + err := ctx.Repo.Repository.ReplaceAllFlags(ctx, newFlags) 41 + if err != nil { 42 + ctx.Flash.Error(ctx.Tr("repo.admin.failed_to_replace_flags")) 43 + log.Error("Error replacing repository flags for repo %d: %v", ctx.Repo.Repository.ID, err) 44 + } else { 45 + ctx.Flash.Success(ctx.Tr("repo.admin.flags_replaced")) 46 + } 47 + 48 + ctx.Redirect(ctx.Repo.Repository.HTMLURL() + "/flags") 49 + }
+8
routers/web/web.go
··· 38 38 "code.gitea.io/gitea/routers/web/repo" 39 39 "code.gitea.io/gitea/routers/web/repo/actions" 40 40 "code.gitea.io/gitea/routers/web/repo/badges" 41 + repo_flags "code.gitea.io/gitea/routers/web/repo/flags" 41 42 repo_setting "code.gitea.io/gitea/routers/web/repo/setting" 42 43 "code.gitea.io/gitea/routers/web/user" 43 44 user_setting "code.gitea.io/gitea/routers/web/user/setting" ··· 1574 1575 gitHTTPRouters(m) 1575 1576 }) 1576 1577 }) 1578 + 1579 + if setting.Repository.EnableFlags { 1580 + m.Group("/{username}/{reponame}/flags", func() { 1581 + m.Get("", repo_flags.Manage) 1582 + m.Post("", repo_flags.ManagePost) 1583 + }, adminReq, context.RepoAssignment, context.UnitTypes()) 1584 + } 1577 1585 // ***** END: Repository ***** 1578 1586 1579 1587 m.Group("/notifications", func() {
templates/custom/repo_flag_banners.tmpl

This is a binary file and will not be displayed.

+8
templates/repo/admin_flags.tmpl
··· 1 + {{if .Repository.IsFlagged $.Context}} 2 + <div class="ui info message" style="text-align: left"> 3 + <strong>{{ctx.Locale.Tr "repo.admin.enabled_flags"}}</strong> 4 + {{range .Repository.ListFlags $.Context}} 5 + <span class="ui label">{{.Name}}</span> 6 + {{end}} 7 + </div> 8 + {{end}}
+33
templates/repo/flags.tmpl
··· 1 + {{template "base/head" .}} 2 + <div role="main" aria-label="{{.Title}}" class="page-content repository"> 3 + {{template "repo/header" .}} 4 + <div class="ui container"> 5 + {{template "base/alert" .}} 6 + <div class="user-main-content twelve wide column"> 7 + <h4 class="ui top attached header"> 8 + {{ctx.Locale.Tr "repo.admin.manage_flags"}} 9 + </h4> 10 + <div class="ui attached segment"> 11 + <form class="ui form" action="{{.Link}}" method="post"> 12 + {{.CsrfTokenHtml}} 13 + <strong>{{ctx.Locale.Tr "repo.admin.enabled_flags"}}</strong> 14 + <div class="ui segment gt-pl-4"> 15 + {{range $flag, $checked := .Flags}} 16 + <div class="field"> 17 + <div class="ui checkbox{{if $checked}} checked{{end}}"> 18 + <input name="flags" type="checkbox" value="{{$flag}}" {{if $checked}}checked{{end}}> 19 + <label>{{$flag}}</label> 20 + </div> 21 + </div> 22 + {{end}} 23 + </div> 24 + 25 + <div class="field"> 26 + <button class="ui primary button">{{ctx.Locale.Tr "repo.admin.update_flags"}}</button> 27 + </div> 28 + </form> 29 + </div> 30 + </div> 31 + </div> 32 + </div> 33 + {{template "base/footer" .}}
+6
templates/repo/header.tmpl
··· 212 212 213 213 {{template "custom/extra_tabs" .}} 214 214 215 + {{if and RepoFlagsEnabled .SignedUser.IsAdmin}} 216 + <a class="{{if .IsRepoFlagsPage}}active {{end}}item" href="{{.RepoLink}}/flags"> 217 + {{svg "octicon-milestone"}} {{ctx.Locale.Tr "repo.admin.manage_flags"}} 218 + </a> 219 + {{end}} 220 + 215 221 {{if .Permission.IsAdmin}} 216 222 <a class="{{if .PageIsRepoSettings}}active {{end}} item" href="{{.RepoLink}}/settings"> 217 223 {{svg "octicon-tools"}} {{ctx.Locale.Tr "repo.settings"}}
+8
templates/repo/home.tmpl
··· 52 52 </div> 53 53 </div> 54 54 {{end}} 55 + 56 + {{if RepoFlagsEnabled}} 57 + {{template "custom/repo_flag_banners" .}} 58 + {{if .SignedUser.IsAdmin}} 59 + {{template "repo/admin_flags" .}} 60 + {{end}} 61 + {{end}} 62 + 55 63 {{if .Repository.IsArchived}} 56 64 <div class="ui warning message gt-text-center"> 57 65 {{if .Repository.ArchivedUnix.IsZero}}
+242
tests/integration/repo_flags_test.go
··· 1 + // Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved. 2 + // SPDX-License-Identifier: MIT 3 + 4 + package integration 5 + 6 + import ( 7 + "fmt" 8 + "net/http" 9 + "net/http/httptest" 10 + "testing" 11 + 12 + "code.gitea.io/gitea/models/db" 13 + repo_model "code.gitea.io/gitea/models/repo" 14 + "code.gitea.io/gitea/models/unittest" 15 + user_model "code.gitea.io/gitea/models/user" 16 + "code.gitea.io/gitea/modules/setting" 17 + "code.gitea.io/gitea/modules/test" 18 + "code.gitea.io/gitea/routers" 19 + "code.gitea.io/gitea/tests" 20 + 21 + "github.com/stretchr/testify/assert" 22 + ) 23 + 24 + func TestRepositoryFlagsUIDisabled(t *testing.T) { 25 + defer tests.PrepareTestEnv(t)() 26 + defer test.MockVariableValue(&setting.Repository.EnableFlags, false)() 27 + defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())() 28 + 29 + admin := unittest.AssertExistsAndLoadBean(t, &user_model.User{IsAdmin: true}) 30 + session := loginUser(t, admin.Name) 31 + 32 + // With the repo flags feature disabled, the /flags route is 404 33 + req := NewRequest(t, "GET", "/user2/repo1/flags") 34 + session.MakeRequest(t, req, http.StatusNotFound) 35 + 36 + // With the repo flags feature disabled, the "Modify flags" tab does not 37 + // appear for instance admins 38 + req = NewRequest(t, "GET", "/user2/repo1") 39 + resp := session.MakeRequest(t, req, http.StatusOK) 40 + doc := NewHTMLParser(t, resp.Body) 41 + flagsLinkCount := doc.Find(fmt.Sprintf(`a[href="%s/flags"]`, "/user2/repo1")).Length() 42 + assert.Equal(t, 0, flagsLinkCount) 43 + } 44 + 45 + func TestRepositoryFlagsUI(t *testing.T) { 46 + defer tests.PrepareTestEnv(t)() 47 + defer test.MockVariableValue(&setting.Repository.EnableFlags, true)() 48 + defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())() 49 + 50 + // ******************* 51 + // ** Preparations ** 52 + // ******************* 53 + flaggedRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) 54 + unflaggedRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4}) 55 + 56 + // ************** 57 + // ** Helpers ** 58 + // ************** 59 + 60 + adminUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{IsAdmin: true}).Name 61 + flaggedOwner := "user2" 62 + flaggedRepoURLStr := "/user2/repo1" 63 + unflaggedOwner := "user5" 64 + unflaggedRepoURLStr := "/user5/repo4" 65 + otherUser := "user4" 66 + 67 + ensureFlags := func(repo *repo_model.Repository, flags []string) func() { 68 + repo.ReplaceAllFlags(db.DefaultContext, flags) 69 + 70 + return func() { 71 + repo.ReplaceAllFlags(db.DefaultContext, flags) 72 + } 73 + } 74 + 75 + // Tests: 76 + // - Presence of the link 77 + // - Number of flags listed in the admin-only message box 78 + // - Whether there's a link to /user/repo/flags 79 + // - Whether /user/repo/flags is OK or Forbidden 80 + assertFlagAccessAndCount := func(t *testing.T, user, repoURL string, hasAccess bool, expectedFlagCount int) { 81 + t.Helper() 82 + 83 + var expectedLinkCount int 84 + var expectedStatus int 85 + if hasAccess { 86 + expectedLinkCount = 1 87 + expectedStatus = http.StatusOK 88 + } else { 89 + expectedLinkCount = 0 90 + if user != "" { 91 + expectedStatus = http.StatusForbidden 92 + } else { 93 + expectedStatus = http.StatusSeeOther 94 + } 95 + } 96 + 97 + var resp *httptest.ResponseRecorder 98 + var session *TestSession 99 + req := NewRequest(t, "GET", repoURL) 100 + if user != "" { 101 + session = loginUser(t, user) 102 + resp = session.MakeRequest(t, req, http.StatusOK) 103 + } else { 104 + resp = MakeRequest(t, req, http.StatusOK) 105 + } 106 + doc := NewHTMLParser(t, resp.Body) 107 + 108 + flagsLinkCount := doc.Find(fmt.Sprintf(`a[href="%s/flags"]`, repoURL)).Length() 109 + assert.Equal(t, expectedLinkCount, flagsLinkCount) 110 + 111 + flagCount := doc.Find(".ui.info.message .ui.label").Length() 112 + assert.Equal(t, expectedFlagCount, flagCount) 113 + 114 + req = NewRequest(t, "GET", fmt.Sprintf("%s/flags", repoURL)) 115 + if user != "" { 116 + session.MakeRequest(t, req, expectedStatus) 117 + } else { 118 + MakeRequest(t, req, expectedStatus) 119 + } 120 + } 121 + 122 + // Ensures that given a repo owner and a repo: 123 + // - An instance admin has access to flags, and sees the list on the repo home 124 + // - A repo admin does not have access to either, and does not see the list 125 + // - A passer by has no access to either, and does not see the list 126 + runTests := func(t *testing.T, ownerUser, repoURL string, expectedFlagCount int) { 127 + t.Run("as instance admin", func(t *testing.T) { 128 + defer tests.PrintCurrentTest(t)() 129 + 130 + assertFlagAccessAndCount(t, adminUser, repoURL, true, expectedFlagCount) 131 + }) 132 + t.Run("as owner", func(t *testing.T) { 133 + defer tests.PrintCurrentTest(t)() 134 + 135 + assertFlagAccessAndCount(t, ownerUser, repoURL, false, 0) 136 + }) 137 + t.Run("as other user", func(t *testing.T) { 138 + defer tests.PrintCurrentTest(t)() 139 + 140 + assertFlagAccessAndCount(t, otherUser, repoURL, false, 0) 141 + }) 142 + t.Run("as non-logged in user", func(t *testing.T) { 143 + defer tests.PrintCurrentTest(t)() 144 + 145 + assertFlagAccessAndCount(t, "", repoURL, false, 0) 146 + }) 147 + } 148 + 149 + // ************************** 150 + // ** The tests themselves ** 151 + // ************************** 152 + t.Run("unflagged repo", func(t *testing.T) { 153 + defer tests.PrintCurrentTest(t)() 154 + defer ensureFlags(unflaggedRepo, []string{})() 155 + 156 + runTests(t, unflaggedOwner, unflaggedRepoURLStr, 0) 157 + }) 158 + 159 + t.Run("flagged repo", func(t *testing.T) { 160 + defer tests.PrintCurrentTest(t)() 161 + defer ensureFlags(flaggedRepo, []string{"test-flag"})() 162 + 163 + runTests(t, flaggedOwner, flaggedRepoURLStr, 1) 164 + }) 165 + 166 + t.Run("modifying flags", func(t *testing.T) { 167 + defer tests.PrintCurrentTest(t)() 168 + 169 + session := loginUser(t, adminUser) 170 + flaggedRepoManageURL := fmt.Sprintf("%s/flags", flaggedRepoURLStr) 171 + unflaggedRepoManageURL := fmt.Sprintf("%s/flags", unflaggedRepoURLStr) 172 + 173 + assertUIFlagStates := func(t *testing.T, url string, flagStates map[string]bool) { 174 + t.Helper() 175 + 176 + req := NewRequest(t, "GET", url) 177 + resp := session.MakeRequest(t, req, http.StatusOK) 178 + 179 + doc := NewHTMLParser(t, resp.Body) 180 + flagBoxes := doc.Find(`input[name="flags"]`) 181 + assert.Equal(t, len(flagStates), flagBoxes.Length()) 182 + 183 + for name, state := range flagStates { 184 + _, checked := doc.Find(fmt.Sprintf(`input[value="%s"]`, name)).Attr("checked") 185 + assert.Equal(t, state, checked) 186 + } 187 + } 188 + 189 + t.Run("flag presence on the UI", func(t *testing.T) { 190 + defer tests.PrintCurrentTest(t)() 191 + defer ensureFlags(flaggedRepo, []string{"test-flag"})() 192 + 193 + assertUIFlagStates(t, flaggedRepoManageURL, map[string]bool{"test-flag": true}) 194 + }) 195 + 196 + t.Run("setting.Repository.SettableFlags is respected", func(t *testing.T) { 197 + defer tests.PrintCurrentTest(t)() 198 + defer test.MockVariableValue(&setting.Repository.SettableFlags, []string{"featured", "no-license"})() 199 + defer ensureFlags(flaggedRepo, []string{"test-flag"})() 200 + 201 + assertUIFlagStates(t, flaggedRepoManageURL, map[string]bool{ 202 + "test-flag": true, 203 + "featured": false, 204 + "no-license": false, 205 + }) 206 + }) 207 + 208 + t.Run("removing flags", func(t *testing.T) { 209 + defer tests.PrintCurrentTest(t)() 210 + defer ensureFlags(flaggedRepo, []string{"test-flag"})() 211 + 212 + flagged := flaggedRepo.IsFlagged(db.DefaultContext) 213 + assert.True(t, flagged) 214 + 215 + req := NewRequestWithValues(t, "POST", flaggedRepoManageURL, map[string]string{ 216 + "_csrf": GetCSRF(t, session, flaggedRepoManageURL), 217 + }) 218 + session.MakeRequest(t, req, http.StatusSeeOther) 219 + 220 + flagged = flaggedRepo.IsFlagged(db.DefaultContext) 221 + assert.False(t, flagged) 222 + 223 + assertUIFlagStates(t, flaggedRepoManageURL, map[string]bool{}) 224 + }) 225 + 226 + t.Run("adding flags", func(t *testing.T) { 227 + defer tests.PrintCurrentTest(t)() 228 + defer ensureFlags(unflaggedRepo, []string{})() 229 + 230 + flagged := unflaggedRepo.IsFlagged(db.DefaultContext) 231 + assert.False(t, flagged) 232 + 233 + req := NewRequestWithValues(t, "POST", unflaggedRepoManageURL, map[string]string{ 234 + "_csrf": GetCSRF(t, session, unflaggedRepoManageURL), 235 + "flags": "test-flag", 236 + }) 237 + session.MakeRequest(t, req, http.StatusSeeOther) 238 + 239 + assertUIFlagStates(t, unflaggedRepoManageURL, map[string]bool{"test-flag": true}) 240 + }) 241 + }) 242 + }