From bf1750a291d1204f4ec1d4d604ea3c5966bae7dd Mon Sep 17 00:00:00 2001 From: Robert Kaussow Date: Tue, 26 Nov 2024 11:50:48 +0100 Subject: [PATCH] Add PipelineListsOptions to woodpecker-go (#3652) --- cli/deploy/deploy.go | 9 +- cli/pipeline/info.go | 2 +- cli/pipeline/last.go | 6 +- cli/pipeline/list.go | 34 +- cli/pipeline/list_test.go | 2 +- cli/pipeline/ps.go | 3 +- cli/pipeline/start.go | 9 +- cli/repo/repo_add.go | 7 +- cli/repo/repo_list.go | 13 +- cli/repo/repo_sync.go | 7 +- woodpecker-go/woodpecker/interface.go | 21 +- woodpecker-go/woodpecker/list_options.go | 25 + woodpecker-go/woodpecker/list_options_test.go | 44 ++ woodpecker-go/woodpecker/mocks/client.go | 148 +++--- woodpecker-go/woodpecker/repo.go | 141 ++++-- woodpecker-go/woodpecker/repo_test.go | 444 ++++++++++++++++++ woodpecker-go/woodpecker/user.go | 34 +- woodpecker-go/woodpecker/user_test.go | 72 +++ 18 files changed, 861 insertions(+), 160 deletions(-) create mode 100644 woodpecker-go/woodpecker/list_options.go create mode 100644 woodpecker-go/woodpecker/list_options_test.go create mode 100644 woodpecker-go/woodpecker/repo_test.go diff --git a/cli/deploy/deploy.go b/cli/deploy/deploy.go index a037d109d..813b7b39a 100644 --- a/cli/deploy/deploy.go +++ b/cli/deploy/deploy.go @@ -87,7 +87,7 @@ func deploy(ctx context.Context, c *cli.Command) error { var number int64 if pipelineArg == "last" { // Fetch the pipeline number from the last pipeline - pipelines, err := client.PipelineList(repoID) + pipelines, err := client.PipelineList(repoID, woodpecker.PipelineListOptions{}) if err != nil { return err } @@ -121,9 +121,12 @@ func deploy(ctx context.Context, c *cli.Command) error { return fmt.Errorf("please specify the target environment (i.e. production)") } - params := internal.ParseKeyPair(c.StringSlice("param")) + opt := woodpecker.DeployOptions{ + DeployTo: env, + Params: internal.ParseKeyPair(c.StringSlice("param")), + } - deploy, err := client.Deploy(repoID, number, env, params) + deploy, err := client.Deploy(repoID, number, opt) if err != nil { return err } diff --git a/cli/pipeline/info.go b/cli/pipeline/info.go index 660c1c72b..27d05a244 100644 --- a/cli/pipeline/info.go +++ b/cli/pipeline/info.go @@ -48,7 +48,7 @@ func pipelineInfo(ctx context.Context, c *cli.Command) error { var number int64 if pipelineArg == "last" || len(pipelineArg) == 0 { // Fetch the pipeline number from the last pipeline - pipeline, err := client.PipelineLast(repoID, "") + pipeline, err := client.PipelineLast(repoID, woodpecker.PipelineLastOptions{}) if err != nil { return err } diff --git a/cli/pipeline/last.go b/cli/pipeline/last.go index 3d911d0fa..131c20902 100644 --- a/cli/pipeline/last.go +++ b/cli/pipeline/last.go @@ -49,7 +49,11 @@ func pipelineLast(ctx context.Context, c *cli.Command) error { return err } - pipeline, err := client.PipelineLast(repoID, c.String("branch")) + opt := woodpecker.PipelineLastOptions{ + Branch: c.String("branch"), + } + + pipeline, err := client.PipelineLast(repoID, opt) if err != nil { return err } diff --git a/cli/pipeline/list.go b/cli/pipeline/list.go index 5fab1a653..ceadc9777 100644 --- a/cli/pipeline/list.go +++ b/cli/pipeline/list.go @@ -16,6 +16,7 @@ package pipeline import ( "context" + "time" "github.com/urfave/cli/v3" @@ -24,6 +25,7 @@ import ( "go.woodpecker-ci.org/woodpecker/v2/woodpecker-go/woodpecker" ) +//nolint:mnd func buildPipelineListCmd() *cli.Command { return &cli.Command{ Name: "ls", @@ -46,9 +48,26 @@ func buildPipelineListCmd() *cli.Command { &cli.IntFlag{ Name: "limit", Usage: "limit the list size", - //nolint:mnd Value: 25, }, + &cli.TimestampFlag{ + Name: "before", + Usage: "only return pipelines before this RFC3339 date", + Config: cli.TimestampConfig{ + Layouts: []string{ + time.RFC3339, + }, + }, + }, + &cli.TimestampFlag{ + Name: "after", + Usage: "only return pipelines after this RFC3339 date", + Config: cli.TimestampConfig{ + Layouts: []string{ + time.RFC3339, + }, + }, + }, }...), } } @@ -74,7 +93,18 @@ func pipelineList(_ context.Context, c *cli.Command, client woodpecker.Client) ( return resources, err } - pipelines, err := client.PipelineList(repoID) + opt := woodpecker.PipelineListOptions{} + before := c.Timestamp("before") + after := c.Timestamp("after") + + if !before.IsZero() { + opt.Before = before + } + if !after.IsZero() { + opt.After = after + } + + pipelines, err := client.PipelineList(repoID, opt) if err != nil { return resources, err } diff --git a/cli/pipeline/list_test.go b/cli/pipeline/list_test.go index 655c71a09..20ebefb30 100644 --- a/cli/pipeline/list_test.go +++ b/cli/pipeline/list_test.go @@ -107,7 +107,7 @@ func TestPipelineList(t *testing.T) { for _, tt := range testtases { t.Run(tt.name, func(t *testing.T) { mockClient := mocks.NewClient(t) - mockClient.On("PipelineList", mock.Anything).Return(tt.pipelines, tt.pipelineErr) + mockClient.On("PipelineList", mock.Anything, mock.Anything).Return(tt.pipelines, tt.pipelineErr) mockClient.On("RepoLookup", mock.Anything).Return(&woodpecker.Repo{ID: tt.repoID}, nil) command := buildPipelineListCmd() diff --git a/cli/pipeline/ps.go b/cli/pipeline/ps.go index 6aca08545..5468d480a 100644 --- a/cli/pipeline/ps.go +++ b/cli/pipeline/ps.go @@ -25,6 +25,7 @@ import ( "go.woodpecker-ci.org/woodpecker/v2/cli/common" "go.woodpecker-ci.org/woodpecker/v2/cli/internal" + "go.woodpecker-ci.org/woodpecker/v2/woodpecker-go/woodpecker" ) var pipelinePsCmd = &cli.Command{ @@ -51,7 +52,7 @@ func pipelinePs(ctx context.Context, c *cli.Command) error { if pipelineArg == "last" || len(pipelineArg) == 0 { // Fetch the pipeline number from the last pipeline - pipeline, err := client.PipelineLast(repoID, "") + pipeline, err := client.PipelineLast(repoID, woodpecker.PipelineLastOptions{}) if err != nil { return err } diff --git a/cli/pipeline/start.go b/cli/pipeline/start.go index 4eeeaf5f2..4a9b380a2 100644 --- a/cli/pipeline/start.go +++ b/cli/pipeline/start.go @@ -23,6 +23,7 @@ import ( "github.com/urfave/cli/v3" "go.woodpecker-ci.org/woodpecker/v2/cli/internal" + "go.woodpecker-ci.org/woodpecker/v2/woodpecker-go/woodpecker" ) var pipelineStartCmd = &cli.Command{ @@ -54,7 +55,7 @@ func pipelineStart(ctx context.Context, c *cli.Command) (err error) { var number int64 if pipelineArg == "last" { // Fetch the pipeline number from the last pipeline - pipeline, err := client.PipelineLast(repoID, "") + pipeline, err := client.PipelineLast(repoID, woodpecker.PipelineLastOptions{}) if err != nil { return err } @@ -69,9 +70,11 @@ func pipelineStart(ctx context.Context, c *cli.Command) (err error) { } } - params := internal.ParseKeyPair(c.StringSlice("param")) + opt := woodpecker.PipelineStartOptions{ + Params: internal.ParseKeyPair(c.StringSlice("param")), + } - pipeline, err := client.PipelineStart(repoID, number, params) + pipeline, err := client.PipelineStart(repoID, number, opt) if err != nil { return err } diff --git a/cli/repo/repo_add.go b/cli/repo/repo_add.go index 594b8dd73..55601dd85 100644 --- a/cli/repo/repo_add.go +++ b/cli/repo/repo_add.go @@ -22,6 +22,7 @@ import ( "github.com/urfave/cli/v3" "go.woodpecker-ci.org/woodpecker/v2/cli/internal" + "go.woodpecker-ci.org/woodpecker/v2/woodpecker-go/woodpecker" ) var repoAddCmd = &cli.Command{ @@ -43,7 +44,11 @@ func repoAdd(ctx context.Context, c *cli.Command) error { return err } - repo, err := client.RepoPost(int64(forgeRemoteID)) + opt := woodpecker.RepoPostOptions{ + ForgeRemoteID: int64(forgeRemoteID), + } + + repo, err := client.RepoPost(opt) if err != nil { return err } diff --git a/cli/repo/repo_list.go b/cli/repo/repo_list.go index d3e4d1dd3..4e2ae0f85 100644 --- a/cli/repo/repo_list.go +++ b/cli/repo/repo_list.go @@ -23,6 +23,7 @@ import ( "go.woodpecker-ci.org/woodpecker/v2/cli/common" "go.woodpecker-ci.org/woodpecker/v2/cli/internal" + "go.woodpecker-ci.org/woodpecker/v2/woodpecker-go/woodpecker" ) var repoListCmd = &cli.Command{ @@ -36,6 +37,10 @@ var repoListCmd = &cli.Command{ Name: "org", Usage: "filter by organization", }, + &cli.BoolFlag{ + Name: "all", + Usage: "query all repos, including inactive ones", + }, }, } @@ -45,7 +50,11 @@ func repoList(ctx context.Context, c *cli.Command) error { return err } - repos, err := client.RepoList() + opt := woodpecker.RepoListOptions{ + All: c.Bool("all"), + } + + repos, err := client.RepoList(opt) if err != nil || len(repos) == 0 { return err } @@ -68,4 +77,4 @@ func repoList(ctx context.Context, c *cli.Command) error { } // Template for repository list items. -var tmplRepoList = "\x1b[33m{{ .FullName }}\x1b[0m (id: {{ .ID }}, forgeRemoteID: {{ .ForgeRemoteID }})" +var tmplRepoList = "\x1b[33m{{ .FullName }}\x1b[0m (id: {{ .ID }}, forgeRemoteID: {{ .ForgeRemoteID }}, isActive: {{ .IsActive }})" diff --git a/cli/repo/repo_sync.go b/cli/repo/repo_sync.go index 17727eb83..b9bbdaa63 100644 --- a/cli/repo/repo_sync.go +++ b/cli/repo/repo_sync.go @@ -23,6 +23,7 @@ import ( "go.woodpecker-ci.org/woodpecker/v2/cli/common" "go.woodpecker-ci.org/woodpecker/v2/cli/internal" + "go.woodpecker-ci.org/woodpecker/v2/woodpecker-go/woodpecker" ) var repoSyncCmd = &cli.Command{ @@ -40,7 +41,11 @@ func repoSync(ctx context.Context, c *cli.Command) error { return err } - repos, err := client.RepoListOpts(true) + opt := woodpecker.RepoListOptions{ + All: true, + } + + repos, err := client.RepoList(opt) if err != nil || len(repos) == 0 { return err } diff --git a/woodpecker-go/woodpecker/interface.go b/woodpecker-go/woodpecker/interface.go index 95ff60859..0ffed90ba 100644 --- a/woodpecker-go/woodpecker/interface.go +++ b/woodpecker-go/woodpecker/interface.go @@ -54,20 +54,16 @@ type Client interface { // RepoList returns a list of all repositories to which the user has explicit // access in the host system. - RepoList() ([]*Repo, error) - - // RepoListOpts returns a list of all repositories to which the user has - // explicit access in the host system. - RepoListOpts(bool) ([]*Repo, error) + RepoList(opt RepoListOptions) ([]*Repo, error) // RepoPost activates a repository. - RepoPost(forgeRemoteID int64) (*Repo, error) + RepoPost(opt RepoPostOptions) (*Repo, error) // RepoPatch updates a repository. RepoPatch(repoID int64, repo *RepoPatch) (*Repo, error) // RepoMove moves the repository - RepoMove(repoID int64, dst string) error + RepoMove(repoID int64, opt RepoMoveOptions) error // RepoChown updates a repository owner. RepoChown(repoID int64) (*Repo, error) @@ -81,13 +77,12 @@ type Client interface { // Pipeline returns a repository pipeline by number. Pipeline(repoID, pipeline int64) (*Pipeline, error) - // PipelineLast returns the latest repository pipeline by branch. An empty branch - // will result in the default branch. - PipelineLast(repoID int64, branch string) (*Pipeline, error) + // PipelineLast returns the latest repository pipeline. + PipelineLast(repoID int64, opt PipelineLastOptions) (*Pipeline, error) // PipelineList returns a list of recent pipelines for the // the specified repository. - PipelineList(repoID int64) ([]*Pipeline, error) + PipelineList(repoID int64, opt PipelineListOptions) ([]*Pipeline, error) // PipelineQueue returns a list of enqueued pipelines. PipelineQueue() ([]*Feed, error) @@ -96,7 +91,7 @@ type Client interface { PipelineCreate(repoID int64, opts *PipelineOptions) (*Pipeline, error) // PipelineStart re-starts a stopped pipeline. - PipelineStart(repoID, num int64, params map[string]string) (*Pipeline, error) + PipelineStart(repoID, num int64, opt PipelineStartOptions) (*Pipeline, error) // PipelineStop stops the given pipeline. PipelineStop(repoID, pipeline int64) error @@ -118,7 +113,7 @@ type Client interface { // Deploy triggers a deployment for an existing pipeline using the specified // target environment. - Deploy(repoID, pipeline int64, env string, params map[string]string) (*Pipeline, error) + Deploy(repoID, pipeline int64, opt DeployOptions) (*Pipeline, error) // LogsPurge purges the pipeline logs for the specified pipeline. LogsPurge(repoID, pipeline int64) error diff --git a/woodpecker-go/woodpecker/list_options.go b/woodpecker-go/woodpecker/list_options.go new file mode 100644 index 000000000..8107fa308 --- /dev/null +++ b/woodpecker-go/woodpecker/list_options.go @@ -0,0 +1,25 @@ +package woodpecker + +import ( + "fmt" + "net/url" +) + +// ListOptions represents the options for the Woodpecker API pagination. +type ListOptions struct { + Page int + PerPage int +} + +// getURLQuery returns the query string for the ListOptions. +func (o ListOptions) getURLQuery() url.Values { + query := make(url.Values) + if o.Page > 0 { + query.Add("page", fmt.Sprintf("%d", o.Page)) + } + if o.PerPage > 0 { + query.Add("perPage", fmt.Sprintf("%d", o.PerPage)) + } + + return query +} diff --git a/woodpecker-go/woodpecker/list_options_test.go b/woodpecker-go/woodpecker/list_options_test.go new file mode 100644 index 000000000..22d45d92e --- /dev/null +++ b/woodpecker-go/woodpecker/list_options_test.go @@ -0,0 +1,44 @@ +package woodpecker + +import ( + "net/url" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestListOptions_getURLQuery(t *testing.T) { + tests := []struct { + name string + opts ListOptions + expected url.Values + }{ + { + name: "no options", + opts: ListOptions{}, + expected: url.Values{}, + }, + { + name: "with page", + opts: ListOptions{Page: 2}, + expected: url.Values{"page": {"2"}}, + }, + { + name: "with per page", + opts: ListOptions{PerPage: 10}, + expected: url.Values{"perPage": {"10"}}, + }, + { + name: "with page and per page", + opts: ListOptions{Page: 3, PerPage: 20}, + expected: url.Values{"page": {"3"}, "perPage": {"20"}}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := tt.opts.getURLQuery() + assert.Equal(t, tt.expected, actual) + }) + } +} diff --git a/woodpecker-go/woodpecker/mocks/client.go b/woodpecker-go/woodpecker/mocks/client.go index 4e670aa92..3850520b1 100644 --- a/woodpecker-go/woodpecker/mocks/client.go +++ b/woodpecker-go/woodpecker/mocks/client.go @@ -323,9 +323,9 @@ func (_m *Client) CronUpdate(repoID int64, cron *woodpecker.Cron) (*woodpecker.C return r0, r1 } -// Deploy provides a mock function with given fields: repoID, pipeline, env, params -func (_m *Client) Deploy(repoID int64, pipeline int64, env string, params map[string]string) (*woodpecker.Pipeline, error) { - ret := _m.Called(repoID, pipeline, env, params) +// Deploy provides a mock function with given fields: repoID, pipeline, opt +func (_m *Client) Deploy(repoID int64, pipeline int64, opt woodpecker.DeployOptions) (*woodpecker.Pipeline, error) { + ret := _m.Called(repoID, pipeline, opt) if len(ret) == 0 { panic("no return value specified for Deploy") @@ -333,19 +333,19 @@ func (_m *Client) Deploy(repoID int64, pipeline int64, env string, params map[st var r0 *woodpecker.Pipeline var r1 error - if rf, ok := ret.Get(0).(func(int64, int64, string, map[string]string) (*woodpecker.Pipeline, error)); ok { - return rf(repoID, pipeline, env, params) + if rf, ok := ret.Get(0).(func(int64, int64, woodpecker.DeployOptions) (*woodpecker.Pipeline, error)); ok { + return rf(repoID, pipeline, opt) } - if rf, ok := ret.Get(0).(func(int64, int64, string, map[string]string) *woodpecker.Pipeline); ok { - r0 = rf(repoID, pipeline, env, params) + if rf, ok := ret.Get(0).(func(int64, int64, woodpecker.DeployOptions) *woodpecker.Pipeline); ok { + r0 = rf(repoID, pipeline, opt) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*woodpecker.Pipeline) } } - if rf, ok := ret.Get(1).(func(int64, int64, string, map[string]string) error); ok { - r1 = rf(repoID, pipeline, env, params) + if rf, ok := ret.Get(1).(func(int64, int64, woodpecker.DeployOptions) error); ok { + r1 = rf(repoID, pipeline, opt) } else { r1 = ret.Error(1) } @@ -1151,9 +1151,9 @@ func (_m *Client) PipelineKill(repoID int64, pipeline int64) error { return r0 } -// PipelineLast provides a mock function with given fields: repoID, branch -func (_m *Client) PipelineLast(repoID int64, branch string) (*woodpecker.Pipeline, error) { - ret := _m.Called(repoID, branch) +// PipelineLast provides a mock function with given fields: repoID, opt +func (_m *Client) PipelineLast(repoID int64, opt woodpecker.PipelineLastOptions) (*woodpecker.Pipeline, error) { + ret := _m.Called(repoID, opt) if len(ret) == 0 { panic("no return value specified for PipelineLast") @@ -1161,19 +1161,19 @@ func (_m *Client) PipelineLast(repoID int64, branch string) (*woodpecker.Pipelin var r0 *woodpecker.Pipeline var r1 error - if rf, ok := ret.Get(0).(func(int64, string) (*woodpecker.Pipeline, error)); ok { - return rf(repoID, branch) + if rf, ok := ret.Get(0).(func(int64, woodpecker.PipelineLastOptions) (*woodpecker.Pipeline, error)); ok { + return rf(repoID, opt) } - if rf, ok := ret.Get(0).(func(int64, string) *woodpecker.Pipeline); ok { - r0 = rf(repoID, branch) + if rf, ok := ret.Get(0).(func(int64, woodpecker.PipelineLastOptions) *woodpecker.Pipeline); ok { + r0 = rf(repoID, opt) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*woodpecker.Pipeline) } } - if rf, ok := ret.Get(1).(func(int64, string) error); ok { - r1 = rf(repoID, branch) + if rf, ok := ret.Get(1).(func(int64, woodpecker.PipelineLastOptions) error); ok { + r1 = rf(repoID, opt) } else { r1 = ret.Error(1) } @@ -1181,9 +1181,9 @@ func (_m *Client) PipelineLast(repoID int64, branch string) (*woodpecker.Pipelin return r0, r1 } -// PipelineList provides a mock function with given fields: repoID -func (_m *Client) PipelineList(repoID int64) ([]*woodpecker.Pipeline, error) { - ret := _m.Called(repoID) +// PipelineList provides a mock function with given fields: repoID, opt +func (_m *Client) PipelineList(repoID int64, opt woodpecker.PipelineListOptions) ([]*woodpecker.Pipeline, error) { + ret := _m.Called(repoID, opt) if len(ret) == 0 { panic("no return value specified for PipelineList") @@ -1191,19 +1191,19 @@ func (_m *Client) PipelineList(repoID int64) ([]*woodpecker.Pipeline, error) { var r0 []*woodpecker.Pipeline var r1 error - if rf, ok := ret.Get(0).(func(int64) ([]*woodpecker.Pipeline, error)); ok { - return rf(repoID) + if rf, ok := ret.Get(0).(func(int64, woodpecker.PipelineListOptions) ([]*woodpecker.Pipeline, error)); ok { + return rf(repoID, opt) } - if rf, ok := ret.Get(0).(func(int64) []*woodpecker.Pipeline); ok { - r0 = rf(repoID) + if rf, ok := ret.Get(0).(func(int64, woodpecker.PipelineListOptions) []*woodpecker.Pipeline); ok { + r0 = rf(repoID, opt) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]*woodpecker.Pipeline) } } - if rf, ok := ret.Get(1).(func(int64) error); ok { - r1 = rf(repoID) + if rf, ok := ret.Get(1).(func(int64, woodpecker.PipelineListOptions) error); ok { + r1 = rf(repoID, opt) } else { r1 = ret.Error(1) } @@ -1271,9 +1271,9 @@ func (_m *Client) PipelineQueue() ([]*woodpecker.Feed, error) { return r0, r1 } -// PipelineStart provides a mock function with given fields: repoID, num, params -func (_m *Client) PipelineStart(repoID int64, num int64, params map[string]string) (*woodpecker.Pipeline, error) { - ret := _m.Called(repoID, num, params) +// PipelineStart provides a mock function with given fields: repoID, num, opt +func (_m *Client) PipelineStart(repoID int64, num int64, opt woodpecker.PipelineStartOptions) (*woodpecker.Pipeline, error) { + ret := _m.Called(repoID, num, opt) if len(ret) == 0 { panic("no return value specified for PipelineStart") @@ -1281,19 +1281,19 @@ func (_m *Client) PipelineStart(repoID int64, num int64, params map[string]strin var r0 *woodpecker.Pipeline var r1 error - if rf, ok := ret.Get(0).(func(int64, int64, map[string]string) (*woodpecker.Pipeline, error)); ok { - return rf(repoID, num, params) + if rf, ok := ret.Get(0).(func(int64, int64, woodpecker.PipelineStartOptions) (*woodpecker.Pipeline, error)); ok { + return rf(repoID, num, opt) } - if rf, ok := ret.Get(0).(func(int64, int64, map[string]string) *woodpecker.Pipeline); ok { - r0 = rf(repoID, num, params) + if rf, ok := ret.Get(0).(func(int64, int64, woodpecker.PipelineStartOptions) *woodpecker.Pipeline); ok { + r0 = rf(repoID, num, opt) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*woodpecker.Pipeline) } } - if rf, ok := ret.Get(1).(func(int64, int64, map[string]string) error); ok { - r1 = rf(repoID, num, params) + if rf, ok := ret.Get(1).(func(int64, int64, woodpecker.PipelineStartOptions) error); ok { + r1 = rf(repoID, num, opt) } else { r1 = ret.Error(1) } @@ -1565,9 +1565,9 @@ func (_m *Client) RepoDel(repoID int64) error { return r0 } -// RepoList provides a mock function with given fields: -func (_m *Client) RepoList() ([]*woodpecker.Repo, error) { - ret := _m.Called() +// RepoList provides a mock function with given fields: opt +func (_m *Client) RepoList(opt woodpecker.RepoListOptions) ([]*woodpecker.Repo, error) { + ret := _m.Called(opt) if len(ret) == 0 { panic("no return value specified for RepoList") @@ -1575,49 +1575,19 @@ func (_m *Client) RepoList() ([]*woodpecker.Repo, error) { var r0 []*woodpecker.Repo var r1 error - if rf, ok := ret.Get(0).(func() ([]*woodpecker.Repo, error)); ok { - return rf() + if rf, ok := ret.Get(0).(func(woodpecker.RepoListOptions) ([]*woodpecker.Repo, error)); ok { + return rf(opt) } - if rf, ok := ret.Get(0).(func() []*woodpecker.Repo); ok { - r0 = rf() + if rf, ok := ret.Get(0).(func(woodpecker.RepoListOptions) []*woodpecker.Repo); ok { + r0 = rf(opt) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]*woodpecker.Repo) } } - if rf, ok := ret.Get(1).(func() error); ok { - r1 = rf() - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// RepoListOpts provides a mock function with given fields: _a0 -func (_m *Client) RepoListOpts(_a0 bool) ([]*woodpecker.Repo, error) { - ret := _m.Called(_a0) - - if len(ret) == 0 { - panic("no return value specified for RepoListOpts") - } - - var r0 []*woodpecker.Repo - var r1 error - if rf, ok := ret.Get(0).(func(bool) ([]*woodpecker.Repo, error)); ok { - return rf(_a0) - } - if rf, ok := ret.Get(0).(func(bool) []*woodpecker.Repo); ok { - r0 = rf(_a0) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]*woodpecker.Repo) - } - } - - if rf, ok := ret.Get(1).(func(bool) error); ok { - r1 = rf(_a0) + if rf, ok := ret.Get(1).(func(woodpecker.RepoListOptions) error); ok { + r1 = rf(opt) } else { r1 = ret.Error(1) } @@ -1655,17 +1625,17 @@ func (_m *Client) RepoLookup(repoFullName string) (*woodpecker.Repo, error) { return r0, r1 } -// RepoMove provides a mock function with given fields: repoID, dst -func (_m *Client) RepoMove(repoID int64, dst string) error { - ret := _m.Called(repoID, dst) +// RepoMove provides a mock function with given fields: repoID, opt +func (_m *Client) RepoMove(repoID int64, opt woodpecker.RepoMoveOptions) error { + ret := _m.Called(repoID, opt) if len(ret) == 0 { panic("no return value specified for RepoMove") } var r0 error - if rf, ok := ret.Get(0).(func(int64, string) error); ok { - r0 = rf(repoID, dst) + if rf, ok := ret.Get(0).(func(int64, woodpecker.RepoMoveOptions) error); ok { + r0 = rf(repoID, opt) } else { r0 = ret.Error(0) } @@ -1703,9 +1673,9 @@ func (_m *Client) RepoPatch(repoID int64, repo *woodpecker.RepoPatch) (*woodpeck return r0, r1 } -// RepoPost provides a mock function with given fields: forgeRemoteID -func (_m *Client) RepoPost(forgeRemoteID int64) (*woodpecker.Repo, error) { - ret := _m.Called(forgeRemoteID) +// RepoPost provides a mock function with given fields: opt +func (_m *Client) RepoPost(opt woodpecker.RepoPostOptions) (*woodpecker.Repo, error) { + ret := _m.Called(opt) if len(ret) == 0 { panic("no return value specified for RepoPost") @@ -1713,19 +1683,19 @@ func (_m *Client) RepoPost(forgeRemoteID int64) (*woodpecker.Repo, error) { var r0 *woodpecker.Repo var r1 error - if rf, ok := ret.Get(0).(func(int64) (*woodpecker.Repo, error)); ok { - return rf(forgeRemoteID) + if rf, ok := ret.Get(0).(func(woodpecker.RepoPostOptions) (*woodpecker.Repo, error)); ok { + return rf(opt) } - if rf, ok := ret.Get(0).(func(int64) *woodpecker.Repo); ok { - r0 = rf(forgeRemoteID) + if rf, ok := ret.Get(0).(func(woodpecker.RepoPostOptions) *woodpecker.Repo); ok { + r0 = rf(opt) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*woodpecker.Repo) } } - if rf, ok := ret.Get(1).(func(int64) error); ok { - r1 = rf(forgeRemoteID) + if rf, ok := ret.Get(1).(func(woodpecker.RepoPostOptions) error); ok { + r1 = rf(opt) } else { r1 = ret.Error(1) } diff --git a/woodpecker-go/woodpecker/repo.go b/woodpecker-go/woodpecker/repo.go index a96137359..d46077ee7 100644 --- a/woodpecker-go/woodpecker/repo.go +++ b/woodpecker-go/woodpecker/repo.go @@ -1,12 +1,17 @@ package woodpecker -import "fmt" +import ( + "fmt" + "net/url" + "strconv" + "time" +) const ( - pathRepoPost = "%s/api/repos?forge_remote_id=%d" + pathRepoPost = "%s/api/repos" pathRepo = "%s/api/repos/%d" pathRepoLookup = "%s/api/repos/lookup/%s" - pathRepoMove = "%s/api/repos/%d/move?to=%s" + pathRepoMove = "%s/api/repos/%d/move" pathChown = "%s/api/repos/%d/chown" pathRepair = "%s/api/repos/%d/repair" pathPipelines = "%s/api/repos/%d/pipelines" @@ -24,6 +29,84 @@ const ( pathRepoCron = "%s/api/repos/%d/cron/%d" ) +type PipelineListOptions struct { + ListOptions + Before time.Time + After time.Time +} + +type DeployOptions struct { + DeployTo string // override the target deploy value + Params map[string]string // custom KEY=value parameters to be injected into the step environment +} + +type PipelineStartOptions struct { + Params map[string]string // custom KEY=value parameters to be injected into the step environment +} + +type PipelineLastOptions struct { + Branch string // last pipeline from given branch, an empty branch will result in the default branch +} + +type RepoPostOptions struct { + ForgeRemoteID int64 +} + +type RepoMoveOptions struct { + To string +} + +// QueryEncode returns the URL query parameters for the PipelineListOptions. +func (opt *PipelineListOptions) QueryEncode() string { + query := opt.getURLQuery() + if !opt.Before.IsZero() { + query.Add("before", opt.Before.Format(time.RFC3339)) + } + if !opt.After.IsZero() { + query.Add("after", opt.After.Format(time.RFC3339)) + } + return query.Encode() +} + +// QueryEncode returns the URL query parameters for the DeployOptions. +func (opt *DeployOptions) QueryEncode() string { + query := mapValues(opt.Params) + if opt.DeployTo != "" { + query.Add("deploy_to", opt.DeployTo) + } + query.Add("event", EventDeploy) + return query.Encode() +} + +// QueryEncode returns the URL query parameters for the PipelineStartOptions. +func (opt *PipelineStartOptions) QueryEncode() string { + query := mapValues(opt.Params) + return query.Encode() +} + +// QueryEncode returns the URL query parameters for the PipelineLastOptions. +func (opt *PipelineLastOptions) QueryEncode() string { + query := make(url.Values) + if opt.Branch != "" { + query.Add("branch", opt.Branch) + } + return query.Encode() +} + +// QueryEncode returns the URL query parameters for the RepoPostOptions. +func (opt *RepoPostOptions) QueryEncode() string { + query := make(url.Values) + query.Add("forge_remote_id", strconv.FormatInt(opt.ForgeRemoteID, 10)) + return query.Encode() +} + +// QueryEncode returns the URL query parameters for the RepoMoveOptions. +func (opt *RepoMoveOptions) QueryEncode() string { + query := make(url.Values) + query.Add("to", opt.To) + return query.Encode() +} + // Repo returns a repository by id. func (c *client) Repo(repoID int64) (*Repo, error) { out := new(Repo) @@ -41,10 +124,12 @@ func (c *client) RepoLookup(fullName string) (*Repo, error) { } // RepoPost activates a repository. -func (c *client) RepoPost(forgeRemoteID int64) (*Repo, error) { +func (c *client) RepoPost(opt RepoPostOptions) (*Repo, error) { out := new(Repo) - uri := fmt.Sprintf(pathRepoPost, c.addr, forgeRemoteID) - err := c.post(uri, nil, out) + uri, _ := url.Parse(fmt.Sprintf(pathRepoPost, c.addr)) + uri.RawQuery = opt.QueryEncode() + fmt.Println("!!!!!!!!!!", uri.String()) + err := c.post(uri.String(), nil, out) return out, err } @@ -78,9 +163,10 @@ func (c *client) RepoDel(repoID int64) error { } // RepoMove moves a repository. -func (c *client) RepoMove(repoID int64, newFullName string) error { - uri := fmt.Sprintf(pathRepoMove, c.addr, repoID, newFullName) - return c.post(uri, nil, nil) +func (c *client) RepoMove(repoID int64, opt RepoMoveOptions) error { + uri, _ := url.Parse(fmt.Sprintf(pathRepoMove, c.addr, repoID)) + uri.RawQuery = opt.QueryEncode() + return c.post(uri.String(), nil, nil) } // Registry returns a registry by hostname. @@ -202,23 +288,22 @@ func (c *client) Pipeline(repoID, pipeline int64) (*Pipeline, error) { return out, err } -// Pipeline returns the latest repository pipeline by branch. -func (c *client) PipelineLast(repoID int64, branch string) (*Pipeline, error) { +// Pipeline returns the latest repository pipeline. +func (c *client) PipelineLast(repoID int64, opt PipelineLastOptions) (*Pipeline, error) { out := new(Pipeline) - uri := fmt.Sprintf(pathPipeline, c.addr, repoID, "latest") - if len(branch) != 0 { - uri += "?branch=" + branch - } - err := c.get(uri, out) + uri, _ := url.Parse(fmt.Sprintf(pathPipeline, c.addr, repoID, "latest")) + uri.RawQuery = opt.QueryEncode() + err := c.get(uri.String(), out) return out, err } // PipelineList returns a list of recent pipelines for the // the specified repository. -func (c *client) PipelineList(repoID int64) ([]*Pipeline, error) { +func (c *client) PipelineList(repoID int64, opt PipelineListOptions) ([]*Pipeline, error) { var out []*Pipeline - uri := fmt.Sprintf(pathPipelines, c.addr, repoID) - err := c.get(uri, &out) + uri, _ := url.Parse(fmt.Sprintf(pathPipelines, c.addr, repoID)) + uri.RawQuery = opt.QueryEncode() + err := c.get(uri.String(), &out) return out, err } @@ -231,11 +316,11 @@ func (c *client) PipelineCreate(repoID int64, options *PipelineOptions) (*Pipeli } // PipelineStart re-starts a stopped pipeline. -func (c *client) PipelineStart(repoID, pipeline int64, params map[string]string) (*Pipeline, error) { +func (c *client) PipelineStart(repoID, pipeline int64, opt PipelineStartOptions) (*Pipeline, error) { out := new(Pipeline) - val := mapValues(params) - uri := fmt.Sprintf(pathPipeline, c.addr, repoID, pipeline) - err := c.post(uri+"?"+val.Encode(), nil, out) + uri, _ := url.Parse(fmt.Sprintf(pathPipeline, c.addr, repoID, pipeline)) + uri.RawQuery = opt.QueryEncode() + err := c.post(uri.String(), nil, out) return out, err } @@ -278,13 +363,11 @@ func (c *client) LogsPurge(repoID, pipeline int64) error { // Deploy triggers a deployment for an existing pipeline using the // specified target environment. -func (c *client) Deploy(repoID, pipeline int64, env string, params map[string]string) (*Pipeline, error) { +func (c *client) Deploy(repoID, pipeline int64, opt DeployOptions) (*Pipeline, error) { out := new(Pipeline) - val := mapValues(params) - val.Set("event", EventDeploy) - val.Set("deploy_to", env) - uri := fmt.Sprintf(pathPipeline, c.addr, repoID, pipeline) - err := c.post(uri+"?"+val.Encode(), nil, out) + uri, _ := url.Parse(fmt.Sprintf(pathPipeline, c.addr, repoID, pipeline)) + uri.RawQuery = opt.QueryEncode() + err := c.post(uri.String(), nil, out) return out, err } diff --git a/woodpecker-go/woodpecker/repo_test.go b/woodpecker-go/woodpecker/repo_test.go new file mode 100644 index 000000000..442d8c4c4 --- /dev/null +++ b/woodpecker-go/woodpecker/repo_test.go @@ -0,0 +1,444 @@ +package woodpecker + +import ( + "fmt" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestPipelineList(t *testing.T) { + tests := []struct { + name string + handler http.HandlerFunc + opts PipelineListOptions + wantErr bool + expectedLength int + expectedIDs []int64 + }{ + { + name: "success", + handler: func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodGet, r.Method) + assert.Equal(t, "/api/repos/123/pipelines?after=2023-01-15T00%3A00%3A00Z&before=2023-01-16T00%3A00%3A00Z&page=2&perPage=10", r.URL.RequestURI()) + + w.WriteHeader(http.StatusOK) + _, err := fmt.Fprint(w, `[{"id":1},{"id":2}]`) + assert.NoError(t, err) + }, + opts: PipelineListOptions{ + ListOptions: ListOptions{ + Page: 2, + PerPage: 10, + }, + Before: time.Date(2023, 1, 16, 0, 0, 0, 0, time.UTC), + After: time.Date(2023, 1, 15, 0, 0, 0, 0, time.UTC), + }, + expectedLength: 2, + expectedIDs: []int64{1, 2}, + }, + { + name: "empty ListOptions", + handler: func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodGet, r.Method) + assert.Equal(t, "/api/repos/123/pipelines", r.URL.RequestURI()) + + w.WriteHeader(http.StatusOK) + _, err := fmt.Fprint(w, `[{"id":1},{"id":2}]`) + assert.NoError(t, err) + }, + opts: PipelineListOptions{}, + expectedLength: 2, + expectedIDs: []int64{1, 2}, + }, + { + name: "server error", + handler: func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + }, + opts: PipelineListOptions{}, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ts := httptest.NewServer(tt.handler) + defer ts.Close() + + client := NewClient(ts.URL, http.DefaultClient) + + pipelines, err := client.PipelineList(123, tt.opts) + if tt.wantErr { + assert.Error(t, err) + assert.Nil(t, pipelines) + return + } + + assert.NoError(t, err) + assert.Len(t, pipelines, tt.expectedLength) + for i, id := range tt.expectedIDs { + assert.Equal(t, id, pipelines[i].ID) + } + }) + } +} + +func TestClientDeploy(t *testing.T) { + tests := []struct { + name string + handler http.HandlerFunc + repoID int64 + pipelineID int64 + opts DeployOptions + wantErr bool + expectedPipeline *Pipeline + }{ + { + name: "success", + handler: func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodPost, r.Method) + assert.Equal(t, "/api/repos/123/pipelines/456?event=deployment", r.URL.RequestURI()) + + w.WriteHeader(http.StatusOK) + _, err := fmt.Fprint(w, `{"id":789}`) + assert.NoError(t, err) + }, + repoID: 123, + pipelineID: 456, + opts: DeployOptions{}, + expectedPipeline: &Pipeline{ + ID: 789, + }, + }, + { + name: "error", + handler: func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + }, + repoID: 123, + pipelineID: 456, + opts: DeployOptions{}, + wantErr: true, + }, + { + name: "with options", + handler: func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodPost, r.Method) + assert.Equal(t, "/api/repos/123/pipelines/456?deploy_to=production&event=deployment", r.URL.RequestURI()) + + w.WriteHeader(http.StatusOK) + _, err := fmt.Fprint(w, `{"id":789}`) + assert.NoError(t, err) + }, + repoID: 123, + pipelineID: 456, + opts: DeployOptions{ + DeployTo: "production", + }, + expectedPipeline: &Pipeline{ + ID: 789, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ts := httptest.NewServer(tt.handler) + defer ts.Close() + + client := NewClient(ts.URL, http.DefaultClient) + + pipeline, err := client.Deploy(tt.repoID, tt.pipelineID, tt.opts) + if tt.wantErr { + assert.Error(t, err) + return + } + + assert.NoError(t, err) + assert.Equal(t, tt.expectedPipeline, pipeline) + }) + } +} + +func TestClientPipelineStart(t *testing.T) { + tests := []struct { + name string + handler http.HandlerFunc + repoID int64 + pipelineID int64 + opts PipelineStartOptions + wantErr bool + expectedPipeline *Pipeline + }{ + { + name: "success", + handler: func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodPost, r.Method) + assert.Equal(t, "/api/repos/123/pipelines/456", r.URL.RequestURI()) + + w.WriteHeader(http.StatusOK) + _, err := fmt.Fprint(w, `{"id":789}`) + assert.NoError(t, err) + }, + repoID: 123, + pipelineID: 456, + opts: PipelineStartOptions{}, + expectedPipeline: &Pipeline{ + ID: 789, + }, + }, + { + name: "error", + handler: func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + }, + repoID: 123, + pipelineID: 456, + opts: PipelineStartOptions{}, + wantErr: true, + }, + { + name: "with options", + handler: func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodPost, r.Method) + assert.Equal(t, "/api/repos/123/pipelines/456?foo=bar", r.URL.RequestURI()) + + w.WriteHeader(http.StatusOK) + _, err := fmt.Fprint(w, `{"id":789}`) + assert.NoError(t, err) + }, + repoID: 123, + pipelineID: 456, + opts: PipelineStartOptions{ + Params: map[string]string{"foo": "bar"}, + }, + expectedPipeline: &Pipeline{ + ID: 789, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ts := httptest.NewServer(tt.handler) + defer ts.Close() + + client := NewClient(ts.URL, http.DefaultClient) + + pipeline, err := client.PipelineStart(tt.repoID, tt.pipelineID, tt.opts) + if tt.wantErr { + assert.Error(t, err) + return + } + + assert.NoError(t, err) + assert.Equal(t, tt.expectedPipeline, pipeline) + }) + } +} + +func TestClient_PipelineLast(t *testing.T) { + tests := []struct { + name string + handler http.HandlerFunc + repoID int64 + opts PipelineLastOptions + expected *Pipeline + wantErr bool + }{ + { + name: "success", + handler: func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/api/repos/1/pipelines/latest?branch=main", r.URL.Path+"?"+r.URL.RawQuery) + w.WriteHeader(http.StatusOK) + _, err := fmt.Fprint(w, `{"id":1,"number":1,"status":"success","event":"push","branch":"main"}`) + assert.NoError(t, err) + }, + repoID: 1, + opts: PipelineLastOptions{Branch: "main"}, + expected: &Pipeline{ + ID: 1, + Number: 1, + Status: "success", + Event: "push", + Branch: "main", + }, + wantErr: false, + }, + { + name: "server error", + handler: func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + }, + repoID: 1, + opts: PipelineLastOptions{}, + expected: nil, + wantErr: true, + }, + { + name: "invalid response", + handler: func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + _, err := fmt.Fprint(w, `invalid json`) + assert.NoError(t, err) + }, + repoID: 1, + opts: PipelineLastOptions{}, + expected: nil, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ts := httptest.NewServer(tt.handler) + defer ts.Close() + + client := NewClient(ts.URL, http.DefaultClient) + pipeline, err := client.PipelineLast(tt.repoID, tt.opts) + + if tt.wantErr { + assert.Error(t, err) + return + } + + assert.NoError(t, err) + assert.Equal(t, tt.expected, pipeline) + }) + } +} + +func TestClientRepoPost(t *testing.T) { + tests := []struct { + name string + handler http.HandlerFunc + opts RepoPostOptions + expected *Repo + wantErr bool + }{ + { + name: "success", + handler: func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodPost, r.Method) + assert.Equal(t, "/api/repos?forge_remote_id=10", r.URL.RequestURI()) + + w.WriteHeader(http.StatusOK) + _, err := fmt.Fprint(w, `{"id":1,"name":"test","owner":"owner","full_name":"owner/test","forge_remote_id":"10"}`) + assert.NoError(t, err) + }, + opts: RepoPostOptions{ + ForgeRemoteID: 10, + }, + expected: &Repo{ + ID: 1, + ForgeRemoteID: "10", + Name: "test", + Owner: "owner", + FullName: "owner/test", + }, + wantErr: false, + }, + { + name: "server error", + handler: func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + }, + opts: RepoPostOptions{}, + expected: nil, + wantErr: true, + }, + { + name: "invalid response", + handler: func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + _, err := fmt.Fprint(w, `invalid json`) + assert.NoError(t, err) + }, + opts: RepoPostOptions{}, + expected: nil, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ts := httptest.NewServer(tt.handler) + defer ts.Close() + + client := NewClient(ts.URL, http.DefaultClient) + repo, err := client.RepoPost(tt.opts) + + if tt.wantErr { + assert.Error(t, err) + return + } + + assert.NoError(t, err) + assert.Equal(t, tt.expected, repo) + }) + } +} + +func TestClientRepoMove(t *testing.T) { + tests := []struct { + name string + handler http.HandlerFunc + repoID int64 + opts RepoMoveOptions + wantErr bool + }{ + { + name: "success", + handler: func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodPost, r.Method) + assert.Equal(t, "/api/repos/123/move?to=new_owner", r.URL.RequestURI()) + w.WriteHeader(http.StatusOK) + }, + repoID: 123, + opts: RepoMoveOptions{ + To: "new_owner", + }, + wantErr: false, + }, + { + name: "server error", + handler: func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + }, + repoID: 123, + opts: RepoMoveOptions{}, + wantErr: true, + }, + { + name: "invalid options", + handler: func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodPost, r.Method) + w.WriteHeader(http.StatusBadRequest) + }, + repoID: 123, + opts: RepoMoveOptions{}, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ts := httptest.NewServer(tt.handler) + defer ts.Close() + + client := NewClient(ts.URL, http.DefaultClient) + err := client.RepoMove(tt.repoID, tt.opts) + + if tt.wantErr { + assert.Error(t, err) + return + } + + assert.NoError(t, err) + }) + } +} diff --git a/woodpecker-go/woodpecker/user.go b/woodpecker-go/woodpecker/user.go index 6cba11e53..448c28706 100644 --- a/woodpecker-go/woodpecker/user.go +++ b/woodpecker-go/woodpecker/user.go @@ -1,6 +1,9 @@ package woodpecker -import "fmt" +import ( + "fmt" + "net/url" +) const ( pathSelf = "%s/api/user" @@ -9,6 +12,19 @@ const ( pathUser = "%s/api/users/%s" ) +type RepoListOptions struct { + All bool // query all repos, including inactive ones +} + +// QueryEncode returns the URL query parameters for the RepoListOptions. +func (opt *RepoListOptions) QueryEncode() string { + query := make(url.Values) + if opt.All { + query.Add("all", "true") + } + return query.Encode() +} + // Self returns the currently authenticated user. func (c *client) Self() (*User, error) { out := new(User) @@ -58,18 +74,10 @@ func (c *client) UserDel(login string) error { // RepoList returns a list of all repositories to which // the user has explicit access in the host system. -func (c *client) RepoList() ([]*Repo, error) { +func (c *client) RepoList(opt RepoListOptions) ([]*Repo, error) { var out []*Repo - uri := fmt.Sprintf(pathRepos, c.addr) - err := c.get(uri, &out) - return out, err -} - -// RepoListOpts returns a list of all repositories to which -// the user has explicit access in the host system. -func (c *client) RepoListOpts(all bool) ([]*Repo, error) { - var out []*Repo - uri := fmt.Sprintf(pathRepos+"?all=%v", c.addr, all) - err := c.get(uri, &out) + uri, _ := url.Parse(fmt.Sprintf(pathRepos, c.addr)) + uri.RawQuery = opt.QueryEncode() + err := c.get(uri.String(), &out) return out, err } diff --git a/woodpecker-go/woodpecker/user_test.go b/woodpecker-go/woodpecker/user_test.go index 4d99226c1..01d2666a0 100644 --- a/woodpecker-go/woodpecker/user_test.go +++ b/woodpecker-go/woodpecker/user_test.go @@ -265,3 +265,75 @@ func TestClient_UserDel(t *testing.T) { }) } } + +func TestClient_RepoList(t *testing.T) { + tests := []struct { + name string + handler http.HandlerFunc + opt RepoListOptions + expected []*Repo + wantErr bool + }{ + { + name: "success", + handler: func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + _, err := fmt.Fprint(w, `[{"id":1,"name":"repo1"},{"id":2,"name":"repo2"}]`) + assert.NoError(t, err) + }, + opt: RepoListOptions{}, + expected: []*Repo{{ID: 1, Name: "repo1"}, {ID: 2, Name: "repo2"}}, + wantErr: false, + }, + { + name: "empty response", + handler: func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + _, err := fmt.Fprint(w, `[]`) + assert.NoError(t, err) + }, + opt: RepoListOptions{}, + expected: []*Repo{}, + wantErr: false, + }, + { + name: "server error", + handler: func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + }, + opt: RepoListOptions{}, + expected: nil, + wantErr: true, + }, + { + name: "with options", + handler: func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/api/user/repos?all=true", r.URL.RequestURI()) + w.WriteHeader(http.StatusOK) + _, err := fmt.Fprint(w, `[]`) + assert.NoError(t, err) + }, + opt: RepoListOptions{All: true}, + expected: []*Repo{}, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ts := httptest.NewServer(tt.handler) + defer ts.Close() + + client := NewClient(ts.URL, http.DefaultClient) + repos, err := client.RepoList(tt.opt) + + if tt.wantErr { + assert.Error(t, err) + return + } + + assert.NoError(t, err) + assert.Equal(t, tt.expected, repos) + }) + } +}