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] API support for repository flags

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 commit bac9f0225d47e159afa90e5bbea9562cbc860dae)
(cherry picked from commit e7f5c1ba141ac7f8c7834b5048d0ffd3ce50900b)
(cherry picked from commit 95d9fe19cf3ed5787855ac2a442d29104498aa36)
(cherry picked from commit 7fc51991e405ea8d44fd6b4b4de13ad65da63ae7)

authored by

Gergely Nagy and committed by
Earl Warren
639b428c 36f7c162

+686
+9
modules/structs/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 structs 5 + 6 + // ReplaceFlagsOption options when replacing the flags of a repository 7 + type ReplaceFlagsOption struct { 8 + Flags []string `json:"flags"` 9 + }
+12
routers/api/v1/api.go
··· 1096 1096 m.Get("/permission", repo.GetRepoPermissions) 1097 1097 }) 1098 1098 }, reqToken()) 1099 + if setting.Repository.EnableFlags { 1100 + m.Group("/flags", func() { 1101 + m.Combo("").Get(repo.ListFlags). 1102 + Put(bind(api.ReplaceFlagsOption{}), repo.ReplaceAllFlags). 1103 + Delete(repo.DeleteAllFlags) 1104 + m.Group("/{flag}", func() { 1105 + m.Combo("").Get(repo.HasFlag). 1106 + Put(repo.AddFlag). 1107 + Delete(repo.DeleteFlag) 1108 + }) 1109 + }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryAdmin), reqToken(), reqSiteAdmin()) 1110 + } 1099 1111 m.Get("/assignees", reqToken(), reqAnyRepoReader(), repo.GetAssignees) 1100 1112 m.Get("/reviewers", reqToken(), reqAnyRepoReader(), repo.GetReviewers) 1101 1113 m.Group("/teams", func() {
+245
routers/api/v1/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 + "net/http" 8 + 9 + "code.gitea.io/gitea/modules/context" 10 + api "code.gitea.io/gitea/modules/structs" 11 + "code.gitea.io/gitea/modules/web" 12 + ) 13 + 14 + func ListFlags(ctx *context.APIContext) { 15 + // swagger:operation GET /repos/{owner}/{repo}/flags repository repoListFlags 16 + // --- 17 + // summary: List a repository's flags 18 + // produces: 19 + // - application/json 20 + // parameters: 21 + // - name: owner 22 + // in: path 23 + // description: owner of the repo 24 + // type: string 25 + // required: true 26 + // - name: repo 27 + // in: path 28 + // description: name of the repo 29 + // type: string 30 + // required: true 31 + // responses: 32 + // "200": 33 + // "$ref": "#/responses/StringSlice" 34 + // "403": 35 + // "$ref": "#/responses/forbidden" 36 + // "404": 37 + // "$ref": "#/responses/notFound" 38 + 39 + repoFlags, err := ctx.Repo.Repository.ListFlags(ctx) 40 + if err != nil { 41 + ctx.InternalServerError(err) 42 + return 43 + } 44 + 45 + flags := make([]string, len(repoFlags)) 46 + for i := range repoFlags { 47 + flags[i] = repoFlags[i].Name 48 + } 49 + 50 + ctx.SetTotalCountHeader(int64(len(repoFlags))) 51 + ctx.JSON(http.StatusOK, flags) 52 + } 53 + 54 + func ReplaceAllFlags(ctx *context.APIContext) { 55 + // swagger:operation PUT /repos/{owner}/{repo}/flags repository repoReplaceAllFlags 56 + // --- 57 + // summary: Replace all flags of a repository 58 + // produces: 59 + // - application/json 60 + // parameters: 61 + // - name: owner 62 + // in: path 63 + // description: owner of the repo 64 + // type: string 65 + // required: true 66 + // - name: repo 67 + // in: path 68 + // description: name of the repo 69 + // type: string 70 + // required: true 71 + // - name: body 72 + // in: body 73 + // schema: 74 + // "$ref": "#/definitions/ReplaceFlagsOption" 75 + // responses: 76 + // "204": 77 + // "$ref": "#/responses/empty" 78 + // "403": 79 + // "$ref": "#/responses/forbidden" 80 + // "404": 81 + // "$ref": "#/responses/notFound" 82 + 83 + flagsForm := web.GetForm(ctx).(*api.ReplaceFlagsOption) 84 + 85 + if err := ctx.Repo.Repository.ReplaceAllFlags(ctx, flagsForm.Flags); err != nil { 86 + ctx.InternalServerError(err) 87 + return 88 + } 89 + 90 + ctx.Status(http.StatusNoContent) 91 + } 92 + 93 + func DeleteAllFlags(ctx *context.APIContext) { 94 + // swagger:operation DELETE /repos/{owner}/{repo}/flags repository repoDeleteAllFlags 95 + // --- 96 + // summary: Remove all flags from a repository 97 + // produces: 98 + // - application/json 99 + // parameters: 100 + // - name: owner 101 + // in: path 102 + // description: owner of the repo 103 + // type: string 104 + // required: true 105 + // - name: repo 106 + // in: path 107 + // description: name of the repo 108 + // type: string 109 + // required: true 110 + // responses: 111 + // "204": 112 + // "$ref": "#/responses/empty" 113 + // "403": 114 + // "$ref": "#/responses/forbidden" 115 + // "404": 116 + // "$ref": "#/responses/notFound" 117 + 118 + if err := ctx.Repo.Repository.ReplaceAllFlags(ctx, nil); err != nil { 119 + ctx.InternalServerError(err) 120 + return 121 + } 122 + 123 + ctx.Status(http.StatusNoContent) 124 + } 125 + 126 + func HasFlag(ctx *context.APIContext) { 127 + // swagger:operation GET /repos/{owner}/{repo}/flags/{flag} repository repoCheckFlag 128 + // --- 129 + // summary: Check if a repository has a given flag 130 + // produces: 131 + // - application/json 132 + // parameters: 133 + // - name: owner 134 + // in: path 135 + // description: owner of the repo 136 + // type: string 137 + // required: true 138 + // - name: repo 139 + // in: path 140 + // description: name of the repo 141 + // type: string 142 + // required: true 143 + // - name: flag 144 + // in: path 145 + // description: name of the flag 146 + // type: string 147 + // required: true 148 + // responses: 149 + // "204": 150 + // "$ref": "#/responses/empty" 151 + // "403": 152 + // "$ref": "#/responses/forbidden" 153 + // "404": 154 + // "$ref": "#/responses/notFound" 155 + 156 + hasFlag := ctx.Repo.Repository.HasFlag(ctx, ctx.Params(":flag")) 157 + if hasFlag { 158 + ctx.Status(http.StatusNoContent) 159 + } else { 160 + ctx.NotFound() 161 + } 162 + } 163 + 164 + func AddFlag(ctx *context.APIContext) { 165 + // swagger:operation PUT /repos/{owner}/{repo}/flags/{flag} repository repoAddFlag 166 + // --- 167 + // summary: Add a flag to a repository 168 + // produces: 169 + // - application/json 170 + // parameters: 171 + // - name: owner 172 + // in: path 173 + // description: owner of the repo 174 + // type: string 175 + // required: true 176 + // - name: repo 177 + // in: path 178 + // description: name of the repo 179 + // type: string 180 + // required: true 181 + // - name: flag 182 + // in: path 183 + // description: name of the flag 184 + // type: string 185 + // required: true 186 + // responses: 187 + // "204": 188 + // "$ref": "#/responses/empty" 189 + // "403": 190 + // "$ref": "#/responses/forbidden" 191 + // "404": 192 + // "$ref": "#/responses/notFound" 193 + 194 + flag := ctx.Params(":flag") 195 + 196 + if ctx.Repo.Repository.HasFlag(ctx, flag) { 197 + ctx.Status(http.StatusNoContent) 198 + return 199 + } 200 + 201 + if err := ctx.Repo.Repository.AddFlag(ctx, flag); err != nil { 202 + ctx.InternalServerError(err) 203 + return 204 + } 205 + ctx.Status(http.StatusNoContent) 206 + } 207 + 208 + func DeleteFlag(ctx *context.APIContext) { 209 + // swagger:operation DELETE /repos/{owner}/{repo}/flags/{flag} repository repoDeleteFlag 210 + // --- 211 + // summary: Remove a flag from a repository 212 + // produces: 213 + // - application/json 214 + // parameters: 215 + // - name: owner 216 + // in: path 217 + // description: owner of the repo 218 + // type: string 219 + // required: true 220 + // - name: repo 221 + // in: path 222 + // description: name of the repo 223 + // type: string 224 + // required: true 225 + // - name: flag 226 + // in: path 227 + // description: name of the flag 228 + // type: string 229 + // required: true 230 + // responses: 231 + // "204": 232 + // "$ref": "#/responses/empty" 233 + // "403": 234 + // "$ref": "#/responses/forbidden" 235 + // "404": 236 + // "$ref": "#/responses/notFound" 237 + 238 + flag := ctx.Params(":flag") 239 + 240 + if _, err := ctx.Repo.Repository.DeleteFlag(ctx, flag); err != nil { 241 + ctx.InternalServerError(err) 242 + return 243 + } 244 + ctx.Status(http.StatusNoContent) 245 + }
+3
routers/api/v1/swagger/options.go
··· 18 18 AddCollaboratorOption api.AddCollaboratorOption 19 19 20 20 // in:body 21 + ReplaceFlagsOption api.ReplaceFlagsOption 22 + 23 + // in:body 21 24 CreateEmailOption api.CreateEmailOption 22 25 // in:body 23 26 DeleteEmailOption api.DeleteEmailOption
+268
templates/swagger/v1_json.tmpl
··· 4992 4992 } 4993 4993 } 4994 4994 }, 4995 + "/repos/{owner}/{repo}/flags": { 4996 + "get": { 4997 + "produces": [ 4998 + "application/json" 4999 + ], 5000 + "tags": [ 5001 + "repository" 5002 + ], 5003 + "summary": "List a repository's flags", 5004 + "operationId": "repoListFlags", 5005 + "parameters": [ 5006 + { 5007 + "type": "string", 5008 + "description": "owner of the repo", 5009 + "name": "owner", 5010 + "in": "path", 5011 + "required": true 5012 + }, 5013 + { 5014 + "type": "string", 5015 + "description": "name of the repo", 5016 + "name": "repo", 5017 + "in": "path", 5018 + "required": true 5019 + } 5020 + ], 5021 + "responses": { 5022 + "200": { 5023 + "$ref": "#/responses/StringSlice" 5024 + }, 5025 + "403": { 5026 + "$ref": "#/responses/forbidden" 5027 + }, 5028 + "404": { 5029 + "$ref": "#/responses/notFound" 5030 + } 5031 + } 5032 + }, 5033 + "put": { 5034 + "produces": [ 5035 + "application/json" 5036 + ], 5037 + "tags": [ 5038 + "repository" 5039 + ], 5040 + "summary": "Replace all flags of a repository", 5041 + "operationId": "repoReplaceAllFlags", 5042 + "parameters": [ 5043 + { 5044 + "type": "string", 5045 + "description": "owner of the repo", 5046 + "name": "owner", 5047 + "in": "path", 5048 + "required": true 5049 + }, 5050 + { 5051 + "type": "string", 5052 + "description": "name of the repo", 5053 + "name": "repo", 5054 + "in": "path", 5055 + "required": true 5056 + }, 5057 + { 5058 + "name": "body", 5059 + "in": "body", 5060 + "schema": { 5061 + "$ref": "#/definitions/ReplaceFlagsOption" 5062 + } 5063 + } 5064 + ], 5065 + "responses": { 5066 + "204": { 5067 + "$ref": "#/responses/empty" 5068 + }, 5069 + "403": { 5070 + "$ref": "#/responses/forbidden" 5071 + }, 5072 + "404": { 5073 + "$ref": "#/responses/notFound" 5074 + } 5075 + } 5076 + }, 5077 + "delete": { 5078 + "produces": [ 5079 + "application/json" 5080 + ], 5081 + "tags": [ 5082 + "repository" 5083 + ], 5084 + "summary": "Remove all flags from a repository", 5085 + "operationId": "repoDeleteAllFlags", 5086 + "parameters": [ 5087 + { 5088 + "type": "string", 5089 + "description": "owner of the repo", 5090 + "name": "owner", 5091 + "in": "path", 5092 + "required": true 5093 + }, 5094 + { 5095 + "type": "string", 5096 + "description": "name of the repo", 5097 + "name": "repo", 5098 + "in": "path", 5099 + "required": true 5100 + } 5101 + ], 5102 + "responses": { 5103 + "204": { 5104 + "$ref": "#/responses/empty" 5105 + }, 5106 + "403": { 5107 + "$ref": "#/responses/forbidden" 5108 + }, 5109 + "404": { 5110 + "$ref": "#/responses/notFound" 5111 + } 5112 + } 5113 + } 5114 + }, 5115 + "/repos/{owner}/{repo}/flags/{flag}": { 5116 + "get": { 5117 + "produces": [ 5118 + "application/json" 5119 + ], 5120 + "tags": [ 5121 + "repository" 5122 + ], 5123 + "summary": "Check if a repository has a given flag", 5124 + "operationId": "repoCheckFlag", 5125 + "parameters": [ 5126 + { 5127 + "type": "string", 5128 + "description": "owner of the repo", 5129 + "name": "owner", 5130 + "in": "path", 5131 + "required": true 5132 + }, 5133 + { 5134 + "type": "string", 5135 + "description": "name of the repo", 5136 + "name": "repo", 5137 + "in": "path", 5138 + "required": true 5139 + }, 5140 + { 5141 + "type": "string", 5142 + "description": "name of the flag", 5143 + "name": "flag", 5144 + "in": "path", 5145 + "required": true 5146 + } 5147 + ], 5148 + "responses": { 5149 + "204": { 5150 + "$ref": "#/responses/empty" 5151 + }, 5152 + "403": { 5153 + "$ref": "#/responses/forbidden" 5154 + }, 5155 + "404": { 5156 + "$ref": "#/responses/notFound" 5157 + } 5158 + } 5159 + }, 5160 + "put": { 5161 + "produces": [ 5162 + "application/json" 5163 + ], 5164 + "tags": [ 5165 + "repository" 5166 + ], 5167 + "summary": "Add a flag to a repository", 5168 + "operationId": "repoAddFlag", 5169 + "parameters": [ 5170 + { 5171 + "type": "string", 5172 + "description": "owner of the repo", 5173 + "name": "owner", 5174 + "in": "path", 5175 + "required": true 5176 + }, 5177 + { 5178 + "type": "string", 5179 + "description": "name of the repo", 5180 + "name": "repo", 5181 + "in": "path", 5182 + "required": true 5183 + }, 5184 + { 5185 + "type": "string", 5186 + "description": "name of the flag", 5187 + "name": "flag", 5188 + "in": "path", 5189 + "required": true 5190 + } 5191 + ], 5192 + "responses": { 5193 + "204": { 5194 + "$ref": "#/responses/empty" 5195 + }, 5196 + "403": { 5197 + "$ref": "#/responses/forbidden" 5198 + }, 5199 + "404": { 5200 + "$ref": "#/responses/notFound" 5201 + } 5202 + } 5203 + }, 5204 + "delete": { 5205 + "produces": [ 5206 + "application/json" 5207 + ], 5208 + "tags": [ 5209 + "repository" 5210 + ], 5211 + "summary": "Remove a flag from a repository", 5212 + "operationId": "repoDeleteFlag", 5213 + "parameters": [ 5214 + { 5215 + "type": "string", 5216 + "description": "owner of the repo", 5217 + "name": "owner", 5218 + "in": "path", 5219 + "required": true 5220 + }, 5221 + { 5222 + "type": "string", 5223 + "description": "name of the repo", 5224 + "name": "repo", 5225 + "in": "path", 5226 + "required": true 5227 + }, 5228 + { 5229 + "type": "string", 5230 + "description": "name of the flag", 5231 + "name": "flag", 5232 + "in": "path", 5233 + "required": true 5234 + } 5235 + ], 5236 + "responses": { 5237 + "204": { 5238 + "$ref": "#/responses/empty" 5239 + }, 5240 + "403": { 5241 + "$ref": "#/responses/forbidden" 5242 + }, 5243 + "404": { 5244 + "$ref": "#/responses/notFound" 5245 + } 5246 + } 5247 + } 5248 + }, 4995 5249 "/repos/{owner}/{repo}/forks": { 4996 5250 "get": { 4997 5251 "produces": [ ··· 22008 22262 "type": "string", 22009 22263 "uniqueItems": true, 22010 22264 "x-go-name": "NewName" 22265 + } 22266 + }, 22267 + "x-go-package": "code.gitea.io/gitea/modules/structs" 22268 + }, 22269 + "ReplaceFlagsOption": { 22270 + "description": "ReplaceFlagsOption options when replacing the flags of a repository", 22271 + "type": "object", 22272 + "properties": { 22273 + "flags": { 22274 + "type": "array", 22275 + "items": { 22276 + "type": "string" 22277 + }, 22278 + "x-go-name": "Flags" 22011 22279 } 22012 22280 }, 22013 22281 "x-go-package": "code.gitea.io/gitea/modules/structs"
+149
tests/integration/repo_flags_test.go
··· 7 7 "fmt" 8 8 "net/http" 9 9 "net/http/httptest" 10 + "slices" 10 11 "testing" 11 12 13 + auth_model "code.gitea.io/gitea/models/auth" 12 14 "code.gitea.io/gitea/models/db" 13 15 repo_model "code.gitea.io/gitea/models/repo" 14 16 "code.gitea.io/gitea/models/unittest" 15 17 user_model "code.gitea.io/gitea/models/user" 16 18 "code.gitea.io/gitea/modules/setting" 19 + api "code.gitea.io/gitea/modules/structs" 17 20 "code.gitea.io/gitea/modules/test" 18 21 "code.gitea.io/gitea/routers" 19 22 "code.gitea.io/gitea/tests" ··· 40 43 doc := NewHTMLParser(t, resp.Body) 41 44 flagsLinkCount := doc.Find(fmt.Sprintf(`a[href="%s/flags"]`, "/user2/repo1")).Length() 42 45 assert.Equal(t, 0, flagsLinkCount) 46 + } 47 + 48 + func TestRepositoryFlagsAPI(t *testing.T) { 49 + defer tests.PrepareTestEnv(t)() 50 + defer test.MockVariableValue(&setting.Repository.EnableFlags, true)() 51 + defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())() 52 + 53 + // ************* 54 + // ** Helpers ** 55 + // ************* 56 + 57 + adminUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{IsAdmin: true}).Name 58 + normalUserBean := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) 59 + assert.False(t, normalUserBean.IsAdmin) 60 + normalUser := normalUserBean.Name 61 + 62 + assertAccess := func(t *testing.T, user, method, uri string, expectedStatus int) { 63 + session := loginUser(t, user) 64 + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeReadAdmin) 65 + 66 + req := NewRequestf(t, method, "/api/v1/repos/user2/repo1/flags%s", uri).AddTokenAuth(token) 67 + MakeRequest(t, req, expectedStatus) 68 + } 69 + 70 + // *********** 71 + // ** Tests ** 72 + // *********** 73 + 74 + t.Run("API access", func(t *testing.T) { 75 + t.Run("as admin", func(t *testing.T) { 76 + defer tests.PrintCurrentTest(t)() 77 + 78 + assertAccess(t, adminUser, "GET", "", http.StatusOK) 79 + }) 80 + 81 + t.Run("as normal user", func(t *testing.T) { 82 + defer tests.PrintCurrentTest(t)() 83 + 84 + assertAccess(t, normalUser, "GET", "", http.StatusForbidden) 85 + }) 86 + }) 87 + 88 + t.Run("token scopes", func(t *testing.T) { 89 + defer tests.PrintCurrentTest(t)() 90 + 91 + // Trying to access the API with a token that lacks permissions, will 92 + // fail, even if the token owner is an instance admin. 93 + session := loginUser(t, adminUser) 94 + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) 95 + 96 + req := NewRequest(t, "GET", "/api/v1/repos/user2/repo1/flags").AddTokenAuth(token) 97 + MakeRequest(t, req, http.StatusForbidden) 98 + }) 99 + 100 + t.Run("setting.Repository.EnableFlags is respected", func(t *testing.T) { 101 + defer tests.PrintCurrentTest(t)() 102 + defer test.MockVariableValue(&setting.Repository.EnableFlags, false)() 103 + defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())() 104 + 105 + t.Run("as admin", func(t *testing.T) { 106 + defer tests.PrintCurrentTest(t)() 107 + 108 + assertAccess(t, adminUser, "GET", "", http.StatusNotFound) 109 + }) 110 + 111 + t.Run("as normal user", func(t *testing.T) { 112 + defer tests.PrintCurrentTest(t)() 113 + 114 + assertAccess(t, normalUser, "GET", "", http.StatusNotFound) 115 + }) 116 + }) 117 + 118 + t.Run("API functionality", func(t *testing.T) { 119 + defer tests.PrintCurrentTest(t)() 120 + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4}) 121 + defer func() { 122 + repo.ReplaceAllFlags(db.DefaultContext, []string{}) 123 + }() 124 + 125 + baseURLFmtStr := "/api/v1/repos/user5/repo4/flags%s" 126 + 127 + session := loginUser(t, adminUser) 128 + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteAdmin) 129 + 130 + // Listing flags 131 + req := NewRequestf(t, "GET", baseURLFmtStr, "").AddTokenAuth(token) 132 + resp := MakeRequest(t, req, http.StatusOK) 133 + var flags []string 134 + DecodeJSON(t, resp, &flags) 135 + assert.Empty(t, flags) 136 + 137 + // Replacing all tags works, twice in a row 138 + for i := 0; i < 2; i++ { 139 + req = NewRequestWithJSON(t, "PUT", fmt.Sprintf(baseURLFmtStr, ""), &api.ReplaceFlagsOption{ 140 + Flags: []string{"flag-1", "flag-2", "flag-3"}, 141 + }).AddTokenAuth(token) 142 + MakeRequest(t, req, http.StatusNoContent) 143 + } 144 + 145 + // The list now includes all three flags 146 + req = NewRequestf(t, "GET", baseURLFmtStr, "").AddTokenAuth(token) 147 + resp = MakeRequest(t, req, http.StatusOK) 148 + DecodeJSON(t, resp, &flags) 149 + assert.Len(t, flags, 3) 150 + for _, flag := range []string{"flag-1", "flag-2", "flag-3"} { 151 + assert.True(t, slices.Contains(flags, flag)) 152 + } 153 + 154 + // Check a flag that is on the repo 155 + req = NewRequestf(t, "GET", baseURLFmtStr, "/flag-1").AddTokenAuth(token) 156 + MakeRequest(t, req, http.StatusNoContent) 157 + 158 + // Check a flag that isn't on the repo 159 + req = NewRequestf(t, "GET", baseURLFmtStr, "/no-such-flag").AddTokenAuth(token) 160 + MakeRequest(t, req, http.StatusNotFound) 161 + 162 + // We can add the same flag twice 163 + for i := 0; i < 2; i++ { 164 + req = NewRequestf(t, "PUT", baseURLFmtStr, "/brand-new-flag").AddTokenAuth(token) 165 + MakeRequest(t, req, http.StatusNoContent) 166 + } 167 + 168 + // The new flag is there 169 + req = NewRequestf(t, "GET", baseURLFmtStr, "/brand-new-flag").AddTokenAuth(token) 170 + MakeRequest(t, req, http.StatusNoContent) 171 + 172 + // We can delete a flag, twice 173 + for i := 0; i < 2; i++ { 174 + req = NewRequestf(t, "DELETE", baseURLFmtStr, "/flag-3").AddTokenAuth(token) 175 + MakeRequest(t, req, http.StatusNoContent) 176 + } 177 + 178 + // We can delete a flag that wasn't there 179 + req = NewRequestf(t, "DELETE", baseURLFmtStr, "/no-such-flag").AddTokenAuth(token) 180 + MakeRequest(t, req, http.StatusNoContent) 181 + 182 + // We can delete all of the flags in one go, too 183 + req = NewRequestf(t, "DELETE", baseURLFmtStr, "").AddTokenAuth(token) 184 + MakeRequest(t, req, http.StatusNoContent) 185 + 186 + // ..once all flags are deleted, none are listed, either 187 + req = NewRequestf(t, "GET", baseURLFmtStr, "").AddTokenAuth(token) 188 + resp = MakeRequest(t, req, http.StatusOK) 189 + DecodeJSON(t, resp, &flags) 190 + assert.Empty(t, flags) 191 + }) 43 192 } 44 193 45 194 func TestRepositoryFlagsUI(t *testing.T) {