github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/pkg/plugins/approve/approve_test.go (about)

     1  /*
     2  Copyright 2017 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 approve
    18  
    19  import (
    20  	"fmt"
    21  	"net/url"
    22  	"os"
    23  	"path/filepath"
    24  	"reflect"
    25  	"regexp"
    26  	"strings"
    27  	"testing"
    28  	"time"
    29  
    30  	"github.com/google/go-cmp/cmp"
    31  	"github.com/sirupsen/logrus"
    32  
    33  	"k8s.io/apimachinery/pkg/util/sets"
    34  	"sigs.k8s.io/yaml"
    35  
    36  	"sigs.k8s.io/prow/pkg/config"
    37  	"sigs.k8s.io/prow/pkg/github"
    38  	"sigs.k8s.io/prow/pkg/github/fakegithub"
    39  	"sigs.k8s.io/prow/pkg/labels"
    40  	"sigs.k8s.io/prow/pkg/layeredsets"
    41  	"sigs.k8s.io/prow/pkg/plugins"
    42  	"sigs.k8s.io/prow/pkg/plugins/approve/approvers"
    43  	"sigs.k8s.io/prow/pkg/plugins/ownersconfig"
    44  	"sigs.k8s.io/prow/pkg/repoowners"
    45  )
    46  
    47  const prNumber = 1
    48  
    49  func newTestComment(user, body string) github.IssueComment {
    50  	return github.IssueComment{User: github.User{Login: user}, Body: body}
    51  }
    52  
    53  func newTestCommentTime(t time.Time, user, body string) github.IssueComment {
    54  	c := newTestComment(user, body)
    55  	c.CreatedAt = t
    56  	return c
    57  }
    58  
    59  func newTestReview(user, body string, state github.ReviewState) github.Review {
    60  	return github.Review{User: github.User{Login: user}, Body: body, State: state}
    61  }
    62  
    63  func newTestReviewTime(t time.Time, user, body string, state github.ReviewState) github.Review {
    64  	r := newTestReview(user, body, state)
    65  	r.SubmittedAt = t
    66  	return r
    67  }
    68  
    69  func newFakeGitHubClient(hasLabel, humanApproved bool, files []string, comments []github.IssueComment, reviews []github.Review) *fakegithub.FakeClient {
    70  	labels := []string{"org/repo#1:lgtm"}
    71  	if hasLabel {
    72  		labels = append(labels, fmt.Sprintf("org/repo#%v:approved", prNumber))
    73  	}
    74  	var changes []github.PullRequestChange
    75  	for _, file := range files {
    76  		changes = append(changes, github.PullRequestChange{Filename: file})
    77  	}
    78  	fgc := fakegithub.NewFakeClient()
    79  	fgc.IssueLabelsAdded = labels
    80  	fgc.PullRequestChanges = map[int][]github.PullRequestChange{prNumber: changes}
    81  	fgc.IssueComments = map[int][]github.IssueComment{prNumber: comments}
    82  	fgc.Reviews = map[int][]github.Review{prNumber: reviews}
    83  	fgc.WasLabelAddedByHumanVal = humanApproved
    84  	return fgc
    85  }
    86  
    87  type fakeRepo struct {
    88  	approvers map[string]layeredsets.String
    89  	// directory -> approver
    90  	leafApprovers map[string]sets.Set[string]
    91  	// toApprove -> directoryWithOwnersFile
    92  	approverOwners map[string]string
    93  	// dir -> allowed
    94  	autoApproveUnownedSubfolders map[string]bool
    95  	dirDenylist                  []*regexp.Regexp
    96  }
    97  
    98  func (fr fakeRepo) Filenames() ownersconfig.Filenames {
    99  	return ownersconfig.FakeFilenames
   100  }
   101  
   102  func (fr fakeRepo) Approvers(path string) layeredsets.String {
   103  	ret := fr.approvers[path]
   104  	if ret.Len() > 0 || path == "" {
   105  		return ret
   106  	}
   107  
   108  	p := filepath.Dir(path)
   109  	if p == "." {
   110  		p = ""
   111  	}
   112  	return fr.Approvers(p)
   113  }
   114  func (fr fakeRepo) LeafApprovers(path string) sets.Set[string] {
   115  	ret := fr.leafApprovers[path]
   116  	if ret.Len() > 0 || path == "" {
   117  		return ret
   118  	}
   119  
   120  	p := filepath.Dir(path)
   121  	if p == "." {
   122  		p = ""
   123  	}
   124  	return fr.LeafApprovers(p)
   125  }
   126  func (fr fakeRepo) FindApproverOwnersForFile(path string) string {
   127  	return fr.approverOwners[path]
   128  }
   129  func (fr fakeRepo) IsNoParentOwners(path string) bool {
   130  	return false
   131  }
   132  func (fr fakeRepo) IsAutoApproveUnownedSubfolders(ownerFilePath string) bool {
   133  	return fr.autoApproveUnownedSubfolders[ownerFilePath]
   134  }
   135  func (fr fakeRepo) TopLevelApprovers() sets.Set[string] {
   136  	return nil
   137  }
   138  
   139  func (fr fakeRepo) ParseSimpleConfig(path string) (repoowners.SimpleConfig, error) {
   140  	dir := filepath.Dir(path)
   141  	for _, re := range fr.dirDenylist {
   142  		if re.MatchString(dir) {
   143  			return repoowners.SimpleConfig{}, filepath.SkipDir
   144  		}
   145  	}
   146  
   147  	b, err := os.ReadFile(path)
   148  	if err != nil {
   149  		return repoowners.SimpleConfig{}, err
   150  	}
   151  	full := new(repoowners.SimpleConfig)
   152  	err = yaml.Unmarshal(b, full)
   153  	return *full, err
   154  }
   155  
   156  func (fr fakeRepo) ParseFullConfig(path string) (repoowners.FullConfig, error) {
   157  	dir := filepath.Dir(path)
   158  	for _, re := range fr.dirDenylist {
   159  		if re.MatchString(dir) {
   160  			return repoowners.FullConfig{}, filepath.SkipDir
   161  		}
   162  	}
   163  
   164  	b, err := os.ReadFile(path)
   165  	if err != nil {
   166  		return repoowners.FullConfig{}, err
   167  	}
   168  	full := new(repoowners.FullConfig)
   169  	err = yaml.Unmarshal(b, full)
   170  	return *full, err
   171  }
   172  
   173  func TestHandle(t *testing.T) {
   174  	// This function does not need to test IsApproved, that is tested in approvers/approvers_test.go.
   175  
   176  	// includes tests with mixed case usernames
   177  	// includes tests with stale notifications
   178  	tests := []struct {
   179  		name          string
   180  		branch        string
   181  		prBody        string
   182  		hasLabel      bool
   183  		humanApproved bool
   184  		files         []string
   185  		comments      []github.IssueComment
   186  		reviews       []github.Review
   187  
   188  		selfApprove         bool
   189  		needsIssue          bool
   190  		lgtmActsAsApprove   bool
   191  		reviewActsAsApprove bool
   192  		githubLinkURL       *url.URL
   193  
   194  		expectDelete    bool
   195  		expectComment   bool
   196  		expectedComment string
   197  		expectToggle    bool
   198  	}{
   199  
   200  		// breaking cases
   201  		// case: /approve in PR body
   202  		{
   203  			name:                "initial notification (approved)",
   204  			hasLabel:            false,
   205  			files:               []string{"c/c.go"},
   206  			comments:            []github.IssueComment{},
   207  			reviews:             []github.Review{},
   208  			selfApprove:         true,
   209  			needsIssue:          false,
   210  			lgtmActsAsApprove:   false,
   211  			reviewActsAsApprove: false,
   212  			githubLinkURL:       &url.URL{Scheme: "https", Host: "github.com"},
   213  
   214  			expectDelete:  false,
   215  			expectToggle:  true,
   216  			expectComment: true,
   217  			expectedComment: `[APPROVALNOTIFIER] This PR is **APPROVED**
   218  
   219  This pull-request has been approved by: *<a href="#" title="Author self-approved">cjwagner</a>*
   220  
   221  The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=org%2Frepo).
   222  
   223  The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)
   224  
   225  <details >
   226  Needs approval from an approver in each of these files:
   227  
   228  - ~~[c/OWNERS](https://github.com/org/repo/blob/master/c/OWNERS)~~ [cjwagner]
   229  
   230  Approvers can indicate their approval by writing ` + "`/approve`" + ` in a comment
   231  Approvers can cancel approval by writing ` + "`/approve cancel`" + ` in a comment
   232  </details>
   233  <!-- META={"approvers":[]} -->`,
   234  		},
   235  		{
   236  			name:                "initial notification (unapproved)",
   237  			hasLabel:            false,
   238  			files:               []string{"c/c.go"},
   239  			comments:            []github.IssueComment{},
   240  			reviews:             []github.Review{},
   241  			selfApprove:         false,
   242  			needsIssue:          false,
   243  			lgtmActsAsApprove:   false,
   244  			reviewActsAsApprove: false,
   245  			githubLinkURL:       &url.URL{Scheme: "https", Host: "github.com"},
   246  
   247  			expectDelete:  false,
   248  			expectToggle:  false,
   249  			expectComment: true,
   250  			expectedComment: `[APPROVALNOTIFIER] This PR is **NOT APPROVED**
   251  
   252  This pull-request has been approved by:
   253  **Once this PR has been reviewed and has the lgtm label**, please ask for approval from [cjwagner](https://github.com/cjwagner). For more information see [the Kubernetes Code Review Process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process).
   254  
   255  The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=org%2Frepo).
   256  
   257  <details open>
   258  Needs approval from an approver in each of these files:
   259  
   260  - **[c/OWNERS](https://github.com/org/repo/blob/master/c/OWNERS)**
   261  
   262  Approvers can indicate their approval by writing ` + "`/approve`" + ` in a comment
   263  Approvers can cancel approval by writing ` + "`/approve cancel`" + ` in a comment
   264  </details>
   265  <!-- META={"approvers":["cjwagner"]} -->`,
   266  		},
   267  		{
   268  			name:                "no-issue comment",
   269  			hasLabel:            false,
   270  			files:               []string{"a/a.go"},
   271  			comments:            []github.IssueComment{newTestComment("Alice", "stuff\n/approve no-issue \nmore stuff")},
   272  			reviews:             []github.Review{},
   273  			selfApprove:         false,
   274  			needsIssue:          true,
   275  			lgtmActsAsApprove:   false,
   276  			reviewActsAsApprove: false,
   277  			githubLinkURL:       &url.URL{Scheme: "https", Host: "github.com"},
   278  
   279  			expectDelete:  false,
   280  			expectToggle:  true,
   281  			expectComment: true,
   282  			expectedComment: `[APPROVALNOTIFIER] This PR is **APPROVED**
   283  
   284  This pull-request has been approved by: *<a href="" title="Approved">Alice</a>*
   285  
   286  Associated issue requirement bypassed by: *<a href="" title="Approved">Alice</a>*
   287  
   288  The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=org%2Frepo).
   289  
   290  The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)
   291  
   292  <details >
   293  Needs approval from an approver in each of these files:
   294  
   295  - ~~[a/OWNERS](https://github.com/org/repo/blob/master/a/OWNERS)~~ [Alice]
   296  
   297  Approvers can indicate their approval by writing ` + "`/approve`" + ` in a comment
   298  Approvers can cancel approval by writing ` + "`/approve cancel`" + ` in a comment
   299  </details>
   300  <!-- META={"approvers":[]} -->`,
   301  		},
   302  		{
   303  			name:                "issue provided in PR body",
   304  			prBody:              "some changes that fix #42.\n/assign",
   305  			hasLabel:            false,
   306  			files:               []string{"a/a.go"},
   307  			comments:            []github.IssueComment{newTestComment("Alice", "stuff\n/approve")},
   308  			reviews:             []github.Review{},
   309  			selfApprove:         false,
   310  			needsIssue:          true,
   311  			lgtmActsAsApprove:   false,
   312  			reviewActsAsApprove: false,
   313  			githubLinkURL:       &url.URL{Scheme: "https", Host: "github.com"},
   314  
   315  			expectDelete:  false,
   316  			expectToggle:  true,
   317  			expectComment: true,
   318  			expectedComment: `[APPROVALNOTIFIER] This PR is **APPROVED**
   319  
   320  This pull-request has been approved by: *<a href="" title="Approved">Alice</a>*
   321  
   322  Associated issue: *#42*
   323  
   324  The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=org%2Frepo).
   325  
   326  The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)
   327  
   328  <details >
   329  Needs approval from an approver in each of these files:
   330  
   331  - ~~[a/OWNERS](https://github.com/org/repo/blob/master/a/OWNERS)~~ [Alice]
   332  
   333  Approvers can indicate their approval by writing ` + "`/approve`" + ` in a comment
   334  Approvers can cancel approval by writing ` + "`/approve cancel`" + ` in a comment
   335  </details>
   336  <!-- META={"approvers":[]} -->`,
   337  		},
   338  		{
   339  			name:     "non-implicit self approve no-issue",
   340  			hasLabel: false,
   341  			files:    []string{"a/a.go", "c/c.go"},
   342  			comments: []github.IssueComment{
   343  				newTestComment("ALIcE", "stuff\n/approve"),
   344  				newTestComment("cjwagner", "stuff\n/approve no-issue"),
   345  			},
   346  			reviews:             []github.Review{},
   347  			selfApprove:         false,
   348  			needsIssue:          true,
   349  			lgtmActsAsApprove:   false,
   350  			reviewActsAsApprove: false,
   351  			githubLinkURL:       &url.URL{Scheme: "https", Host: "github.com"},
   352  
   353  			expectDelete:    false,
   354  			expectToggle:    true,
   355  			expectComment:   true,
   356  			expectedComment: "",
   357  		},
   358  		{
   359  			name:     "implicit self approve, missing issue",
   360  			hasLabel: false,
   361  			files:    []string{"a/a.go", "c/c.go"},
   362  			comments: []github.IssueComment{
   363  				newTestComment("ALIcE", "stuff\n/approve"),
   364  				newTestCommentTime(time.Now(), "k8s-ci-robot", `[APPROVALNOTIFIER] This PR is **NOT APPROVED**
   365  
   366  This pull-request has been approved by: *<a href="" title="Approved">ALIcE</a>*, *<a href="#" title="Author self-approved">cjwagner</a>*
   367  
   368  *No associated issue*. Update pull-request body to add a reference to an issue, or get approval with `+"`/approve no-issue`"+`
   369  
   370  The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=org%2Frepo).
   371  
   372  The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)
   373  
   374  <details >
   375  Needs approval from an approver in each of these files:
   376  
   377  - ~~[a/OWNERS](https://github.com/org/repo/blob/master/a/OWNERS)~~ [ALIcE]
   378  - ~~[c/OWNERS](https://github.com/org/repo/blob/master/c/OWNERS)~~ [cjwagner]
   379  
   380  Approvers can indicate their approval by writing `+"`/approve`"+` in a comment
   381  Approvers can cancel approval by writing `+"`/approve cancel`"+` in a comment
   382  </details>
   383  <!-- META={"approvers":[]} -->`),
   384  			},
   385  			reviews:             []github.Review{},
   386  			selfApprove:         true,
   387  			needsIssue:          true,
   388  			lgtmActsAsApprove:   false,
   389  			reviewActsAsApprove: false,
   390  			githubLinkURL:       &url.URL{Scheme: "https", Host: "github.com"},
   391  
   392  			expectDelete:  false,
   393  			expectToggle:  false,
   394  			expectComment: false,
   395  		},
   396  		{
   397  			name:     "remove approval with /approve cancel",
   398  			hasLabel: true,
   399  			files:    []string{"a/a.go"},
   400  			comments: []github.IssueComment{
   401  				newTestComment("Alice", "/approve no-issue"),
   402  				newTestComment("k8s-ci-robot", "[APPROVALNOTIFIER] This PR is **APPROVED**\n\nblah"),
   403  				newTestComment("Alice", "stuff\n/approve cancel \nmore stuff"),
   404  			},
   405  			reviews:             []github.Review{},
   406  			selfApprove:         true, // no-op test
   407  			needsIssue:          true,
   408  			lgtmActsAsApprove:   false,
   409  			reviewActsAsApprove: false,
   410  			githubLinkURL:       &url.URL{Scheme: "https", Host: "github.com"},
   411  
   412  			expectDelete:  true,
   413  			expectToggle:  true,
   414  			expectComment: true,
   415  			expectedComment: `[APPROVALNOTIFIER] This PR is **NOT APPROVED**
   416  
   417  This pull-request has been approved by: *<a href="#" title="Author self-approved">cjwagner</a>*
   418  **Once this PR has been reviewed and has the lgtm label**, please assign [alice](https://github.com/alice) for approval. For more information see [the Kubernetes Code Review Process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process).
   419  
   420  *No associated issue*. Update pull-request body to add a reference to an issue, or get approval with ` + "`/approve no-issue`" + `
   421  
   422  The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=org%2Frepo).
   423  
   424  <details open>
   425  Needs approval from an approver in each of these files:
   426  
   427  - **[a/OWNERS](https://github.com/org/repo/blob/master/a/OWNERS)**
   428  
   429  Approvers can indicate their approval by writing ` + "`/approve`" + ` in a comment
   430  Approvers can cancel approval by writing ` + "`/approve cancel`" + ` in a comment
   431  </details>
   432  <!-- META={"approvers":["alice"]} -->`,
   433  		},
   434  		{
   435  			name:     "remove approval with remove-approve",
   436  			hasLabel: true,
   437  			files:    []string{"a/a.go"},
   438  			comments: []github.IssueComment{
   439  				newTestComment("Alice", "/approve no-issue"),
   440  				newTestComment("k8s-ci-robot", "[APPROVALNOTIFIER] This PR is **APPROVED**\n\nblah"),
   441  				newTestComment("Alice", "stuff\n/remove-approve \nmore stuff"),
   442  			},
   443  			reviews:             []github.Review{},
   444  			selfApprove:         true, // no-op test
   445  			needsIssue:          true,
   446  			lgtmActsAsApprove:   false,
   447  			reviewActsAsApprove: false,
   448  			githubLinkURL:       &url.URL{Scheme: "https", Host: "github.com"},
   449  
   450  			expectDelete:  true,
   451  			expectToggle:  true,
   452  			expectComment: true,
   453  			expectedComment: `[APPROVALNOTIFIER] This PR is **NOT APPROVED**
   454  
   455  This pull-request has been approved by: *<a href="#" title="Author self-approved">cjwagner</a>*
   456  **Once this PR has been reviewed and has the lgtm label**, please assign [alice](https://github.com/alice) for approval. For more information see [the Kubernetes Code Review Process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process).
   457  
   458  *No associated issue*. Update pull-request body to add a reference to an issue, or get approval with ` + "`/approve no-issue`" + `
   459  
   460  The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=org%2Frepo).
   461  
   462  <details open>
   463  Needs approval from an approver in each of these files:
   464  
   465  - **[a/OWNERS](https://github.com/org/repo/blob/master/a/OWNERS)**
   466  
   467  Approvers can indicate their approval by writing ` + "`/approve`" + ` in a comment
   468  Approvers can cancel approval by writing ` + "`/approve cancel`" + ` in a comment
   469  </details>
   470  <!-- META={"approvers":["alice"]} -->`,
   471  		},
   472  		{
   473  			name:     "remove approval after sync",
   474  			prBody:   "Changes the thing.\n fixes #42",
   475  			hasLabel: true,
   476  			files:    []string{"a/a.go", "b/b.go"},
   477  			comments: []github.IssueComment{
   478  				newTestComment("bOb", "stuff\n/approve \nblah"),
   479  				newTestComment("k8s-ci-robot", "[APPROVALNOTIFIER] This PR is **APPROVED**\n\nblah"),
   480  			},
   481  			reviews:             []github.Review{},
   482  			selfApprove:         true, // no-op test
   483  			needsIssue:          false,
   484  			lgtmActsAsApprove:   false,
   485  			reviewActsAsApprove: false,
   486  			githubLinkURL:       &url.URL{Scheme: "https", Host: "github.com"},
   487  
   488  			expectDelete:  true,
   489  			expectToggle:  true,
   490  			expectComment: true,
   491  		},
   492  		{
   493  			name:     "cancel implicit self approve",
   494  			prBody:   "Changes the thing.\n fixes #42",
   495  			hasLabel: true,
   496  			files:    []string{"c/c.go"},
   497  			comments: []github.IssueComment{
   498  				newTestComment("k8s-ci-robot", "[APPROVALNOTIFIER] This PR is **APPROVED**\n\nblah"),
   499  				newTestCommentTime(time.Now(), "CJWagner", "stuff\n/approve cancel \nmore stuff"),
   500  			},
   501  			reviews:             []github.Review{},
   502  			selfApprove:         true,
   503  			needsIssue:          true,
   504  			lgtmActsAsApprove:   false,
   505  			reviewActsAsApprove: false,
   506  			githubLinkURL:       &url.URL{Scheme: "https", Host: "github.com"},
   507  
   508  			expectDelete:  true,
   509  			expectToggle:  true,
   510  			expectComment: true,
   511  		},
   512  		{
   513  			name:     "remove-approve for implicit self approveS",
   514  			prBody:   "Changes the thing.\n fixes #42",
   515  			hasLabel: true,
   516  			files:    []string{"c/c.go"},
   517  			comments: []github.IssueComment{
   518  				newTestComment("k8s-ci-robot", "[APPROVALNOTIFIER] This PR is **APPROVED**\n\nblah"),
   519  				newTestCommentTime(time.Now(), "CJWagner", "stuff\n/remove-approve \nmore stuff"),
   520  			},
   521  			reviews:             []github.Review{},
   522  			selfApprove:         true,
   523  			needsIssue:          true,
   524  			lgtmActsAsApprove:   false,
   525  			reviewActsAsApprove: false,
   526  			githubLinkURL:       &url.URL{Scheme: "https", Host: "github.com"},
   527  
   528  			expectDelete:  true,
   529  			expectToggle:  true,
   530  			expectComment: true,
   531  		},
   532  		{
   533  			name:     "cancel implicit self approve (with lgtm-after-commit message)",
   534  			prBody:   "Changes the thing.\n fixes #42",
   535  			hasLabel: true,
   536  			files:    []string{"c/c.go"},
   537  			comments: []github.IssueComment{
   538  				newTestComment("k8s-ci-robot", "[APPROVALNOTIFIER] This PR is **APPROVED**\n\nblah"),
   539  				newTestCommentTime(time.Now(), "CJWagner", "/lgtm cancel //PR changed after LGTM, removing LGTM."),
   540  			},
   541  			reviews:             []github.Review{},
   542  			selfApprove:         true,
   543  			needsIssue:          true,
   544  			lgtmActsAsApprove:   true,
   545  			reviewActsAsApprove: false,
   546  			githubLinkURL:       &url.URL{Scheme: "https", Host: "github.com"},
   547  
   548  			expectDelete:  true,
   549  			expectToggle:  true,
   550  			expectComment: true,
   551  		},
   552  		{
   553  			name:     "up to date, poked by pr sync",
   554  			prBody:   "Finally fixes kubernetes/kubernetes#1\n",
   555  			hasLabel: true,
   556  			files:    []string{"a/a.go", "a/aa.go"},
   557  			comments: []github.IssueComment{
   558  				newTestComment("alice", "stuff\n/approve\nblah"),
   559  				newTestCommentTime(time.Now(), "k8s-ci-robot", `[APPROVALNOTIFIER] This PR is **APPROVED**
   560  
   561  This pull-request has been approved by: *<a href="" title="Approved">alice</a>*
   562  
   563  Associated issue: *#1*
   564  
   565  The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=org%2Frepo).
   566  
   567  The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)
   568  
   569  <details >
   570  Needs approval from an approver in each of these files:
   571  
   572  - ~~[a/OWNERS](https://github.com/org/repo/blob/master/a/OWNERS)~~ [alice]
   573  
   574  Approvers can indicate their approval by writing `+"`/approve`"+` in a comment
   575  Approvers can cancel approval by writing `+"`/approve cancel`"+` in a comment
   576  </details>
   577  <!-- META={"approvers":[]} -->`),
   578  			},
   579  			reviews:             []github.Review{},
   580  			selfApprove:         false,
   581  			needsIssue:          true,
   582  			lgtmActsAsApprove:   false,
   583  			reviewActsAsApprove: false,
   584  			githubLinkURL:       &url.URL{Scheme: "https", Host: "github.com"},
   585  
   586  			expectDelete:  false,
   587  			expectToggle:  false,
   588  			expectComment: false,
   589  		},
   590  		{
   591  			name:     "out of date, poked by pr sync",
   592  			prBody:   "Finally fixes kubernetes/kubernetes#1\n",
   593  			hasLabel: false,
   594  			files:    []string{"a/a.go", "a/aa.go"}, // previous commits may have been ["b/b.go"]
   595  			comments: []github.IssueComment{
   596  				newTestComment("alice", "stuff\n/approve\nblah"),
   597  				newTestCommentTime(time.Now(), "k8s-ci-robot", "[APPROVALNOTIFIER] This PR is **NOT APPROVED**\n\nblah"),
   598  			},
   599  			reviews:             []github.Review{},
   600  			selfApprove:         false,
   601  			needsIssue:          true,
   602  			lgtmActsAsApprove:   false,
   603  			reviewActsAsApprove: false,
   604  			githubLinkURL:       &url.URL{Scheme: "https", Host: "github.com"},
   605  
   606  			expectDelete:  true,
   607  			expectToggle:  true,
   608  			expectComment: true,
   609  		},
   610  		{
   611  			name:          "human added approve",
   612  			hasLabel:      true,
   613  			humanApproved: true,
   614  			files:         []string{"a/a.go"},
   615  			comments: []github.IssueComment{
   616  				newTestComment("k8s-ci-robot", "[APPROVALNOTIFIER] This PR is **NOT APPROVED**\n\nblah"),
   617  			},
   618  			reviews:             []github.Review{},
   619  			selfApprove:         false,
   620  			needsIssue:          false,
   621  			lgtmActsAsApprove:   false,
   622  			reviewActsAsApprove: false,
   623  			githubLinkURL:       &url.URL{Scheme: "https", Host: "github.com"},
   624  
   625  			expectDelete:  true,
   626  			expectToggle:  false,
   627  			expectComment: true,
   628  			expectedComment: `[APPROVALNOTIFIER] This PR is **APPROVED**
   629  
   630  Approval requirements bypassed by manually added approval.
   631  
   632  This pull-request has been approved by:
   633  
   634  The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=org%2Frepo).
   635  
   636  The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)
   637  
   638  <details >
   639  Needs approval from an approver in each of these files:
   640  
   641  - **[a/OWNERS](https://github.com/org/repo/blob/master/a/OWNERS)**
   642  
   643  Approvers can indicate their approval by writing ` + "`/approve`" + ` in a comment
   644  Approvers can cancel approval by writing ` + "`/approve cancel`" + ` in a comment
   645  </details>
   646  <!-- META={"approvers":["alice"]} -->`,
   647  		},
   648  		{
   649  			name:     "lgtm means approve",
   650  			prBody:   "This is a great PR that will fix\nlots of things!",
   651  			hasLabel: false,
   652  			files:    []string{"a/a.go", "a/aa.go"},
   653  			comments: []github.IssueComment{
   654  				newTestComment("k8s-ci-robot", "[APPROVALNOTIFIER] This PR is **NOT APPROVED**\n\nblah"),
   655  				newTestCommentTime(time.Now(), "alice", "stuff\n/lgtm\nblah"),
   656  			},
   657  			reviews:             []github.Review{},
   658  			selfApprove:         false,
   659  			needsIssue:          false,
   660  			lgtmActsAsApprove:   true,
   661  			reviewActsAsApprove: false,
   662  			githubLinkURL:       &url.URL{Scheme: "https", Host: "github.com"},
   663  
   664  			expectDelete:  true,
   665  			expectToggle:  true,
   666  			expectComment: true,
   667  		},
   668  		{
   669  			name:     "lgtm does not mean approve",
   670  			prBody:   "This is a great PR that will fix\nlots of things!",
   671  			hasLabel: false,
   672  			files:    []string{"a/a.go", "a/aa.go"},
   673  			comments: []github.IssueComment{
   674  				newTestComment("k8s-ci-robot", `[APPROVALNOTIFIER] This PR is **NOT APPROVED**
   675  
   676  This pull-request has been approved by:
   677  **Once this PR has been reviewed and has the lgtm label**, please assign [alice](https://github.com/alice) for approval. For more information see [the Kubernetes Code Review Process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process).
   678  
   679  The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=org%2Frepo).
   680  
   681  <details open>
   682  Needs approval from an approver in each of these files:
   683  
   684  - **[a/OWNERS](https://github.com/org/repo/blob/master/a/OWNERS)**
   685  
   686  Approvers can indicate their approval by writing `+"`/approve`"+` in a comment
   687  Approvers can cancel approval by writing `+"`/approve cancel`"+` in a comment
   688  </details>
   689  <!-- META={"approvers":["alice"]} -->`),
   690  				newTestCommentTime(time.Now(), "alice", "stuff\n/lgtm\nblah"),
   691  			},
   692  			reviews:             []github.Review{},
   693  			selfApprove:         false,
   694  			needsIssue:          false,
   695  			lgtmActsAsApprove:   false,
   696  			reviewActsAsApprove: false,
   697  			githubLinkURL:       &url.URL{Scheme: "https", Host: "github.com"},
   698  
   699  			expectDelete:  false,
   700  			expectToggle:  false,
   701  			expectComment: false,
   702  		},
   703  		{
   704  			name:                "approve in review body with empty state",
   705  			hasLabel:            false,
   706  			files:               []string{"a/a.go"},
   707  			comments:            []github.IssueComment{},
   708  			reviews:             []github.Review{newTestReview("Alice", "stuff\n/approve", "")},
   709  			selfApprove:         false,
   710  			needsIssue:          false,
   711  			lgtmActsAsApprove:   false,
   712  			reviewActsAsApprove: false,
   713  			githubLinkURL:       &url.URL{Scheme: "https", Host: "github.com"},
   714  
   715  			expectDelete:  false,
   716  			expectToggle:  true,
   717  			expectComment: true,
   718  			expectedComment: `[APPROVALNOTIFIER] This PR is **APPROVED**
   719  
   720  This pull-request has been approved by: *<a href="" title="Approved">Alice</a>*
   721  
   722  The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=org%2Frepo).
   723  
   724  The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)
   725  
   726  <details >
   727  Needs approval from an approver in each of these files:
   728  
   729  - ~~[a/OWNERS](https://github.com/org/repo/blob/master/a/OWNERS)~~ [Alice]
   730  
   731  Approvers can indicate their approval by writing ` + "`/approve`" + ` in a comment
   732  Approvers can cancel approval by writing ` + "`/approve cancel`" + ` in a comment
   733  </details>
   734  <!-- META={"approvers":[]} -->`,
   735  		},
   736  		{
   737  			name:                "approved review but reviewActsAsApprove disabled",
   738  			hasLabel:            false,
   739  			files:               []string{"c/c.go"},
   740  			comments:            []github.IssueComment{},
   741  			reviews:             []github.Review{newTestReview("cjwagner", "stuff", github.ReviewStateApproved)},
   742  			selfApprove:         false,
   743  			needsIssue:          false,
   744  			lgtmActsAsApprove:   false,
   745  			reviewActsAsApprove: false,
   746  			githubLinkURL:       &url.URL{Scheme: "https", Host: "github.com"},
   747  
   748  			expectDelete:  false,
   749  			expectToggle:  false,
   750  			expectComment: true,
   751  			expectedComment: `[APPROVALNOTIFIER] This PR is **NOT APPROVED**
   752  
   753  This pull-request has been approved by:
   754  **Once this PR has been reviewed and has the lgtm label**, please ask for approval from [cjwagner](https://github.com/cjwagner). For more information see [the Kubernetes Code Review Process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process).
   755  
   756  The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=org%2Frepo).
   757  
   758  <details open>
   759  Needs approval from an approver in each of these files:
   760  
   761  - **[c/OWNERS](https://github.com/org/repo/blob/master/c/OWNERS)**
   762  
   763  Approvers can indicate their approval by writing ` + "`/approve`" + ` in a comment
   764  Approvers can cancel approval by writing ` + "`/approve cancel`" + ` in a comment
   765  </details>
   766  <!-- META={"approvers":["cjwagner"]} -->`,
   767  		},
   768  		{
   769  			name:                "approved review with reviewActsAsApprove enabled",
   770  			hasLabel:            false,
   771  			files:               []string{"a/a.go"},
   772  			comments:            []github.IssueComment{},
   773  			reviews:             []github.Review{newTestReview("Alice", "stuff", github.ReviewStateApproved)},
   774  			selfApprove:         false,
   775  			needsIssue:          false,
   776  			lgtmActsAsApprove:   false,
   777  			reviewActsAsApprove: true,
   778  			githubLinkURL:       &url.URL{Scheme: "https", Host: "github.com"},
   779  
   780  			expectDelete:  false,
   781  			expectToggle:  true,
   782  			expectComment: true,
   783  			expectedComment: `[APPROVALNOTIFIER] This PR is **APPROVED**
   784  
   785  This pull-request has been approved by: *<a href="" title="Approved">Alice</a>*
   786  
   787  The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=org%2Frepo).
   788  
   789  The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)
   790  
   791  <details >
   792  Needs approval from an approver in each of these files:
   793  
   794  - ~~[a/OWNERS](https://github.com/org/repo/blob/master/a/OWNERS)~~ [Alice]
   795  
   796  Approvers can indicate their approval by writing ` + "`/approve`" + ` in a comment
   797  Approvers can cancel approval by writing ` + "`/approve cancel`" + ` in a comment
   798  </details>
   799  <!-- META={"approvers":[]} -->`,
   800  		},
   801  		{
   802  			name:     "reviews in non-approving state (should not approve)",
   803  			hasLabel: false,
   804  			files:    []string{"c/c.go"},
   805  			comments: []github.IssueComment{},
   806  			reviews: []github.Review{
   807  				newTestReview("cjwagner", "stuff", "COMMENTED"),
   808  				newTestReview("cjwagner", "unsubmitted stuff", "PENDING"),
   809  				newTestReview("cjwagner", "dismissed stuff", "DISMISSED"),
   810  			},
   811  			selfApprove:         false,
   812  			needsIssue:          false,
   813  			lgtmActsAsApprove:   false,
   814  			reviewActsAsApprove: true,
   815  			githubLinkURL:       &url.URL{Scheme: "https", Host: "github.com"},
   816  
   817  			expectDelete:  false,
   818  			expectToggle:  false,
   819  			expectComment: true,
   820  			expectedComment: `[APPROVALNOTIFIER] This PR is **NOT APPROVED**
   821  
   822  This pull-request has been approved by:
   823  **Once this PR has been reviewed and has the lgtm label**, please ask for approval from [cjwagner](https://github.com/cjwagner). For more information see [the Kubernetes Code Review Process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process).
   824  
   825  The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=org%2Frepo).
   826  
   827  <details open>
   828  Needs approval from an approver in each of these files:
   829  
   830  - **[c/OWNERS](https://github.com/org/repo/blob/master/c/OWNERS)**
   831  
   832  Approvers can indicate their approval by writing ` + "`/approve`" + ` in a comment
   833  Approvers can cancel approval by writing ` + "`/approve cancel`" + ` in a comment
   834  </details>
   835  <!-- META={"approvers":["cjwagner"]} -->`,
   836  		},
   837  		{
   838  			name:     "review in request changes state means cancel",
   839  			hasLabel: true,
   840  			files:    []string{"c/c.go"},
   841  			comments: []github.IssueComment{
   842  				newTestCommentTime(time.Now().Add(time.Hour), "k8s-ci-robot", "[APPROVALNOTIFIER] This PR is **APPROVED**\n\nblah"), // second
   843  			},
   844  			reviews: []github.Review{
   845  				newTestReviewTime(time.Now(), "cjwagner", "yep", github.ReviewStateApproved),                           // first
   846  				newTestReviewTime(time.Now().Add(time.Hour*2), "cjwagner", "nope", github.ReviewStateChangesRequested), // third
   847  			},
   848  			selfApprove:         false,
   849  			needsIssue:          false,
   850  			lgtmActsAsApprove:   false,
   851  			reviewActsAsApprove: true,
   852  			githubLinkURL:       &url.URL{Scheme: "https", Host: "github.com"},
   853  
   854  			expectDelete:  true,
   855  			expectToggle:  true,
   856  			expectComment: true,
   857  			expectedComment: `[APPROVALNOTIFIER] This PR is **NOT APPROVED**
   858  
   859  This pull-request has been approved by:
   860  **Once this PR has been reviewed and has the lgtm label**, please ask for approval from [cjwagner](https://github.com/cjwagner). For more information see [the Kubernetes Code Review Process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process).
   861  
   862  The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=org%2Frepo).
   863  
   864  <details open>
   865  Needs approval from an approver in each of these files:
   866  
   867  - **[c/OWNERS](https://github.com/org/repo/blob/master/c/OWNERS)**
   868  
   869  Approvers can indicate their approval by writing ` + "`/approve`" + ` in a comment
   870  Approvers can cancel approval by writing ` + "`/approve cancel`" + ` in a comment
   871  </details>
   872  <!-- META={"approvers":["cjwagner"]} -->`,
   873  		},
   874  		{
   875  			name:     "dismissed review doesn't cancel prior approval",
   876  			hasLabel: true,
   877  			files:    []string{"a/a.go"},
   878  			comments: []github.IssueComment{
   879  				newTestCommentTime(time.Now().Add(time.Hour), "k8s-ci-robot", "[APPROVALNOTIFIER] This PR is **APPROVED**\n\nblah"), // second
   880  			},
   881  			reviews: []github.Review{
   882  				newTestReviewTime(time.Now(), "Alice", "yep", github.ReviewStateApproved),                         // first
   883  				newTestReviewTime(time.Now().Add(time.Hour*2), "Alice", "dismissed", github.ReviewStateDismissed), // third
   884  			},
   885  			selfApprove:         false,
   886  			needsIssue:          false,
   887  			lgtmActsAsApprove:   false,
   888  			reviewActsAsApprove: true,
   889  			githubLinkURL:       &url.URL{Scheme: "https", Host: "github.com"},
   890  
   891  			expectDelete:  true,
   892  			expectToggle:  false,
   893  			expectComment: true,
   894  			expectedComment: `[APPROVALNOTIFIER] This PR is **APPROVED**
   895  
   896  This pull-request has been approved by: *<a href="" title="Approved">Alice</a>*
   897  
   898  The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=org%2Frepo).
   899  
   900  The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)
   901  
   902  <details >
   903  Needs approval from an approver in each of these files:
   904  
   905  - ~~[a/OWNERS](https://github.com/org/repo/blob/master/a/OWNERS)~~ [Alice]
   906  
   907  Approvers can indicate their approval by writing ` + "`/approve`" + ` in a comment
   908  Approvers can cancel approval by writing ` + "`/approve cancel`" + ` in a comment
   909  </details>
   910  <!-- META={"approvers":[]} -->`,
   911  		},
   912  		{
   913  			name:     "approve cancel command supersedes earlier approved review",
   914  			hasLabel: true,
   915  			files:    []string{"c/c.go"},
   916  			comments: []github.IssueComment{
   917  				newTestCommentTime(time.Now().Add(time.Hour), "k8s-ci-robot", "[APPROVALNOTIFIER] This PR is **APPROVED**\n\nblah"), // second
   918  				newTestCommentTime(time.Now().Add(time.Hour*2), "cjwagner", "stuff\n/approve cancel \nmore stuff"),                  // third
   919  			},
   920  			reviews: []github.Review{
   921  				newTestReviewTime(time.Now(), "cjwagner", "yep", github.ReviewStateApproved), // first
   922  			},
   923  			selfApprove:         false,
   924  			needsIssue:          false,
   925  			lgtmActsAsApprove:   false,
   926  			reviewActsAsApprove: true,
   927  			githubLinkURL:       &url.URL{Scheme: "https", Host: "github.com"},
   928  
   929  			expectDelete:  true,
   930  			expectToggle:  true,
   931  			expectComment: true,
   932  			expectedComment: `[APPROVALNOTIFIER] This PR is **NOT APPROVED**
   933  
   934  This pull-request has been approved by:
   935  **Once this PR has been reviewed and has the lgtm label**, please ask for approval from [cjwagner](https://github.com/cjwagner). For more information see [the Kubernetes Code Review Process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process).
   936  
   937  The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=org%2Frepo).
   938  
   939  <details open>
   940  Needs approval from an approver in each of these files:
   941  
   942  - **[c/OWNERS](https://github.com/org/repo/blob/master/c/OWNERS)**
   943  
   944  Approvers can indicate their approval by writing ` + "`/approve`" + ` in a comment
   945  Approvers can cancel approval by writing ` + "`/approve cancel`" + ` in a comment
   946  </details>
   947  <!-- META={"approvers":["cjwagner"]} -->`,
   948  		},
   949  		{
   950  			name:     "remove-approve command supersedes earlier approved review",
   951  			hasLabel: true,
   952  			files:    []string{"c/c.go"},
   953  			comments: []github.IssueComment{
   954  				newTestCommentTime(time.Now().Add(time.Hour), "k8s-ci-robot", "[APPROVALNOTIFIER] This PR is **APPROVED**\n\nblah"), // second
   955  				newTestCommentTime(time.Now().Add(time.Hour*2), "cjwagner", "stuff\n/remove-approve \nmore stuff"),                  // third
   956  			},
   957  			reviews: []github.Review{
   958  				newTestReviewTime(time.Now(), "cjwagner", "yep", github.ReviewStateApproved), // first
   959  			},
   960  			selfApprove:         false,
   961  			needsIssue:          false,
   962  			lgtmActsAsApprove:   false,
   963  			reviewActsAsApprove: true,
   964  			githubLinkURL:       &url.URL{Scheme: "https", Host: "github.com"},
   965  
   966  			expectDelete:  true,
   967  			expectToggle:  true,
   968  			expectComment: true,
   969  			expectedComment: `[APPROVALNOTIFIER] This PR is **NOT APPROVED**
   970  
   971  This pull-request has been approved by:
   972  **Once this PR has been reviewed and has the lgtm label**, please ask for approval from [cjwagner](https://github.com/cjwagner). For more information see [the Kubernetes Code Review Process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process).
   973  
   974  The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=org%2Frepo).
   975  
   976  <details open>
   977  Needs approval from an approver in each of these files:
   978  
   979  - **[c/OWNERS](https://github.com/org/repo/blob/master/c/OWNERS)**
   980  
   981  Approvers can indicate their approval by writing ` + "`/approve`" + ` in a comment
   982  Approvers can cancel approval by writing ` + "`/approve cancel`" + ` in a comment
   983  </details>
   984  <!-- META={"approvers":["cjwagner"]} -->`,
   985  		},
   986  		{
   987  			name:     "approve cancel command supersedes simultaneous approved review",
   988  			hasLabel: false,
   989  			files:    []string{"a/a.go", "c/c.go"},
   990  			comments: []github.IssueComment{},
   991  			reviews: []github.Review{
   992  				newTestReview("cjwagner", "/approve cancel", github.ReviewStateApproved),
   993  			},
   994  			selfApprove:         false,
   995  			needsIssue:          false,
   996  			lgtmActsAsApprove:   false,
   997  			reviewActsAsApprove: true,
   998  			githubLinkURL:       &url.URL{Scheme: "https", Host: "github.com"},
   999  
  1000  			expectDelete:  false,
  1001  			expectToggle:  false,
  1002  			expectComment: true,
  1003  			expectedComment: `[APPROVALNOTIFIER] This PR is **NOT APPROVED**
  1004  
  1005  This pull-request has been approved by:
  1006  **Once this PR has been reviewed and has the lgtm label**, please ask for approval from [cjwagner](https://github.com/cjwagner) and additionally assign [alice](https://github.com/alice) for approval. For more information see [the Kubernetes Code Review Process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process).
  1007  
  1008  The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=org%2Frepo).
  1009  
  1010  <details open>
  1011  Needs approval from an approver in each of these files:
  1012  
  1013  - **[a/OWNERS](https://github.com/org/repo/blob/master/a/OWNERS)**
  1014  - **[c/OWNERS](https://github.com/org/repo/blob/master/c/OWNERS)**
  1015  
  1016  Approvers can indicate their approval by writing ` + "`/approve`" + ` in a comment
  1017  Approvers can cancel approval by writing ` + "`/approve cancel`" + ` in a comment
  1018  </details>
  1019  <!-- META={"approvers":["alice","cjwagner"]} -->`,
  1020  		},
  1021  		{
  1022  			name:     "remove-approve command supersedes simultaneous approved review",
  1023  			hasLabel: false,
  1024  			files:    []string{"a/a.go", "c/c.go"},
  1025  			comments: []github.IssueComment{},
  1026  			reviews: []github.Review{
  1027  				newTestReview("cjwagner", "/remove-approve", github.ReviewStateApproved),
  1028  			},
  1029  			selfApprove:         false,
  1030  			needsIssue:          false,
  1031  			lgtmActsAsApprove:   false,
  1032  			reviewActsAsApprove: true,
  1033  			githubLinkURL:       &url.URL{Scheme: "https", Host: "github.com"},
  1034  
  1035  			expectDelete:  false,
  1036  			expectToggle:  false,
  1037  			expectComment: true,
  1038  			expectedComment: `[APPROVALNOTIFIER] This PR is **NOT APPROVED**
  1039  
  1040  This pull-request has been approved by:
  1041  **Once this PR has been reviewed and has the lgtm label**, please ask for approval from [cjwagner](https://github.com/cjwagner) and additionally assign [alice](https://github.com/alice) for approval. For more information see [the Kubernetes Code Review Process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process).
  1042  
  1043  The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=org%2Frepo).
  1044  
  1045  <details open>
  1046  Needs approval from an approver in each of these files:
  1047  
  1048  - **[a/OWNERS](https://github.com/org/repo/blob/master/a/OWNERS)**
  1049  - **[c/OWNERS](https://github.com/org/repo/blob/master/c/OWNERS)**
  1050  
  1051  Approvers can indicate their approval by writing ` + "`/approve`" + ` in a comment
  1052  Approvers can cancel approval by writing ` + "`/approve cancel`" + ` in a comment
  1053  </details>
  1054  <!-- META={"approvers":["alice","cjwagner"]} -->`,
  1055  		},
  1056  		{
  1057  			name:                "approve command supersedes simultaneous changes requested review",
  1058  			hasLabel:            false,
  1059  			files:               []string{"a/a.go"},
  1060  			comments:            []github.IssueComment{},
  1061  			reviews:             []github.Review{newTestReview("Alice", "/approve", github.ReviewStateChangesRequested)},
  1062  			selfApprove:         false,
  1063  			needsIssue:          false,
  1064  			lgtmActsAsApprove:   false,
  1065  			reviewActsAsApprove: true,
  1066  			githubLinkURL:       &url.URL{Scheme: "https", Host: "github.com"},
  1067  
  1068  			expectDelete:  false,
  1069  			expectToggle:  true,
  1070  			expectComment: true,
  1071  			expectedComment: `[APPROVALNOTIFIER] This PR is **APPROVED**
  1072  
  1073  This pull-request has been approved by: *<a href="" title="Approved">Alice</a>*
  1074  
  1075  The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=org%2Frepo).
  1076  
  1077  The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)
  1078  
  1079  <details >
  1080  Needs approval from an approver in each of these files:
  1081  
  1082  - ~~[a/OWNERS](https://github.com/org/repo/blob/master/a/OWNERS)~~ [Alice]
  1083  
  1084  Approvers can indicate their approval by writing ` + "`/approve`" + ` in a comment
  1085  Approvers can cancel approval by writing ` + "`/approve cancel`" + ` in a comment
  1086  </details>
  1087  <!-- META={"approvers":[]} -->`,
  1088  		},
  1089  		{
  1090  			name:                "different branch, initial notification (approved)",
  1091  			branch:              "dev",
  1092  			hasLabel:            false,
  1093  			files:               []string{"c/c.go"},
  1094  			comments:            []github.IssueComment{},
  1095  			reviews:             []github.Review{},
  1096  			selfApprove:         true,
  1097  			needsIssue:          false,
  1098  			lgtmActsAsApprove:   false,
  1099  			reviewActsAsApprove: false,
  1100  			githubLinkURL:       &url.URL{Scheme: "https", Host: "github.com"},
  1101  
  1102  			expectDelete:  false,
  1103  			expectToggle:  true,
  1104  			expectComment: true,
  1105  			expectedComment: `[APPROVALNOTIFIER] This PR is **APPROVED**
  1106  
  1107  This pull-request has been approved by: *<a href="#" title="Author self-approved">cjwagner</a>*
  1108  
  1109  The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=org%2Frepo).
  1110  
  1111  The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)
  1112  
  1113  <details >
  1114  Needs approval from an approver in each of these files:
  1115  
  1116  - ~~[c/OWNERS](https://github.com/org/repo/blob/dev/c/OWNERS)~~ [cjwagner]
  1117  
  1118  Approvers can indicate their approval by writing ` + "`/approve`" + ` in a comment
  1119  Approvers can cancel approval by writing ` + "`/approve cancel`" + ` in a comment
  1120  </details>
  1121  <!-- META={"approvers":[]} -->`,
  1122  		},
  1123  		{
  1124  			name:                "different GitHub link URL",
  1125  			branch:              "dev",
  1126  			hasLabel:            false,
  1127  			files:               []string{"c/c.go"},
  1128  			comments:            []github.IssueComment{},
  1129  			reviews:             []github.Review{},
  1130  			selfApprove:         true,
  1131  			needsIssue:          false,
  1132  			lgtmActsAsApprove:   false,
  1133  			reviewActsAsApprove: false,
  1134  			githubLinkURL:       &url.URL{Scheme: "https", Host: "github.mycorp.com"},
  1135  
  1136  			expectDelete:  false,
  1137  			expectToggle:  true,
  1138  			expectComment: true,
  1139  			expectedComment: `[APPROVALNOTIFIER] This PR is **APPROVED**
  1140  
  1141  This pull-request has been approved by: *<a href="#" title="Author self-approved">cjwagner</a>*
  1142  
  1143  The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=org%2Frepo).
  1144  
  1145  The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)
  1146  
  1147  <details >
  1148  Needs approval from an approver in each of these files:
  1149  
  1150  - ~~[c/OWNERS](https://github.mycorp.com/org/repo/blob/dev/c/OWNERS)~~ [cjwagner]
  1151  
  1152  Approvers can indicate their approval by writing ` + "`/approve`" + ` in a comment
  1153  Approvers can cancel approval by writing ` + "`/approve cancel`" + ` in a comment
  1154  </details>
  1155  <!-- META={"approvers":[]} -->`,
  1156  		},
  1157  		{
  1158  			name:                "Approved because of AutoApproveUnownedSubfolders:",
  1159  			hasLabel:            true,
  1160  			humanApproved:       true,
  1161  			files:               []string{"d/new-folder/new_file.go"},
  1162  			selfApprove:         false,
  1163  			needsIssue:          false,
  1164  			lgtmActsAsApprove:   false,
  1165  			reviewActsAsApprove: false,
  1166  			githubLinkURL:       &url.URL{Scheme: "https", Host: "github.com"},
  1167  
  1168  			expectDelete:  false,
  1169  			expectToggle:  false,
  1170  			expectComment: true,
  1171  			expectedComment: `[APPROVALNOTIFIER] This PR is **APPROVED**
  1172  
  1173  This pull-request has been approved by:
  1174  
  1175  The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=org%2Frepo).
  1176  
  1177  The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)
  1178  
  1179  <details >
  1180  Needs approval from an approver in each of these files:
  1181  
  1182  
  1183  Approvers can indicate their approval by writing ` + "`/approve`" + ` in a comment
  1184  Approvers can cancel approval by writing ` + "`/approve cancel`" + ` in a comment
  1185  </details>
  1186  <!-- META={"approvers":[]} -->`,
  1187  		},
  1188  		{
  1189  			name:                "don't suggest to /assign already assigned cjwagner",
  1190  			hasLabel:            false,
  1191  			files:               []string{"a/a.go", "c/c.go"},
  1192  			comments:            []github.IssueComment{},
  1193  			reviews:             []github.Review{newTestReview("Alice", "/approve", github.ReviewStateChangesRequested)},
  1194  			selfApprove:         false,
  1195  			needsIssue:          false,
  1196  			lgtmActsAsApprove:   false,
  1197  			reviewActsAsApprove: true,
  1198  			githubLinkURL:       &url.URL{Scheme: "https", Host: "github.com"},
  1199  
  1200  			expectDelete:  false,
  1201  			expectToggle:  false,
  1202  			expectComment: true,
  1203  			expectedComment: `[APPROVALNOTIFIER] This PR is **NOT APPROVED**
  1204  
  1205  This pull-request has been approved by: *<a href="" title="Approved">Alice</a>*
  1206  **Once this PR has been reviewed and has the lgtm label**, please ask for approval from [cjwagner](https://github.com/cjwagner). For more information see [the Kubernetes Code Review Process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process).
  1207  
  1208  The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=org%2Frepo).
  1209  
  1210  <details open>
  1211  Needs approval from an approver in each of these files:
  1212  
  1213  - ~~[a/OWNERS](https://github.com/org/repo/blob/master/a/OWNERS)~~ [Alice]
  1214  - **[c/OWNERS](https://github.com/org/repo/blob/master/c/OWNERS)**
  1215  
  1216  Approvers can indicate their approval by writing ` + "`/approve`" + ` in a comment
  1217  Approvers can cancel approval by writing ` + "`/approve cancel`" + ` in a comment
  1218  </details>
  1219  <!-- META={"approvers":["cjwagner"]} -->`,
  1220  		},
  1221  	}
  1222  
  1223  	fr := fakeRepo{
  1224  		approvers: map[string]layeredsets.String{
  1225  			"a":   layeredsets.NewString("alice"),
  1226  			"a/b": layeredsets.NewString("alice", "bob"),
  1227  			"c":   layeredsets.NewString("cblecker", "cjwagner"),
  1228  		},
  1229  		leafApprovers: map[string]sets.Set[string]{
  1230  			"a":   sets.New[string]("alice"),
  1231  			"a/b": sets.New[string]("bob"),
  1232  			"c":   sets.New[string]("cblecker", "cjwagner"),
  1233  		},
  1234  		approverOwners: map[string]string{
  1235  			"a/a.go":                   "a",
  1236  			"a/aa.go":                  "a",
  1237  			"a/b/b.go":                 "a/b",
  1238  			"c/c.go":                   "c",
  1239  			"d/new-folder/new_file.go": "d",
  1240  		},
  1241  		autoApproveUnownedSubfolders: map[string]bool{
  1242  			"d": true,
  1243  		},
  1244  	}
  1245  
  1246  	for _, test := range tests {
  1247  		t.Run(test.name, func(t *testing.T) {
  1248  			fghc := newFakeGitHubClient(test.hasLabel, test.humanApproved, test.files, test.comments, test.reviews)
  1249  			branch := "master"
  1250  			if test.branch != "" {
  1251  				branch = test.branch
  1252  			}
  1253  
  1254  			rsa := !test.selfApprove
  1255  			irs := !test.reviewActsAsApprove
  1256  			if err := handle(
  1257  				logrus.WithField("plugin", "approve"),
  1258  				fghc,
  1259  				fr,
  1260  				config.GitHubOptions{
  1261  					LinkURL: test.githubLinkURL,
  1262  				},
  1263  				&plugins.Approve{
  1264  					Repos:               []string{"org/repo"},
  1265  					RequireSelfApproval: &rsa,
  1266  					IssueRequired:       test.needsIssue,
  1267  					LgtmActsAsApprove:   test.lgtmActsAsApprove,
  1268  					IgnoreReviewState:   &irs,
  1269  					CommandHelpLink:     "https://go.k8s.io/bot-commands",
  1270  					PrProcessLink:       "https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process",
  1271  				},
  1272  				&state{
  1273  					org:       "org",
  1274  					repo:      "repo",
  1275  					branch:    branch,
  1276  					number:    prNumber,
  1277  					body:      test.prBody,
  1278  					author:    "cjwagner",
  1279  					assignees: []github.User{{Login: "spxtr"}},
  1280  				},
  1281  			); err != nil {
  1282  				t.Errorf("[%s] Unexpected error handling event: %v.", test.name, err)
  1283  			}
  1284  
  1285  			if test.expectDelete {
  1286  				if len(fghc.IssueCommentsDeleted) != 1 {
  1287  					t.Errorf(
  1288  						"[%s] Expected 1 notification to be deleted but %d notifications were deleted.",
  1289  						test.name,
  1290  						len(fghc.IssueCommentsDeleted),
  1291  					)
  1292  				}
  1293  			} else {
  1294  				if len(fghc.IssueCommentsDeleted) != 0 {
  1295  					t.Errorf(
  1296  						"[%s] Expected 0 notifications to be deleted but %d notification was deleted.",
  1297  						test.name,
  1298  						len(fghc.IssueCommentsDeleted),
  1299  					)
  1300  				}
  1301  			}
  1302  			if test.expectComment {
  1303  				if len(fghc.IssueCommentsAdded) != 1 {
  1304  					t.Errorf(
  1305  						"[%s] Expected 1 notification to be added but %d notifications were added.",
  1306  						test.name,
  1307  						len(fghc.IssueCommentsAdded),
  1308  					)
  1309  				} else if expect, got := fmt.Sprintf("org/repo#%v:", prNumber)+test.expectedComment, fghc.IssueCommentsAdded[0]; test.expectedComment != "" && got != expect {
  1310  					t.Errorf("expected notification differs from actual: %s", cmp.Diff(expect, got))
  1311  				}
  1312  			} else {
  1313  				if len(fghc.IssueCommentsAdded) != 0 {
  1314  					t.Errorf(
  1315  						"[%s] Expected 0 notifications to be added but %d notification was added.",
  1316  						test.name,
  1317  						len(fghc.IssueCommentsAdded),
  1318  					)
  1319  				}
  1320  			}
  1321  
  1322  			labelAdded := false
  1323  			for _, l := range fghc.IssueLabelsAdded {
  1324  				if l == fmt.Sprintf("org/repo#%v:approved", prNumber) {
  1325  					if labelAdded {
  1326  						t.Errorf("[%s] The approved label was applied to a PR that already had it!", test.name)
  1327  					}
  1328  					labelAdded = true
  1329  				}
  1330  			}
  1331  			if test.hasLabel {
  1332  				labelAdded = false
  1333  			}
  1334  			toggled := labelAdded
  1335  			for _, l := range fghc.IssueLabelsRemoved {
  1336  				if l == fmt.Sprintf("org/repo#%v:approved", prNumber) {
  1337  					if !test.hasLabel {
  1338  						t.Errorf("[%s] The approved label was removed from a PR that doesn't have it!", test.name)
  1339  					}
  1340  					toggled = true
  1341  				}
  1342  			}
  1343  			if test.expectToggle != toggled {
  1344  				t.Errorf(
  1345  					"[%s] Expected 'approved' label toggled: %t, but got %t.",
  1346  					test.name,
  1347  					test.expectToggle,
  1348  					toggled,
  1349  				)
  1350  			}
  1351  		})
  1352  	}
  1353  }
  1354  
  1355  // TODO: cache approvers 'GetFilesApprovers' and 'GetCCs' since these are called repeatedly and are
  1356  // expensive.
  1357  
  1358  type fakeOwnersClient struct{}
  1359  
  1360  func (foc fakeOwnersClient) LoadRepoOwners(org, repo, base string) (repoowners.RepoOwner, error) {
  1361  	return fakeRepoOwners{}, nil
  1362  }
  1363  
  1364  type fakeRepoOwners struct {
  1365  	fakeRepo
  1366  }
  1367  
  1368  func (fro fakeRepoOwners) AllApprovers() sets.Set[string] {
  1369  	return sets.Set[string]{}
  1370  }
  1371  
  1372  func (fro fakeRepoOwners) AllOwners() sets.Set[string] {
  1373  	return sets.Set[string]{}
  1374  }
  1375  
  1376  func (fro fakeRepoOwners) AllReviewers() sets.Set[string] {
  1377  	return sets.Set[string]{}
  1378  }
  1379  
  1380  func (fro fakeRepoOwners) FindLabelsForFile(path string) sets.Set[string] {
  1381  	return sets.New[string]()
  1382  }
  1383  
  1384  func (fro fakeRepoOwners) FindReviewersOwnersForFile(path string) string {
  1385  	return ""
  1386  }
  1387  
  1388  func (fro fakeRepoOwners) LeafReviewers(path string) sets.Set[string] {
  1389  	return sets.New[string]()
  1390  }
  1391  
  1392  func (fro fakeRepoOwners) Reviewers(path string) layeredsets.String {
  1393  	return layeredsets.NewString()
  1394  }
  1395  
  1396  func (fro fakeRepoOwners) RequiredReviewers(path string) sets.Set[string] {
  1397  	return sets.New[string]()
  1398  }
  1399  
  1400  func TestHandleGenericComment(t *testing.T) {
  1401  	tests := []struct {
  1402  		name              string
  1403  		commentEvent      github.GenericCommentEvent
  1404  		lgtmActsAsApprove bool
  1405  		expectHandle      bool
  1406  		expectState       *state
  1407  	}{
  1408  		{
  1409  			name: "valid approve command",
  1410  			commentEvent: github.GenericCommentEvent{
  1411  				Action: github.GenericCommentActionCreated,
  1412  				IsPR:   true,
  1413  				Body:   "/approve",
  1414  				Number: 1,
  1415  				User: github.User{
  1416  					Login: "author",
  1417  				},
  1418  				IssueBody: "Fix everything",
  1419  				IssueAuthor: github.User{
  1420  					Login: "P.R. Author",
  1421  				},
  1422  			},
  1423  			expectHandle: true,
  1424  			expectState: &state{
  1425  				org:       "org",
  1426  				repo:      "repo",
  1427  				branch:    "branch",
  1428  				number:    1,
  1429  				body:      "Fix everything",
  1430  				author:    "P.R. Author",
  1431  				assignees: nil,
  1432  				htmlURL:   "",
  1433  			},
  1434  		},
  1435  		{
  1436  			name: "not comment created",
  1437  			commentEvent: github.GenericCommentEvent{
  1438  				Action: github.GenericCommentActionEdited,
  1439  				IsPR:   true,
  1440  				Body:   "/approve",
  1441  				Number: 1,
  1442  				User: github.User{
  1443  					Login: "author",
  1444  				},
  1445  			},
  1446  			expectHandle: false,
  1447  		},
  1448  		{
  1449  			name: "not PR",
  1450  			commentEvent: github.GenericCommentEvent{
  1451  				Action: github.GenericCommentActionEdited,
  1452  				IsPR:   false,
  1453  				Body:   "/approve",
  1454  				Number: 1,
  1455  				User: github.User{
  1456  					Login: "author",
  1457  				},
  1458  			},
  1459  			expectHandle: false,
  1460  		},
  1461  		{
  1462  			name: "closed PR",
  1463  			commentEvent: github.GenericCommentEvent{
  1464  				Action: github.GenericCommentActionCreated,
  1465  				IsPR:   true,
  1466  				Body:   "/approve",
  1467  				Number: 1,
  1468  				User: github.User{
  1469  					Login: "author",
  1470  				},
  1471  				IssueState: "closed",
  1472  			},
  1473  			expectHandle: false,
  1474  		},
  1475  		{
  1476  			name: "no approve command",
  1477  			commentEvent: github.GenericCommentEvent{
  1478  				Action: github.GenericCommentActionCreated,
  1479  				IsPR:   true,
  1480  				Body:   "stuff",
  1481  				Number: 1,
  1482  				User: github.User{
  1483  					Login: "author",
  1484  				},
  1485  			},
  1486  			expectHandle: false,
  1487  		},
  1488  		{
  1489  			name: "lgtm without lgtmActsAsApprove",
  1490  			commentEvent: github.GenericCommentEvent{
  1491  				Action: github.GenericCommentActionCreated,
  1492  				IsPR:   true,
  1493  				Body:   "/lgtm",
  1494  				Number: 1,
  1495  				User: github.User{
  1496  					Login: "author",
  1497  				},
  1498  			},
  1499  			expectHandle: false,
  1500  		},
  1501  		{
  1502  			name: "lgtm with lgtmActsAsApprove",
  1503  			commentEvent: github.GenericCommentEvent{
  1504  				Action: github.GenericCommentActionCreated,
  1505  				IsPR:   true,
  1506  				Body:   "/lgtm",
  1507  				Number: 1,
  1508  				User: github.User{
  1509  					Login: "author",
  1510  				},
  1511  			},
  1512  			lgtmActsAsApprove: true,
  1513  			expectHandle:      true,
  1514  		},
  1515  	}
  1516  
  1517  	var handled bool
  1518  	var gotState *state
  1519  	handleFunc = func(log *logrus.Entry, ghc githubClient, repo approvers.Repo, githubConfig config.GitHubOptions, opts *plugins.Approve, pr *state) error {
  1520  		gotState = pr
  1521  		handled = true
  1522  		return nil
  1523  	}
  1524  	defer func() {
  1525  		handleFunc = handle
  1526  	}()
  1527  
  1528  	repo := github.Repo{
  1529  		Owner: github.User{
  1530  			Login: "org",
  1531  		},
  1532  		Name: "repo",
  1533  	}
  1534  	pr := github.PullRequest{
  1535  		Base: github.PullRequestBranch{
  1536  			Ref: "branch",
  1537  		},
  1538  		Number: 1,
  1539  	}
  1540  	fghc := fakegithub.NewFakeClient()
  1541  	fghc.PullRequests = map[int]*github.PullRequest{1: &pr}
  1542  
  1543  	for _, test := range tests {
  1544  		test.commentEvent.Repo = repo
  1545  		githubConfig := config.GitHubOptions{
  1546  			LinkURL: &url.URL{
  1547  				Scheme: "https",
  1548  				Host:   "github.com",
  1549  			},
  1550  		}
  1551  		config := &plugins.Configuration{}
  1552  		config.Approve = append(config.Approve, plugins.Approve{
  1553  			Repos:             []string{test.commentEvent.Repo.Owner.Login},
  1554  			LgtmActsAsApprove: test.lgtmActsAsApprove,
  1555  		})
  1556  		err := handleGenericComment(
  1557  			logrus.WithField("plugin", "approve"),
  1558  			fghc,
  1559  			fakeOwnersClient{},
  1560  			githubConfig,
  1561  			config,
  1562  			&test.commentEvent,
  1563  		)
  1564  
  1565  		if test.expectHandle && !handled {
  1566  			t.Errorf("%s: expected call to handleFunc, but it wasn't called", test.name)
  1567  		}
  1568  
  1569  		if !test.expectHandle && handled {
  1570  			t.Errorf("%s: expected no call to handleFunc, but it was called", test.name)
  1571  		}
  1572  
  1573  		if test.expectState != nil && !reflect.DeepEqual(test.expectState, gotState) {
  1574  			t.Errorf("%s: expected PR state to equal: %#v, but got: %#v", test.name, test.expectState, gotState)
  1575  		}
  1576  
  1577  		if err != nil {
  1578  			t.Errorf("%s: error calling handleGenericComment: %v", test.name, err)
  1579  		}
  1580  		handled = false
  1581  	}
  1582  }
  1583  
  1584  // GitHub webhooks send state as lowercase, so force it to lowercase here.
  1585  func stateToLower(s github.ReviewState) github.ReviewState {
  1586  	return github.ReviewState(strings.ToLower(string(s)))
  1587  }
  1588  
  1589  func TestHandleReview(t *testing.T) {
  1590  	tests := []struct {
  1591  		name                string
  1592  		reviewEvent         github.ReviewEvent
  1593  		lgtmActsAsApprove   bool
  1594  		reviewActsAsApprove bool
  1595  		expectHandle        bool
  1596  		expectState         *state
  1597  	}{
  1598  		{
  1599  			name: "approved state",
  1600  			reviewEvent: github.ReviewEvent{
  1601  				Action: github.ReviewActionSubmitted,
  1602  				Review: github.Review{
  1603  					Body: "looks good",
  1604  					User: github.User{
  1605  						Login: "author",
  1606  					},
  1607  					State: stateToLower(github.ReviewStateApproved),
  1608  				},
  1609  			},
  1610  			reviewActsAsApprove: true,
  1611  			expectHandle:        true,
  1612  			expectState: &state{
  1613  				org:       "org",
  1614  				repo:      "repo",
  1615  				branch:    "branch",
  1616  				number:    1,
  1617  				body:      "Fix everything",
  1618  				author:    "P.R. Author",
  1619  				assignees: nil,
  1620  				htmlURL:   "",
  1621  			},
  1622  		},
  1623  		{
  1624  			name: "changes requested state",
  1625  			reviewEvent: github.ReviewEvent{
  1626  				Action: github.ReviewActionSubmitted,
  1627  				Review: github.Review{
  1628  					Body: "looks bad",
  1629  					User: github.User{
  1630  						Login: "author",
  1631  					},
  1632  					State: stateToLower(github.ReviewStateChangesRequested),
  1633  				},
  1634  			},
  1635  			reviewActsAsApprove: true,
  1636  			expectHandle:        true,
  1637  		},
  1638  		{
  1639  			name: "pending review state",
  1640  			reviewEvent: github.ReviewEvent{
  1641  				Action: github.ReviewActionSubmitted,
  1642  				Review: github.Review{
  1643  					Body: "looks good",
  1644  					User: github.User{
  1645  						Login: "author",
  1646  					},
  1647  					State: stateToLower(github.ReviewStatePending),
  1648  				},
  1649  			},
  1650  			reviewActsAsApprove: true,
  1651  			expectHandle:        false,
  1652  		},
  1653  		{
  1654  			name: "edited review",
  1655  			reviewEvent: github.ReviewEvent{
  1656  				Action: github.ReviewActionEdited,
  1657  				Review: github.Review{
  1658  					Body: "looks good",
  1659  					User: github.User{
  1660  						Login: "author",
  1661  					},
  1662  					State: stateToLower(github.ReviewStateApproved),
  1663  				},
  1664  			},
  1665  			reviewActsAsApprove: true,
  1666  			expectHandle:        false,
  1667  		},
  1668  		{
  1669  			name: "dismissed review",
  1670  			reviewEvent: github.ReviewEvent{
  1671  				Action: github.ReviewActionDismissed,
  1672  				Review: github.Review{
  1673  					Body: "looks good",
  1674  					User: github.User{
  1675  						Login: "author",
  1676  					},
  1677  					State: stateToLower(github.ReviewStateDismissed),
  1678  				},
  1679  			},
  1680  			reviewActsAsApprove: true,
  1681  			expectHandle:        true,
  1682  		},
  1683  		{
  1684  			name: "approve command",
  1685  			reviewEvent: github.ReviewEvent{
  1686  				Action: github.ReviewActionSubmitted,
  1687  				Review: github.Review{
  1688  					Body: "/approve",
  1689  					User: github.User{
  1690  						Login: "author",
  1691  					},
  1692  					State: stateToLower(github.ReviewStateApproved),
  1693  				},
  1694  			},
  1695  			reviewActsAsApprove: true,
  1696  			expectHandle:        false,
  1697  		},
  1698  		{
  1699  			name: "lgtm command",
  1700  			reviewEvent: github.ReviewEvent{
  1701  				Action: github.ReviewActionSubmitted,
  1702  				Review: github.Review{
  1703  					Body: "/lgtm",
  1704  					User: github.User{
  1705  						Login: "author",
  1706  					},
  1707  					State: stateToLower(github.ReviewStateApproved),
  1708  				},
  1709  			},
  1710  			lgtmActsAsApprove:   true,
  1711  			reviewActsAsApprove: true,
  1712  			expectHandle:        false,
  1713  		},
  1714  		{
  1715  			name: "feature disabled",
  1716  			reviewEvent: github.ReviewEvent{
  1717  				Action: github.ReviewActionSubmitted,
  1718  				Review: github.Review{
  1719  					Body: "looks good",
  1720  					User: github.User{
  1721  						Login: "author",
  1722  					},
  1723  					State: stateToLower(github.ReviewStateApproved),
  1724  				},
  1725  			},
  1726  			reviewActsAsApprove: false,
  1727  			expectHandle:        false,
  1728  		},
  1729  	}
  1730  
  1731  	var handled bool
  1732  	var gotState *state
  1733  	handleFunc = func(log *logrus.Entry, ghc githubClient, repo approvers.Repo, config config.GitHubOptions, opts *plugins.Approve, pr *state) error {
  1734  		gotState = pr
  1735  		handled = true
  1736  		return nil
  1737  	}
  1738  	defer func() {
  1739  		handleFunc = handle
  1740  	}()
  1741  
  1742  	repo := github.Repo{
  1743  		Owner: github.User{
  1744  			Login: "org",
  1745  		},
  1746  		Name: "repo",
  1747  	}
  1748  	pr := github.PullRequest{
  1749  		User: github.User{
  1750  			Login: "P.R. Author",
  1751  		},
  1752  		Base: github.PullRequestBranch{
  1753  			Ref: "branch",
  1754  		},
  1755  		Number: 1,
  1756  		Body:   "Fix everything",
  1757  	}
  1758  	fghc := fakegithub.NewFakeClient()
  1759  	fghc.PullRequests = map[int]*github.PullRequest{1: &pr}
  1760  
  1761  	for _, test := range tests {
  1762  		test.reviewEvent.Repo = repo
  1763  		test.reviewEvent.PullRequest = pr
  1764  		githubConfig := config.GitHubOptions{
  1765  			LinkURL: &url.URL{
  1766  				Scheme: "https",
  1767  				Host:   "github.com",
  1768  			},
  1769  		}
  1770  		config := &plugins.Configuration{}
  1771  		irs := !test.reviewActsAsApprove
  1772  		config.Approve = append(config.Approve, plugins.Approve{
  1773  			Repos:             []string{test.reviewEvent.Repo.Owner.Login},
  1774  			LgtmActsAsApprove: test.lgtmActsAsApprove,
  1775  			IgnoreReviewState: &irs,
  1776  		})
  1777  		err := handleReview(
  1778  			logrus.WithField("plugin", "approve"),
  1779  			fghc,
  1780  			fakeOwnersClient{},
  1781  			githubConfig,
  1782  			config,
  1783  			&test.reviewEvent,
  1784  		)
  1785  
  1786  		if test.expectHandle && !handled {
  1787  			t.Errorf("%s: expected call to handleFunc, but it wasn't called", test.name)
  1788  		}
  1789  
  1790  		if !test.expectHandle && handled {
  1791  			t.Errorf("%s: expected no call to handleFunc, but it was called", test.name)
  1792  		}
  1793  
  1794  		if test.expectState != nil && !reflect.DeepEqual(test.expectState, gotState) {
  1795  			t.Errorf("%s: expected PR state to equal: %#v, but got: %#v", test.name, test.expectState, gotState)
  1796  		}
  1797  
  1798  		if err != nil {
  1799  			t.Errorf("%s: error calling handleReview: %v", test.name, err)
  1800  		}
  1801  		handled = false
  1802  	}
  1803  }
  1804  
  1805  func TestHandlePullRequest(t *testing.T) {
  1806  	tests := []struct {
  1807  		name         string
  1808  		prEvent      github.PullRequestEvent
  1809  		expectHandle bool
  1810  		expectState  *state
  1811  	}{
  1812  		{
  1813  			name: "pr opened",
  1814  			prEvent: github.PullRequestEvent{
  1815  				Action: github.PullRequestActionOpened,
  1816  				PullRequest: github.PullRequest{
  1817  					User: github.User{
  1818  						Login: "P.R. Author",
  1819  					},
  1820  					Base: github.PullRequestBranch{
  1821  						Ref: "branch",
  1822  					},
  1823  					Body: "Fix everything",
  1824  				},
  1825  				Number: 1,
  1826  			},
  1827  			expectHandle: true,
  1828  			expectState: &state{
  1829  				org:       "org",
  1830  				repo:      "repo",
  1831  				branch:    "branch",
  1832  				number:    1,
  1833  				body:      "Fix everything",
  1834  				author:    "P.R. Author",
  1835  				assignees: nil,
  1836  				htmlURL:   "",
  1837  			},
  1838  		},
  1839  		{
  1840  			name: "pr reopened",
  1841  			prEvent: github.PullRequestEvent{
  1842  				Action: github.PullRequestActionReopened,
  1843  			},
  1844  			expectHandle: true,
  1845  		},
  1846  		{
  1847  			name: "pr sync",
  1848  			prEvent: github.PullRequestEvent{
  1849  				Action: github.PullRequestActionSynchronize,
  1850  			},
  1851  			expectHandle: true,
  1852  		},
  1853  		{
  1854  			name: "pr labeled",
  1855  			prEvent: github.PullRequestEvent{
  1856  				Action: github.PullRequestActionLabeled,
  1857  				Label: github.Label{
  1858  					Name: labels.Approved,
  1859  				},
  1860  			},
  1861  			expectHandle: true,
  1862  		},
  1863  		{
  1864  			name: "pr another label",
  1865  			prEvent: github.PullRequestEvent{
  1866  				Action: github.PullRequestActionLabeled,
  1867  				Label: github.Label{
  1868  					Name: "some-label",
  1869  				},
  1870  			},
  1871  			expectHandle: false,
  1872  		},
  1873  		{
  1874  			name: "pr closed",
  1875  			prEvent: github.PullRequestEvent{
  1876  				Action: github.PullRequestActionLabeled,
  1877  				Label: github.Label{
  1878  					Name: labels.Approved,
  1879  				},
  1880  				PullRequest: github.PullRequest{
  1881  					State: "closed",
  1882  				},
  1883  			},
  1884  			expectHandle: false,
  1885  		},
  1886  		{
  1887  			name: "pr review requested",
  1888  			prEvent: github.PullRequestEvent{
  1889  				Action: github.PullRequestActionReviewRequested,
  1890  			},
  1891  			expectHandle: false,
  1892  		},
  1893  	}
  1894  
  1895  	var handled bool
  1896  	var gotState *state
  1897  	handleFunc = func(log *logrus.Entry, ghc githubClient, repo approvers.Repo, githubConfig config.GitHubOptions, opts *plugins.Approve, pr *state) error {
  1898  		gotState = pr
  1899  		handled = true
  1900  		return nil
  1901  	}
  1902  	defer func() {
  1903  		handleFunc = handle
  1904  	}()
  1905  
  1906  	repo := github.Repo{
  1907  		Owner: github.User{
  1908  			Login: "org",
  1909  		},
  1910  		Name: "repo",
  1911  	}
  1912  	fghc := fakegithub.NewFakeClient()
  1913  
  1914  	for _, test := range tests {
  1915  		test.prEvent.Repo = repo
  1916  		err := handlePullRequest(
  1917  			logrus.WithField("plugin", "approve"),
  1918  			fghc,
  1919  			fakeOwnersClient{},
  1920  			config.GitHubOptions{
  1921  				LinkURL: &url.URL{
  1922  					Scheme: "https",
  1923  					Host:   "github.com",
  1924  				},
  1925  			},
  1926  			&plugins.Configuration{},
  1927  			&test.prEvent,
  1928  		)
  1929  
  1930  		if test.expectHandle && !handled {
  1931  			t.Errorf("%s: expected call to handleFunc, but it wasn't called", test.name)
  1932  		}
  1933  
  1934  		if !test.expectHandle && handled {
  1935  			t.Errorf("%s: expected no call to handleFunc, but it was called", test.name)
  1936  		}
  1937  
  1938  		if test.expectState != nil && !reflect.DeepEqual(test.expectState, gotState) {
  1939  			t.Errorf("%s: expected PR state to equal: %#v, but got: %#v", test.name, test.expectState, gotState)
  1940  		}
  1941  
  1942  		if err != nil {
  1943  			t.Errorf("%s: error calling handlePullRequest: %v", test.name, err)
  1944  		}
  1945  		handled = false
  1946  	}
  1947  }
  1948  
  1949  func TestHelpProvider(t *testing.T) {
  1950  	enabledRepos := []config.OrgRepo{
  1951  		{Org: "org1", Repo: "repo"},
  1952  		{Org: "org2", Repo: "repo"},
  1953  	}
  1954  	cases := []struct {
  1955  		name         string
  1956  		config       *plugins.Configuration
  1957  		enabledRepos []config.OrgRepo
  1958  		err          bool
  1959  	}{
  1960  		{
  1961  			name:         "Empty config",
  1962  			config:       &plugins.Configuration{},
  1963  			enabledRepos: enabledRepos,
  1964  		},
  1965  		{
  1966  			name: "All configs enabled",
  1967  			config: &plugins.Configuration{
  1968  				Approve: []plugins.Approve{
  1969  					{
  1970  						Repos:               []string{"org2/repo"},
  1971  						IssueRequired:       true,
  1972  						RequireSelfApproval: &[]bool{true}[0],
  1973  						LgtmActsAsApprove:   true,
  1974  						IgnoreReviewState:   &[]bool{true}[0],
  1975  					},
  1976  				},
  1977  			},
  1978  			enabledRepos: enabledRepos,
  1979  		},
  1980  	}
  1981  	for _, c := range cases {
  1982  		t.Run(c.name, func(t *testing.T) {
  1983  			_, err := helpProvider(c.config, c.enabledRepos)
  1984  			if err != nil && !c.err {
  1985  				t.Fatalf("helpProvider error: %v", err)
  1986  			}
  1987  		})
  1988  	}
  1989  }