sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/plugins/cat/cat_test.go (about)

     1  /*
     2  Copyright 2018 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 cat
    18  
    19  import (
    20  	"errors"
    21  	"flag"
    22  	"fmt"
    23  	"io"
    24  	"net/http"
    25  	"net/http/httptest"
    26  	"regexp"
    27  	"strings"
    28  	"testing"
    29  
    30  	"github.com/sirupsen/logrus"
    31  
    32  	"sigs.k8s.io/prow/pkg/github"
    33  	"sigs.k8s.io/prow/pkg/github/fakegithub"
    34  )
    35  
    36  type fakeClowder string
    37  
    38  var human = flag.Bool("human", false, "Enable to run additional manual tests")
    39  var category = flag.String("category", "", "Request a particular category if set")
    40  var movieCat = flag.Bool("gif", false, "Specifically request a GIF image if set")
    41  var keyPath = flag.String("key-path", "", "Path to api key if set")
    42  
    43  func (c fakeClowder) readCat(category string, movieCat bool, grumpyRoot string) (string, error) {
    44  	if category == "error" {
    45  		return "", errors.New(string(c))
    46  	}
    47  	return fmt.Sprintf("![fake cat image](%s)", c), nil
    48  }
    49  
    50  func TestRealCat(t *testing.T) {
    51  	if !*human {
    52  		t.Skip("Real cats disabled for automation. Manual users can add --human [--category=foo]")
    53  	}
    54  	if *keyPath != "" {
    55  		meow.setKey(*keyPath, logrus.WithField("plugin", pluginName))
    56  	}
    57  
    58  	if cat, err := meow.readCat(*category, *movieCat, defaultGrumpyRoot); err != nil {
    59  		t.Errorf("Could not read cats from %#v: %v", meow, err)
    60  	} else {
    61  		fmt.Println(cat)
    62  	}
    63  }
    64  
    65  func TestUrl(t *testing.T) {
    66  	cases := []struct {
    67  		name     string
    68  		url      string
    69  		category string
    70  		key      string
    71  		movie    bool
    72  		require  []string
    73  		deny     []string
    74  	}{
    75  		{
    76  			name: "only url",
    77  			url:  "http://foo",
    78  		},
    79  		{
    80  			name:    "key",
    81  			url:     "http://foo",
    82  			key:     "blah",
    83  			require: []string{"api_key=blah"},
    84  			deny:    []string{"category=", "mime_types=gif"},
    85  		},
    86  		{
    87  			name:     "category",
    88  			url:      "http://foo",
    89  			category: "bar",
    90  			require:  []string{"category=bar"},
    91  			deny:     []string{"api_key=", "mime_types=gif"},
    92  		},
    93  		{
    94  			name:    "movie",
    95  			url:     "http://foo",
    96  			movie:   true,
    97  			require: []string{"mime_types=gif"},
    98  			deny:    []string{"category=this", "api_key=that"},
    99  		},
   100  		{
   101  			name:     "category and movie",
   102  			url:      "http://foo",
   103  			category: "this",
   104  			movie:    true,
   105  			require:  []string{"mime_types=gif", "category=this", "&"},
   106  			deny:     []string{"api_key="},
   107  		},
   108  		{
   109  			name:     "category and key",
   110  			url:      "http://foo",
   111  			category: "this",
   112  			key:      "that",
   113  			require:  []string{"category=this", "api_key=that", "&"},
   114  			deny:     []string{"mime_types=gif"},
   115  		},
   116  		{
   117  			name:     "category, key, and movie",
   118  			url:      "http://foo",
   119  			category: "this",
   120  			key:      "that",
   121  			movie:    true,
   122  			require:  []string{"category=this", "api_key=that", "&", "mime_types=gif"},
   123  		},
   124  	}
   125  
   126  	for _, tc := range cases {
   127  		rc := realClowder{
   128  			url: tc.url,
   129  			key: tc.key,
   130  		}
   131  		url := rc.URL(tc.category, tc.movie)
   132  		for _, r := range tc.require {
   133  			if !strings.Contains(url, r) {
   134  				t.Errorf("%s: %s does not contain %s", tc.name, url, r)
   135  			}
   136  		}
   137  		for _, d := range tc.deny {
   138  			if strings.Contains(url, d) {
   139  				t.Errorf("%s: %s contained unexpected %s", tc.name, url, d)
   140  			}
   141  		}
   142  	}
   143  }
   144  
   145  func TestGrumpy(t *testing.T) {
   146  	// fake server for images
   147  	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   148  		body := "binary image"
   149  		io.WriteString(w, body)
   150  	}))
   151  	defer ts.Close()
   152  
   153  	cases := []struct {
   154  		name     string
   155  		url      string
   156  		category string
   157  		key      string
   158  		movie    bool
   159  		require  []string
   160  		deny     []string
   161  	}{
   162  		{
   163  			name:     "category",
   164  			url:      "http://foo",
   165  			category: "bar",
   166  			movie:    false,
   167  			deny:     []string{ts.URL + "/" + grumpyIMG},
   168  		},
   169  		{
   170  			name:     "category and movie",
   171  			url:      "http://foo",
   172  			category: "this",
   173  			movie:    true,
   174  			deny:     []string{ts.URL + "/" + grumpyIMG},
   175  		},
   176  		{
   177  			name:     "grumpy cat no keyword",
   178  			url:      "http://foo",
   179  			category: "no",
   180  			key:      "that",
   181  			movie:    false,
   182  			require:  []string{ts.URL + "/" + grumpyIMG},
   183  		},
   184  		{
   185  			name:     "grumpy cat grumpy keyword",
   186  			url:      "http://foo",
   187  			category: "grumpy",
   188  			key:      "that",
   189  			movie:    false,
   190  			require:  []string{ts.URL + "/" + grumpyIMG},
   191  		},
   192  	}
   193  
   194  	for _, tc := range cases {
   195  		rc := realClowder{
   196  			url: tc.url,
   197  			key: tc.key,
   198  		}
   199  		url, _ := rc.readCat(tc.category, tc.movie, ts.URL+"/")
   200  		for _, r := range tc.require {
   201  			if !strings.Contains(url, r) {
   202  				t.Errorf("%s: %s does not contain %s", tc.name, url, r)
   203  			}
   204  		}
   205  		for _, d := range tc.deny {
   206  			if strings.Contains(url, d) {
   207  				t.Errorf("%s: %s contained unexpected %s", tc.name, url, d)
   208  			}
   209  		}
   210  	}
   211  }
   212  
   213  func TestFormat(t *testing.T) {
   214  	re := regexp.MustCompile(`!\[.+\]\(.+\)`)
   215  	basicURL := "http://example.com"
   216  	testcases := []struct {
   217  		name string
   218  		img  string
   219  		err  bool
   220  	}{
   221  		{
   222  			name: "basically works",
   223  			img:  basicURL,
   224  			err:  false,
   225  		},
   226  		{
   227  			name: "empty image",
   228  			img:  "",
   229  			err:  true,
   230  		},
   231  		{
   232  			name: "bad image",
   233  			img:  "http://still a bad url",
   234  			err:  true,
   235  		},
   236  	}
   237  	for _, tc := range testcases {
   238  		ret, err := catResult{
   239  			Image: tc.img,
   240  		}.Format()
   241  
   242  		switch {
   243  		case tc.err:
   244  			if err == nil {
   245  				t.Errorf("%s: failed to raise an error", tc.name)
   246  			}
   247  		case err != nil:
   248  			t.Errorf("%s: unexpected error: %v", tc.name, err)
   249  		case !re.MatchString(ret):
   250  			t.Errorf("%s: bad return value: %s", tc.name, ret)
   251  		}
   252  	}
   253  }
   254  
   255  // Medium integration test (depends on ability to open a TCP port)
   256  func TestHttpResponse(t *testing.T) {
   257  	// create test cases for handling content length of images
   258  	contentLength := make(map[string]string)
   259  	contentLength["/cat.jpg"] = "717987"
   260  	contentLength["/bigcat.jpg"] = "12647753"
   261  
   262  	// fake server for images
   263  	ts2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   264  		if s, ok := contentLength[r.URL.Path]; ok {
   265  			body := "binary image"
   266  			w.Header().Set("Content-Length", s)
   267  			io.WriteString(w, body)
   268  		} else {
   269  			t.Errorf("Cannot find content length for %s", r.URL.Path)
   270  		}
   271  	}))
   272  	defer ts2.Close()
   273  
   274  	// create test cases for handling http responses
   275  	img := ts2.URL + "/cat.jpg"
   276  	bigimg := ts2.URL + "/bigcat.jpg"
   277  	validResponse := fmt.Sprintf(`[{"id":"valid","url":"%s"}]`, img)
   278  	var testcases = []struct {
   279  		name     string
   280  		path     string
   281  		response string
   282  		valid    bool
   283  		code     int
   284  	}{
   285  		{
   286  			name:     "valid",
   287  			path:     "/valid",
   288  			response: validResponse,
   289  			valid:    true,
   290  		},
   291  		{
   292  			name:     "image too big",
   293  			path:     "/too-big",
   294  			response: fmt.Sprintf(`[{"id":"toobig","url":"%s"}]`, bigimg),
   295  		},
   296  		{
   297  			name: "return-406",
   298  			path: "/return-406",
   299  			code: 406,
   300  			response: `
   301  <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
   302  <html><head>
   303  <title>406 Not Acceptable</title>
   304  </head><body>
   305  <h1>Not Acceptable</h1>
   306  <p>An appropriate representation of the requested resource /api/images/get could not be found on this server.</p>
   307  Available variants:
   308  <ul>
   309  <li><a href="get.php">get.php</a> , type x-mapp-php5</li>
   310  </ul>
   311  </body></html>`,
   312  		},
   313  		{
   314  			name:     "no-cats-in-json",
   315  			path:     "/no-cats-in-json",
   316  			response: "[]",
   317  		},
   318  		{
   319  			name:     "no-image-in-json",
   320  			path:     "/no-image-in-json",
   321  			response: "[{}]",
   322  		},
   323  	}
   324  
   325  	// fake server for image urls
   326  	pathToResponse := make(map[string]string)
   327  	for _, testcase := range testcases {
   328  		pathToResponse[testcase.path] = testcase.response
   329  	}
   330  	codes := make(map[string]int)
   331  	for _, tc := range testcases {
   332  		codes[tc.path] = tc.code
   333  	}
   334  	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   335  		code := codes[r.URL.Path]
   336  		if code > 0 {
   337  			w.WriteHeader(code)
   338  		}
   339  		if r, ok := pathToResponse[r.URL.Path]; ok {
   340  			io.WriteString(w, r)
   341  		} else {
   342  			io.WriteString(w, validResponse)
   343  		}
   344  	}))
   345  	defer ts.Close()
   346  
   347  	// github fake client
   348  	fc := fakegithub.NewFakeClient()
   349  	fc.IssueComments = make(map[int][]github.IssueComment)
   350  
   351  	// run test for each case
   352  	for _, testcase := range testcases {
   353  		fakemeow := &realClowder{url: ts.URL + testcase.path}
   354  		cat, err := fakemeow.readCat(*category, *movieCat, "")
   355  		if testcase.valid && err != nil {
   356  			t.Errorf("For case %s, didn't expect error: %v", testcase.name, err)
   357  		} else if !testcase.valid && err == nil {
   358  			t.Errorf("For case %s, expected error, received cat: %s", testcase.name, cat)
   359  		} else if testcase.valid && cat == "" {
   360  			t.Errorf("For case %s, got an empty cat", testcase.name)
   361  		}
   362  	}
   363  
   364  	// fully test handling a comment
   365  	comment := "/meowvie space"
   366  
   367  	e := &github.GenericCommentEvent{
   368  		Action:     github.GenericCommentActionCreated,
   369  		Body:       comment,
   370  		Number:     5,
   371  		IssueState: "open",
   372  	}
   373  	if err := handle(fc, logrus.WithField("plugin", pluginName), e, &realClowder{url: ts.URL + "/?format=json"}, func() {}); err != nil {
   374  		t.Errorf("didn't expect error: %v", err)
   375  		return
   376  	}
   377  	if len(fc.IssueComments[5]) != 1 {
   378  		t.Error("should have commented.")
   379  		return
   380  	}
   381  	if c := fc.IssueComments[5][0]; !strings.Contains(c.Body, img) {
   382  		t.Errorf("missing image url: %s from comment: %v", img, c)
   383  	}
   384  
   385  }
   386  
   387  // Small, unit tests
   388  func TestCats(t *testing.T) {
   389  	var testcases = []struct {
   390  		name          string
   391  		action        github.GenericCommentEventAction
   392  		body          string
   393  		state         string
   394  		pr            bool
   395  		shouldComment bool
   396  		shouldError   bool
   397  	}{
   398  		{
   399  			name:          "ignore edited comment",
   400  			state:         "open",
   401  			action:        github.GenericCommentActionEdited,
   402  			body:          "/meow",
   403  			shouldComment: false,
   404  			shouldError:   false,
   405  		},
   406  		{
   407  			name:          "leave cat on pr",
   408  			state:         "open",
   409  			action:        github.GenericCommentActionCreated,
   410  			body:          "/meow",
   411  			pr:            true,
   412  			shouldComment: true,
   413  			shouldError:   false,
   414  		},
   415  		{
   416  			name:          "leave cat on issue",
   417  			state:         "open",
   418  			action:        github.GenericCommentActionCreated,
   419  			body:          "/meow",
   420  			shouldComment: true,
   421  			shouldError:   false,
   422  		},
   423  		{
   424  			name:          "leave cat on issue, trailing space",
   425  			state:         "open",
   426  			action:        github.GenericCommentActionCreated,
   427  			body:          "/meow \r",
   428  			shouldComment: true,
   429  			shouldError:   false,
   430  		},
   431  		{
   432  			name:          "categorical cat",
   433  			state:         "open",
   434  			action:        github.GenericCommentActionCreated,
   435  			body:          "/meow clothes",
   436  			shouldComment: true,
   437  			shouldError:   false,
   438  		},
   439  		{
   440  			name:          "bad cat",
   441  			state:         "open",
   442  			action:        github.GenericCommentActionCreated,
   443  			body:          "/meow error",
   444  			shouldComment: true,
   445  			shouldError:   true,
   446  		},
   447  		{
   448  			name:          "movie cat",
   449  			state:         "open",
   450  			action:        github.GenericCommentActionCreated,
   451  			body:          "/meowvie",
   452  			shouldComment: true,
   453  			shouldError:   false,
   454  		},
   455  		{
   456  			name:          "categorical movie cat",
   457  			state:         "open",
   458  			action:        github.GenericCommentActionCreated,
   459  			body:          "/meowvie space",
   460  			shouldComment: true,
   461  			shouldError:   false,
   462  		},
   463  	}
   464  	for _, tc := range testcases {
   465  		fc := fakegithub.NewFakeClient()
   466  		fc.IssueComments = make(map[int][]github.IssueComment)
   467  		e := &github.GenericCommentEvent{
   468  			Action:     tc.action,
   469  			Body:       tc.body,
   470  			Number:     5,
   471  			IssueState: tc.state,
   472  			IsPR:       tc.pr,
   473  		}
   474  		err := handle(fc, logrus.WithField("plugin", pluginName), e, fakeClowder("tubbs"), func() {})
   475  		if !tc.shouldError && err != nil {
   476  			t.Errorf("%s: didn't expect error: %v", tc.name, err)
   477  			continue
   478  		} else if tc.shouldError && err == nil {
   479  			t.Errorf("%s: expected an error to occur", tc.name)
   480  			continue
   481  		}
   482  		if tc.shouldComment && len(fc.IssueComments[5]) != 1 {
   483  			t.Errorf("%s: should have commented.", tc.name)
   484  		} else if tc.shouldComment {
   485  			shouldImage := !tc.shouldError
   486  			body := fc.IssueComments[5][0].Body
   487  			hasImage := strings.Contains(body, "![")
   488  			if hasImage && !shouldImage {
   489  				t.Errorf("%s: unexpected image in %s", tc.name, body)
   490  			} else if !hasImage && shouldImage {
   491  				t.Errorf("%s: no image in %s", tc.name, body)
   492  			}
   493  		} else if !tc.shouldComment && len(fc.IssueComments[5]) != 0 {
   494  			t.Errorf("%s: should not have commented.", tc.name)
   495  		}
   496  	}
   497  }