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 }