From 18da9ff46b4544d4d2c156c2d98bef6dfa534f44 Mon Sep 17 00:00:00 2001 From: Soule BA Date: Wed, 22 Sep 2021 15:57:30 +0200 Subject: [PATCH] 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/pull_requests.go | 318 +++++++++++++++++++++++ stash/pull_requests_test.go | 492 ++++++++++++++++++++++++++++++++++++ 3 files changed, 812 insertions(+) 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 13340014..82e95bc5 100644 --- a/stash/client.go +++ b/stash/client.go @@ -111,6 +111,7 @@ type Client struct { Repositories Repositories Branches Branches Commits Commits + PullRequests PullRequests } // RateLimiter is the interface that wraps the basic Wait method. @@ -208,6 +209,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/pull_requests.go b/stash/pull_requests.go new file mode 100644 index 00000000..aa590053 --- /dev/null +++ b/stash/pull_requests.go @@ -0,0 +1,318 @@ +/* +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), *query, nil, nil) + 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) + } + + // As nothing is done with the response body, it is safe to close here + // to avoid leaking connections + defer resp.Body.Close() + 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)), nil, nil, nil) + 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) + } + + // As nothing is done with the response body, it is safe to close here + // to avoid leaking connections + defer resp.Body.Close() + 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"}} + req, err := s.Client.NewRequest(ctx, http.MethodPost, newURI(projectsURI, projectKey, RepositoriesURI, repositorySlug, pullRequestsURI), nil, pr, 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"}} + req, err := s.Client.NewRequest(ctx, http.MethodPut, newURI(projectsURI, projectKey, RepositoriesURI, repositorySlug, pullRequestsURI, strconv.Itoa(pr.ID)), nil, pr, 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) + } + + // As nothing is done with the response body, it is safe to close here + // to avoid leaking connections + defer resp.Body.Close() + 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"}} + req, err := s.Client.NewRequest(ctx, http.MethodDelete, newURI(projectsURI, projectKey, RepositoriesURI, repositorySlug, pullRequestsURI, strconv.Itoa(IDVersion.ID)), nil, IDVersion.Version, 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) + } + + // As nothing is done with the response body, it is safe to close here + // to avoid leaking connections + defer resp.Body.Close() + 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..b724722f --- /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 commit 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: "Commit service", + Description: "A service that manages commits.", + State: "OPEN", + Open: true, + Closed: false, + FromRef: Ref{ + ID: "refs/heads/feature-commit", + 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) + } + } + }) + } +}