github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/pkg/github/fakegithub/fakegithub.go (about) 1 /* 2 Copyright 2016 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 fakegithub 18 19 import ( 20 "context" 21 "errors" 22 "fmt" 23 "regexp" 24 "sort" 25 "strings" 26 "sync" 27 28 githubql "github.com/shurcooL/githubv4" 29 30 "k8s.io/apimachinery/pkg/util/sets" 31 32 "sigs.k8s.io/prow/pkg/github" 33 ) 34 35 const botName = "k8s-ci-robot" 36 37 const ( 38 // Bot is the exported botName 39 Bot = botName 40 // TestRef is the ref returned when calling GetRef 41 TestRef = "abcde" 42 ) 43 44 // FakeClient is like client, but fake. 45 type FakeClient struct { 46 Issues map[int]*github.Issue 47 IssueID int 48 OrgMembers map[string][]string 49 Collaborators []string 50 IssueComments map[int][]github.IssueComment 51 IssueCommentID int 52 PullRequests map[int]*github.PullRequest 53 PullRequestChanges map[int][]github.PullRequestChange 54 PullRequestComments map[int][]github.ReviewComment 55 PullRequestReviewCommentID int 56 PullRequestReviewComments map[int][]github.ReviewComment 57 ReviewID int 58 Reviews map[int][]github.Review 59 CombinedStatuses map[string]*github.CombinedStatus 60 CreatedStatuses map[string][]github.Status 61 IssueEvents map[int][]github.ListedIssueEvent 62 Commits map[string]github.RepositoryCommit 63 64 // All Labels That Exist In The Repo 65 RepoLabelsExisting []string 66 // org/repo#number:label 67 IssueLabelsAdded []string 68 IssueLabelsExisting []string 69 IssueLabelsRemoved []string 70 71 // org/repo#number:body 72 IssueCommentsAdded []string 73 // org/repo#issuecommentid:body 74 IssueCommentsEdited []string 75 // org/repo#issuecommentid 76 IssueCommentsDeleted []string 77 78 // org/repo#number:body 79 PullRequestReviewCommentsAdded []string 80 81 // org/repo#issuecommentid:reaction 82 IssueReactionsAdded []string 83 CommentReactionsAdded []string 84 85 // org/repo#number:assignee 86 AssigneesAdded []string 87 88 // org/repo#number:milestone (represents the milestone for a specific issue) 89 Milestone int 90 MilestoneMap map[string]int 91 92 // list of commits for each PR 93 // org/repo#number:[]commit 94 CommitMap map[string][]github.RepositoryCommit 95 96 // Fake remote git storage. File name are keys 97 // and values map SHA to content 98 RemoteFiles map[string]map[string]string 99 100 // Fake remote git storage. Directory name are keys 101 // and values map SHA to directory content 102 RemoteDirectories map[string]map[string][]github.DirectoryContent 103 104 // A list of refs that got deleted via DeleteRef 105 RefsDeleted []struct{ Org, Repo, Ref string } 106 107 // A map of repo names to projects 108 RepoProjects map[string][]github.Project 109 110 // A map of project name to columns 111 ProjectColumnsMap map[string][]github.ProjectColumn 112 113 // Maps column ID to the list of project cards 114 ColumnCardsMap map[int][]github.ProjectCard 115 116 // Maps project name to maps of column ID to columnName 117 ColumnIDMap map[string]map[int]string 118 119 // The project and column names for an issue or PR 120 Project string 121 Column string 122 OrgRepoIssueLabels map[string][]github.Label 123 OrgProjects map[string][]github.Project 124 125 // Maps org name to the list of hooks 126 OrgHooks map[string][]github.Hook 127 // Maps repo name to the list of hooks 128 RepoHooks map[string][]github.Hook 129 130 // A map of invitation id to user repository invitations 131 UserRepoInvitations map[int]github.UserRepoInvitation 132 // A map of organization invitations by name 133 UserOrgInvitations map[string]github.UserOrgInvitation 134 135 // Error will be returned if set. Currently only implemented for CreateStatus 136 Error error 137 138 // GetRepoError will be returned if set when GetRepo is called 139 GetRepoError error 140 141 // ListIssueCommentsWithContextError will be returned if set when ListIssueCommentsWithContext is called 142 ListIssueCommentsWithContextError error 143 144 // WasLabelAddedByHumanVal determines the return of the method with the same name 145 WasLabelAddedByHumanVal bool 146 147 // lock to be thread safe 148 lock sync.RWMutex 149 150 // Team is a map org->teamSlug->TeamWithMembers 151 Teams map[string]map[string]TeamWithMembers 152 153 // Reviewers Requested 154 ReviewersRequested []string 155 } 156 157 type TeamWithMembers struct { 158 Team github.Team 159 Members sets.Set[string] 160 } 161 162 func (f *FakeClient) BotUser() (*github.UserData, error) { 163 return &github.UserData{Login: botName}, nil 164 } 165 166 func (f *FakeClient) BotUserCheckerWithContext(_ context.Context) (func(candidate string) bool, error) { 167 return f.BotUserChecker() 168 } 169 170 func (f *FakeClient) BotUserChecker() (func(candidate string) bool, error) { 171 return func(candidate string) bool { 172 candidate = strings.TrimSuffix(candidate, "[bot]") 173 return candidate == botName 174 }, nil 175 } 176 177 func NewFakeClient() *FakeClient { 178 return &FakeClient{ 179 Issues: make(map[int]*github.Issue), 180 OrgMembers: make(map[string][]string), 181 IssueComments: make(map[int][]github.IssueComment), 182 PullRequests: make(map[int]*github.PullRequest), 183 PullRequestChanges: make(map[int][]github.PullRequestChange), 184 PullRequestComments: make(map[int][]github.ReviewComment), 185 Reviews: make(map[int][]github.Review), 186 CombinedStatuses: make(map[string]*github.CombinedStatus), 187 CreatedStatuses: make(map[string][]github.Status), 188 IssueEvents: make(map[int][]github.ListedIssueEvent), 189 Commits: make(map[string]github.RepositoryCommit), 190 191 MilestoneMap: make(map[string]int), 192 CommitMap: make(map[string][]github.RepositoryCommit), 193 RemoteFiles: make(map[string]map[string]string), 194 195 RepoProjects: make(map[string][]github.Project), 196 ProjectColumnsMap: make(map[string][]github.ProjectColumn), 197 ColumnCardsMap: make(map[int][]github.ProjectCard), 198 ColumnIDMap: make(map[string]map[int]string), 199 OrgRepoIssueLabels: make(map[string][]github.Label), 200 OrgProjects: make(map[string][]github.Project), 201 OrgHooks: make(map[string][]github.Hook), 202 RepoHooks: make(map[string][]github.Hook), 203 UserRepoInvitations: make(map[int]github.UserRepoInvitation), 204 UserOrgInvitations: make(map[string]github.UserOrgInvitation), 205 } 206 } 207 208 // IsMember returns true if user is in org. 209 func (f *FakeClient) IsMember(org, user string) (bool, error) { 210 f.lock.RLock() 211 defer f.lock.RUnlock() 212 for _, m := range f.OrgMembers[org] { 213 if m == user { 214 return true, nil 215 } 216 } 217 return false, nil 218 } 219 220 func (f *FakeClient) WasLabelAddedByHuman(_, _ string, _ int, _ string) (bool, error) { 221 f.lock.RLock() 222 defer f.lock.RUnlock() 223 return f.WasLabelAddedByHumanVal, nil 224 } 225 226 // ListOpenIssues returns f.issues 227 // To mock a mix of issues and pull requests, see github.Issue.PullRequest 228 func (f *FakeClient) ListOpenIssues(org, repo string) ([]github.Issue, error) { 229 f.lock.RLock() 230 defer f.lock.RUnlock() 231 var issues []github.Issue 232 for _, issue := range f.Issues { 233 issues = append(issues, *issue) 234 } 235 return issues, nil 236 } 237 238 // ListIssueComments returns comments. 239 func (f *FakeClient) ListIssueComments(owner, repo string, number int) ([]github.IssueComment, error) { 240 return f.ListIssueCommentsWithContext(context.Background(), owner, repo, number) 241 } 242 243 func (f *FakeClient) ListIssueCommentsWithContext(ctx context.Context, owner, repo string, number int) ([]github.IssueComment, error) { 244 f.lock.RLock() 245 defer f.lock.RUnlock() 246 if f.ListIssueCommentsWithContextError != nil { 247 return nil, f.ListIssueCommentsWithContextError 248 } 249 return append([]github.IssueComment{}, f.IssueComments[number]...), nil 250 } 251 252 // ListPullRequestComments returns review comments. 253 func (f *FakeClient) ListPullRequestComments(owner, repo string, number int) ([]github.ReviewComment, error) { 254 f.lock.RLock() 255 defer f.lock.RUnlock() 256 return append([]github.ReviewComment{}, f.PullRequestComments[number]...), nil 257 } 258 259 // ListReviews returns reviews. 260 func (f *FakeClient) ListReviews(owner, repo string, number int) ([]github.Review, error) { 261 f.lock.RLock() 262 defer f.lock.RUnlock() 263 return append([]github.Review{}, f.Reviews[number]...), nil 264 } 265 266 // ListIssueEvents returns issue events 267 func (f *FakeClient) ListIssueEvents(owner, repo string, number int) ([]github.ListedIssueEvent, error) { 268 f.lock.RLock() 269 defer f.lock.RUnlock() 270 return append([]github.ListedIssueEvent{}, f.IssueEvents[number]...), nil 271 } 272 273 // CreateComment adds a comment to a PR 274 func (f *FakeClient) CreateComment(owner, repo string, number int, comment string) error { 275 return f.CreateCommentWithContext(context.Background(), owner, repo, number, comment) 276 } 277 278 func (f *FakeClient) CreateCommentWithContext(_ context.Context, owner, repo string, number int, comment string) error { 279 f.lock.Lock() 280 defer f.lock.Unlock() 281 f.IssueCommentID++ 282 f.IssueCommentsAdded = append(f.IssueCommentsAdded, fmt.Sprintf("%s/%s#%d:%s", owner, repo, number, comment)) 283 f.IssueComments[number] = append(f.IssueComments[number], github.IssueComment{ 284 ID: f.IssueCommentID, 285 Body: comment, 286 User: github.User{Login: botName}, 287 }) 288 return nil 289 } 290 291 // EditComment edits a comment. 292 func (f *FakeClient) EditComment(org, repo string, ID int, comment string) error { 293 return f.EditCommentWithContext(context.Background(), org, repo, ID, comment) 294 } 295 296 func (f *FakeClient) EditCommentWithContext(_ context.Context, org, repo string, ID int, comment string) error { 297 f.lock.Lock() 298 defer f.lock.Unlock() 299 f.IssueCommentsEdited = append(f.IssueCommentsEdited, fmt.Sprintf("%s/%s#%d:%s", org, repo, ID, comment)) 300 for _, ics := range f.IssueComments { 301 for _, ic := range ics { 302 if ic.ID == ID { 303 ic.Body = comment 304 } 305 } 306 } 307 return nil 308 } 309 310 // CreateReview adds a review to a PR 311 func (f *FakeClient) CreateReview(org, repo string, number int, r github.DraftReview) error { 312 f.lock.Lock() 313 defer f.lock.Unlock() 314 f.ReviewID++ 315 f.Reviews[number] = append(f.Reviews[number], github.Review{ 316 ID: f.ReviewID, 317 User: github.User{Login: botName}, 318 Body: r.Body, 319 }) 320 return nil 321 } 322 323 // CreateCommentReaction adds emoji to a comment. 324 func (f *FakeClient) CreateCommentReaction(org, repo string, ID int, reaction string) error { 325 f.lock.Lock() 326 defer f.lock.Unlock() 327 f.CommentReactionsAdded = append(f.CommentReactionsAdded, fmt.Sprintf("%s/%s#%d:%s", org, repo, ID, reaction)) 328 return nil 329 } 330 331 // CreateIssueReaction adds an emoji to an issue. 332 func (f *FakeClient) CreateIssueReaction(org, repo string, ID int, reaction string) error { 333 f.lock.Lock() 334 defer f.lock.Unlock() 335 f.IssueReactionsAdded = append(f.IssueReactionsAdded, fmt.Sprintf("%s/%s#%d:%s", org, repo, ID, reaction)) 336 return nil 337 } 338 339 // DeleteComment deletes a comment. 340 func (f *FakeClient) DeleteComment(owner, repo string, ID int) error { 341 return f.DeleteCommentWithContext(context.Background(), owner, repo, ID) 342 } 343 344 func (f *FakeClient) DeleteCommentWithContext(_ context.Context, owner, repo string, ID int) error { 345 f.lock.Lock() 346 defer f.lock.Unlock() 347 f.IssueCommentsDeleted = append(f.IssueCommentsDeleted, fmt.Sprintf("%s/%s#%d", owner, repo, ID)) 348 for num, ics := range f.IssueComments { 349 for i, ic := range ics { 350 if ic.ID == ID { 351 f.IssueComments[num] = append(ics[:i], ics[i+1:]...) 352 return nil 353 } 354 } 355 } 356 return fmt.Errorf("could not find issue comment %d", ID) 357 } 358 359 // DeleteStaleComments deletes comments flagged by isStale. 360 func (f *FakeClient) DeleteStaleComments(org, repo string, number int, comments []github.IssueComment, isStale func(github.IssueComment) bool) error { 361 return f.DeleteStaleCommentsWithContext(context.Background(), org, repo, number, comments, isStale) 362 } 363 364 // DeleteStaleCommentsWithContext deletes comments flagged by isStale with a provided context. 365 func (f *FakeClient) DeleteStaleCommentsWithContext(ctx context.Context, org, repo string, number int, comments []github.IssueComment, isStale func(github.IssueComment) bool) error { 366 if comments == nil { 367 comments, _ = f.ListIssueComments(org, repo, number) 368 } 369 for _, comment := range comments { 370 if isStale(comment) { 371 if err := f.DeleteComment(org, repo, comment.ID); err != nil { 372 return fmt.Errorf("failed to delete stale comment with ID '%d'", comment.ID) 373 } 374 } 375 } 376 return nil 377 } 378 379 // GetPullRequest returns details about the PR. 380 func (f *FakeClient) GetPullRequest(owner, repo string, number int) (*github.PullRequest, error) { 381 f.lock.RLock() 382 defer f.lock.RUnlock() 383 val, exists := f.PullRequests[number] 384 if !exists { 385 return nil, fmt.Errorf("pull request number %d does not exist", number) 386 } 387 return val, nil 388 } 389 390 // EditPullRequest edits the pull request. 391 func (f *FakeClient) EditPullRequest(org, repo string, number int, issue *github.PullRequest) (*github.PullRequest, error) { 392 f.lock.Lock() 393 defer f.lock.Unlock() 394 if _, exists := f.PullRequests[number]; !exists { 395 return nil, fmt.Errorf("issue number %d does not exist", number) 396 } 397 f.PullRequests[number] = issue 398 return issue, nil 399 } 400 401 // GetIssue returns the issue. 402 func (f *FakeClient) GetIssue(owner, repo string, number int) (*github.Issue, error) { 403 f.lock.RLock() 404 defer f.lock.RUnlock() 405 val, exists := f.Issues[number] 406 if !exists { 407 return nil, fmt.Errorf("issue number %d does not exist", number) 408 } 409 return val, nil 410 } 411 412 // EditIssue edits the issue. 413 func (f *FakeClient) EditIssue(org, repo string, number int, issue *github.Issue) (*github.Issue, error) { 414 f.lock.Lock() 415 defer f.lock.Unlock() 416 if _, exists := f.Issues[number]; !exists { 417 return nil, fmt.Errorf("issue number %d does not exist", number) 418 } 419 f.Issues[number] = issue 420 return issue, nil 421 } 422 423 // CreateIssue creates the issue. 424 func (f *FakeClient) CreateIssue(org, repo, title, body string, milestone int, labels, assignees []string) (int, error) { 425 f.lock.Lock() 426 defer f.lock.Unlock() 427 f.IssueID++ 428 if f.Issues == nil { 429 f.Issues = make(map[int]*github.Issue) 430 } 431 var ls []github.Label 432 for _, l := range labels { 433 ls = append(ls, github.Label{Name: l}) 434 } 435 var as []github.User 436 for _, a := range assignees { 437 as = append(as, github.User{Name: a}) 438 } 439 new := &github.Issue{ 440 ID: f.IssueID, 441 Title: title, 442 Body: body, 443 Milestone: github.Milestone{Number: milestone}, 444 Labels: ls, 445 Assignees: as, 446 } 447 f.Issues[f.IssueID] = new 448 f.IssueComments[f.IssueID] = make([]github.IssueComment, 0) 449 return new.ID, nil 450 } 451 452 func (f *FakeClient) CloseIssue(org, repo string, number int) error { 453 f.lock.Lock() 454 defer f.lock.Unlock() 455 456 if _, ok := f.Issues[number]; !ok { 457 return fmt.Errorf("issue number %d does not exist", number) 458 } 459 460 f.Issues[number].State = "closed" 461 f.Issues[number].StateReason = "completed" 462 463 return nil 464 } 465 466 func (f *FakeClient) CloseIssueAsNotPlanned(org, repo string, number int) error { 467 f.lock.Lock() 468 defer f.lock.Unlock() 469 470 if _, ok := f.Issues[number]; !ok { 471 return fmt.Errorf("issue number %d does not exist", number) 472 } 473 474 f.Issues[number].State = "closed" 475 f.Issues[number].StateReason = "not_planned" 476 477 return nil 478 } 479 480 // GetPullRequestChanges returns the file modifications in a PR. 481 func (f *FakeClient) GetPullRequestChanges(org, repo string, number int) ([]github.PullRequestChange, error) { 482 f.lock.RLock() 483 defer f.lock.RUnlock() 484 return f.PullRequestChanges[number], nil 485 } 486 487 // GetRef returns the hash of a ref. 488 func (f *FakeClient) GetRef(owner, repo, ref string) (string, error) { 489 return TestRef, nil 490 } 491 492 // DeleteRef returns an error indicating if deletion of the given ref was successful 493 func (f *FakeClient) DeleteRef(owner, repo, ref string) error { 494 f.lock.Lock() 495 defer f.lock.Unlock() 496 f.RefsDeleted = append(f.RefsDeleted, struct{ Org, Repo, Ref string }{Org: owner, Repo: repo, Ref: ref}) 497 return nil 498 } 499 500 // GetSingleCommit returns a single commit. 501 func (f *FakeClient) GetSingleCommit(org, repo, SHA string) (github.RepositoryCommit, error) { 502 f.lock.RLock() 503 defer f.lock.RUnlock() 504 return f.Commits[SHA], nil 505 } 506 507 // CreateStatus adds a status context to a commit. 508 func (f *FakeClient) CreateStatus(owner, repo, SHA string, s github.Status) error { 509 return f.CreateStatusWithContext(context.Background(), owner, repo, SHA, s) 510 } 511 func (f *FakeClient) CreateStatusWithContext(_ context.Context, owner, repo, SHA string, s github.Status) error { 512 f.lock.Lock() 513 defer f.lock.Unlock() 514 if f.Error != nil { 515 return f.Error 516 } 517 if f.CreatedStatuses == nil { 518 f.CreatedStatuses = make(map[string][]github.Status) 519 } 520 statuses := f.CreatedStatuses[SHA] 521 var updated bool 522 for i := range statuses { 523 if statuses[i].Context == s.Context { 524 statuses[i] = s 525 updated = true 526 } 527 } 528 if !updated { 529 statuses = append(statuses, s) 530 } 531 f.CreatedStatuses[SHA] = statuses 532 f.CombinedStatuses[SHA] = &github.CombinedStatus{ 533 SHA: SHA, 534 Statuses: statuses, 535 } 536 return nil 537 } 538 539 // ListStatuses returns individual status contexts on a commit. 540 func (f *FakeClient) ListStatuses(org, repo, ref string) ([]github.Status, error) { 541 f.lock.RLock() 542 defer f.lock.RUnlock() 543 return f.CreatedStatuses[ref], nil 544 } 545 546 // GetCombinedStatus returns the overall status for a commit. 547 func (f *FakeClient) GetCombinedStatus(owner, repo, ref string) (*github.CombinedStatus, error) { 548 f.lock.RLock() 549 defer f.lock.RUnlock() 550 return f.CombinedStatuses[ref], nil 551 } 552 553 // GetRepoLabels gets labels in a repo. 554 func (f *FakeClient) GetRepoLabels(owner, repo string) ([]github.Label, error) { 555 f.lock.RLock() 556 defer f.lock.RUnlock() 557 la := []github.Label{} 558 for _, l := range f.RepoLabelsExisting { 559 la = append(la, github.Label{Name: l}) 560 } 561 return la, nil 562 } 563 564 // AddRepoLabel adds a defined label given org/repo 565 func (f *FakeClient) AddRepoLabel(org, repo, label, description, color string) error { 566 f.lock.RLock() 567 defer f.lock.RUnlock() 568 569 f.RepoLabelsExisting = append(f.RepoLabelsExisting, label) 570 return nil 571 } 572 573 // GetIssueLabels gets labels on an issue 574 func (f *FakeClient) GetIssueLabels(owner, repo string, number int) ([]github.Label, error) { 575 f.lock.RLock() 576 defer f.lock.RUnlock() 577 re := regexp.MustCompile(fmt.Sprintf(`^%s/%s#%d:(.*)$`, owner, repo, number)) 578 la := []github.Label{} 579 allLabels := sets.New[string](f.IssueLabelsExisting...) 580 allLabels.Insert(f.IssueLabelsAdded...) 581 allLabels.Delete(f.IssueLabelsRemoved...) 582 for _, l := range sets.List(allLabels) { 583 groups := re.FindStringSubmatch(l) 584 if groups != nil { 585 la = append(la, github.Label{Name: groups[1]}) 586 } 587 } 588 return la, nil 589 } 590 591 // AddLabel adds a label 592 func (f *FakeClient) AddLabel(owner, repo string, number int, label string) error { 593 return f.AddLabelsWithContext(context.Background(), owner, repo, number, label) 594 } 595 596 // AddLabelWithContext adds a label with a provided context 597 func (f *FakeClient) AddLabelWithContext(ctx context.Context, owner, repo string, number int, label string) error { 598 return f.AddLabelsWithContext(context.Background(), owner, repo, number, label) 599 } 600 601 // AddLabels adds a list of labels 602 func (f *FakeClient) AddLabels(owner, repo string, number int, labels ...string) error { 603 return f.AddLabelsWithContext(context.Background(), owner, repo, number, labels...) 604 } 605 606 // AddLabelsWithContext adds a list of labels with a provided context 607 func (f *FakeClient) AddLabelsWithContext(ctx context.Context, owner, repo string, number int, labels ...string) error { 608 f.lock.Lock() 609 defer f.lock.Unlock() 610 for _, label := range labels { 611 labelString := fmt.Sprintf("%s/%s#%d:%s", owner, repo, number, label) 612 if sets.New[string](f.IssueLabelsAdded...).Has(labelString) { 613 return fmt.Errorf("cannot add %v to %s/%s/#%d", label, owner, repo, number) 614 } 615 if f.RepoLabelsExisting == nil { 616 f.IssueLabelsAdded = append(f.IssueLabelsAdded, labelString) 617 continue 618 } 619 620 var repoLabelExists bool 621 for _, l := range f.RepoLabelsExisting { 622 if label == l { 623 f.IssueLabelsAdded = append(f.IssueLabelsAdded, labelString) 624 repoLabelExists = true 625 break 626 } 627 } 628 if !repoLabelExists { 629 return fmt.Errorf("cannot add %v to %s/%s/#%d", label, owner, repo, number) 630 } 631 } 632 return nil 633 } 634 635 // RemoveLabel removes a label 636 func (f *FakeClient) RemoveLabel(owner, repo string, number int, label string) error { 637 return f.RemoveLabelWithContext(context.Background(), owner, repo, number, label) 638 } 639 640 // RemoveLabelWithContext removes a label with a provided context 641 func (f *FakeClient) RemoveLabelWithContext(ctx context.Context, owner, repo string, number int, label string) error { 642 f.lock.Lock() 643 defer f.lock.Unlock() 644 labelString := fmt.Sprintf("%s/%s#%d:%s", owner, repo, number, label) 645 if !sets.NewString(f.IssueLabelsRemoved...).Has(labelString) { 646 f.IssueLabelsRemoved = append(f.IssueLabelsRemoved, labelString) 647 return nil 648 } 649 return fmt.Errorf("cannot remove %v from %s/%s/#%d", label, owner, repo, number) 650 } 651 652 // FindIssues returns the same results as FindIssuesWithOrg 653 func (f *FakeClient) FindIssues(query, sort string, asc bool) ([]github.Issue, error) { 654 return f.FindIssuesWithOrg("", query, sort, asc) 655 } 656 657 // FindIssuesWithOrg returns f.Issues 658 func (f *FakeClient) FindIssuesWithOrg(org, query, sort string, asc bool) ([]github.Issue, error) { 659 f.lock.RLock() 660 defer f.lock.RUnlock() 661 var issues []github.Issue 662 for _, issue := range f.Issues { 663 issues = append(issues, *issue) 664 } 665 for _, pr := range f.PullRequests { 666 issues = append(issues, github.Issue{ 667 User: pr.User, 668 Number: pr.Number, 669 }) 670 } 671 return issues, nil 672 } 673 674 // AssignIssue adds assignees. 675 func (f *FakeClient) AssignIssue(owner, repo string, number int, assignees []string) error { 676 f.lock.Lock() 677 defer f.lock.Unlock() 678 var m github.MissingUsers 679 for _, a := range assignees { 680 if a == "not-in-the-org" { 681 m.Users = append(m.Users, a) 682 continue 683 } 684 f.AssigneesAdded = append(f.AssigneesAdded, fmt.Sprintf("%s/%s#%d:%s", owner, repo, number, a)) 685 } 686 if m.Users == nil { 687 return nil 688 } 689 return m 690 } 691 692 // GetFile returns the bytes of the file. 693 func (f *FakeClient) GetFile(org, repo, file, commit string) ([]byte, error) { 694 f.lock.RLock() 695 defer f.lock.RUnlock() 696 contents, ok := f.RemoteFiles[file] 697 if !ok { 698 return nil, fmt.Errorf("could not find file %s", file) 699 } 700 if commit == "" { 701 if master, ok := contents["master"]; ok { 702 return []byte(master), nil 703 } 704 705 return nil, fmt.Errorf("could not find file %s in master", file) 706 } 707 708 if content, ok := contents[commit]; ok { 709 return []byte(content), nil 710 } 711 712 return nil, fmt.Errorf("could not find file %s with ref %s", file, commit) 713 } 714 715 // ListTeams return a list of fake teams that correspond to the fake team members returned by ListTeamMembers 716 func (f *FakeClient) ListTeams(org string) ([]github.Team, error) { 717 f.lock.RLock() 718 defer f.lock.RUnlock() 719 return []github.Team{ 720 { 721 ID: 0, 722 Slug: "admins", 723 Name: "Admins", 724 }, 725 { 726 ID: 42, 727 Slug: "leads", 728 Name: "Leads", 729 }, 730 }, nil 731 } 732 733 // ListTeamMembers return a fake team with a single "sig-lead" GitHub teammember 734 func (f *FakeClient) ListTeamMembers(org string, teamID int, role string) ([]github.TeamMember, error) { 735 f.lock.RLock() 736 defer f.lock.RUnlock() 737 if role != github.RoleAll { 738 return nil, fmt.Errorf("unsupported role %v (only all supported)", role) 739 } 740 teams := map[int][]github.TeamMember{ 741 0: {{Login: "default-sig-lead"}}, 742 42: {{Login: "sig-lead"}}, 743 } 744 members, ok := teams[teamID] 745 if !ok { 746 return []github.TeamMember{}, nil 747 } 748 return members, nil 749 } 750 751 // ListTeamMembers return a fake team with a single "sig-lead" GitHub teammember 752 func (f *FakeClient) ListTeamMembersBySlug(org, teamSlug, role string) ([]github.TeamMember, error) { 753 f.lock.RLock() 754 defer f.lock.RUnlock() 755 if role != github.RoleAll { 756 return nil, fmt.Errorf("unsupported role %v (only all supported)", role) 757 } 758 teams := map[string][]github.TeamMember{ 759 "admins": {{Login: "default-sig-lead"}}, 760 "leads": {{Login: "sig-lead"}}, 761 } 762 members, ok := teams[teamSlug] 763 if !ok { 764 return []github.TeamMember{}, nil 765 } 766 return members, nil 767 } 768 769 func (f *FakeClient) TeamBySlugHasMember(org string, teamSlug string, memberLogin string) (bool, error) { 770 f.lock.RLock() 771 defer f.lock.RUnlock() 772 if f.Teams[org] != nil { 773 return f.Teams[org][teamSlug].Members.Has(memberLogin), nil 774 } 775 return false, nil 776 } 777 778 // IsCollaborator returns true if the user is a collaborator of the repo. 779 func (f *FakeClient) IsCollaborator(org, repo, login string) (bool, error) { 780 f.lock.RLock() 781 defer f.lock.RUnlock() 782 normed := github.NormLogin(login) 783 for _, collab := range f.Collaborators { 784 if github.NormLogin(collab) == normed { 785 return true, nil 786 } 787 } 788 return false, nil 789 } 790 791 // ListCollaborators lists the collaborators. 792 func (f *FakeClient) ListCollaborators(org, repo string) ([]github.User, error) { 793 f.lock.RLock() 794 defer f.lock.RUnlock() 795 result := make([]github.User, 0, len(f.Collaborators)) 796 for _, login := range f.Collaborators { 797 result = append(result, github.User{Login: login}) 798 } 799 return result, nil 800 } 801 802 // ClearMilestone removes the milestone 803 func (f *FakeClient) ClearMilestone(org, repo string, issueNum int) error { 804 f.Milestone = 0 805 return nil 806 } 807 808 // SetMilestone sets the milestone. 809 func (f *FakeClient) SetMilestone(org, repo string, issueNum, milestoneNum int) error { 810 f.lock.Lock() 811 defer f.lock.Unlock() 812 if milestoneNum < 0 { 813 return fmt.Errorf("Milestone Numbers Cannot Be Negative") 814 } 815 f.Milestone = milestoneNum 816 return nil 817 } 818 819 // ListMilestones lists milestones. 820 func (f *FakeClient) ListMilestones(org, repo string) ([]github.Milestone, error) { 821 f.lock.RLock() 822 defer f.lock.RUnlock() 823 milestones := []github.Milestone{} 824 for k, v := range f.MilestoneMap { 825 milestones = append(milestones, github.Milestone{Title: k, Number: v, State: "open"}) 826 } 827 return milestones, nil 828 } 829 830 // ListPullRequestCommits lists commits for a given PR. 831 func (f *FakeClient) ListPullRequestCommits(org, repo string, prNumber int) ([]github.RepositoryCommit, error) { 832 f.lock.RLock() 833 defer f.lock.RUnlock() 834 k := fmt.Sprintf("%s/%s#%d", org, repo, prNumber) 835 return f.CommitMap[k], nil 836 } 837 838 // GetRepoProjects returns the list of projects under a repo. 839 func (f *FakeClient) GetRepoProjects(owner, repo string) ([]github.Project, error) { 840 f.lock.Lock() 841 defer f.lock.Unlock() 842 return f.RepoProjects[fmt.Sprintf("%s/%s", owner, repo)], nil 843 } 844 845 // GetOrgProjects returns the list of projects under an org 846 func (f *FakeClient) GetOrgProjects(org string) ([]github.Project, error) { 847 f.lock.RLock() 848 defer f.lock.RUnlock() 849 return f.RepoProjects[fmt.Sprintf("%s/*", org)], nil 850 } 851 852 // GetProjectColumns returns the list of columns for a given project. 853 func (f *FakeClient) GetProjectColumns(org string, projectID int) ([]github.ProjectColumn, error) { 854 f.lock.RLock() 855 defer f.lock.RUnlock() 856 // Get project name 857 for _, projects := range f.RepoProjects { 858 for _, project := range projects { 859 if projectID == project.ID { 860 return f.ProjectColumnsMap[project.Name], nil 861 } 862 } 863 } 864 return nil, fmt.Errorf("Cannot find project ID") 865 } 866 867 // CreateProjectCard creates a project card under a given column. 868 func (f *FakeClient) CreateProjectCard(org string, columnID int, projectCard github.ProjectCard) (*github.ProjectCard, error) { 869 cards, err := f.GetColumnProjectCards(org, columnID) 870 if err != nil { 871 return nil, err 872 } 873 f.lock.Lock() 874 defer f.lock.Unlock() 875 876 for project, columnIDMap := range f.ColumnIDMap { 877 if _, exists := columnIDMap[columnID]; exists { 878 for id := range columnIDMap { 879 // Make sure that we behave same as github API 880 // Create project will generate an error when the card already exist in the project 881 for _, existingCard := range cards { 882 if existingCard.ContentURL == projectCard.ContentURL { 883 return nil, fmt.Errorf("Card already exist in the project: %s, column %d, cannot add to column %d", project, id, columnID) 884 } 885 } 886 } 887 } 888 columnName, exists := columnIDMap[columnID] 889 if exists { 890 f.ColumnCardsMap[columnID] = append( 891 f.ColumnCardsMap[columnID], 892 projectCard, 893 ) 894 f.Column = columnName 895 f.Project = project 896 return &projectCard, nil 897 } 898 } 899 return nil, fmt.Errorf("Provided column %d does not exist, ColumnIDMap is %v", columnID, f.ColumnIDMap) 900 } 901 902 // DeleteProjectCard deletes the project card of a specific issue or PR 903 func (f *FakeClient) DeleteProjectCard(org string, projectCardID int) error { 904 f.lock.Lock() 905 defer f.lock.Unlock() 906 if f.ColumnCardsMap == nil { 907 return fmt.Errorf("Project card doesn't exist") 908 } 909 f.Project = "" 910 f.Column = "" 911 newCards := []github.ProjectCard{} 912 oldColumnID := -1 913 for column, cards := range f.ColumnCardsMap { 914 removalIndex := -1 915 for i, existingCard := range cards { 916 if existingCard.ContentID == projectCardID { 917 oldColumnID = column 918 removalIndex = i 919 break 920 } 921 } 922 if removalIndex != -1 { 923 newCards = cards 924 newCards[removalIndex] = newCards[len(newCards)-1] 925 newCards = newCards[:len(newCards)-1] 926 break 927 } 928 } 929 // Update the old column's list of project cards 930 if oldColumnID != -1 { 931 f.ColumnCardsMap[oldColumnID] = newCards 932 } 933 return nil 934 } 935 936 // GetColumnProjectCards fetches project cards under given column 937 func (f *FakeClient) GetColumnProjectCards(org string, columnID int) ([]github.ProjectCard, error) { 938 f.lock.RLock() 939 if f.ColumnCardsMap == nil { 940 f.ColumnCardsMap = make(map[int][]github.ProjectCard) 941 } 942 res := f.ColumnCardsMap[columnID] 943 f.lock.RUnlock() 944 return res, nil 945 } 946 947 // GetColumnProjectCard fetches project card if the content_url in the card matched the issue/pr 948 func (f *FakeClient) GetColumnProjectCard(org string, columnID int, contentURL string) (*github.ProjectCard, error) { 949 cards, err := f.GetColumnProjectCards(org, columnID) 950 if err != nil { 951 return nil, err 952 } 953 for _, existingCard := range cards { 954 if existingCard.ContentURL == contentURL { 955 return &existingCard, nil 956 } 957 } 958 return nil, nil 959 } 960 961 func (f *FakeClient) GetRepos(org string, isUser bool) ([]github.Repo, error) { 962 return []github.Repo{ 963 { 964 Owner: github.User{ 965 Login: "kubernetes", 966 }, 967 Name: "kubernetes", 968 }, 969 { 970 Owner: github.User{ 971 Login: "kubernetes", 972 }, 973 Name: "community", 974 }, 975 }, nil 976 } 977 978 func (f *FakeClient) GetRepo(owner, name string) (github.FullRepo, error) { 979 if f.GetRepoError != nil { 980 return github.FullRepo{}, f.GetRepoError 981 } 982 return github.FullRepo{ 983 Repo: github.Repo{ 984 Owner: github.User{Login: owner}, 985 Name: name, 986 HasIssues: true, 987 HasWiki: true, 988 DefaultBranch: "master", 989 Description: fmt.Sprintf("Test Repo: %s", name), 990 }, 991 }, nil 992 } 993 994 // MoveProjectCard moves a specific project card to a specified column in the same project 995 func (f *FakeClient) MoveProjectCard(org string, projectCardID int, newColumnID int) error { 996 f.lock.Lock() 997 defer f.lock.Unlock() 998 // Remove project card from old column 999 newCards := []github.ProjectCard{} 1000 oldColumnID := -1 1001 projectCard := github.ProjectCard{} 1002 for column, cards := range f.ColumnCardsMap { 1003 removalIndex := -1 1004 for i, existingCard := range cards { 1005 if existingCard.ContentID == projectCardID { 1006 oldColumnID = column 1007 removalIndex = i 1008 projectCard = existingCard 1009 break 1010 } 1011 } 1012 if removalIndex != -1 { 1013 newCards = cards 1014 newCards[removalIndex] = newCards[len(newCards)-1] 1015 newCards = newCards[:len(newCards)-1] 1016 } 1017 } 1018 if oldColumnID != -1 { 1019 // Update the old column's list of project cards 1020 f.ColumnCardsMap[oldColumnID] = newCards 1021 } 1022 1023 for project, columnIDMap := range f.ColumnIDMap { 1024 if columnName, exists := columnIDMap[newColumnID]; exists { 1025 // Add project card to new column 1026 f.ColumnCardsMap[newColumnID] = append( 1027 f.ColumnCardsMap[newColumnID], 1028 projectCard, 1029 ) 1030 f.Column = columnName 1031 f.Project = project 1032 break 1033 } 1034 } 1035 1036 return nil 1037 } 1038 1039 // TeamHasMember checks if a user belongs to a team 1040 func (f *FakeClient) TeamHasMember(org string, teamID int, memberLogin string) (bool, error) { 1041 teamMembers, _ := f.ListTeamMembers(org, teamID, github.RoleAll) 1042 for _, member := range teamMembers { 1043 if member.Login == memberLogin { 1044 return true, nil 1045 } 1046 } 1047 return false, nil 1048 } 1049 1050 func (f *FakeClient) GetTeamBySlug(slug string, org string) (*github.Team, error) { 1051 teams, _ := f.ListTeams(org) 1052 for _, team := range teams { 1053 if team.Name == slug { 1054 return &team, nil 1055 } 1056 } 1057 return &github.Team{}, nil 1058 } 1059 1060 func (f *FakeClient) CreatePullRequest(org, repo, title, body, head, base string, canModify bool) (int, error) { 1061 f.lock.Lock() 1062 defer f.lock.Unlock() 1063 if f.PullRequests == nil { 1064 f.PullRequests = map[int]*github.PullRequest{} 1065 } 1066 if f.Issues == nil { 1067 f.Issues = map[int]*github.Issue{} 1068 } 1069 for i := 0; i < 999; i++ { 1070 if f.PullRequests[i] != nil || f.Issues[i] != nil { 1071 continue 1072 } 1073 f.PullRequests[i] = &github.PullRequest{ 1074 Number: i, 1075 Base: github.PullRequestBranch{ 1076 Ref: base, 1077 Repo: github.Repo{Owner: github.User{Login: org}, Name: repo}, 1078 }, 1079 } 1080 f.Issues[i] = &github.Issue{Number: i} 1081 return i, nil 1082 } 1083 1084 return 0, errors.New("FakeClient supports only 999 PullRequests") 1085 } 1086 1087 func (f *FakeClient) UpdatePullRequest(org, repo string, number int, title, body *string, open *bool, branch *string, canModify *bool) error { 1088 f.lock.Lock() 1089 defer f.lock.Unlock() 1090 pr, found := f.PullRequests[number] 1091 if !found { 1092 return fmt.Errorf("no pr with number %d found", number) 1093 } 1094 if title != nil { 1095 pr.Title = *title 1096 } 1097 if body != nil { 1098 pr.Body = *body 1099 } 1100 return nil 1101 } 1102 1103 // Query simply exists to allow the fake client to match the interface for packages that need it. 1104 // It does not modify the passed interface at all. 1105 func (f *FakeClient) Query(ctx context.Context, q interface{}, vars map[string]interface{}) error { 1106 return nil 1107 } 1108 1109 // GetDirectory returns the contents of the file. 1110 func (f *FakeClient) GetDirectory(org, repo, dir, commit string) ([]github.DirectoryContent, error) { 1111 contents, ok := f.RemoteDirectories[dir] 1112 if !ok { 1113 return nil, fmt.Errorf("could not find dir %s", dir) 1114 } 1115 if commit == "" { 1116 if master, ok := contents["master"]; ok { 1117 return master, nil 1118 } 1119 1120 return nil, fmt.Errorf("could not find dir %s in master", dir) 1121 } 1122 1123 if content, ok := contents[commit]; ok { 1124 return content, nil 1125 } 1126 1127 return nil, fmt.Errorf("could not find dir %s with ref %s", dir, commit) 1128 } 1129 1130 // CreatePullRequestReviewComment adds a comment on a PR. 1131 func (f *FakeClient) CreatePullRequestReviewComment(owner, repo string, number int, rc github.ReviewComment) error { 1132 f.lock.Lock() 1133 defer f.lock.Unlock() 1134 f.PullRequestReviewCommentID++ 1135 f.PullRequestReviewCommentsAdded = append(f.PullRequestReviewCommentsAdded, fmt.Sprintf("%s/%s#%d:%s", owner, repo, number, rc.Body)) 1136 f.PullRequestReviewComments[number] = append(f.PullRequestReviewComments[number], rc) 1137 return nil 1138 } 1139 1140 func (f *FakeClient) ListCurrentUserRepoInvitations() ([]github.UserRepoInvitation, error) { 1141 var ret []github.UserRepoInvitation 1142 for _, inv := range f.UserRepoInvitations { 1143 ret = append(ret, inv) 1144 } 1145 1146 sort.Slice(ret, func(p, q int) bool { 1147 return ret[p].InvitationID < ret[q].InvitationID 1148 }) 1149 1150 return ret, nil 1151 } 1152 1153 func (f *FakeClient) AcceptUserRepoInvitation(invitationID int) error { 1154 if _, ok := f.UserRepoInvitations[invitationID]; !ok { 1155 return fmt.Errorf("couldn't find invitation id: %d", invitationID) 1156 } 1157 1158 delete(f.UserRepoInvitations, invitationID) 1159 return nil 1160 } 1161 1162 func (f *FakeClient) AcceptUserOrgInvitation(org string) error { 1163 if _, ok := f.UserOrgInvitations[org]; !ok { 1164 return fmt.Errorf("couldn't find invitation for org: %s", org) 1165 } 1166 1167 delete(f.UserOrgInvitations, org) 1168 return nil 1169 } 1170 1171 func (f *FakeClient) ListCurrentUserOrgInvitations() ([]github.UserOrgInvitation, error) { 1172 var ret []github.UserOrgInvitation 1173 for _, inv := range f.UserOrgInvitations { 1174 ret = append(ret, inv) 1175 } 1176 1177 sort.Slice(ret, func(p, q int) bool { 1178 return ret[p].Org.Login < ret[q].Org.Login 1179 }) 1180 1181 return ret, nil 1182 } 1183 1184 func (f *FakeClient) MutateWithGitHubAppsSupport(ctx context.Context, m interface{}, input githubql.Input, vars map[string]interface{}, org string) error { 1185 return nil 1186 } 1187 1188 func (f *FakeClient) GetFailedActionRunsByHeadBranch(org, repo, branchName, headSHA string) ([]github.WorkflowRun, error) { 1189 return []github.WorkflowRun{}, nil 1190 } 1191 1192 func (f *FakeClient) TriggerGitHubWorkflow(org, repo string, id int) error { 1193 return nil 1194 } 1195 1196 func (f *FakeClient) TriggerFailedGitHubWorkflow(org, repo string, id int) error { 1197 return nil 1198 } 1199 1200 func (f *FakeClient) RequestReview(org, repo string, number int, logins []string) error { 1201 f.ReviewersRequested = logins 1202 return nil 1203 }