k8s.io/test-infra@v0.0.0-20240520184403-27c6b4c223d8/robots/commenter/main_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 main
    18  
    19  import (
    20  	"errors"
    21  	"fmt"
    22  	"strconv"
    23  	"strings"
    24  	"testing"
    25  	"time"
    26  
    27  	"sigs.k8s.io/prow/pkg/github"
    28  )
    29  
    30  func TestParseHTMLURL(t *testing.T) {
    31  	cases := []struct {
    32  		name string
    33  		url  string
    34  		org  string
    35  		repo string
    36  		num  int
    37  		fail bool
    38  	}{
    39  		{
    40  			name: "normal issue",
    41  			url:  "https://github.com/org/repo/issues/1234",
    42  			org:  "org",
    43  			repo: "repo",
    44  			num:  1234,
    45  		},
    46  		{
    47  			name: "normal pull",
    48  			url:  "https://github.com/pull-org/pull-repo/pull/5555",
    49  			org:  "pull-org",
    50  			repo: "pull-repo",
    51  			num:  5555,
    52  		},
    53  		{
    54  			name: "different host",
    55  			url:  "ftp://gitlab.whatever/org/repo/issues/6666",
    56  			org:  "org",
    57  			repo: "repo",
    58  			num:  6666,
    59  		},
    60  		{
    61  			name: "string issue",
    62  			url:  "https://github.com/org/repo/issues/future",
    63  			fail: true,
    64  		},
    65  		{
    66  			name: "weird issue",
    67  			url:  "https://gubernator.k8s.io/build/kubernetes-jenkins/logs/ci-kubernetes-e2e-gci-gce/11947/",
    68  			fail: true,
    69  		},
    70  	}
    71  
    72  	for _, tc := range cases {
    73  		org, repo, num, err := parseHTMLURL(tc.url)
    74  		if err != nil && !tc.fail {
    75  			t.Errorf("%s: should not have produced error: %v", tc.name, err)
    76  		} else if err == nil && tc.fail {
    77  			t.Errorf("%s: failed to produce an error", tc.name)
    78  		} else {
    79  			if org != tc.org {
    80  				t.Errorf("%s: org %s != expected %s", tc.name, org, tc.org)
    81  			}
    82  			if repo != tc.repo {
    83  				t.Errorf("%s: repo %s != expected %s", tc.name, repo, tc.repo)
    84  			}
    85  			if num != tc.num {
    86  				t.Errorf("%s: num %d != expected %d", tc.name, num, tc.num)
    87  			}
    88  		}
    89  	}
    90  }
    91  
    92  func TestMakeQuery(t *testing.T) {
    93  	cases := []struct {
    94  		name       string
    95  		query      string
    96  		archived   bool
    97  		closed     bool
    98  		locked     bool
    99  		dur        time.Duration
   100  		expected   []string
   101  		unexpected []string
   102  		err        bool
   103  	}{
   104  		{
   105  			name:       "basic query",
   106  			query:      "hello world",
   107  			expected:   []string{"hello world", "is:open", "archived:false"},
   108  			unexpected: []string{"updated:", "openhello", "worldis"},
   109  		},
   110  		{
   111  			name:       "basic archived",
   112  			query:      "hello world",
   113  			archived:   true,
   114  			expected:   []string{"hello world", "is:open", "is:unlocked"},
   115  			unexpected: []string{"archived:false"},
   116  		},
   117  		{
   118  			name:       "basic closed",
   119  			query:      "hello world",
   120  			closed:     true,
   121  			expected:   []string{"hello world", "archived:false", "is:unlocked"},
   122  			unexpected: []string{"is:open"},
   123  		},
   124  		{
   125  			name:       "basic locked",
   126  			query:      "hello world",
   127  			locked:     true,
   128  			expected:   []string{"hello world", "is:open", "archived:false"},
   129  			unexpected: []string{"is:unlocked"},
   130  		},
   131  		{
   132  			name:     "basic duration",
   133  			query:    "hello",
   134  			dur:      1 * time.Hour,
   135  			expected: []string{"hello", "updated:<"},
   136  		},
   137  		{
   138  			name:       "weird characters not escaped",
   139  			query:      "oh yeah!@#$&*()",
   140  			expected:   []string{"!", "@", "#", " "},
   141  			unexpected: []string{"%", "+"},
   142  		},
   143  		{
   144  			name:     "linebreaks are replaced by whitespaces",
   145  			query:    "label:foo\nlabel:bar",
   146  			expected: []string{"label:foo label:bar"},
   147  		},
   148  		{
   149  			name:   "include closed with is:open query errors",
   150  			query:  "hello is:open",
   151  			closed: true,
   152  			err:    true,
   153  		},
   154  		{
   155  			name:     "archived:false with include-archived errors",
   156  			query:    "hello archived:false",
   157  			archived: true,
   158  			err:      true,
   159  		},
   160  		{
   161  			name:  "archived:true without includeArchived errors",
   162  			query: "hello archived:true",
   163  			err:   true,
   164  		},
   165  		{
   166  			name:  "is:closed without includeClosed errors",
   167  			query: "hello is:closed",
   168  			err:   true,
   169  		},
   170  		{
   171  			name:  "is:locked without includeLocked errors",
   172  			query: "hello is:locked",
   173  			err:   true,
   174  		},
   175  		{
   176  			name:   "is:unlocked with includeLocked errors",
   177  			query:  "hello is:unlocked",
   178  			locked: true,
   179  			err:    true,
   180  		},
   181  	}
   182  
   183  	for _, tc := range cases {
   184  		actual, err := makeQuery(tc.query, tc.archived, tc.closed, tc.locked, tc.dur)
   185  		if err != nil && !tc.err {
   186  			t.Errorf("%s: unexpected error: %v", tc.name, err)
   187  		} else if err == nil && tc.err {
   188  			t.Errorf("%s: failed to raise an error", tc.name)
   189  		}
   190  		for _, e := range tc.expected {
   191  			if !strings.Contains(actual, e) {
   192  				t.Errorf("%s: could not find %s in %s", tc.name, e, actual)
   193  			}
   194  		}
   195  		for _, u := range tc.unexpected {
   196  			if strings.Contains(actual, u) {
   197  				t.Errorf("%s: should not have found %s in %s", tc.name, u, actual)
   198  			}
   199  		}
   200  	}
   201  }
   202  
   203  func makeIssue(owner, repo string, number int, title string) github.Issue {
   204  	return github.Issue{
   205  		HTMLURL: fmt.Sprintf("fake://localhost/%s/%s/pull/%d", owner, repo, number),
   206  		Title:   title,
   207  	}
   208  }
   209  
   210  type fakeClient struct {
   211  	comments []int
   212  	issues   []github.Issue
   213  }
   214  
   215  // Fakes Creating a client, using the same signature as github.Client
   216  func (c *fakeClient) CreateComment(owner, repo string, number int, comment string) error {
   217  	if strings.Contains(comment, "error") || repo == "error" {
   218  		return errors.New(comment)
   219  	}
   220  	c.comments = append(c.comments, number)
   221  	return nil
   222  }
   223  
   224  // Fakes searching for issues, using the same signature as github.Client
   225  func (c *fakeClient) FindIssues(query, sort string, asc bool) ([]github.Issue, error) {
   226  	if strings.Contains(query, "error") {
   227  		return nil, errors.New(query)
   228  	}
   229  	ret := []github.Issue{}
   230  	for _, i := range c.issues {
   231  		if strings.Contains(i.Title, query) {
   232  			ret = append(ret, i)
   233  		}
   234  	}
   235  	return ret, nil
   236  }
   237  
   238  func TestRun(t *testing.T) {
   239  	manyIssues := []github.Issue{}
   240  	manyComments := []int{}
   241  	for i := 0; i < 100; i++ {
   242  		manyIssues = append(manyIssues, makeIssue("o", "r", i, "many "+strconv.Itoa(i)))
   243  		manyComments = append(manyComments, i)
   244  	}
   245  
   246  	cases := []struct {
   247  		name     string
   248  		query    string
   249  		comment  string
   250  		template bool
   251  		ceiling  int
   252  		client   fakeClient
   253  		expected []int
   254  		err      bool
   255  	}{
   256  		{
   257  			name:     "find all",
   258  			query:    "many",
   259  			comment:  "found you",
   260  			client:   fakeClient{issues: manyIssues},
   261  			expected: manyComments,
   262  		},
   263  		{
   264  			name:     "find first 10",
   265  			query:    "many",
   266  			ceiling:  10,
   267  			comment:  "hey",
   268  			client:   fakeClient{issues: manyIssues},
   269  			expected: []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9},
   270  		},
   271  		{
   272  			name:    "find none",
   273  			query:   "none",
   274  			comment: "this should not happen",
   275  			client:  fakeClient{issues: manyIssues},
   276  		},
   277  		{
   278  			name:    "search error",
   279  			query:   "this search should error",
   280  			comment: "comment",
   281  			client:  fakeClient{issues: manyIssues},
   282  			err:     true,
   283  		},
   284  		{
   285  			name:    "comment error",
   286  			query:   "problematic",
   287  			comment: "rolo tomassi",
   288  			client: fakeClient{issues: []github.Issue{
   289  				makeIssue("o", "r", 1, "problematic this should work"),
   290  				makeIssue("o", "error", 2, "problematic expect an error"),
   291  				makeIssue("o", "r", 3, "problematic works as well"),
   292  			}},
   293  			err:      true,
   294  			expected: []int{1, 3},
   295  		},
   296  		{
   297  			name:     "template comment",
   298  			query:    "67",
   299  			client:   fakeClient{issues: manyIssues},
   300  			comment:  "https://gubernator.k8s.io/pr/{{.Org}}/{{.Repo}}/{{.Number}}",
   301  			template: true,
   302  			expected: []int{67},
   303  		},
   304  		{
   305  			name:     "bad template errors",
   306  			query:    "67",
   307  			client:   fakeClient{issues: manyIssues},
   308  			comment:  "Bad {{.UnknownField}}",
   309  			template: true,
   310  			err:      true,
   311  		},
   312  	}
   313  
   314  	for _, tc := range cases {
   315  		ignoreSorting := ""
   316  		ignoreOrder := false
   317  		err := run(&tc.client, tc.query, ignoreSorting, ignoreOrder, false, makeCommenter(tc.comment, tc.template), tc.ceiling)
   318  		if tc.err && err == nil {
   319  			t.Errorf("%s: failed to received an error", tc.name)
   320  			continue
   321  		}
   322  		if !tc.err && err != nil {
   323  			t.Errorf("%s: unexpected error: %v", tc.name, err)
   324  			continue
   325  		}
   326  		if len(tc.expected) != len(tc.client.comments) {
   327  			t.Errorf("%s: expected comments %v != actual %v", tc.name, tc.expected, tc.client.comments)
   328  			continue
   329  		}
   330  		missing := []int{}
   331  		for _, e := range tc.expected {
   332  			found := false
   333  			for _, cmt := range tc.client.comments {
   334  				if cmt == e {
   335  					found = true
   336  					break
   337  				}
   338  			}
   339  			if !found {
   340  				missing = append(missing, e)
   341  			}
   342  		}
   343  		if len(missing) > 0 {
   344  			t.Errorf("%s: missing %v from actual comments %v", tc.name, missing, tc.client.comments)
   345  		}
   346  	}
   347  }
   348  
   349  func TestMakeCommenter(t *testing.T) {
   350  	m := meta{
   351  		Number: 10,
   352  		Org:    "org",
   353  		Repo:   "repo",
   354  		Issue: github.Issue{
   355  			Number:  10,
   356  			HTMLURL: "url",
   357  			Title:   "title",
   358  		},
   359  	}
   360  	cases := []struct {
   361  		name     string
   362  		comment  string
   363  		template bool
   364  		expected string
   365  		err      bool
   366  	}{
   367  		{
   368  			name:     "string works",
   369  			comment:  "hello world {{.Number}} {{.Invalid}}",
   370  			expected: "hello world {{.Number}} {{.Invalid}}",
   371  		},
   372  		{
   373  			name:     "template works",
   374  			comment:  "N={{.Number}} R={{.Repo}} O={{.Org}} U={{.Issue.HTMLURL}} T={{.Issue.Title}}",
   375  			template: true,
   376  			expected: "N=10 R=repo O=org U=url T=title",
   377  		},
   378  		{
   379  			name:     "bad template errors",
   380  			comment:  "Bad {{.UnknownField}} Template",
   381  			expected: "Bad ",
   382  			template: true,
   383  			err:      true,
   384  		},
   385  	}
   386  
   387  	for _, tc := range cases {
   388  		c := makeCommenter(tc.comment, tc.template)
   389  		actual, err := c(m)
   390  		if actual != tc.expected {
   391  			t.Errorf("%s: expected '%s' != actual '%s'", tc.name, tc.expected, actual)
   392  		}
   393  		if err != nil && !tc.err {
   394  			t.Errorf("%s: unexpected err: %v", tc.name, err)
   395  		}
   396  		if err == nil && tc.err {
   397  			t.Errorf("%s: failed to raise an exception", tc.name)
   398  		}
   399  	}
   400  }