From 05ef60b4553a40b6c492cffed9fe35dd15caaf75 Mon Sep 17 00:00:00 2001 From: Soule BA Date: Wed, 22 Sep 2021 09:59:10 +0200 Subject: [PATCH 1/3] adds branches service to the stash client BranchesService permits retrieving branches and listing them for a given repository. Signed-off-by: Soule BA --- stash/branch.go | 165 +++++++++++++++++++++++++++++++++++++++++ stash/branch_test.go | 155 ++++++++++++++++++++++++++++++++++++++ stash/client.go | 46 ++++++------ stash/groups_test.go | 1 - stash/projects_test.go | 1 - stash/users_test.go | 5 +- 6 files changed, 346 insertions(+), 27 deletions(-) create mode 100644 stash/branch.go create mode 100644 stash/branch_test.go diff --git a/stash/branch.go b/stash/branch.go new file mode 100644 index 00000000..68c0e388 --- /dev/null +++ b/stash/branch.go @@ -0,0 +1,165 @@ +/* +Copyright 2021 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package stash + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" +) + +const ( + branchesURI = "branches" + defaultBranchURI = "default" +) + +// Branches interface defines the methods that can be used to +// retrieve branches of a repository. +type Branches interface { + List(ctx context.Context, projectKey, repositorySlug string, opts *PagingOptions) (*BranchList, error) + Get(ctx context.Context, projectKey, repositorySlug, branchID string) (*Branch, error) + Default(ctx context.Context, projectKey, repositorySlug string) (*Branch, error) +} + +// BranchesService is a client for communicating with stash branches endpoint +// bitbucket-server API docs: https://docs.atlassian.com/bitbucket-server/rest/5.16.0/bitbucket-rest.html +type BranchesService service + +// Branch represents a branch of a repository. +type Branch struct { + // Session is the session object for the branch. + Session `json:"sessionInfo,omitempty"` + // DisplayID is the branch name e.g. main. + DisplayID string `json:"displayId,omitempty"` + // ID is the branch reference e.g. refs/heads/main. + ID string `json:"id,omitempty"` + // IsDefault is true if this is the default branch. + IsDefault bool `json:"isDefault,omitempty"` + // LatestChangeset is the latest changeset on this branch. + LatestChangeset string `json:"latestChangeset,omitempty"` + // LatestCommit is the latest commit on this branch. + LatestCommit string `json:"latestCommit,omitempty"` + // Type is the type of branch. + Type string `json:"type,omitempty"` +} + +// BranchList is a list of branches. +type BranchList struct { + // Paging is the paging information. + Paging + // Branches is the list of branches. + Branches []*Branch `json:"values,omitempty"` +} + +// GetBranches returns the list of branches. +func (b *BranchList) GetBranches() []*Branch { + return b.Branches +} + +// List returns the list of branches. +// Paging is optional and is enabled by providing a PagingOptions struct. +// A pointer to a BranchList struct is returned to retrieve the next page of results. +// List uses the endpoint "GET /rest/api/1.0/projects/{projectKey}/repos/{repositorySlug}/branches". +// https://docs.atlassian.com/bitbucket-server/rest/5.16.0/bitbucket-rest.html +func (s *BranchesService) List(ctx context.Context, projectKey, repositorySlug string, opts *PagingOptions) (*BranchList, error) { + query := addPaging(url.Values{}, opts) + req, err := s.Client.NewRequest(ctx, http.MethodGet, newURI(projectsURI, projectKey, RepositoriesURI, repositorySlug, branchesURI), WithQuery(query)) + if err != nil { + return nil, fmt.Errorf("list branches request creation failed: %w", err) + } + res, resp, err := s.Client.Do(req) + if err != nil { + return nil, fmt.Errorf("list branches failed: %w", err) + } + + if resp != nil && resp.StatusCode == http.StatusNotFound { + return nil, ErrNotFound + } + + b := &BranchList{} + + if err := json.Unmarshal(res, b); err != nil { + return nil, fmt.Errorf("list branches for repository failed, unable to unmarshall repository json: %w", err) + } + + for _, branches := range b.GetBranches() { + branches.Session.set(resp) + } + + return b, nil +} + +// Get retrieves a stash branch given it's ID i.e a git reference. +// Get uses the endpoint +// "GET /rest/api/1.0/projects/{projectKey}/repos/{repositorySlug}/branches?base&details&filterText&orderBy". +// https://docs.atlassian.com/bitbucket-server/rest/5.16.0/bitbucket-rest.html +func (s *BranchesService) Get(ctx context.Context, projectKey, repositorySlug, branchID string) (*Branch, error) { + query := url.Values{ + "filterText": []string{branchID}, + } + + req, err := s.Client.NewRequest(ctx, http.MethodGet, newURI(projectsURI, projectKey, RepositoriesURI, repositorySlug, branchesURI), WithQuery(query)) + if err != nil { + return nil, fmt.Errorf("get branch request creation failed: %w", err) + } + res, resp, err := s.Client.Do(req) + if err != nil { + return nil, fmt.Errorf("get branch failed: %w", err) + } + + if resp != nil && resp.StatusCode == http.StatusNotFound { + return nil, ErrNotFound + } + + b := &Branch{} + if err := json.Unmarshal(res, b); err != nil { + return nil, fmt.Errorf("get branch for repository failed, unable to unmarshall repository json: %w", err) + } + + b.Session.set(resp) + return b, nil + +} + +// Default retrieves the default branch of a repository. +// Default uses the endpoint "GET /rest/api/1.0/projects/{projectKey}/repos/{repositorySlug}/branches/default". +// https://docs.atlassian.com/bitbucket-server/rest/5.16.0/bitbucket-rest.html +func (s *BranchesService) Default(ctx context.Context, projectKey, repositorySlug string) (*Branch, error) { + req, err := s.Client.NewRequest(ctx, http.MethodGet, newURI(projectsURI, projectKey, RepositoriesURI, repositorySlug, branchesURI, defaultBranchURI)) + if err != nil { + return nil, fmt.Errorf("get branch request creation failed: %w", err) + } + res, resp, err := s.Client.Do(req) + if err != nil { + return nil, fmt.Errorf("get branch failed: %w", err) + } + + if resp != nil && resp.StatusCode == http.StatusNotFound { + return nil, ErrNotFound + } + + b := &Branch{} + if err := json.Unmarshal(res, b); err != nil { + return nil, fmt.Errorf("list branches for repository failed, unable to unmarshall repository json: %w", err) + } + + b.Session.set(resp) + + return b, nil +} diff --git a/stash/branch_test.go b/stash/branch_test.go new file mode 100644 index 00000000..10a99fd3 --- /dev/null +++ b/stash/branch_test.go @@ -0,0 +1,155 @@ +/* +Copyright 2021 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package stash + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestGetBranch(t *testing.T) { + tests := []struct { + name string + branchID string + }{ + { + name: "test branch does not exist", + branchID: "features", + }, + { + name: "test main branch", + branchID: "refs/heads/main", + }, + } + + validBranchID := []string{"refs/heads/main"} + + mux, client := setup(t) + + path := fmt.Sprintf("%s/%s/prj1/%s/repo1/%s", stashURIprefix, projectsURI, RepositoriesURI, branchesURI) + mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { + for _, substr := range validBranchID { + if r.URL.Query().Get("filterText") == substr { + w.WriteHeader(http.StatusOK) + u := &Branch{ + ID: "refs/heads/main", + DisplayID: "main", + } + json.NewEncoder(w).Encode(u) + return + } + } + + http.Error(w, "The specified branch does not exist", http.StatusNotFound) + + return + + }) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + b, err := client.Branches.Get(ctx, "prj1", "repo1", tt.branchID) + if err != nil { + if err != ErrNotFound { + t.Fatalf("Branches.Get returned error: %v", err) + } + return + } + + if b.ID != tt.branchID { + t.Errorf("Branches.Get returned branch:\n%s, want:\n%s", b.ID, tt.branchID) + } + + }) + } +} + +func TestListBranches(t *testing.T) { + bIDs := []*Branch{ + {ID: "refs/heads/main"}, {ID: "refs/heads/release"}, {ID: "refs/heads/feature"}, {ID: "refs/heads/hotfix"}} + + mux, client := setup(t) + + path := fmt.Sprintf("%s/%s/prj1/%s/repo1/%s", stashURIprefix, projectsURI, RepositoriesURI, branchesURI) + mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + b := struct { + Branches []*Branch `json:"values"` + }{[]*Branch{ + bIDs[0], + bIDs[1], + bIDs[2], + bIDs[3], + }} + json.NewEncoder(w).Encode(b) + return + + }) + ctx := context.Background() + list, err := client.Branches.List(ctx, "prj1", "repo1", nil) + if err != nil { + t.Fatalf("Branches.List returned error: %v", err) + } + + if diff := cmp.Diff(bIDs, list.Branches); diff != "" { + t.Errorf("Branches.List returned diff (want -> got):\n%s", diff) + } + +} + +func TestDefaultBranch(t *testing.T) { + d := struct { + ID string `json:"id"` + DisplayID string `json:"displayId"` + }{ + "refs/heads/main", + "main", + } + + mux, client := setup(t) + + path := fmt.Sprintf("%s/%s/prj1/%s/repo1/%s/%s", stashURIprefix, projectsURI, RepositoriesURI, branchesURI, defaultBranchURI) + mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + u := &Branch{ + ID: d.ID, + DisplayID: d.DisplayID, + } + json.NewEncoder(w).Encode(u) + + return + }) + + ctx := context.Background() + b, err := client.Branches.Default(ctx, "prj1", "repo1") + if err != nil { + if err != ErrNotFound { + t.Fatalf("Branches.Default returned error: %v", err) + } + return + } + + if b.ID != d.ID { + t.Errorf("Branches.Default returned branch:\n%s, want:\n %s", b.ID, d.ID) + } +} diff --git a/stash/client.go b/stash/client.go index 1e2629f2..e6f3b266 100644 --- a/stash/client.go +++ b/stash/client.go @@ -107,6 +107,7 @@ type Client struct { Projects Projects Git Git Repositories Repositories + Branches Branches } // RateLimiter is the interface that wraps the basic Wait method. @@ -202,6 +203,7 @@ func NewClient(httpClient *http.Client, host string, header *http.Header, logger c.Projects = &ProjectsService{Client: c} c.Git = &GitService{Client: c} c.Repositories = &RepositoriesService{Client: c} + c.Branches = &BranchesService{Client: c} return c, nil } @@ -335,42 +337,42 @@ func (c *Client) configureLimiter() error { return nil } -// RequestOptions defines the optional parameters for the request. -type RequestOptions struct { - // Body is the request body. - Body io.Reader - // Header is the request header. - Header http.Header - // Query is the request query. - Query url.Values +// requestOptions defines the optional parameters for the request. +type requestOptions struct { + // body is the request body. + body io.Reader + // header is the request header. + header http.Header + // query is the request query. + query url.Values } // RequestOptionFunc is a function that set request options. -type RequestOptionFunc func(*RequestOptions) +type RequestOptionFunc func(*requestOptions) // WithQuery adds the query parameters to the request. func WithQuery(query url.Values) RequestOptionFunc { - return func(r *RequestOptions) { + return func(r *requestOptions) { if query != nil { - r.Query = query + r.query = query } } } // WithBody adds the body to the request. func WithBody(body io.Reader) RequestOptionFunc { - return func(r *RequestOptions) { + return func(r *requestOptions) { if body != nil { - r.Body = body + r.body = body } } } // WithHeader adds the headers to the request. func WithHeader(header http.Header) RequestOptionFunc { - return func(r *RequestOptions) { + return func(r *requestOptions) { if header != nil { - r.Header = header + r.header = header } } } @@ -395,18 +397,18 @@ func (c *Client) NewRequest(ctx context.Context, method string, path string, opt method = http.MethodGet } - r := RequestOptions{} + r := requestOptions{} for _, opt := range opts { opt(&r) } - if r.Query == nil { - r.Query = url.Values{} + if r.query == nil { + r.query = url.Values{} } - u.RawQuery = r.Query.Encode() + u.RawQuery = r.query.Encode() - req, err := http.NewRequest(method, u.String(), r.Body) + req, err := http.NewRequest(method, u.String(), r.body) if err != nil { return req, fmt.Errorf("failed create request for %s %s, %w", method, u.String(), err) } @@ -421,8 +423,8 @@ func (c *Client) NewRequest(ctx context.Context, method string, path string, opt } } - if r.Header != nil { - for k, v := range r.Header { + if r.header != nil { + for k, v := range r.header { for _, s := range v { req.Header.Add(k, s) } diff --git a/stash/groups_test.go b/stash/groups_test.go index c4d8e7b7..ae4b1ad0 100644 --- a/stash/groups_test.go +++ b/stash/groups_test.go @@ -31,7 +31,6 @@ func TestGetGroup(t *testing.T) { tests := []struct { name string groupName string - output string }{ { name: "test group does not exist", diff --git a/stash/projects_test.go b/stash/projects_test.go index a9beb788..f9d13411 100644 --- a/stash/projects_test.go +++ b/stash/projects_test.go @@ -31,7 +31,6 @@ func TestGetProject(t *testing.T) { tests := []struct { name string projectName string - output string }{ { name: "test project does not exist", diff --git a/stash/users_test.go b/stash/users_test.go index 753a8d4b..051d5899 100644 --- a/stash/users_test.go +++ b/stash/users_test.go @@ -54,9 +54,8 @@ func setup(t *testing.T) (*http.ServeMux, *Client) { func TestGetUser(t *testing.T) { tests := []struct { - name string - slug string - output string + name string + slug string }{ { name: "test user does not exist", From 424663989cf7dc7c52a62d0caa2f74fb786e7ea8 Mon Sep 17 00:00:00 2001 From: Soule BA Date: Wed, 22 Sep 2021 14:59:51 +0200 Subject: [PATCH 2/3] adds commits service to the stash client CommitsService permits retrieving commits and listing them for a given repository. Signed-off-by: Soule BA --- stash/client.go | 2 + stash/commits.go | 141 ++++++++++++++++++++++++++++++++++++++++++ stash/commits_test.go | 125 +++++++++++++++++++++++++++++++++++++ 3 files changed, 268 insertions(+) create mode 100644 stash/commits.go create mode 100644 stash/commits_test.go diff --git a/stash/client.go b/stash/client.go index e6f3b266..68431529 100644 --- a/stash/client.go +++ b/stash/client.go @@ -108,6 +108,7 @@ type Client struct { Git Git Repositories Repositories Branches Branches + Commits Commits } // RateLimiter is the interface that wraps the basic Wait method. @@ -204,6 +205,7 @@ func NewClient(httpClient *http.Client, host string, header *http.Header, logger c.Git = &GitService{Client: c} c.Repositories = &RepositoriesService{Client: c} c.Branches = &BranchesService{Client: c} + c.Commits = &CommitsService{Client: c} return c, nil } diff --git a/stash/commits.go b/stash/commits.go new file mode 100644 index 00000000..48b836cb --- /dev/null +++ b/stash/commits.go @@ -0,0 +1,141 @@ +/* +Copyright 2021 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package stash + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" +) + +const ( + commitsURI = "commits" +) + +// Commits interface defines the methods that can be used to +// retrieve commits of a repository. +type Commits interface { + List(ctx context.Context, projectKey, repositorySlug string, opts *PagingOptions) (*CommitList, error) + Get(ctx context.Context, projectKey, repositorySlug, commitID string) (*CommitObject, error) +} + +// CommitsService is a client for communicating with stash commits endpoint +// bitbucket-server API docs: https://docs.atlassian.com/bitbucket-server/rest/5.16.0/bitbucket-rest.html +type CommitsService service + +// CommitObject represents a commit in stash +type CommitObject struct { + // Session is the session object for the branch. + Session `json:"sessionInfo,omitempty"` + // Author is the author of the commit. + Author User `json:"author,omitempty"` + // AuthorTimestamp is the timestamp of the author of the commit. + AuthorTimestamp int64 `json:"authorTimestamp,omitempty"` + // Committer is the committer of the commit. + Committer User `json:"committer,omitempty"` + // CommitterTimestamp is the timestamp of the committer of the commit. + CommitterTimestamp int64 `json:"committerTimestamp,omitempty"` + // DisplayID is the display ID of the commit. + DisplayID string `json:"displayId,omitempty"` + // ID is the ID of the commit i.e the SHA1. + ID string `json:"id,omitempty"` + // Message is the message of the commit. + Message string `json:"message,omitempty"` + // Parents is the list of parents of the commit. + Parents []*Parent `json:"parents,omitempty"` +} + +// Parent represents a parent of a commit. +type Parent struct { + // DisplayID is the display ID of the commit. + DisplayID string `json:"displayId,omitempty"` + // ID is the ID of the commit i.e the SHA1. + ID string `json:"id,omitempty"` +} + +// CommitList represents a list of commits in stash +type CommitList struct { + // Paging is the paging information. + Paging + // Commits is the list of commits. + Commits []*CommitObject `json:"values,omitempty"` +} + +// GetCommits returns the list of commits +func (c *CommitList) GetCommits() []*CommitObject { + return c.Commits +} + +// List returns the list of commits. +// Paging is optional and is enabled by providing a PagingOptions struct. +// A pointer to a CommitList struct is returned to retrieve the next page of results. +// List uses the endpoint "GET /rest/api/1.0/projects/{projectKey}/repos/{repositorySlug}/commits". +// https://docs.atlassian.com/bitbucket-server/rest/5.16.0/bitbucket-rest.html +func (s *CommitsService) List(ctx context.Context, projectKey, repositorySlug string, opts *PagingOptions) (*CommitList, error) { + query := addPaging(url.Values{}, opts) + req, err := s.Client.NewRequest(ctx, http.MethodGet, newURI(projectsURI, projectKey, RepositoriesURI, repositorySlug, commitsURI), WithQuery(query)) + if err != nil { + return nil, fmt.Errorf("list commits request creation failed: %w", err) + } + res, resp, err := s.Client.Do(req) + if err != nil { + return nil, fmt.Errorf("list commits failed: %w", err) + } + + if resp != nil && resp.StatusCode == http.StatusNotFound { + return nil, ErrNotFound + } + + c := &CommitList{} + if err := json.Unmarshal(res, c); err != nil { + return nil, fmt.Errorf("list commits for repository failed, unable to unmarshall repository json: %w", err) + } + + for _, commit := range c.GetCommits() { + commit.Session.set(resp) + } + return c, nil +} + +// Get retrieves a stash commit given it's ID i.e a SHA1. +// Get uses the endpoint "GET /rest/api/1.0/projects/{projectKey}/repos/{repositorySlug}/commits/{commitID}". +// https://docs.atlassian.com/bitbucket-server/rest/5.16.0/bitbucket-rest.html +func (s *CommitsService) Get(ctx context.Context, projectKey, repositorySlug, commitID string) (*CommitObject, error) { + req, err := s.Client.NewRequest(ctx, http.MethodGet, newURI(projectsURI, projectKey, RepositoriesURI, repositorySlug, commitsURI, commitID)) + if err != nil { + return nil, fmt.Errorf("get commit request creation failed: %w", err) + } + res, resp, err := s.Client.Do(req) + if err != nil { + return nil, fmt.Errorf("get commit failed: %w", err) + } + + if resp != nil && resp.StatusCode == http.StatusNotFound { + return nil, ErrNotFound + } + + c := &CommitObject{} + if err := json.Unmarshal(res, c); err != nil { + return nil, fmt.Errorf("get commit failed, unable to unmarshall json: %w", err) + } + + c.Session.set(resp) + + return c, nil +} diff --git a/stash/commits_test.go b/stash/commits_test.go new file mode 100644 index 00000000..1fada27f --- /dev/null +++ b/stash/commits_test.go @@ -0,0 +1,125 @@ +/* +Copyright 2021 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package stash + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "path" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestGetCommit(t *testing.T) { + tests := []struct { + name string + commitID string + }{ + { + name: "test a commit", + commitID: "abcdef0123abcdef4567abcdef8987abcdef6543", + }, + { + name: "test commit does not exist", + commitID: "*°0#13jbkjfbvsqbùbjùrdfbgzeo'àtu)éuçt&-y", + }, + } + + validCommitID := []string{"abcdef0123abcdef4567abcdef8987abcdef6543"} + + mux, client := setup(t) + + fmt.Println("commit") + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := fmt.Sprintf("%s/%s/prj1/%s/repo1/%s/%s", stashURIprefix, projectsURI, RepositoriesURI, commitsURI, tt.commitID) + mux.HandleFunc(p, func(w http.ResponseWriter, r *http.Request) { + for _, substr := range validCommitID { + if path.Base(r.URL.String()) == substr { + w.WriteHeader(http.StatusOK) + c := &CommitObject{ + ID: substr, + DisplayID: substr[:10], + } + + json.NewEncoder(w).Encode(c) + return + } + } + + http.Error(w, "The specified commit does not exist", http.StatusNotFound) + + return + + }) + + ctx := context.Background() + c, err := client.Commits.Get(ctx, "prj1", "repo1", tt.commitID) + if err != nil { + if err != ErrNotFound { + t.Fatalf("Commits.Get returned error: %v", err) + } + return + } + + if c.ID != tt.commitID { + t.Fatalf("Commits.Get returned commit %s, want %s", c.ID, tt.commitID) + } + + }) + } +} + +func TestListCommits(t *testing.T) { + cIDs := []*CommitObject{ + {ID: "abcdef0123abcdef4567abcdef8987abcdef6543"}, + {ID: "aerfdef09893abcdef4567abcdef898abcdef652"}, + {ID: "abcdef3456abcdef4567abcdef8987abcdef6657"}, + {ID: "abcdef9876abcdef4567abcdef8987abcdef4357"}} + + mux, client := setup(t) + + path := fmt.Sprintf("%s/%s/prj1/%s/repo1/%s", stashURIprefix, projectsURI, RepositoriesURI, commitsURI) + mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + b := struct { + Commits []*CommitObject `json:"values"` + }{[]*CommitObject{ + cIDs[0], + cIDs[1], + cIDs[2], + cIDs[3], + }} + json.NewEncoder(w).Encode(b) + return + + }) + ctx := context.Background() + list, err := client.Commits.List(ctx, "prj1", "repo1", nil) + if err != nil { + t.Fatalf("Commits.List returned error: %v", err) + } + + if diff := cmp.Diff(cIDs, list.Commits); diff != "" { + t.Errorf("Commits.List returned diff (want -> got):\n%s", diff) + } + +} From b0bfb8d465ce9ba737dcc7491c28533752ac57e7 Mon Sep 17 00:00:00 2001 From: Soule BA Date: Wed, 22 Sep 2021 15:57:30 +0200 Subject: [PATCH 3/3] adds pull requests service to the stash client PullRequestsService permits retrieving pull requests and listing them for a given repository. Signed-off-by: Soule BA --- stash/client.go | 2 + stash/commits_test.go | 2 - stash/pull_requests.go | 315 +++++++++++++++++++++++ stash/pull_requests_test.go | 492 ++++++++++++++++++++++++++++++++++++ stash/repositories.go | 8 +- 5 files changed, 813 insertions(+), 6 deletions(-) create mode 100644 stash/pull_requests.go create mode 100644 stash/pull_requests_test.go diff --git a/stash/client.go b/stash/client.go index 68431529..35816bf9 100644 --- a/stash/client.go +++ b/stash/client.go @@ -109,6 +109,7 @@ type Client struct { Repositories Repositories Branches Branches Commits Commits + PullRequests PullRequests } // RateLimiter is the interface that wraps the basic Wait method. @@ -206,6 +207,7 @@ func NewClient(httpClient *http.Client, host string, header *http.Header, logger c.Repositories = &RepositoriesService{Client: c} c.Branches = &BranchesService{Client: c} c.Commits = &CommitsService{Client: c} + c.PullRequests = &PullRequestsService{Client: c} return c, nil } diff --git a/stash/commits_test.go b/stash/commits_test.go index 1fada27f..73590717 100644 --- a/stash/commits_test.go +++ b/stash/commits_test.go @@ -46,8 +46,6 @@ func TestGetCommit(t *testing.T) { mux, client := setup(t) - fmt.Println("commit") - for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { p := fmt.Sprintf("%s/%s/prj1/%s/repo1/%s/%s", stashURIprefix, projectsURI, RepositoriesURI, commitsURI, tt.commitID) diff --git a/stash/pull_requests.go b/stash/pull_requests.go new file mode 100644 index 00000000..ee0248dc --- /dev/null +++ b/stash/pull_requests.go @@ -0,0 +1,315 @@ +/* +Copyright 2021 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package stash + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" + "strconv" +) + +const ( + pullRequestsURI = "pull-requests" +) + +// PullRequests interface defines the methods that can be used to +// retrieve pull requests of a repository. +type PullRequests interface { + Get(ctx context.Context, projectKey, repositorySlug string, prID int) (*PullRequest, error) + List(ctx context.Context, projectKey, repositorySlug string, opts *PagingOptions) (*PullRequestList, error) + Create(ctx context.Context, projectKey, repositorySlug string, pr *CreatePullRequest) (*PullRequest, error) + Update(ctx context.Context, projectKey, repositorySlug string, pr *PullRequest) (*PullRequest, error) + Delete(ctx context.Context, projectKey, repositorySlug string, IDVersion IDVersion) error +} + +// PullRequestsService is a client for communicating with stash pull requests endpoint +// bitbucket-server API docs: https://docs.atlassian.com/bitbucket-server/rest/5.16.0/bitbucket-rest.html +type PullRequestsService service + +// Participant is a participant of a pull request +type Participant struct { + // Approved indicates if the participant has approved the pull request + Approved bool `json:"approved,omitempty"` + // Role indicates the role of the participant + Role string `json:"role,omitempty"` + // Status indicates the status of the participant + Status string `json:"status,omitempty"` + // User is the participant + User `json:"user,omitempty"` +} + +// Ref represents a git reference +type Ref struct { + // DisplayID is the reference name + DisplayID string `json:"displayId,omitempty"` + // ID is the reference id i.e a git reference + ID string `json:"id,omitempty"` + // LatestCommit is the latest commit of the reference + LatestCommit string `json:"latestCommit,omitempty"` + // Repository is the repository of the reference + Repository `json:"repository,omitempty"` + // Type is the type of the reference + Type string `json:"type,omitempty"` +} + +// CreatePullRequest creates a pull request from +// a source branch or tag to a target branch. +type CreatePullRequest struct { + // Closed indicates if the pull request is closed + Closed bool `json:"closed,omitempty"` + // Description is the description of the pull request + Description string `json:"description,omitempty"` + // FromRef is the source branch or tag + FromRef Ref `json:"fromRef,omitempty"` + // Locked indicates if the pull request is locked + Locked bool `json:"locked,omitempty"` + // Open indicates if the pull request is open + Open bool `json:"open,omitempty"` + // State is the state of the pull request + State string `json:"state,omitempty"` + // Title is the title of the pull request + Title string `json:"title,omitempty"` + // ToRef is the target branch + ToRef Ref `json:"toRef,omitempty"` + // Reviewers is the list of reviewers + Reviewers []User `json:"reviewers,omitempty"` +} + +// IDVersion is a pull request id and version +type IDVersion struct { + // ID is the id of the pull request + ID int `json:"id"` + // Version is the version of the pull request + Version int `json:"version"` +} + +// PullRequest is a pull request +type PullRequest struct { + // Session is the session of the pull request + Session `json:"sessionInfo,omitempty"` + // Author is the author of the pull request + Author Participant `json:"author,omitempty"` + // Closed indicates if the pull request is closed + Closed bool `json:"closed,omitempty"` + // CreatedDate is the creation date of the pull request + CreatedDate int64 `json:"createdDate,omitempty"` + // Description is the description of the pull request + Description string `json:"description,omitempty"` + // FromRef is the source branch or tag + FromRef Ref `json:"fromRef,omitempty"` + IDVersion + // Links is a set of hyperlinks that link to other related resources. + Links `json:"links,omitempty"` + // Locked indicates if the pull request is locked + Locked bool `json:"locked,omitempty"` + // Open indicates if the pull request is open + Open bool `json:"open,omitempty"` + // Participants are the participants of the pull request + Participants []Participant `json:"participants,omitempty"` + // Properties are the properties of the pull request + Properties Properties `json:"properties,omitempty"` + // Reviewers are the reviewers of the pull request + Reviewers []Participant `json:"reviewers,omitempty"` + // State is the state of the pull request + State string `json:"state,omitempty"` + // Title is the title of the pull request + Title string `json:"title,omitempty"` + // ToRef is the target branch + ToRef Ref `json:"toRef,omitempty"` + // UpdatedDate is the update date of the pull request + UpdatedDate int64 `json:"updatedDate,omitempty"` +} + +// Properties are the properties of a pull request +type Properties struct { + // MergeResult is the merge result of the pull request + MergeResult MergeResult `json:"mergeResult,omitempty"` + // OpenTaskCount is the number of open tasks + OpenTaskCount float64 `json:"openTaskCount,omitempty"` + // ResolvedTaskCount is the number of resolved tasks + ResolvedTaskCount float64 `json:"resolvedTaskCount,omitempty"` +} + +// MergeResult is the merge result of a pull request +type MergeResult struct { + // Current is the current merge result + Current bool `json:"current,omitempty"` + // Outcome is the outcome of the merge + Outcome string `json:"outcome,omitempty"` +} + +// PullRequestList is a list of pull requests +type PullRequestList struct { + // Paging is the paging information + Paging + // PullRequests are the pull requests + PullRequests []*PullRequest `json:"values,omitempty"` +} + +// GetPullRequests returns a list of pull requests +func (p *PullRequestList) GetPullRequests() []*PullRequest { + return p.PullRequests +} + +// List returns the list of pull requests. +// Paging is optional and is enabled by providing a PagingOptions struct. +// A pointer to a PullRequestsList struct is returned to retrieve the next page of results. +// List uses the endpoint "GET /rest/api/1.0/projects/{projectKey}/repos/{repositorySlug}/pull-requests". +// https://docs.atlassian.com/bitbucket-server/rest/5.16.0/bitbucket-rest.html +func (s *PullRequestsService) List(ctx context.Context, projectKey, repositorySlug string, opts *PagingOptions) (*PullRequestList, error) { + query := addPaging(url.Values{}, opts) + req, err := s.Client.NewRequest(ctx, http.MethodGet, newURI(projectsURI, projectKey, RepositoriesURI, repositorySlug, pullRequestsURI), WithQuery(query)) + if err != nil { + return nil, fmt.Errorf("list pull requests request creation failed: %w", err) + } + res, resp, err := s.Client.Do(req) + if err != nil { + return nil, fmt.Errorf("list pull requests failed: %w", err) + } + + if resp != nil && resp.StatusCode == http.StatusNotFound { + return nil, ErrNotFound + } + + p := &PullRequestList{} + if err := json.Unmarshal(res, p); err != nil { + return nil, fmt.Errorf("list pull requests failed, unable to unmarshal repository list json: %w", err) + } + + for _, r := range p.GetPullRequests() { + r.Session.set(resp) + } + + return p, nil +} + +// Get retrieves a pull request given it's ID. +// Get uses the endpoint "GET /rest/api/1.0/projects/{projectKey}/repos/{repositorySlug}/pull-requests/{pullRequestId}". +// https://docs.atlassian.com/bitbucket-server/rest/5.16.0/bitbucket-rest.html +func (s *PullRequestsService) Get(ctx context.Context, projectKey, repositorySlug string, prID int) (*PullRequest, error) { + req, err := s.Client.NewRequest(ctx, http.MethodGet, newURI(projectsURI, projectKey, RepositoriesURI, repositorySlug, pullRequestsURI, strconv.Itoa(prID))) + if err != nil { + return nil, fmt.Errorf("get pull request request creation failed: %w", err) + } + res, resp, err := s.Client.Do(req) + if err != nil { + return nil, fmt.Errorf("get pull request failed: %w", err) + } + + if resp != nil && resp.StatusCode == http.StatusNotFound { + return nil, ErrNotFound + } + + p := &PullRequest{} + if err := json.Unmarshal(res, p); err != nil { + return nil, fmt.Errorf("get pull request failed, unable to unmarshal repository list json: %w", err) + } + + p.Session.set(resp) + + return p, nil +} + +// Create creates a pull request. +// Create uses the endpoint "POST /rest/api/1.0/projects/{projectKey}/repos/{repositorySlug}/pull-requests". +func (s *PullRequestsService) Create(ctx context.Context, projectKey, repositorySlug string, pr *CreatePullRequest) (*PullRequest, error) { + header := http.Header{"Content-Type": []string{"application/json"}} + body, err := marshallBody(pr) + if err != nil { + return nil, fmt.Errorf("failed to marshall pull request: %v", err) + } + req, err := s.Client.NewRequest(ctx, http.MethodPost, newURI(projectsURI, projectKey, RepositoriesURI, repositorySlug, pullRequestsURI), WithBody(body), WithHeader(header)) + if err != nil { + return nil, fmt.Errorf("create pull request request creation failed: %w", err) + } + res, resp, err := s.Client.Do(req) + if err != nil { + return nil, fmt.Errorf("create pull request failed: %w", err) + } + + p := &PullRequest{} + if err := json.Unmarshal(res, p); err != nil { + return nil, fmt.Errorf("create pull request failed, unable to unmarshal repository list json: %w", err) + } + + p.Session.set(resp) + + return p, nil +} + +// Update updates the pull request with the given ID +// Update uses the endpoint "PUT /rest/api/1.0/projects/{projectKey}/repos/{repositorySlug}/pull-requests/{pullRequestId}". +func (s *PullRequestsService) Update(ctx context.Context, projectKey, repositorySlug string, pr *PullRequest) (*PullRequest, error) { + header := http.Header{"Content-Type": []string{"application/json"}} + body, err := marshallBody(pr) + if err != nil { + return nil, fmt.Errorf("failed to marshall pull request: %v", err) + } + req, err := s.Client.NewRequest(ctx, http.MethodPut, newURI(projectsURI, projectKey, RepositoriesURI, repositorySlug, pullRequestsURI, strconv.Itoa(pr.ID)), WithBody(body), WithHeader(header)) + if err != nil { + return nil, fmt.Errorf("update pull request creation failed: %w", err) + } + res, resp, err := s.Client.Do(req) + if err != nil { + return nil, fmt.Errorf("update pull failed: %w", err) + } + + if resp != nil && resp.StatusCode == http.StatusNotFound { + return nil, ErrNotFound + } + + p := &PullRequest{} + if err := json.Unmarshal(res, p); err != nil { + return nil, fmt.Errorf("create pull request failed, unable to unmarshal repository list json: %w", err) + } + + p.Session.set(resp) + + return p, nil +} + +// Delete deletes the pull request with the given ID +// Delete uses the endpoint "DELETE /rest/api/1.0/projects/{projectKey}/repos/{repositorySlug}/pull-requests/{pullRequestId}". +// To call this resource, users must: +// - be the pull request author, if the system is configured to allow authors to delete their own pull requests (this is the default) OR +// - have repository administrator permission for the repository the pull request is targeting +// A body containing the ID and version of the pull request must be provided with this request. +// { +// "id": 1, +// "version": 1 +// } +func (s *PullRequestsService) Delete(ctx context.Context, projectKey, repositorySlug string, IDVersion IDVersion) error { + header := http.Header{"Content-Type": []string{"application/json"}} + body, err := marshallBody(IDVersion.Version) + req, err := s.Client.NewRequest(ctx, http.MethodDelete, newURI(projectsURI, projectKey, RepositoriesURI, repositorySlug, pullRequestsURI, strconv.Itoa(IDVersion.ID)), WithBody(body), WithHeader(header)) + if err != nil { + return fmt.Errorf("delete pull request frequest creation failed: %w", err) + } + _, resp, err := s.Client.Do(req) + if err != nil { + return fmt.Errorf("delete pull request for repository failed: %w", err) + } + + if resp != nil && resp.StatusCode == http.StatusNotFound { + return ErrNotFound + } + + return nil +} diff --git a/stash/pull_requests_test.go b/stash/pull_requests_test.go new file mode 100644 index 00000000..db622668 --- /dev/null +++ b/stash/pull_requests_test.go @@ -0,0 +1,492 @@ +/* +Copyright 2021 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package stash + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "path" + "strconv" + "strings" + "testing" + "time" + + "github.com/google/go-cmp/cmp" +) + +func TestGetPR(t *testing.T) { + tests := []struct { + name string + prID int + }{ + { + name: "test a pull request", + prID: 101, + }, + { + name: "test pull request does not exist", + prID: -1, + }, + } + + validPRID := []string{"101"} + + mux, client := setup(t) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := fmt.Sprintf("%s/%s/prj1/%s/repo1/%s/%s", stashURIprefix, projectsURI, RepositoriesURI, pullRequestsURI, strconv.Itoa(tt.prID)) + mux.HandleFunc(p, func(w http.ResponseWriter, r *http.Request) { + for _, substr := range validPRID { + if path.Base(r.URL.String()) == substr { + w.WriteHeader(http.StatusOK) + c := &PullRequest{ + IDVersion: IDVersion{ + ID: 101, + }, + Author: Participant{ + User: User{ + Name: "test", + }, + Role: "AUTHOR", + }, + FromRef: Ref{ + ID: "refs/heads/feature-ABC-123", + }, + ToRef: Ref{ + ID: "refs/heads/main", + }, + Properties: Properties{ + MergeResult: MergeResult{ + Current: true, + Outcome: "SUCCESS", + }, + }, + } + + json.NewEncoder(w).Encode(c) + return + } + } + + http.Error(w, "The specified pr does not exist", http.StatusNotFound) + + return + + }) + + ctx := context.Background() + c, err := client.PullRequests.Get(ctx, "prj1", "repo1", tt.prID) + if err != nil { + if err != ErrNotFound { + t.Fatalf("PullRequest.Get returned error: %v", err) + } + return + } + + if c.ID != tt.prID { + t.Fatalf("PullRequest.Get returned:\n%d, want:\n%d", c.ID, tt.prID) + } + + }) + } +} + +func TestListPRs(t *testing.T) { + prIDs := []*PullRequest{ + {IDVersion: IDVersion{ID: 101}}, + {IDVersion: IDVersion{ID: 102}}, + {IDVersion: IDVersion{ID: 103}}, + {IDVersion: IDVersion{ID: 104}}, + } + + mux, client := setup(t) + + path := fmt.Sprintf("%s/%s/prj1/%s/repo1/%s", stashURIprefix, projectsURI, RepositoriesURI, pullRequestsURI) + mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + b := struct { + PRs []*PullRequest `json:"values"` + }{[]*PullRequest{ + prIDs[0], + prIDs[1], + prIDs[2], + prIDs[3], + }} + json.NewEncoder(w).Encode(b) + return + + }) + ctx := context.Background() + list, err := client.PullRequests.List(ctx, "prj1", "repo1", nil) + if err != nil { + t.Fatalf("PullRequests.List returned error: %v", err) + } + + if diff := cmp.Diff(prIDs, list.PullRequests); diff != "" { + t.Errorf("PullRequests.List returned diff (want -> got):\n%s", diff) + } + +} + +func TestCreatePR(t *testing.T) { + tests := []struct { + name string + pr CreatePullRequest + }{ + { + name: "pr 1", + pr: CreatePullRequest{ + Title: "PR service", + Description: "A service that manages prs.", + State: "OPEN", + Open: true, + Closed: false, + FromRef: Ref{ + ID: "refs/heads/feature-pr", + Repository: Repository{ + Slug: "my-repo", + Project: Project{ + Key: "prj", + }, + }, + }, + ToRef: Ref{ + ID: "refs/heads/main", + Repository: Repository{ + Slug: "my-repo", + Project: Project{ + Key: "prj", + }, + }, + }, + Locked: false, + Reviewers: []User{ + { + Name: "charlie", + }, + }, + }, + }, + { + name: "pr 2", + pr: CreatePullRequest{ + Title: "PR service", + Description: "A service that manages prs.", + State: "OPEN", + Open: true, + Closed: false, + FromRef: Ref{ + ID: "refs/heads/feature-pr", + Repository: Repository{ + Slug: "my-repo", + Project: Project{ + Key: "prj", + }, + }, + }, + ToRef: Ref{ + ID: "refs/heads/main", + Repository: Repository{ + Slug: "my-repo", + Project: Project{ + Key: "prj", + }, + }, + }, + Locked: false, + Reviewers: []User{ + { + Name: "charlie", + }, + }, + }, + }, + { + name: "invalid pr", + pr: CreatePullRequest{ + Title: "Invalid PR", + Description: "This PR is invalid because the ToRef and FromRef are the same branches.", + State: "OPEN", + Open: true, + Closed: false, + FromRef: Ref{ + ID: "refs/heads/main", + Repository: Repository{ + Slug: "my-repo", + Project: Project{ + Key: "prj", + }, + }, + }, + ToRef: Ref{ + ID: "refs/heads/main", + Repository: Repository{ + Slug: "my-repo", + Project: Project{ + Key: "prj", + }, + }, + }, + Locked: false, + Reviewers: []User{ + { + Name: "charlie", + }, + }, + }, + }, + } + + mux, client := setup(t) + + path := fmt.Sprintf("%s/%s/prj/%s/my-repo/%s", stashURIprefix, projectsURI, RepositoriesURI, pullRequestsURI) + mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodPost { + req := &CreatePullRequest{} + json.NewDecoder(r.Body).Decode(req) + if req.FromRef.ID != req.ToRef.ID { + w.WriteHeader(http.StatusOK) + r := &PullRequest{ + IDVersion: IDVersion{ + ID: 1, + Version: 1, + }, + CreatedDate: time.Now().Unix(), + UpdatedDate: time.Now().Unix(), + Title: req.Title, + Description: req.Description, + State: req.State, + Open: req.Open, + Closed: req.Closed, + FromRef: req.FromRef, + ToRef: req.ToRef, + Locked: req.Locked, + Author: Participant{ + User: User{ + Name: "Rob", + }, + }, + Reviewers: []Participant{ + { + User: req.Reviewers[0], + Role: "REVIEWER", + Approved: false, + Status: "UNAPPROVED", + }, + }, + } + json.NewEncoder(w).Encode(r) + return + } + } + + http.Error(w, "The pull request was not created due to same specified branches.", http.StatusConflict) + + return + + }) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + p, err := client.PullRequests.Create(ctx, "prj", "my-repo", &tt.pr) + if err != nil { + if !strings.Contains(err.Error(), "409 Conflict") { + t.Fatalf("PullRequest.Create returned error: %v", err) + } + return + } + + if (p.Title != tt.pr.Title) || (p.FromRef.ID != tt.pr.FromRef.ID) || (p.ToRef.ID != tt.pr.ToRef.ID) { + t.Errorf("PullRequest.Create returned:\n%v, want:\n%v", p, tt.pr) + } + }) + } +} + +func TestUpdatePR(t *testing.T) { + tests := []struct { + name string + pr PullRequest + }{ + { + name: "update description", + pr: PullRequest{ + IDVersion: IDVersion{ + ID: 1, + Version: 2, + }, + Title: "PR service", + Description: "A service that manages prs. It supports get, list, create, update and deletes ops.", + ToRef: Ref{ + ID: "refs/heads/main", + Repository: Repository{ + Slug: "my-repo", + Project: Project{ + Key: "prj", + }, + }, + }, + }, + }, + { + name: "update destination branch", + pr: PullRequest{ + IDVersion: IDVersion{ + ID: 1, + Version: 3, + }, + Title: "PR service", + Description: "A service that manages prs. It supports get, list, create, update and deletes ops.", + ToRef: Ref{ + ID: "refs/heads/develop", + Repository: Repository{ + Slug: "my-repo", + Project: Project{ + Key: "prj", + }, + }, + }, + }, + }, + } + + mux, client := setup(t) + + path := fmt.Sprintf("%s/%s/prj/%s/my-repo/%s/%s", stashURIprefix, projectsURI, RepositoriesURI, pullRequestsURI, strconv.Itoa(tests[0].pr.ID)) + mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodPut { + req := &PullRequest{} + json.NewDecoder(r.Body).Decode(req) + r := &PullRequest{ + IDVersion: IDVersion{ + ID: req.ID, + Version: req.Version, + }, + CreatedDate: time.Now().Unix(), + UpdatedDate: time.Now().Unix(), + Title: req.Title, + Description: req.Description, + FromRef: Ref{ + ID: "refs/heads/feature-pr", + Repository: Repository{ + Slug: "my-repo", + Project: Project{ + Key: "prj", + }, + }, + }, + ToRef: req.ToRef, + Author: Participant{ + User: User{ + Name: "Rob", + }, + }, + Reviewers: []Participant{ + { + User: User{ + Name: "Charlie", + }, + Role: "REVIEWER", + Approved: false, + Status: "UNAPPROVED", + }, + }, + } + + w.WriteHeader(http.StatusOK) + + json.NewEncoder(w).Encode(r) + return + } + + http.Error(w, "The repository was not updated due to a validation error", http.StatusBadRequest) + + return + + }) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + p, err := client.PullRequests.Update(ctx, "prj", "my-repo", &tt.pr) + if err != nil { + t.Fatalf("PullRequests.Update returned error: %v", err) + } + + if (p.Title != tt.pr.Title) || (p.Description != tt.pr.Description) || (p.ToRef.ID != tt.pr.ToRef.ID) { + t.Errorf("PullRequests.Update returned:\n%v, want:\n%v", p, tt.pr) + } + }) + } +} + +func TestDeletePR(t *testing.T) { + tests := []struct { + name string + idVersion IDVersion + }{ + { + name: "test PR does not exist", + idVersion: IDVersion{ + ID: -1, + Version: 2, + }, + }, + { + name: "test a PR", + idVersion: IDVersion{ + ID: 1, + Version: 1, + }, + }, + } + + mux, client := setup(t) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := fmt.Sprintf("%s/%s/prj/%s/my-repo/%s/%s", stashURIprefix, projectsURI, RepositoriesURI, pullRequestsURI, strconv.Itoa(tt.idVersion.ID)) + mux.HandleFunc(p, func(w http.ResponseWriter, r *http.Request) { + id, err := strconv.ParseInt(path.Base(r.URL.String()), 10, 64) + if err != nil { + t.Fatalf("strconv.ParseInt returned error: %v", err) + } + if id >= 0 { + w.WriteHeader(http.StatusNoContent) + w.Write([]byte("204 - OK!")) + return + } + + http.Error(w, "The specified repository or pull request does not exist.", http.StatusNotFound) + + return + + }) + ctx := context.Background() + err := client.PullRequests.Delete(ctx, "prj", "my-repo", tt.idVersion) + if err != nil { + if err != ErrNotFound { + t.Errorf("PullRequests.Delete returned error: %v", err) + } + } + }) + } +} diff --git a/stash/repositories.go b/stash/repositories.go index d75a1b32..bb7a5c6a 100644 --- a/stash/repositories.go +++ b/stash/repositories.go @@ -171,9 +171,9 @@ func (s *RepositoriesService) Get(ctx context.Context, projectKey, repoSlug stri return repo, nil } -func marshallRepository(repo *Repository) (io.ReadCloser, error) { +func marshallBody(b interface{}) (io.ReadCloser, error) { var body io.ReadCloser - jsonBody, err := json.Marshal(repo) + jsonBody, err := json.Marshal(b) if err != nil { return nil, err } @@ -187,7 +187,7 @@ func marshallRepository(repo *Repository) (io.ReadCloser, error) { // The authenticated user must have PROJECT_ADMIN permission for the context project to call this resource. func (s *RepositoriesService) Create(ctx context.Context, repository *Repository) (*Repository, error) { header := http.Header{"Content-Type": []string{"application/json"}} - body, err := marshallRepository(repository) + body, err := marshallBody(repository) if err != nil { return nil, fmt.Errorf("failed to marshall repository: %v", err) } @@ -217,7 +217,7 @@ func (s *RepositoriesService) Create(ctx context.Context, repository *Repository // Update uses the endpoint "PUT /rest/api/1.0/projects/{projectKey}/repos/{repositorySlug}". func (s *RepositoriesService) Update(ctx context.Context, projectKey, repositorySlug string, repository *Repository) (*Repository, error) { header := http.Header{"Content-Type": []string{"application/json"}} - body, err := marshallRepository(repository) + body, err := marshallBody(repository) if err != nil { return nil, fmt.Errorf("failed to marshall repository: %v", err) }