Skip to content

Commit

Permalink
jira: general client improvements
Browse files Browse the repository at this point in the history
This PR fixes the fakejira `CloneIssue` and `CreateIssue` functions
along with adding unit tests for them, adds more extensive issue
function errors for the fakeclient, and modifies the
`DeleteRemoteLinkViaURL` function to return a `bool` indicating whether
a link with the provided URL was found and removed (no remote link with
the provided URL is not an error).

These improvements will be used in a new openshift external prow plugin
called the `jira-lifecycle` plugin, which will replicate the `bugzilla`
plugins functionality, but with the Jira service instead of bugzilla.
  • Loading branch information
AlexNPavel committed Jun 13, 2022
1 parent e739eaf commit 4534311
Show file tree
Hide file tree
Showing 5 changed files with 318 additions and 90 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ require (
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.7.0
github.com/tektoncd/pipeline v0.14.1-0.20200710073957-5eeb17f81999
github.com/trivago/tgo v1.0.7
go.uber.org/zap v1.19.0
go4.org v0.0.0-20201209231011-d4a079459e60
gocloud.dev v0.19.0
Expand Down Expand Up @@ -190,7 +191,6 @@ require (
github.com/prometheus/procfs v0.6.0 // indirect
github.com/sergi/go-diff v1.1.0 // indirect
github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f // indirect
github.com/trivago/tgo v1.0.7 // indirect
github.com/xanzy/ssh-agent v0.3.0 // indirect
go.opencensus.io v0.23.0 // indirect
go.uber.org/atomic v1.7.0 // indirect
Expand Down
170 changes: 94 additions & 76 deletions prow/jira/fakejira/fake.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ package fakejira
import (
"context"
"encoding/json"
"errors"
"fmt"
"strconv"
"strings"
Expand All @@ -31,14 +30,17 @@ import (
)

type FakeClient struct {
Issues []*jira.Issue
ExistingLinks map[string][]jira.RemoteLink
NewLinks []jira.RemoteLink
IssueLinks []*jira.IssueLink
GetIssueError error
Transitions []jira.Transition
Users []*jira.User
SearchResponses map[SearchRequest]SearchResponse
Issues []*jira.Issue
ExistingLinks map[string][]jira.RemoteLink
NewLinks []jira.RemoteLink
RemovedLinks []jira.RemoteLink
IssueLinks []*jira.IssueLink
GetIssueError map[string]error
CreateIssueError map[string]error
UpdateIssueError map[string]error
Transitions []jira.Transition
Users []*jira.User
SearchResponses map[SearchRequest]SearchResponse
}

func (f *FakeClient) ListProjects() (*jira.ProjectList, error) {
Expand All @@ -47,7 +49,9 @@ func (f *FakeClient) ListProjects() (*jira.ProjectList, error) {

func (f *FakeClient) GetIssue(id string) (*jira.Issue, error) {
if f.GetIssueError != nil {
return nil, f.GetIssueError
if err, ok := f.GetIssueError[id]; ok {
return nil, err
}
}
for _, existingIssue := range f.Issues {
if existingIssue.ID == id || existingIssue.Key == id {
Expand All @@ -58,7 +62,11 @@ func (f *FakeClient) GetIssue(id string) (*jira.Issue, error) {
}

func (f *FakeClient) GetRemoteLinks(id string) ([]jira.RemoteLink, error) {
return f.ExistingLinks[id], nil
issue, err := f.GetIssue(id)
if err != nil {
return nil, fmt.Errorf("Failed to get issue when chekcing from remote links: %+v", err)
}
return append(f.ExistingLinks[issue.ID], f.ExistingLinks[issue.Key]...), nil
}

func (f *FakeClient) AddRemoteLink(id string, link *jira.RemoteLink) (*jira.RemoteLink, error) {
Expand Down Expand Up @@ -107,6 +115,13 @@ func (f *FakeClient) AddComment(issueID string, comment *jira.Comment) (*jira.Co
if err != nil {
return nil, fmt.Errorf("Issue %s not found: %v", issueID, err)
}
// make sure the fields exist
if issue.Fields == nil {
issue.Fields = &jira.IssueFields{}
}
if issue.Fields.Comments == nil {
issue.Fields.Comments = &jira.Comments{}
}
issue.Fields.Comments.Comments = append(issue.Fields.Comments.Comments, comment)
return comment, nil
}
Expand All @@ -116,44 +131,73 @@ func (f *FakeClient) CreateIssueLink(link *jira.IssueLink) error {
if err != nil {
return fmt.Errorf("failed to get outward link issue: %v", err)
}
outward.Fields.IssueLinks = append(outward.Fields.IssueLinks, link)
// links in an issue struct do not include the short definition of the the issue they are part of.
// This behavior is used by the external jira-lifecycle hook plugin to identify which direction a clone happened,
// so it needs to be replicated in the fake client
linkForOutward := *link
linkForOutward.OutwardIssue = nil
outward.Fields.IssueLinks = append(outward.Fields.IssueLinks, &linkForOutward)
inward, err := f.GetIssue(link.InwardIssue.ID)
if err != nil {
return fmt.Errorf("failed to get inward link issue: %v", err)
}
inward.Fields.IssueLinks = append(inward.Fields.IssueLinks, link)
linkForInward := *link
linkForInward.InwardIssue = nil
inward.Fields.IssueLinks = append(inward.Fields.IssueLinks, &linkForInward)
f.IssueLinks = append(f.IssueLinks, link)
return nil
}

func (f *FakeClient) CloneIssue(issue *jira.Issue) (*jira.Issue, error) {
// create deep copy of parent so we can modify key and id for child
data, err := json.Marshal(issue)
if err != nil {
return nil, err
}
issueCopy := &jira.Issue{}
err = json.Unmarshal(data, issueCopy)
if err != nil {
return nil, err
}
// set ID and Key to unused id and key
f.updateIssueIDAndKey(issueCopy)
// run generic cloning function
return jiraclient.CloneIssue(f, issueCopy)
return jiraclient.CloneIssue(f, issue)
}

func (f *FakeClient) CreateIssue(issue *jira.Issue) (*jira.Issue, error) {
// check that there are no ID collisions
for _, currIssue := range f.Issues {
if currIssue.ID == issue.ID {
return nil, fmt.Errorf("Issue ID %s already exists", issue.ID)
if f.CreateIssueError != nil {
if err, ok := f.CreateIssueError[issue.Key]; ok {
return nil, err
}
if currIssue.Key == issue.Key {
return nil, fmt.Errorf("Issue key %s already exists", issue.Key)
}
if issue.Fields == nil || issue.Fields.Project.Name == "" {
return nil, fmt.Errorf("issue.fields.project must be set to create new issue")
}
issueCreationErrors := jiraclient.CreateIssueError{}
issueCreationErrors.Errors = make(map[string]string)
// simulate unsettable fields
if issue.Fields.Comments != nil {
issueCreationErrors.Errors["comment"] = "this field cannot be set"
}
if issue.Fields.Status != nil {
issueCreationErrors.Errors["status"] = "this field cannot be set"
}
if len(issueCreationErrors.Errors) != 0 {
data, err := json.Marshal(issueCreationErrors)
if err != nil {
return nil, err
}
return nil, &jiraclient.JiraError{StatusCode: 400, Body: string(data)}
}
// find highest issueID and make new issue one higher
highestID := 0
// find highest ID for issues in the same project to make new key one higher
highestKeyID := 0
keyPrefix := issue.Fields.Project.Name + "-"
for _, issue := range f.Issues {
// all IDs are ints, but represented as strings...
intID, _ := strconv.Atoi(issue.ID)
if intID > highestID {
highestID = intID
}
if strings.HasPrefix(issue.Key, keyPrefix) {
stringID := strings.TrimPrefix(issue.Key, keyPrefix)
intID, _ := strconv.Atoi(stringID)
if intID > highestKeyID {
highestKeyID = intID
}
}
}
f.updateIssueIDAndKey(issue)
issue.ID = strconv.Itoa(highestID + 1)
issue.Key = fmt.Sprintf("%s%d", keyPrefix, highestKeyID+1)
f.Issues = append(f.Issues, issue)
return issue, nil
}
Expand Down Expand Up @@ -211,53 +255,22 @@ func (f *FakeClient) DeleteLink(id string) error {
func (f *FakeClient) DeleteRemoteLink(issueID string, linkID int) error {
for index, remoteLink := range f.ExistingLinks[issueID] {
if remoteLink.ID == linkID {
f.ExistingLinks[issueID] = append(f.ExistingLinks[issueID][:index], f.ExistingLinks[issueID][index+1:]...)
f.RemovedLinks = append(f.RemovedLinks, remoteLink)
if len(f.ExistingLinks[issueID]) == index+1 {
f.ExistingLinks[issueID] = f.ExistingLinks[issueID][:index]
} else {
f.ExistingLinks[issueID] = append(f.ExistingLinks[issueID][:index], f.ExistingLinks[issueID][index+1:]...)
}
return nil
}
}
return fmt.Errorf("failed to find link id %d in issue %s", linkID, issueID)
}

func (f *FakeClient) DeleteRemoteLinkViaURL(issueID, url string) error {
func (f *FakeClient) DeleteRemoteLinkViaURL(issueID, url string) (bool, error) {
return jiraclient.DeleteRemoteLinkViaURL(f, issueID, url)
}

func (f *FakeClient) updateIssueIDAndKey(newIssue *jira.Issue) error {
// ensure that a key is set
if newIssue.Key == "" {
return errors.New("Issue key must be set")
}
// ensure key format is correct
splitKey := strings.Split(newIssue.Key, "-")
if len(splitKey) != 2 {
return fmt.Errorf("Invalid issue key: %s", newIssue.Key)
}

// find highest issueID and make new issue one higher
highestID := -1
for _, issue := range f.Issues {
// all IDs are ints, but represented as strings...
intID, _ := strconv.Atoi(issue.ID)
if intID > highestID {
highestID = intID
}
}
newIssue.ID = strconv.Itoa(highestID + 1)
// if there are issues in the same project, make new issue one above those
highestKeyID := 0
keyPrefix := fmt.Sprintf("%s-", splitKey[0])
for _, issue := range f.Issues {
if strings.HasPrefix(issue.Key, keyPrefix) {
stringID := strings.TrimPrefix(issue.Key, keyPrefix)
intID, _ := strconv.Atoi(stringID)
if intID > highestKeyID {
highestKeyID = intID
}
}
}
newIssue.Key = fmt.Sprintf("%s%d", keyPrefix, highestKeyID)
return nil
}

func (f *FakeClient) GetTransitions(issueID string) ([]jira.Transition, error) {
return f.Transitions, nil
}
Expand Down Expand Up @@ -311,6 +324,11 @@ func (f *FakeClient) GetIssueTargetVersion(issue *jira.Issue) (*[]*jira.Version,
}

func (f *FakeClient) UpdateIssue(issue *jira.Issue) (*jira.Issue, error) {
if f.UpdateIssueError != nil {
if err, ok := f.UpdateIssueError[issue.ID]; ok {
return nil, err
}
}
retrievedIssue, err := f.GetIssue(issue.ID)
if err != nil {
return nil, fmt.Errorf("unable to find issue to update: %v", err)
Expand Down Expand Up @@ -339,11 +357,11 @@ func (f *FakeClient) UpdateIssue(issue *jira.Issue) (*jira.Issue, error) {
if err != nil {
return nil, fmt.Errorf("error converting updated issue to json: %v", err)
}
var newFields *jira.IssueFields
if err := json.Unmarshal(updatedIssueBytes, newFields); err != nil {
var newFields jira.IssueFields
if err := json.Unmarshal(updatedIssueBytes, &newFields); err != nil {
return nil, fmt.Errorf("failed converting updated issue to struct: %v", err)
}
retrievedIssue.Fields = newFields
retrievedIssue.Fields = &newFields
return retrievedIssue, nil
}

Expand Down
Loading

0 comments on commit 4534311

Please sign in to comment.