github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/pkg/jira/fakejira/fake.go (about)

     1  /*
     2  Copyright 2022 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package fakejira
    18  
    19  import (
    20  	"context"
    21  	"encoding/json"
    22  	"fmt"
    23  	"strconv"
    24  	"strings"
    25  
    26  	"github.com/andygrunwald/go-jira"
    27  	"github.com/sirupsen/logrus"
    28  
    29  	jiraclient "sigs.k8s.io/prow/pkg/jira"
    30  )
    31  
    32  type FakeClient struct {
    33  	Issues           []*jira.Issue
    34  	ExistingLinks    map[string][]jira.RemoteLink
    35  	NewLinks         []jira.RemoteLink
    36  	RemovedLinks     []jira.RemoteLink
    37  	IssueLinks       []*jira.IssueLink
    38  	GetIssueError    map[string]error
    39  	CreateIssueError map[string]error
    40  	UpdateIssueError map[string]error
    41  	Transitions      []jira.Transition
    42  	Users            []*jira.User
    43  	SearchResponses  map[SearchRequest]SearchResponse
    44  	ProjectVersions  map[string][]*jira.Version
    45  }
    46  
    47  func (f *FakeClient) ListProjects() (*jira.ProjectList, error) {
    48  	return nil, nil
    49  }
    50  
    51  func (f *FakeClient) GetIssue(id string) (*jira.Issue, error) {
    52  	if f.GetIssueError != nil {
    53  		if err, ok := f.GetIssueError[id]; ok {
    54  			return nil, err
    55  		}
    56  	}
    57  	for _, existingIssue := range f.Issues {
    58  		if existingIssue.ID == id || existingIssue.Key == id {
    59  			return existingIssue, nil
    60  		}
    61  	}
    62  	return nil, jiraclient.NewNotFoundError(fmt.Errorf("No issue %s found", id))
    63  }
    64  
    65  func (f *FakeClient) GetRemoteLinks(id string) ([]jira.RemoteLink, error) {
    66  	issue, err := f.GetIssue(id)
    67  	if err != nil {
    68  		return nil, fmt.Errorf("Failed to get issue when chekcing from remote links: %+v", err)
    69  	}
    70  	return append(f.ExistingLinks[issue.ID], f.ExistingLinks[issue.Key]...), nil
    71  }
    72  
    73  func (f *FakeClient) AddRemoteLink(id string, link *jira.RemoteLink) (*jira.RemoteLink, error) {
    74  	if _, err := f.GetIssue(id); err != nil {
    75  		return nil, err
    76  	}
    77  	f.NewLinks = append(f.NewLinks, *link)
    78  	return link, nil
    79  }
    80  
    81  func (f *FakeClient) JiraClient() *jira.Client {
    82  	panic("not implemented")
    83  }
    84  
    85  const FakeJiraUrl = "https://my-jira.com"
    86  
    87  func (f *FakeClient) JiraURL() string {
    88  	return FakeJiraUrl
    89  }
    90  
    91  func (f *FakeClient) UpdateRemoteLink(id string, link *jira.RemoteLink) error {
    92  	if _, err := f.GetIssue(id); err != nil {
    93  		return err
    94  	}
    95  	if _, found := f.ExistingLinks[id]; !found {
    96  		return jiraclient.NewNotFoundError(fmt.Errorf("Link for issue %s not found", id))
    97  	}
    98  	f.NewLinks = append(f.NewLinks, *link)
    99  	return nil
   100  }
   101  
   102  func (f *FakeClient) Used() bool {
   103  	return true
   104  }
   105  
   106  func (f *FakeClient) WithFields(fields logrus.Fields) jiraclient.Client {
   107  	return f
   108  }
   109  
   110  func (f *FakeClient) ForPlugin(string) jiraclient.Client {
   111  	return f
   112  }
   113  
   114  func (f *FakeClient) AddComment(issueID string, comment *jira.Comment) (*jira.Comment, error) {
   115  	issue, err := f.GetIssue(issueID)
   116  	if err != nil {
   117  		return nil, fmt.Errorf("Issue %s not found: %v", issueID, err)
   118  	}
   119  	// make sure the fields exist
   120  	if issue.Fields == nil {
   121  		issue.Fields = &jira.IssueFields{}
   122  	}
   123  	if issue.Fields.Comments == nil {
   124  		issue.Fields.Comments = &jira.Comments{}
   125  	}
   126  	issue.Fields.Comments.Comments = append(issue.Fields.Comments.Comments, comment)
   127  	return comment, nil
   128  }
   129  
   130  func (f *FakeClient) CreateIssueLink(link *jira.IssueLink) error {
   131  	outward, err := f.GetIssue(link.OutwardIssue.ID)
   132  	if err != nil {
   133  		return fmt.Errorf("failed to get outward link issue: %v", err)
   134  	}
   135  	// when part of an issue struct, the issue link type does not include the
   136  	// short definition of the issue it is in
   137  	linkForOutward := *link
   138  	linkForOutward.OutwardIssue = nil
   139  	outward.Fields.IssueLinks = append(outward.Fields.IssueLinks, &linkForOutward)
   140  	inward, err := f.GetIssue(link.InwardIssue.ID)
   141  	if err != nil {
   142  		return fmt.Errorf("failed to get inward link issue: %v", err)
   143  	}
   144  	linkForInward := *link
   145  	linkForInward.InwardIssue = nil
   146  	inward.Fields.IssueLinks = append(inward.Fields.IssueLinks, &linkForInward)
   147  	f.IssueLinks = append(f.IssueLinks, link)
   148  	return nil
   149  }
   150  
   151  func (f *FakeClient) CloneIssue(issue *jira.Issue) (*jira.Issue, error) {
   152  	return jiraclient.CloneIssue(f, issue)
   153  }
   154  
   155  func (f *FakeClient) CreateIssue(issue *jira.Issue) (*jira.Issue, error) {
   156  	if f.CreateIssueError != nil {
   157  		if err, ok := f.CreateIssueError[issue.Key]; ok {
   158  			return nil, err
   159  		}
   160  	}
   161  	if issue.Fields == nil {
   162  		issue.Fields = &jira.IssueFields{}
   163  	}
   164  	// find highest issueID and make new issue one higher
   165  	highestID := 0
   166  	// find highest ID for issues in the same project to make new key one higher
   167  	highestKeyID := 0
   168  	keyPrefix := issue.Fields.Project.Name + "-"
   169  	for _, issue := range f.Issues {
   170  		// all IDs are ints, but represented as strings...
   171  		intID, _ := strconv.Atoi(issue.ID)
   172  		if intID > highestID {
   173  			highestID = intID
   174  		}
   175  		if strings.HasPrefix(issue.Key, keyPrefix) {
   176  			stringID := strings.TrimPrefix(issue.Key, keyPrefix)
   177  			intID, _ := strconv.Atoi(stringID)
   178  			if intID > highestKeyID {
   179  				highestKeyID = intID
   180  			}
   181  		}
   182  	}
   183  	issue.ID = strconv.Itoa(highestID + 1)
   184  	issue.Key = fmt.Sprintf("%s%d", keyPrefix, highestKeyID+1)
   185  	f.Issues = append(f.Issues, issue)
   186  	return issue, nil
   187  }
   188  
   189  func (f *FakeClient) DeleteLink(id string) error {
   190  	// find link
   191  	var link *jira.IssueLink
   192  	var linkIndex int
   193  	for index, currLink := range f.IssueLinks {
   194  		if currLink.ID == id {
   195  			link = currLink
   196  			linkIndex = index
   197  			break
   198  		}
   199  	}
   200  	if link == nil {
   201  		return fmt.Errorf("no issue link with id %s found", id)
   202  	}
   203  	outward, err := f.GetIssue(link.OutwardIssue.ID)
   204  	if err != nil {
   205  		return fmt.Errorf("failed to get outward link issue: %v", err)
   206  	}
   207  	outwardIssueIndex := -1
   208  	for index, currLink := range outward.Fields.IssueLinks {
   209  		if currLink.ID == link.ID {
   210  			outwardIssueIndex = index
   211  			break
   212  		}
   213  	}
   214  	// should we error if link doesn't exist in one of the linked issues?
   215  	if outwardIssueIndex != -1 {
   216  		outward.Fields.IssueLinks = append(outward.Fields.IssueLinks[:outwardIssueIndex], outward.Fields.IssueLinks[outwardIssueIndex+1:]...)
   217  	}
   218  	inward, err := f.GetIssue(link.InwardIssue.ID)
   219  	if err != nil {
   220  		return fmt.Errorf("failed to get inward link issue: %v", err)
   221  	}
   222  	inwardIssueIndex := -1
   223  	for index, currLink := range inward.Fields.IssueLinks {
   224  		if currLink.ID == link.ID {
   225  			inwardIssueIndex = index
   226  			break
   227  		}
   228  	}
   229  	// should we error if link doesn't exist in one of the linked issues?
   230  	if inwardIssueIndex != -1 {
   231  		inward.Fields.IssueLinks = append(inward.Fields.IssueLinks[:inwardIssueIndex], inward.Fields.IssueLinks[inwardIssueIndex+1:]...)
   232  	}
   233  	f.IssueLinks = append(f.IssueLinks[:linkIndex], f.IssueLinks[linkIndex+1:]...)
   234  	return nil
   235  }
   236  
   237  // TODO: improve handling of remote links in fake client; having a separate NewLinks struct
   238  // that contains links not in existingLinks may limit some aspects of testing
   239  func (f *FakeClient) DeleteRemoteLink(issueID string, linkID int) error {
   240  	for index, remoteLink := range f.ExistingLinks[issueID] {
   241  		if remoteLink.ID == linkID {
   242  			f.RemovedLinks = append(f.RemovedLinks, remoteLink)
   243  			if len(f.ExistingLinks[issueID]) == index+1 {
   244  				f.ExistingLinks[issueID] = f.ExistingLinks[issueID][:index]
   245  			} else {
   246  				f.ExistingLinks[issueID] = append(f.ExistingLinks[issueID][:index], f.ExistingLinks[issueID][index+1:]...)
   247  			}
   248  			return nil
   249  		}
   250  	}
   251  	return fmt.Errorf("failed to find link id %d in issue %s", linkID, issueID)
   252  }
   253  
   254  func (f *FakeClient) DeleteRemoteLinkViaURL(issueID, url string) (bool, error) {
   255  	return jiraclient.DeleteRemoteLinkViaURL(f, issueID, url)
   256  }
   257  
   258  func (f *FakeClient) GetTransitions(issueID string) ([]jira.Transition, error) {
   259  	return f.Transitions, nil
   260  }
   261  
   262  func (f *FakeClient) DoTransition(issueID, transitionID string) error {
   263  	issue, err := f.GetIssue(issueID)
   264  	if err != nil {
   265  		return fmt.Errorf("could not find issue: %v", err)
   266  	}
   267  	var correctTransition *jira.Transition
   268  	for index, transition := range f.Transitions {
   269  		if transition.ID == transitionID {
   270  			correctTransition = &f.Transitions[index]
   271  			break
   272  		}
   273  	}
   274  	if correctTransition == nil {
   275  		return fmt.Errorf("could not find transition with ID %s", transitionID)
   276  	}
   277  	issue.Fields.Status = &correctTransition.To
   278  	return nil
   279  }
   280  
   281  func (f *FakeClient) FindUser(property string) ([]*jira.User, error) {
   282  	var foundUsers []*jira.User
   283  	for _, user := range f.Users {
   284  		// multiple different fields can be matched with this query
   285  		if strings.Contains(user.AccountID, property) ||
   286  			strings.Contains(user.DisplayName, property) ||
   287  			strings.Contains(user.EmailAddress, property) ||
   288  			strings.Contains(user.Name, property) {
   289  			foundUsers = append(foundUsers, user)
   290  		}
   291  	}
   292  	if len(foundUsers) == 0 {
   293  		return nil, fmt.Errorf("Not users found with property %s", property)
   294  	}
   295  	return foundUsers, nil
   296  }
   297  
   298  func (f *FakeClient) GetIssueSecurityLevel(issue *jira.Issue) (*jiraclient.SecurityLevel, error) {
   299  	return jiraclient.GetIssueSecurityLevel(issue)
   300  }
   301  
   302  func (f *FakeClient) GetIssueQaContact(issue *jira.Issue) (*jira.User, error) {
   303  	return jiraclient.GetIssueQaContact(issue)
   304  }
   305  
   306  func (f *FakeClient) GetIssueTargetVersion(issue *jira.Issue) (*[]*jira.Version, error) {
   307  	return jiraclient.GetIssueTargetVersion(issue)
   308  }
   309  
   310  func (f *FakeClient) UpdateIssue(issue *jira.Issue) (*jira.Issue, error) {
   311  	if f.UpdateIssueError != nil {
   312  		if err, ok := f.UpdateIssueError[issue.Key]; ok {
   313  			return nil, err
   314  		}
   315  	}
   316  	retrievedIssue, err := f.GetIssue(issue.Key)
   317  	if err != nil {
   318  		return nil, fmt.Errorf("unable to find issue to update: %v", err)
   319  	}
   320  	// convert `fields` field of both retrieved and provided issue to interfaces and update the non-nil
   321  	// fields from the provided issue to the retrieved one
   322  	var issueFields, retrievedFields map[string]interface{}
   323  	issueBytes, err := json.Marshal(issue.Fields)
   324  	if err != nil {
   325  		return nil, fmt.Errorf("error converting provided issue to json: %v", err)
   326  	}
   327  	if err := json.Unmarshal(issueBytes, &issueFields); err != nil {
   328  		return nil, fmt.Errorf("failed converting provided issue to map: %v", err)
   329  	}
   330  	retrievedIssueBytes, err := json.Marshal(retrievedIssue.Fields)
   331  	if err != nil {
   332  		return nil, fmt.Errorf("error converting original issue to json: %v", err)
   333  	}
   334  	if err := json.Unmarshal(retrievedIssueBytes, &retrievedFields); err != nil {
   335  		return nil, fmt.Errorf("failed converting original issue to map: %v", err)
   336  	}
   337  	for key, value := range issueFields {
   338  		retrievedFields[key] = value
   339  	}
   340  	updatedIssueBytes, err := json.Marshal(retrievedFields)
   341  	if err != nil {
   342  		return nil, fmt.Errorf("error converting updated issue to json: %v", err)
   343  	}
   344  	var newFields jira.IssueFields
   345  	if err := json.Unmarshal(updatedIssueBytes, &newFields); err != nil {
   346  		return nil, fmt.Errorf("failed converting updated issue to struct: %v", err)
   347  	}
   348  	retrievedIssue.Fields = &newFields
   349  	return retrievedIssue, nil
   350  }
   351  
   352  func (f *FakeClient) UpdateStatus(issueID, statusName string) error {
   353  	return jiraclient.UpdateStatus(f, issueID, statusName)
   354  }
   355  
   356  type SearchRequest struct {
   357  	query   string
   358  	options *jira.SearchOptions
   359  }
   360  
   361  type SearchResponse struct {
   362  	issues   []jira.Issue
   363  	response *jira.Response
   364  	error    error
   365  }
   366  
   367  func (f *FakeClient) SearchWithContext(ctx context.Context, jql string, options *jira.SearchOptions) ([]jira.Issue, *jira.Response, error) {
   368  	resp, expected := f.SearchResponses[SearchRequest{query: jql, options: options}]
   369  	if !expected {
   370  		return nil, nil, fmt.Errorf("the query: %s is not registered", jql)
   371  	}
   372  	return resp.issues, resp.response, resp.error
   373  }
   374  
   375  func (f *FakeClient) GetProjectVersions(project string) ([]*jira.Version, error) {
   376  	if versions, ok := f.ProjectVersions[project]; ok {
   377  		return versions, nil
   378  	}
   379  	return []*jira.Version{}, nil
   380  }