diff --git a/repos.go b/repos.go index 5bcd869..6d4ae81 100644 --- a/repos.go +++ b/repos.go @@ -20,6 +20,26 @@ type RepoService struct { Resource[types.Repository, types.CreateRepoOption, types.EditRepoOption] } +// RepoKeyListOptions controls filtering for repository key listings. +type RepoKeyListOptions struct { + KeyID int64 + Fingerprint string +} + +func (o RepoKeyListOptions) queryParams() map[string]string { + query := make(map[string]string, 2) + if o.KeyID != 0 { + query["key_id"] = strconv.FormatInt(o.KeyID, 10) + } + if o.Fingerprint != "" { + query["fingerprint"] = o.Fingerprint + } + if len(query) == 0 { + return nil + } + return query +} + func newRepoService(c *Client) *RepoService { return &RepoService{ Resource: *NewResource[types.Repository, types.CreateRepoOption, types.EditRepoOption]( @@ -126,6 +146,44 @@ func (s *RepoService) DeleteTagProtection(ctx context.Context, owner, repo strin return s.client.Delete(ctx, path) } +// ListKeys returns all deploy keys for a repository. +func (s *RepoService) ListKeys(ctx context.Context, owner, repo string, filters ...RepoKeyListOptions) ([]types.DeployKey, error) { + path := ResolvePath("/api/v1/repos/{owner}/{repo}/keys", pathParams("owner", owner, "repo", repo)) + return ListAll[types.DeployKey](ctx, s.client, path, repoKeyQuery(filters...)) +} + +// IterKeys returns an iterator over all deploy keys for a repository. +func (s *RepoService) IterKeys(ctx context.Context, owner, repo string, filters ...RepoKeyListOptions) iter.Seq2[types.DeployKey, error] { + path := ResolvePath("/api/v1/repos/{owner}/{repo}/keys", pathParams("owner", owner, "repo", repo)) + return ListIter[types.DeployKey](ctx, s.client, path, repoKeyQuery(filters...)) +} + +// GetKey returns a single deploy key by ID. +func (s *RepoService) GetKey(ctx context.Context, owner, repo string, id int64) (*types.DeployKey, error) { + path := ResolvePath("/api/v1/repos/{owner}/{repo}/keys/{id}", pathParams("owner", owner, "repo", repo, "id", int64String(id))) + var out types.DeployKey + if err := s.client.Get(ctx, path, &out); err != nil { + return nil, err + } + return &out, nil +} + +// CreateKey adds a deploy key to a repository. +func (s *RepoService) CreateKey(ctx context.Context, owner, repo string, opts *types.CreateKeyOption) (*types.DeployKey, error) { + path := ResolvePath("/api/v1/repos/{owner}/{repo}/keys", pathParams("owner", owner, "repo", repo)) + var out types.DeployKey + if err := s.client.Post(ctx, path, opts, &out); err != nil { + return nil, err + } + return &out, nil +} + +// DeleteKey removes a deploy key from a repository by ID. +func (s *RepoService) DeleteKey(ctx context.Context, owner, repo string, id int64) error { + path := ResolvePath("/api/v1/repos/{owner}/{repo}/keys/{id}", pathParams("owner", owner, "repo", repo, "id", int64String(id))) + return s.client.Delete(ctx, path) +} + // ListStargazers returns all users who starred a repository. func (s *RepoService) ListStargazers(ctx context.Context, owner, repo string) ([]types.User, error) { path := ResolvePath("/api/v1/repos/{owner}/{repo}/stargazers", pathParams("owner", owner, "repo", repo)) @@ -589,3 +647,23 @@ func (s *RepoService) SyncPushMirrors(ctx context.Context, owner, repo string) e path := ResolvePath("/api/v1/repos/{owner}/{repo}/push_mirrors-sync", pathParams("owner", owner, "repo", repo)) return s.client.Post(ctx, path, nil, nil) } + +func repoKeyQuery(filters ...RepoKeyListOptions) map[string]string { + if len(filters) == 0 { + return nil + } + + query := make(map[string]string, 2) + for _, filter := range filters { + if filter.KeyID != 0 { + query["key_id"] = strconv.FormatInt(filter.KeyID, 10) + } + if filter.Fingerprint != "" { + query["fingerprint"] = filter.Fingerprint + } + } + if len(query) == 0 { + return nil + } + return query +} diff --git a/repos_test.go b/repos_test.go index 4666bc3..50e1f0f 100644 --- a/repos_test.go +++ b/repos_test.go @@ -628,6 +628,123 @@ func TestRepoService_DeleteTagProtection_Good(t *testing.T) { } } +func TestRepoService_ListKeysWithFilters_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/repos/core/go-forge/keys" { + t.Errorf("wrong path: %s", r.URL.Path) + http.NotFound(w, r) + return + } + if got := r.URL.Query().Get("key_id"); got != "7" { + t.Errorf("got key_id=%q, want %q", got, "7") + } + if got := r.URL.Query().Get("fingerprint"); got != "aa:bb:cc" { + t.Errorf("got fingerprint=%q, want %q", got, "aa:bb:cc") + } + if got := r.URL.Query().Get("page"); got != "1" { + t.Errorf("got page=%q, want %q", got, "1") + } + if got := r.URL.Query().Get("limit"); got != "50" { + t.Errorf("got limit=%q, want %q", got, "50") + } + w.Header().Set("X-Total-Count", "1") + json.NewEncoder(w).Encode([]types.DeployKey{{ID: 7, Title: "deploy"}}) + })) + defer srv.Close() + + f := NewForge(srv.URL, "tok") + keys, err := f.Repos.ListKeys(context.Background(), "core", "go-forge", RepoKeyListOptions{ + KeyID: 7, + Fingerprint: "aa:bb:cc", + }) + if err != nil { + t.Fatal(err) + } + if len(keys) != 1 { + t.Fatalf("got %d keys, want 1", len(keys)) + } + if keys[0].ID != 7 || keys[0].Title != "deploy" { + t.Fatalf("got %#v", keys[0]) + } +} + +func TestRepoService_GetKey_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/repos/core/go-forge/keys/7" { + t.Errorf("wrong path: %s", r.URL.Path) + } + json.NewEncoder(w).Encode(types.DeployKey{ID: 7, Title: "deploy"}) + })) + defer srv.Close() + + f := NewForge(srv.URL, "tok") + key, err := f.Repos.GetKey(context.Background(), "core", "go-forge", 7) + if err != nil { + t.Fatal(err) + } + if key.ID != 7 || key.Title != "deploy" { + t.Fatalf("got %#v", key) + } +} + +func TestRepoService_CreateKey_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/repos/core/go-forge/keys" { + t.Errorf("wrong path: %s", r.URL.Path) + } + var opts types.CreateKeyOption + if err := json.NewDecoder(r.Body).Decode(&opts); err != nil { + t.Fatal(err) + } + if opts.Title != "deploy" || opts.Key != "ssh-ed25519 AAAA..." || !opts.ReadOnly { + t.Fatalf("got %#v", opts) + } + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(types.DeployKey{ID: 9, Title: opts.Title, Key: opts.Key, ReadOnly: opts.ReadOnly}) + })) + defer srv.Close() + + f := NewForge(srv.URL, "tok") + key, err := f.Repos.CreateKey(context.Background(), "core", "go-forge", &types.CreateKeyOption{ + Title: "deploy", + Key: "ssh-ed25519 AAAA...", + ReadOnly: true, + }) + if err != nil { + t.Fatal(err) + } + if key.ID != 9 || key.Title != "deploy" || !key.ReadOnly { + t.Fatalf("got %#v", key) + } +} + +func TestRepoService_DeleteKey_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/repos/core/go-forge/keys/7" { + t.Errorf("wrong path: %s", r.URL.Path) + } + w.WriteHeader(http.StatusNoContent) + })) + defer srv.Close() + + f := NewForge(srv.URL, "tok") + if err := f.Repos.DeleteKey(context.Background(), "core", "go-forge", 7); err != nil { + t.Fatal(err) + } +} + func TestRepoService_DeleteTag_Bad_NotFound(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodDelete {