From c63e45d9e7f27706cfbe371b42a8a22b2ba1228e Mon Sep 17 00:00:00 2001 From: Virgil Date: Thu, 2 Apr 2026 01:57:55 +0000 Subject: [PATCH] Add admin quota rule endpoints Co-Authored-By: Virgil --- admin.go | 40 ++++++++++++++ admin_test.go | 147 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 187 insertions(+) diff --git a/admin.go b/admin.go index d15ec41..d77eed3 100644 --- a/admin.go +++ b/admin.go @@ -155,6 +155,46 @@ func (s *AdminService) DeleteQuotaGroup(ctx context.Context, quotagroup string) return s.client.Delete(ctx, path) } +// ListQuotaRules returns all available quota rules. +func (s *AdminService) ListQuotaRules(ctx context.Context) ([]types.QuotaRuleInfo, error) { + return ListAll[types.QuotaRuleInfo](ctx, s.client, "/api/v1/admin/quota/rules", nil) +} + +// CreateQuotaRule creates a new quota rule. +func (s *AdminService) CreateQuotaRule(ctx context.Context, opts *types.CreateQuotaRuleOptions) (*types.QuotaRuleInfo, error) { + var out types.QuotaRuleInfo + if err := s.client.Post(ctx, "/api/v1/admin/quota/rules", opts, &out); err != nil { + return nil, err + } + return &out, nil +} + +// GetQuotaRule returns information about a quota rule. +func (s *AdminService) GetQuotaRule(ctx context.Context, quotarule string) (*types.QuotaRuleInfo, error) { + path := ResolvePath("/api/v1/admin/quota/rules/{quotarule}", Params{"quotarule": quotarule}) + var out types.QuotaRuleInfo + if err := s.client.Get(ctx, path, &out); err != nil { + return nil, err + } + return &out, nil +} + +// EditQuotaRule updates an existing quota rule. +func (s *AdminService) EditQuotaRule(ctx context.Context, quotarule string, opts *types.EditQuotaRuleOptions) (*types.QuotaRuleInfo, error) { + path := ResolvePath("/api/v1/admin/quota/rules/{quotarule}", Params{"quotarule": quotarule}) + var out types.QuotaRuleInfo + if err := s.client.Patch(ctx, path, opts, &out); err != nil { + return nil, err + } + return &out, nil +} + +// DeleteQuotaRule deletes a quota rule. +func (s *AdminService) DeleteQuotaRule(ctx context.Context, quotarule string) error { + path := ResolvePath("/api/v1/admin/quota/rules/{quotarule}", Params{"quotarule": quotarule}) + return s.client.Delete(ctx, path) +} + // SearchEmails searches all email addresses by keyword (admin only). func (s *AdminService) SearchEmails(ctx context.Context, q string) ([]types.Email, error) { return ListAll[types.Email](ctx, s.client, "/api/v1/admin/emails/search", map[string]string{"q": q}) diff --git a/admin_test.go b/admin_test.go index ea0941e..31f4a26 100644 --- a/admin_test.go +++ b/admin_test.go @@ -513,6 +513,153 @@ func TestAdminService_DeleteQuotaGroup_Good(t *testing.T) { } } +func TestAdminService_ListQuotaRules_Good(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + t.Errorf("expected GET, got %s", r.Method) + } + if r.URL.Path != "/api/v1/admin/quota/rules" { + t.Errorf("wrong path: %s", r.URL.Path) + } + w.Header().Set("X-Total-Count", "2") + json.NewEncoder(w).Encode([]types.QuotaRuleInfo{ + {Name: "git", Limit: 200000000, Subjects: []string{"size:repos:all"}}, + {Name: "artifacts", Limit: 50000000, Subjects: []string{"size:assets:artifacts"}}, + }) + })) + defer srv.Close() + + f := NewForge(srv.URL, "tok") + rules, err := f.Admin.ListQuotaRules(context.Background()) + if err != nil { + t.Fatal(err) + } + if len(rules) != 2 { + t.Fatalf("got %d rules, want 2", len(rules)) + } + if rules[0].Name != "git" { + t.Errorf("got name=%q, want %q", rules[0].Name, "git") + } +} + +func TestAdminService_CreateQuotaRule_Good(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + t.Errorf("expected POST, got %s", r.Method) + } + if r.URL.Path != "/api/v1/admin/quota/rules" { + t.Errorf("wrong path: %s", r.URL.Path) + } + var opts types.CreateQuotaRuleOptions + if err := json.NewDecoder(r.Body).Decode(&opts); err != nil { + t.Fatal(err) + } + if opts.Name != "git" || opts.Limit != 200000000 { + t.Fatalf("unexpected options: %+v", opts) + } + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(types.QuotaRuleInfo{ + Name: opts.Name, + Limit: opts.Limit, + Subjects: opts.Subjects, + }) + })) + defer srv.Close() + + f := NewForge(srv.URL, "tok") + rule, err := f.Admin.CreateQuotaRule(context.Background(), &types.CreateQuotaRuleOptions{ + Name: "git", + Limit: 200000000, + Subjects: []string{"size:repos:all"}, + }) + if err != nil { + t.Fatal(err) + } + if rule.Name != "git" || rule.Limit != 200000000 { + t.Errorf("unexpected rule: %+v", rule) + } +} + +func TestAdminService_GetQuotaRule_Good(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + t.Errorf("expected GET, got %s", r.Method) + } + if r.URL.Path != "/api/v1/admin/quota/rules/git" { + t.Errorf("wrong path: %s", r.URL.Path) + } + json.NewEncoder(w).Encode(types.QuotaRuleInfo{ + Name: "git", + Limit: 200000000, + Subjects: []string{"size:repos:all"}, + }) + })) + defer srv.Close() + + f := NewForge(srv.URL, "tok") + rule, err := f.Admin.GetQuotaRule(context.Background(), "git") + if err != nil { + t.Fatal(err) + } + if rule.Name != "git" { + t.Errorf("got name=%q, want %q", rule.Name, "git") + } +} + +func TestAdminService_EditQuotaRule_Good(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPatch { + t.Errorf("expected PATCH, got %s", r.Method) + } + if r.URL.Path != "/api/v1/admin/quota/rules/git" { + t.Errorf("wrong path: %s", r.URL.Path) + } + var opts types.EditQuotaRuleOptions + if err := json.NewDecoder(r.Body).Decode(&opts); err != nil { + t.Fatal(err) + } + if opts.Limit != 500000000 { + t.Fatalf("unexpected options: %+v", opts) + } + json.NewEncoder(w).Encode(types.QuotaRuleInfo{ + Name: "git", + Limit: opts.Limit, + Subjects: opts.Subjects, + }) + })) + defer srv.Close() + + f := NewForge(srv.URL, "tok") + rule, err := f.Admin.EditQuotaRule(context.Background(), "git", &types.EditQuotaRuleOptions{ + Limit: 500000000, + Subjects: []string{"size:repos:all", "size:assets:packages"}, + }) + if err != nil { + t.Fatal(err) + } + if rule.Limit != 500000000 { + t.Errorf("got limit=%d, want 500000000", rule.Limit) + } +} + +func TestAdminService_DeleteQuotaRule_Good(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodDelete { + t.Errorf("expected DELETE, got %s", r.Method) + } + if r.URL.Path != "/api/v1/admin/quota/rules/git" { + t.Errorf("wrong path: %s", r.URL.Path) + } + w.WriteHeader(http.StatusNoContent) + })) + defer srv.Close() + + f := NewForge(srv.URL, "tok") + if err := f.Admin.DeleteQuotaRule(context.Background(), "git"); err != nil { + t.Fatal(err) + } +} + func TestAdminService_SearchEmails_Good(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet {