From 572a1f1ddc805039e8a2adf92b989ce429895e6f Mon Sep 17 00:00:00 2001 From: Virgil Date: Thu, 2 Apr 2026 00:47:58 +0000 Subject: [PATCH] Add admin email endpoints Co-Authored-By: Virgil --- admin.go | 20 +++++++++++++++ admin_test.go | 60 ++++++++++++++++++++++++++++++++++++++++++++ docs/api-contract.md | 4 +++ 3 files changed, 84 insertions(+) diff --git a/admin.go b/admin.go index 825abef..62b25ae 100644 --- a/admin.go +++ b/admin.go @@ -70,6 +70,26 @@ func (s *AdminService) IterOrgs(ctx context.Context) iter.Seq2[types.Organizatio return ListIter[types.Organization](ctx, s.client, "/api/v1/admin/orgs", nil) } +// ListEmails returns all email addresses (admin only). +func (s *AdminService) ListEmails(ctx context.Context) ([]types.Email, error) { + return ListAll[types.Email](ctx, s.client, "/api/v1/admin/emails", nil) +} + +// IterEmails returns an iterator over all email addresses (admin only). +func (s *AdminService) IterEmails(ctx context.Context) iter.Seq2[types.Email, error] { + return ListIter[types.Email](ctx, s.client, "/api/v1/admin/emails", nil) +} + +// 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}) +} + +// IterSearchEmails returns an iterator over all email addresses matching a keyword (admin only). +func (s *AdminService) IterSearchEmails(ctx context.Context, q string) iter.Seq2[types.Email, error] { + return ListIter[types.Email](ctx, s.client, "/api/v1/admin/emails/search", map[string]string{"q": q}) +} + // RunCron runs a cron task by name (admin only). func (s *AdminService) RunCron(ctx context.Context, task string) error { path := ResolvePath("/api/v1/admin/cron/{task}", Params{"task": task}) diff --git a/admin_test.go b/admin_test.go index 0f21d9f..b6e3dc0 100644 --- a/admin_test.go +++ b/admin_test.go @@ -195,6 +195,66 @@ func TestAdminService_ListOrgs_Good(t *testing.T) { } } +func TestAdminService_ListEmails_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/emails" { + t.Errorf("wrong path: %s", r.URL.Path) + } + w.Header().Set("X-Total-Count", "2") + json.NewEncoder(w).Encode([]types.Email{ + {Email: "alice@example.com", Primary: true}, + {Email: "bob@example.com", Verified: true}, + }) + })) + defer srv.Close() + + f := NewForge(srv.URL, "tok") + emails, err := f.Admin.ListEmails(context.Background()) + if err != nil { + t.Fatal(err) + } + if len(emails) != 2 { + t.Errorf("got %d emails, want 2", len(emails)) + } + if emails[0].Email != "alice@example.com" || !emails[0].Primary { + t.Errorf("got first email=%+v, want primary alice@example.com", emails[0]) + } +} + +func TestAdminService_SearchEmails_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/emails/search" { + t.Errorf("wrong path: %s", r.URL.Path) + } + if got := r.URL.Query().Get("q"); got != "alice" { + t.Errorf("got q=%q, want %q", got, "alice") + } + w.Header().Set("X-Total-Count", "1") + json.NewEncoder(w).Encode([]types.Email{ + {Email: "alice@example.com", Primary: true}, + }) + })) + defer srv.Close() + + f := NewForge(srv.URL, "tok") + emails, err := f.Admin.SearchEmails(context.Background(), "alice") + if err != nil { + t.Fatal(err) + } + if len(emails) != 1 { + t.Errorf("got %d emails, want 1", len(emails)) + } + if emails[0].Email != "alice@example.com" { + t.Errorf("got email=%q, want %q", emails[0].Email, "alice@example.com") + } +} + func TestAdminService_ListCron_Good(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { diff --git a/docs/api-contract.md b/docs/api-contract.md index c95f258..dce3f9a 100644 --- a/docs/api-contract.md +++ b/docs/api-contract.md @@ -72,11 +72,15 @@ Coverage notes: rows list direct tests when a symbol is named in test names or r | method | AdminService.EditUser | `func (s *AdminService) EditUser(ctx context.Context, username string, opts map[string]any) error` | EditUser edits an existing user (admin only). | `TestAdminService_Good_EditUser` | | method | AdminService.GenerateRunnerToken | `func (s *AdminService) GenerateRunnerToken(ctx context.Context) (string, error)` | GenerateRunnerToken generates an actions runner registration token. | `TestAdminService_Good_GenerateRunnerToken` | | method | AdminService.IterCron | `func (s *AdminService) IterCron(ctx context.Context) iter.Seq2[types.Cron, error]` | IterCron returns an iterator over all cron tasks (admin only). | No direct tests. | +| method | AdminService.IterEmails | `func (s *AdminService) IterEmails(ctx context.Context) iter.Seq2[types.Email, error]` | IterEmails returns an iterator over all email addresses (admin only). | No direct tests. | | method | AdminService.IterOrgs | `func (s *AdminService) IterOrgs(ctx context.Context) iter.Seq2[types.Organization, error]` | IterOrgs returns an iterator over all organisations (admin only). | No direct tests. | +| method | AdminService.IterSearchEmails | `func (s *AdminService) IterSearchEmails(ctx context.Context, q string) iter.Seq2[types.Email, error]` | IterSearchEmails returns an iterator over all email addresses matching a keyword (admin only). | No direct tests. | | method | AdminService.IterUsers | `func (s *AdminService) IterUsers(ctx context.Context) iter.Seq2[types.User, error]` | IterUsers returns an iterator over all users (admin only). | No direct tests. | | method | AdminService.ListCron | `func (s *AdminService) ListCron(ctx context.Context) ([]types.Cron, error)` | ListCron returns all cron tasks (admin only). | `TestAdminService_Good_ListCron` | +| method | AdminService.ListEmails | `func (s *AdminService) ListEmails(ctx context.Context) ([]types.Email, error)` | ListEmails returns all email addresses (admin only). | `TestAdminService_ListEmails_Good` | | method | AdminService.ListOrgs | `func (s *AdminService) ListOrgs(ctx context.Context) ([]types.Organization, error)` | ListOrgs returns all organisations (admin only). | `TestAdminService_Good_ListOrgs` | | method | AdminService.ListUsers | `func (s *AdminService) ListUsers(ctx context.Context) ([]types.User, error)` | ListUsers returns all users (admin only). | `TestAdminService_Good_ListUsers` | +| method | AdminService.SearchEmails | `func (s *AdminService) SearchEmails(ctx context.Context, q string) ([]types.Email, error)` | SearchEmails searches all email addresses by keyword (admin only). | `TestAdminService_SearchEmails_Good` | | method | AdminService.RenameUser | `func (s *AdminService) RenameUser(ctx context.Context, username, newName string) error` | RenameUser renames a user (admin only). | `TestAdminService_Good_RenameUser` | | method | AdminService.RunCron | `func (s *AdminService) RunCron(ctx context.Context, task string) error` | RunCron runs a cron task by name (admin only). | `TestAdminService_Good_RunCron` | | method | BranchService.CreateBranchProtection | `func (s *BranchService) CreateBranchProtection(ctx context.Context, owner, repo string, opts *types.CreateBranchProtectionOption) (*types.BranchProtection, error)` | CreateBranchProtection creates a new branch protection rule. | `TestBranchService_Good_CreateProtection` |