diff options
Diffstat (limited to '')
-rw-r--r-- | tests/integration/api_quota_management_test.go | 846 |
1 files changed, 846 insertions, 0 deletions
diff --git a/tests/integration/api_quota_management_test.go b/tests/integration/api_quota_management_test.go new file mode 100644 index 0000000..6337e66 --- /dev/null +++ b/tests/integration/api_quota_management_test.go @@ -0,0 +1,846 @@ +// Copyright 2024 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "fmt" + "net/http" + "testing" + + auth_model "code.gitea.io/gitea/models/auth" + "code.gitea.io/gitea/models/db" + quota_model "code.gitea.io/gitea/models/quota" + "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" + "github.com/stretchr/testify/require" +) + +func TestAPIQuotaDisabled(t *testing.T) { + defer tests.PrepareTestEnv(t)() + defer test.MockVariableValue(&setting.Quota.Enabled, false)() + defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())() + + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{IsAdmin: true}) + session := loginUser(t, user.Name) + + req := NewRequest(t, "GET", "/api/v1/user/quota") + session.MakeRequest(t, req, http.StatusNotFound) +} + +func apiCreateUser(t *testing.T, username string) func() { + t.Helper() + + admin := unittest.AssertExistsAndLoadBean(t, &user_model.User{IsAdmin: true}) + session := loginUser(t, admin.Name) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeAll) + + mustChangePassword := false + req := NewRequestWithJSON(t, "POST", "/api/v1/admin/users", api.CreateUserOption{ + Email: "api+" + username + "@example.com", + Username: username, + Password: "password", + MustChangePassword: &mustChangePassword, + }).AddTokenAuth(token) + session.MakeRequest(t, req, http.StatusCreated) + + return func() { + req := NewRequest(t, "DELETE", "/api/v1/admin/users/"+username+"?purge=true").AddTokenAuth(token) + session.MakeRequest(t, req, http.StatusNoContent) + } +} + +func TestAPIQuotaCreateGroupWithRules(t *testing.T) { + defer tests.PrepareTestEnv(t)() + defer test.MockVariableValue(&setting.Quota.Enabled, true)() + defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())() + + // Create two rules in advance + unlimited := int64(-1) + defer createQuotaRule(t, api.CreateQuotaRuleOptions{ + Name: "unlimited", + Limit: &unlimited, + Subjects: []string{"size:all"}, + })() + zero := int64(0) + defer createQuotaRule(t, api.CreateQuotaRuleOptions{ + Name: "deny-git-lfs", + Limit: &zero, + Subjects: []string{"size:git:lfs"}, + })() + + // Log in as admin + admin := unittest.AssertExistsAndLoadBean(t, &user_model.User{IsAdmin: true}) + adminSession := loginUser(t, admin.Name) + adminToken := getTokenForLoggedInUser(t, adminSession, auth_model.AccessTokenScopeAll) + + // Create a new group, with rules specified + req := NewRequestWithJSON(t, "POST", "/api/v1/admin/quota/groups", api.CreateQuotaGroupOptions{ + Name: "group-with-rules", + Rules: []api.CreateQuotaRuleOptions{ + // First: an existing group, unlimited, name only + { + Name: "unlimited", + }, + // Second: an existing group, deny-git-lfs, with different params + { + Name: "deny-git-lfs", + Limit: &unlimited, + }, + // Third: an entirely new group + { + Name: "new-rule", + Subjects: []string{"size:assets:all"}, + }, + }, + }).AddTokenAuth(adminToken) + resp := adminSession.MakeRequest(t, req, http.StatusCreated) + defer func() { + req := NewRequest(t, "DELETE", "/api/v1/admin/quota/groups/group-with-rules").AddTokenAuth(adminToken) + adminSession.MakeRequest(t, req, http.StatusNoContent) + + req = NewRequest(t, "DELETE", "/api/v1/admin/quota/rules/new-rule").AddTokenAuth(adminToken) + adminSession.MakeRequest(t, req, http.StatusNoContent) + }() + + // Verify that we created a group with rules included + var q api.QuotaGroup + DecodeJSON(t, resp, &q) + + assert.Equal(t, "group-with-rules", q.Name) + assert.Len(t, q.Rules, 3) + + // Verify that the previously existing rules are unchanged + rule, err := quota_model.GetRuleByName(db.DefaultContext, "unlimited") + require.NoError(t, err) + assert.NotNil(t, rule) + assert.EqualValues(t, -1, rule.Limit) + assert.EqualValues(t, quota_model.LimitSubjects{quota_model.LimitSubjectSizeAll}, rule.Subjects) + + rule, err = quota_model.GetRuleByName(db.DefaultContext, "deny-git-lfs") + require.NoError(t, err) + assert.NotNil(t, rule) + assert.EqualValues(t, 0, rule.Limit) + assert.EqualValues(t, quota_model.LimitSubjects{quota_model.LimitSubjectSizeGitLFS}, rule.Subjects) + + // Verify that the new rule was also created + rule, err = quota_model.GetRuleByName(db.DefaultContext, "new-rule") + require.NoError(t, err) + assert.NotNil(t, rule) + assert.EqualValues(t, 0, rule.Limit) + assert.EqualValues(t, quota_model.LimitSubjects{quota_model.LimitSubjectSizeAssetsAll}, rule.Subjects) + + t.Run("invalid rule spec", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequestWithJSON(t, "POST", "/api/v1/admin/quota/groups", api.CreateQuotaGroupOptions{ + Name: "group-with-invalid-rule-spec", + Rules: []api.CreateQuotaRuleOptions{ + { + Name: "rule-with-wrong-spec", + Subjects: []string{"valid:false"}, + }, + }, + }).AddTokenAuth(adminToken) + adminSession.MakeRequest(t, req, http.StatusUnprocessableEntity) + }) +} + +func TestAPIQuotaEmptyState(t *testing.T) { + defer tests.PrepareTestEnv(t)() + defer test.MockVariableValue(&setting.Quota.Enabled, true)() + defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())() + + username := "quota-empty-user" + defer apiCreateUser(t, username)() + session := loginUser(t, username) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeAll) + + t.Run("#/admin/users/quota-empty-user/quota", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + admin := unittest.AssertExistsAndLoadBean(t, &user_model.User{IsAdmin: true}) + adminSession := loginUser(t, admin.Name) + adminToken := getTokenForLoggedInUser(t, adminSession, auth_model.AccessTokenScopeAll) + + req := NewRequest(t, "GET", "/api/v1/admin/users/quota-empty-user/quota").AddTokenAuth(adminToken) + resp := adminSession.MakeRequest(t, req, http.StatusOK) + + var q api.QuotaInfo + DecodeJSON(t, resp, &q) + + assert.EqualValues(t, api.QuotaUsed{}, q.Used) + assert.Empty(t, q.Groups) + }) + + t.Run("#/user/quota", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", "/api/v1/user/quota").AddTokenAuth(token) + resp := session.MakeRequest(t, req, http.StatusOK) + + var q api.QuotaInfo + DecodeJSON(t, resp, &q) + + assert.EqualValues(t, api.QuotaUsed{}, q.Used) + assert.Empty(t, q.Groups) + + t.Run("#/user/quota/artifacts", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", "/api/v1/user/quota/artifacts").AddTokenAuth(token) + resp := session.MakeRequest(t, req, http.StatusOK) + + var q api.QuotaUsedArtifactList + DecodeJSON(t, resp, &q) + + assert.Empty(t, q) + }) + + t.Run("#/user/quota/attachments", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", "/api/v1/user/quota/attachments").AddTokenAuth(token) + resp := session.MakeRequest(t, req, http.StatusOK) + + var q api.QuotaUsedAttachmentList + DecodeJSON(t, resp, &q) + + assert.Empty(t, q) + }) + + t.Run("#/user/quota/packages", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", "/api/v1/user/quota/packages").AddTokenAuth(token) + resp := session.MakeRequest(t, req, http.StatusOK) + + var q api.QuotaUsedPackageList + DecodeJSON(t, resp, &q) + + assert.Empty(t, q) + }) + }) +} + +func createQuotaRule(t *testing.T, opts api.CreateQuotaRuleOptions) func() { + t.Helper() + + admin := unittest.AssertExistsAndLoadBean(t, &user_model.User{IsAdmin: true}) + adminSession := loginUser(t, admin.Name) + adminToken := getTokenForLoggedInUser(t, adminSession, auth_model.AccessTokenScopeAll) + + req := NewRequestWithJSON(t, "POST", "/api/v1/admin/quota/rules", opts).AddTokenAuth(adminToken) + adminSession.MakeRequest(t, req, http.StatusCreated) + + return func() { + req := NewRequestf(t, "DELETE", "/api/v1/admin/quota/rules/%s", opts.Name).AddTokenAuth(adminToken) + adminSession.MakeRequest(t, req, http.StatusNoContent) + } +} + +func createQuotaGroup(t *testing.T, name string) func() { + t.Helper() + + admin := unittest.AssertExistsAndLoadBean(t, &user_model.User{IsAdmin: true}) + adminSession := loginUser(t, admin.Name) + adminToken := getTokenForLoggedInUser(t, adminSession, auth_model.AccessTokenScopeAll) + + req := NewRequestWithJSON(t, "POST", "/api/v1/admin/quota/groups", api.CreateQuotaGroupOptions{ + Name: name, + }).AddTokenAuth(adminToken) + adminSession.MakeRequest(t, req, http.StatusCreated) + + return func() { + req := NewRequestf(t, "DELETE", "/api/v1/admin/quota/groups/%s", name).AddTokenAuth(adminToken) + adminSession.MakeRequest(t, req, http.StatusNoContent) + } +} + +func TestAPIQuotaAdminRoutesRules(t *testing.T) { + defer tests.PrepareTestEnv(t)() + defer test.MockVariableValue(&setting.Quota.Enabled, true)() + defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())() + + admin := unittest.AssertExistsAndLoadBean(t, &user_model.User{IsAdmin: true}) + adminSession := loginUser(t, admin.Name) + adminToken := getTokenForLoggedInUser(t, adminSession, auth_model.AccessTokenScopeAll) + + zero := int64(0) + oneKb := int64(1024) + + t.Run("adminCreateQuotaRule", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequestWithJSON(t, "POST", "/api/v1/admin/quota/rules", api.CreateQuotaRuleOptions{ + Name: "deny-all", + Limit: &zero, + Subjects: []string{"size:all"}, + }).AddTokenAuth(adminToken) + resp := adminSession.MakeRequest(t, req, http.StatusCreated) + defer func() { + req := NewRequest(t, "DELETE", "/api/v1/admin/quota/rules/deny-all").AddTokenAuth(adminToken) + adminSession.MakeRequest(t, req, http.StatusNoContent) + }() + + var q api.QuotaRuleInfo + DecodeJSON(t, resp, &q) + + assert.Equal(t, "deny-all", q.Name) + assert.EqualValues(t, 0, q.Limit) + assert.EqualValues(t, []string{"size:all"}, q.Subjects) + + rule, err := quota_model.GetRuleByName(db.DefaultContext, "deny-all") + require.NoError(t, err) + assert.EqualValues(t, 0, rule.Limit) + + t.Run("unhappy path", func(t *testing.T) { + t.Run("missing options", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequestWithJSON(t, "POST", "/api/v1/admin/quota/rules", nil).AddTokenAuth(adminToken) + adminSession.MakeRequest(t, req, http.StatusUnprocessableEntity) + }) + + t.Run("invalid subjects", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequestWithJSON(t, "POST", "/api/v1/admin/quota/rules", api.CreateQuotaRuleOptions{ + Name: "invalid-subjects", + Limit: &zero, + Subjects: []string{"valid:false"}, + }).AddTokenAuth(adminToken) + adminSession.MakeRequest(t, req, http.StatusUnprocessableEntity) + }) + + t.Run("trying to add an existing rule", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + rule := api.CreateQuotaRuleOptions{ + Name: "double-rule", + Limit: &zero, + } + + defer createQuotaRule(t, rule)() + + req := NewRequestWithJSON(t, "POST", "/api/v1/admin/quota/rules", rule).AddTokenAuth(adminToken) + adminSession.MakeRequest(t, req, http.StatusConflict) + }) + }) + }) + + t.Run("adminDeleteQuotaRule", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + createQuotaRule(t, api.CreateQuotaRuleOptions{ + Name: "deny-all", + Limit: &zero, + Subjects: []string{"size:all"}, + }) + + req := NewRequest(t, "DELETE", "/api/v1/admin/quota/rules/deny-all").AddTokenAuth(adminToken) + adminSession.MakeRequest(t, req, http.StatusNoContent) + + rule, err := quota_model.GetRuleByName(db.DefaultContext, "deny-all") + require.NoError(t, err) + assert.Nil(t, rule) + + t.Run("unhappy path", func(t *testing.T) { + t.Run("nonexistent rule", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "DELETE", "/api/v1/admin/quota/rules/does-not-exist").AddTokenAuth(adminToken) + adminSession.MakeRequest(t, req, http.StatusNotFound) + }) + }) + }) + + t.Run("adminEditQuotaRule", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + defer createQuotaRule(t, api.CreateQuotaRuleOptions{ + Name: "deny-all", + Limit: &zero, + Subjects: []string{"size:all"}, + })() + + req := NewRequestWithJSON(t, "PATCH", "/api/v1/admin/quota/rules/deny-all", api.EditQuotaRuleOptions{ + Limit: &oneKb, + }).AddTokenAuth(adminToken) + resp := adminSession.MakeRequest(t, req, http.StatusOK) + + var q api.QuotaRuleInfo + DecodeJSON(t, resp, &q) + assert.EqualValues(t, 1024, q.Limit) + + rule, err := quota_model.GetRuleByName(db.DefaultContext, "deny-all") + require.NoError(t, err) + assert.EqualValues(t, 1024, rule.Limit) + + t.Run("no options", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequestWithJSON(t, "PATCH", "/api/v1/admin/quota/rules/deny-all", nil).AddTokenAuth(adminToken) + adminSession.MakeRequest(t, req, http.StatusOK) + }) + + t.Run("unhappy path", func(t *testing.T) { + t.Run("nonexistent rule", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequestWithJSON(t, "PATCH", "/api/v1/admin/quota/rules/does-not-exist", api.EditQuotaRuleOptions{ + Limit: &oneKb, + }).AddTokenAuth(adminToken) + adminSession.MakeRequest(t, req, http.StatusNotFound) + }) + + t.Run("invalid subjects", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequestWithJSON(t, "PATCH", "/api/v1/admin/quota/rules/deny-all", api.EditQuotaRuleOptions{ + Subjects: &[]string{"valid:false"}, + }).AddTokenAuth(adminToken) + adminSession.MakeRequest(t, req, http.StatusUnprocessableEntity) + }) + }) + }) + + t.Run("adminListQuotaRules", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + defer createQuotaRule(t, api.CreateQuotaRuleOptions{ + Name: "deny-all", + Limit: &zero, + Subjects: []string{"size:all"}, + })() + + req := NewRequest(t, "GET", "/api/v1/admin/quota/rules").AddTokenAuth(adminToken) + resp := adminSession.MakeRequest(t, req, http.StatusOK) + + var rules []api.QuotaRuleInfo + DecodeJSON(t, resp, &rules) + + assert.Len(t, rules, 1) + assert.Equal(t, "deny-all", rules[0].Name) + assert.EqualValues(t, 0, rules[0].Limit) + }) +} + +func TestAPIQuotaAdminRoutesGroups(t *testing.T) { + defer tests.PrepareTestEnv(t)() + defer test.MockVariableValue(&setting.Quota.Enabled, true)() + defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())() + + admin := unittest.AssertExistsAndLoadBean(t, &user_model.User{IsAdmin: true}) + adminSession := loginUser(t, admin.Name) + adminToken := getTokenForLoggedInUser(t, adminSession, auth_model.AccessTokenScopeAll) + + zero := int64(0) + + ruleDenyAll := api.CreateQuotaRuleOptions{ + Name: "deny-all", + Limit: &zero, + Subjects: []string{"size:all"}, + } + + username := "quota-test-user" + defer apiCreateUser(t, username)() + + t.Run("adminCreateQuotaGroup", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequestWithJSON(t, "POST", "/api/v1/admin/quota/groups", api.CreateQuotaGroupOptions{ + Name: "default", + }).AddTokenAuth(adminToken) + resp := adminSession.MakeRequest(t, req, http.StatusCreated) + defer func() { + req := NewRequest(t, "DELETE", "/api/v1/admin/quota/groups/default").AddTokenAuth(adminToken) + adminSession.MakeRequest(t, req, http.StatusNoContent) + }() + + var q api.QuotaGroup + DecodeJSON(t, resp, &q) + + assert.Equal(t, "default", q.Name) + assert.Empty(t, q.Rules) + + group, err := quota_model.GetGroupByName(db.DefaultContext, "default") + require.NoError(t, err) + assert.Equal(t, "default", group.Name) + assert.Empty(t, group.Rules) + + t.Run("unhappy path", func(t *testing.T) { + t.Run("missing options", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequestWithJSON(t, "POST", "/api/v1/admin/quota/groups", nil).AddTokenAuth(adminToken) + adminSession.MakeRequest(t, req, http.StatusUnprocessableEntity) + }) + + t.Run("trying to add an existing group", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + defer createQuotaGroup(t, "duplicate")() + + req := NewRequestWithJSON(t, "POST", "/api/v1/admin/quota/groups", api.CreateQuotaGroupOptions{ + Name: "duplicate", + }).AddTokenAuth(adminToken) + adminSession.MakeRequest(t, req, http.StatusConflict) + }) + }) + }) + + t.Run("adminDeleteQuotaGroup", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + createQuotaGroup(t, "default") + + req := NewRequest(t, "DELETE", "/api/v1/admin/quota/groups/default").AddTokenAuth(adminToken) + adminSession.MakeRequest(t, req, http.StatusNoContent) + + group, err := quota_model.GetGroupByName(db.DefaultContext, "default") + require.NoError(t, err) + assert.Nil(t, group) + + t.Run("unhappy path", func(t *testing.T) { + t.Run("non-existing group", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "DELETE", "/api/v1/admin/quota/groups/does-not-exist").AddTokenAuth(adminToken) + adminSession.MakeRequest(t, req, http.StatusNotFound) + }) + }) + }) + + t.Run("adminAddRuleToQuotaGroup", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + defer createQuotaGroup(t, "default")() + defer createQuotaRule(t, ruleDenyAll)() + + req := NewRequest(t, "PUT", "/api/v1/admin/quota/groups/default/rules/deny-all").AddTokenAuth(adminToken) + adminSession.MakeRequest(t, req, http.StatusNoContent) + + group, err := quota_model.GetGroupByName(db.DefaultContext, "default") + require.NoError(t, err) + assert.Len(t, group.Rules, 1) + assert.Equal(t, "deny-all", group.Rules[0].Name) + + t.Run("unhappy path", func(t *testing.T) { + t.Run("non-existing group", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "PUT", "/api/v1/admin/quota/groups/does-not-exist/rules/deny-all").AddTokenAuth(adminToken) + adminSession.MakeRequest(t, req, http.StatusNotFound) + }) + + t.Run("non-existing rule", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "PUT", "/api/v1/admin/quota/groups/default/rules/does-not-exist").AddTokenAuth(adminToken) + adminSession.MakeRequest(t, req, http.StatusNotFound) + }) + }) + }) + + t.Run("adminRemoveRuleFromQuotaGroup", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + defer createQuotaGroup(t, "default")() + defer createQuotaRule(t, ruleDenyAll)() + + req := NewRequest(t, "PUT", "/api/v1/admin/quota/groups/default/rules/deny-all").AddTokenAuth(adminToken) + adminSession.MakeRequest(t, req, http.StatusNoContent) + + req = NewRequest(t, "DELETE", "/api/v1/admin/quota/groups/default/rules/deny-all").AddTokenAuth(adminToken) + adminSession.MakeRequest(t, req, http.StatusNoContent) + + group, err := quota_model.GetGroupByName(db.DefaultContext, "default") + require.NoError(t, err) + assert.Equal(t, "default", group.Name) + assert.Empty(t, group.Rules) + + t.Run("unhappy path", func(t *testing.T) { + t.Run("non-existing group", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "DELETE", "/api/v1/admin/quota/groups/does-not-exist/rules/deny-all").AddTokenAuth(adminToken) + adminSession.MakeRequest(t, req, http.StatusNotFound) + }) + + t.Run("non-existing rule", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "DELETE", "/api/v1/admin/quota/groups/default/rules/does-not-exist").AddTokenAuth(adminToken) + adminSession.MakeRequest(t, req, http.StatusNotFound) + }) + + t.Run("rule not in group", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + defer createQuotaRule(t, api.CreateQuotaRuleOptions{ + Name: "rule-not-in-group", + Limit: &zero, + })() + + req := NewRequest(t, "DELETE", "/api/v1/admin/quota/groups/default/rules/rule-not-in-group").AddTokenAuth(adminToken) + adminSession.MakeRequest(t, req, http.StatusNotFound) + }) + }) + }) + + t.Run("adminGetQuotaGroup", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + defer createQuotaGroup(t, "default")() + defer createQuotaRule(t, ruleDenyAll)() + + req := NewRequest(t, "PUT", "/api/v1/admin/quota/groups/default/rules/deny-all").AddTokenAuth(adminToken) + adminSession.MakeRequest(t, req, http.StatusNoContent) + + req = NewRequest(t, "GET", "/api/v1/admin/quota/groups/default").AddTokenAuth(adminToken) + resp := adminSession.MakeRequest(t, req, http.StatusOK) + + var q api.QuotaGroup + DecodeJSON(t, resp, &q) + + assert.Equal(t, "default", q.Name) + assert.Len(t, q.Rules, 1) + assert.Equal(t, "deny-all", q.Rules[0].Name) + + t.Run("unhappy path", func(t *testing.T) { + t.Run("non-existing group", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", "/api/v1/admin/quota/groups/does-not-exist").AddTokenAuth(adminToken) + adminSession.MakeRequest(t, req, http.StatusNotFound) + }) + }) + }) + + t.Run("adminListQuotaGroups", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + defer createQuotaGroup(t, "default")() + defer createQuotaRule(t, ruleDenyAll)() + + req := NewRequest(t, "PUT", "/api/v1/admin/quota/groups/default/rules/deny-all").AddTokenAuth(adminToken) + adminSession.MakeRequest(t, req, http.StatusNoContent) + + req = NewRequest(t, "GET", "/api/v1/admin/quota/groups").AddTokenAuth(adminToken) + resp := adminSession.MakeRequest(t, req, http.StatusOK) + + var q api.QuotaGroupList + DecodeJSON(t, resp, &q) + + assert.Len(t, q, 1) + assert.Equal(t, "default", q[0].Name) + assert.Len(t, q[0].Rules, 1) + assert.Equal(t, "deny-all", q[0].Rules[0].Name) + }) + + t.Run("adminAddUserToQuotaGroup", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + defer createQuotaGroup(t, "default")() + + req := NewRequestf(t, "PUT", "/api/v1/admin/quota/groups/default/users/%s", username).AddTokenAuth(adminToken) + adminSession.MakeRequest(t, req, http.StatusNoContent) + + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: username}) + + groups, err := quota_model.GetGroupsForUser(db.DefaultContext, user.ID) + require.NoError(t, err) + assert.Len(t, groups, 1) + assert.Equal(t, "default", groups[0].Name) + + t.Run("unhappy path", func(t *testing.T) { + t.Run("non-existing group", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequestf(t, "PUT", "/api/v1/admin/quota/groups/does-not-exist/users/%s", username).AddTokenAuth(adminToken) + adminSession.MakeRequest(t, req, http.StatusNotFound) + }) + + t.Run("non-existing user", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "PUT", "/api/v1/admin/quota/groups/default/users/this-user-does-not-exist").AddTokenAuth(adminToken) + adminSession.MakeRequest(t, req, http.StatusNotFound) + }) + + t.Run("user already added", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "PUT", "/api/v1/admin/quota/groups/default/users/user1").AddTokenAuth(adminToken) + adminSession.MakeRequest(t, req, http.StatusNoContent) + + req = NewRequest(t, "PUT", "/api/v1/admin/quota/groups/default/users/user1").AddTokenAuth(adminToken) + adminSession.MakeRequest(t, req, http.StatusConflict) + }) + }) + }) + + t.Run("adminRemoveUserFromQuotaGroup", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + defer createQuotaGroup(t, "default")() + + req := NewRequestf(t, "PUT", "/api/v1/admin/quota/groups/default/users/%s", username).AddTokenAuth(adminToken) + adminSession.MakeRequest(t, req, http.StatusNoContent) + + req = NewRequestf(t, "DELETE", "/api/v1/admin/quota/groups/default/users/%s", username).AddTokenAuth(adminToken) + adminSession.MakeRequest(t, req, http.StatusNoContent) + + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: username}) + groups, err := quota_model.GetGroupsForUser(db.DefaultContext, user.ID) + require.NoError(t, err) + assert.Empty(t, groups) + + t.Run("unhappy path", func(t *testing.T) { + t.Run("non-existing group", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequestf(t, "DELETE", "/api/v1/admin/quota/groups/does-not-exist/users/%s", username).AddTokenAuth(adminToken) + adminSession.MakeRequest(t, req, http.StatusNotFound) + }) + + t.Run("non-existing user", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "DELETE", "/api/v1/admin/quota/groups/default/users/does-not-exist").AddTokenAuth(adminToken) + adminSession.MakeRequest(t, req, http.StatusNotFound) + }) + + t.Run("user not in group", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "DELETE", "/api/v1/admin/quota/groups/default/users/user1").AddTokenAuth(adminToken) + adminSession.MakeRequest(t, req, http.StatusNotFound) + }) + }) + }) + + t.Run("adminListUsersInQuotaGroup", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + defer createQuotaGroup(t, "default")() + + req := NewRequestf(t, "PUT", "/api/v1/admin/quota/groups/default/users/%s", username).AddTokenAuth(adminToken) + adminSession.MakeRequest(t, req, http.StatusNoContent) + + req = NewRequest(t, "GET", "/api/v1/admin/quota/groups/default/users").AddTokenAuth(adminToken) + resp := adminSession.MakeRequest(t, req, http.StatusOK) + + var q []api.User + DecodeJSON(t, resp, &q) + + assert.Len(t, q, 1) + assert.Equal(t, username, q[0].UserName) + + t.Run("unhappy path", func(t *testing.T) { + t.Run("non-existing group", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", "/api/v1/admin/quota/groups/does-not-exist/users").AddTokenAuth(adminToken) + adminSession.MakeRequest(t, req, http.StatusNotFound) + }) + }) + }) + + t.Run("adminSetUserQuotaGroups", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + defer createQuotaGroup(t, "default")() + defer createQuotaGroup(t, "test-1")() + defer createQuotaGroup(t, "test-2")() + + req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/admin/users/%s/quota/groups", username), api.SetUserQuotaGroupsOptions{ + Groups: &[]string{"default", "test-1", "test-2"}, + }).AddTokenAuth(adminToken) + adminSession.MakeRequest(t, req, http.StatusNoContent) + + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: username}) + + groups, err := quota_model.GetGroupsForUser(db.DefaultContext, user.ID) + require.NoError(t, err) + assert.Len(t, groups, 3) + + t.Run("unhappy path", func(t *testing.T) { + t.Run("non-existing user", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequestWithJSON(t, "POST", "/api/v1/admin/users/does-not-exist/quota/groups", api.SetUserQuotaGroupsOptions{ + Groups: &[]string{"default", "test-1", "test-2"}, + }).AddTokenAuth(adminToken) + adminSession.MakeRequest(t, req, http.StatusNotFound) + }) + + t.Run("non-existing group", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/admin/users/%s/quota/groups", username), api.SetUserQuotaGroupsOptions{ + Groups: &[]string{"default", "test-1", "test-2", "this-group-does-not-exist"}, + }).AddTokenAuth(adminToken) + adminSession.MakeRequest(t, req, http.StatusUnprocessableEntity) + }) + }) + }) +} + +func TestAPIQuotaUserRoutes(t *testing.T) { + defer tests.PrepareTestEnv(t)() + defer test.MockVariableValue(&setting.Quota.Enabled, true)() + defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())() + + admin := unittest.AssertExistsAndLoadBean(t, &user_model.User{IsAdmin: true}) + adminSession := loginUser(t, admin.Name) + adminToken := getTokenForLoggedInUser(t, adminSession, auth_model.AccessTokenScopeAll) + + // Create a test user + username := "quota-test-user-routes" + defer apiCreateUser(t, username)() + session := loginUser(t, username) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeAll) + + // Set up rules & groups for the user + defer createQuotaGroup(t, "user-routes-deny")() + defer createQuotaGroup(t, "user-routes-1kb")() + + zero := int64(0) + ruleDenyAll := api.CreateQuotaRuleOptions{ + Name: "user-routes-deny-all", + Limit: &zero, + Subjects: []string{"size:all"}, + } + defer createQuotaRule(t, ruleDenyAll)() + oneKb := int64(1024) + rule1KbStuff := api.CreateQuotaRuleOptions{ + Name: "user-routes-1kb", + Limit: &oneKb, + Subjects: []string{"size:assets:attachments:releases", "size:assets:packages:all", "size:git:lfs"}, + } + defer createQuotaRule(t, rule1KbStuff)() + + req := NewRequest(t, "PUT", "/api/v1/admin/quota/groups/user-routes-deny/rules/user-routes-deny-all").AddTokenAuth(adminToken) + adminSession.MakeRequest(t, req, http.StatusNoContent) + req = NewRequest(t, "PUT", "/api/v1/admin/quota/groups/user-routes-1kb/rules/user-routes-1kb").AddTokenAuth(adminToken) + adminSession.MakeRequest(t, req, http.StatusNoContent) + + req = NewRequestf(t, "PUT", "/api/v1/admin/quota/groups/user-routes-deny/users/%s", username).AddTokenAuth(adminToken) + adminSession.MakeRequest(t, req, http.StatusNoContent) + req = NewRequestf(t, "PUT", "/api/v1/admin/quota/groups/user-routes-1kb/users/%s", username).AddTokenAuth(adminToken) + adminSession.MakeRequest(t, req, http.StatusNoContent) + + t.Run("userGetQuota", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", "/api/v1/user/quota").AddTokenAuth(token) + resp := session.MakeRequest(t, req, http.StatusOK) + + var q api.QuotaInfo + DecodeJSON(t, resp, &q) + + assert.Len(t, q.Groups, 2) + assert.Len(t, q.Groups[0].Rules, 1) + assert.Len(t, q.Groups[1].Rules, 1) + }) +} |