sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/plugins/pony/pony_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 pony
    18  
    19  import (
    20  	"encoding/json"
    21  	"flag"
    22  	"fmt"
    23  	"io"
    24  	"net/http"
    25  	"net/http/httptest"
    26  	"strings"
    27  	"testing"
    28  
    29  	"github.com/sirupsen/logrus"
    30  	"sigs.k8s.io/prow/pkg/github"
    31  	"sigs.k8s.io/prow/pkg/github/fakegithub"
    32  )
    33  
    34  type fakeHerd string
    35  
    36  var human = flag.Bool("human", false, "Enable to run additional manual tests")
    37  var ponyFlag = flag.String("pony", "", "Request a particular pony if set")
    38  
    39  func (c fakeHerd) readPony(tags string) (string, error) {
    40  	if tags != "" {
    41  		return tags, nil
    42  	}
    43  	return string(c), nil
    44  }
    45  
    46  func parsePoniesFromComment(comment []github.IssueComment) (ponies int) {
    47  	if comment == nil {
    48  		return
    49  	}
    50  	// Golang doesn't support lookback regex matches. Hence this hack to parse the pony URLs from rest of the comment.
    51  	var rawComment = comment[0].Body
    52  	rawComment = rawComment[strings.Index(rawComment, ":")+1:]
    53  	rawComment = strings.TrimSpace(rawComment[:strings.Index(rawComment, "<details>")])
    54  	return len(strings.Split(rawComment, "\n"))
    55  }
    56  
    57  func TestRealPony(t *testing.T) {
    58  	if !*human {
    59  		t.Skip("Real ponies disabled for automation. Manual users can add --human [--category=foo]")
    60  	}
    61  	if pony, err := ponyURL.readPony(*ponyFlag); err != nil {
    62  		t.Errorf("Could not read pony from %s: %v", ponyURL, err)
    63  	} else {
    64  		fmt.Println(pony)
    65  	}
    66  }
    67  
    68  func TestFormat(t *testing.T) {
    69  	result := formatURLs("http://example.com/small", "http://example.com/full")
    70  	expected := "[![pony image](http://example.com/small)](http://example.com/full)"
    71  	if result != expected {
    72  		t.Errorf("Expected %q, but got %q", expected, result)
    73  	}
    74  }
    75  
    76  // Medium integration test (depends on ability to open a TCP port)
    77  func TestHttpResponse(t *testing.T) {
    78  
    79  	// create test cases for handling content length of images
    80  	contentLength := make(map[string]string)
    81  	contentLength["/pony.jpg"] = "717987"
    82  	contentLength["/horse.png"] = "12647753"
    83  
    84  	// fake server for images
    85  	ts2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    86  		if r.URL.Path == "/full" {
    87  			t.Errorf("Requested full-size image instead of small image.")
    88  			http.NotFound(w, r)
    89  			return
    90  		}
    91  		if s, ok := contentLength[r.URL.Path]; ok {
    92  			body := "binary image"
    93  			w.Header().Set("Content-Length", s)
    94  			io.WriteString(w, body)
    95  		} else {
    96  			t.Errorf("Cannot find content length for %s", r.URL.Path)
    97  		}
    98  	}))
    99  	defer ts2.Close()
   100  
   101  	// setup a stock valid request
   102  	url := ts2.URL + "/pony.jpg"
   103  	b, err := json.Marshal(&ponyResult{
   104  		Pony: ponyResultPony{
   105  			Representations: ponyRepresentations{
   106  				Small: ts2.URL + "/pony.jpg",
   107  				Full:  ts2.URL + "/full",
   108  			},
   109  		},
   110  	})
   111  	if err != nil {
   112  		t.Errorf("Failed to encode test data: %v", err)
   113  	}
   114  
   115  	// create test cases for handling http responses
   116  	validResponse := string(b)
   117  
   118  	type testcase struct {
   119  		name        string
   120  		comment     string
   121  		path        string
   122  		response    string
   123  		expected    string
   124  		expectTag   string
   125  		expectNoTag bool
   126  		isValid     bool
   127  		noPony      bool
   128  	}
   129  
   130  	var testcases = []testcase{
   131  		{
   132  			name:     "valid",
   133  			comment:  "/pony",
   134  			path:     "/valid",
   135  			response: validResponse,
   136  			expected: url,
   137  			isValid:  true,
   138  		},
   139  		{
   140  			name:    "no pony found",
   141  			comment: "/pony",
   142  			path:    "/404",
   143  			noPony:  true,
   144  			isValid: false,
   145  		},
   146  		{
   147  			name:     "invalid JSON",
   148  			comment:  "/pony",
   149  			path:     "/bad-json",
   150  			response: `{"bad-blob": "not-a-url"`,
   151  			isValid:  false,
   152  		},
   153  		{
   154  			name:     "image too big",
   155  			comment:  "/pony",
   156  			path:     "/too-big",
   157  			response: fmt.Sprintf(`{"pony":{"representations": {"small": "%s/horse.png", "full": "%s/full"}}}`, ts2.URL, ts2.URL),
   158  			isValid:  false,
   159  		},
   160  		{
   161  			name:      "has tag",
   162  			comment:   "/pony peach hack",
   163  			path:      "/peach",
   164  			isValid:   true,
   165  			expectTag: "peach hack",
   166  			response:  validResponse,
   167  		},
   168  		{
   169  			name:        "pony embedded in other commands",
   170  			comment:     "/meow\n/pony\n/woof\n\nTesting :)",
   171  			path:        "/embedded",
   172  			isValid:     true,
   173  			expectNoTag: true,
   174  			response:    validResponse,
   175  		},
   176  	}
   177  
   178  	// fake server for image urls
   179  	pathToTestCase := make(map[string]*testcase)
   180  	for _, testcase := range testcases {
   181  		tc := testcase
   182  		pathToTestCase[testcase.path] = &tc
   183  	}
   184  	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   185  		if tc, ok := pathToTestCase[r.URL.Path]; ok {
   186  			if tc.noPony {
   187  				http.NotFound(w, r)
   188  				return
   189  			}
   190  			q := r.URL.Query().Get("q")
   191  			if tc.expectTag != "" && q != tc.expectTag {
   192  				t.Errorf("Expected tag %q, but got %q", tc.expectTag, q)
   193  			}
   194  			if tc.expectNoTag && q != "" {
   195  				t.Errorf("Expected no tag, but got %q", q)
   196  			}
   197  			io.WriteString(w, tc.response)
   198  		} else {
   199  			io.WriteString(w, validResponse)
   200  		}
   201  	}))
   202  	defer ts.Close()
   203  
   204  	// run test for each case
   205  	for _, testcase := range testcases {
   206  		pony, err := realHerd(ts.URL + testcase.path).readPony(testcase.expectTag)
   207  		if testcase.isValid && err != nil {
   208  			t.Errorf("For case %s, didn't expect error: %v", testcase.name, err)
   209  		} else if !testcase.isValid && err == nil {
   210  			t.Errorf("For case %s, expected error, received pony: %s", testcase.name, pony)
   211  		}
   212  
   213  		if !testcase.isValid {
   214  			continue
   215  		}
   216  
   217  		// github fake client
   218  		fc := fakegithub.NewFakeClient()
   219  
   220  		// fully test handling a comment
   221  		e := &github.GenericCommentEvent{
   222  			Action:     github.GenericCommentActionCreated,
   223  			Body:       testcase.comment,
   224  			Number:     5,
   225  			IssueState: "open",
   226  		}
   227  		err = handle(fc, logrus.WithField("plugin", pluginName), e, realHerd(ts.URL+testcase.path))
   228  		if err != nil {
   229  			t.Errorf("tc %s: For comment %s, didn't expect error: %v", testcase.name, testcase.comment, err)
   230  		}
   231  
   232  		if len(fc.IssueComments[5]) != 1 {
   233  			t.Errorf("tc %s: should have commented", testcase.name)
   234  		}
   235  		if c := fc.IssueComments[5][0]; !strings.Contains(c.Body, testcase.expected) {
   236  			t.Errorf("tc %s: missing image url: %s from comment: %v", testcase.name, testcase.expected, c.Body)
   237  		}
   238  	}
   239  }
   240  
   241  // Small, unit tests
   242  func TestPonies(t *testing.T) {
   243  	var testcases = []struct {
   244  		name      string
   245  		action    github.GenericCommentEventAction
   246  		body      string
   247  		state     string
   248  		pr        bool
   249  		numPonies int
   250  	}{
   251  		{
   252  			name:      "ignore edited comment",
   253  			state:     "open",
   254  			action:    github.GenericCommentActionEdited,
   255  			body:      "/pony",
   256  			numPonies: 0,
   257  		},
   258  		{
   259  			name:      "leave pony on pr",
   260  			state:     "open",
   261  			action:    github.GenericCommentActionCreated,
   262  			body:      "/pony",
   263  			pr:        true,
   264  			numPonies: 1,
   265  		},
   266  		{
   267  			name:      "leave pony on issue",
   268  			state:     "open",
   269  			action:    github.GenericCommentActionCreated,
   270  			body:      "/pony",
   271  			numPonies: 1,
   272  		},
   273  		{
   274  			name:      "leave pony on issue, trailing space",
   275  			state:     "open",
   276  			action:    github.GenericCommentActionCreated,
   277  			body:      "/pony \r",
   278  			numPonies: 1,
   279  		},
   280  		{
   281  			name:      "leave pony on issue, tag specified",
   282  			state:     "open",
   283  			action:    github.GenericCommentActionCreated,
   284  			body:      "/pony Twilight Sparkle",
   285  			numPonies: 1,
   286  		},
   287  		{
   288  			name:      "leave pony on issue, tag specified, trailing space",
   289  			state:     "open",
   290  			action:    github.GenericCommentActionCreated,
   291  			body:      "/pony Twilight Sparkle \r",
   292  			numPonies: 1,
   293  		},
   294  		{
   295  			name:      "leave multiple ponies on issue, mixed tags specified, trailing space",
   296  			state:     "open",
   297  			action:    github.GenericCommentActionCreated,
   298  			body:      "/pony one \n/pony \n/pony three \n/pony \n",
   299  			numPonies: 4,
   300  		},
   301  		{
   302  			name:      "More than N ponies on issue but only N are picked",
   303  			state:     "open",
   304  			action:    github.GenericCommentActionCreated,
   305  			body:      "/pony one \n/pony two \n/pony three \n/pony four \n/pony five \n/pony six",
   306  			numPonies: 5,
   307  		},
   308  		{
   309  			name:      "don't leave cats or dogs",
   310  			state:     "open",
   311  			action:    github.GenericCommentActionCreated,
   312  			body:      "/woof\n/meow",
   313  			numPonies: 0,
   314  		},
   315  		{
   316  			name:      "do nothing in the middle of a line",
   317  			state:     "open",
   318  			action:    github.GenericCommentActionCreated,
   319  			body:      "did you know that /pony makes ponies happen?",
   320  			numPonies: 0,
   321  		},
   322  	}
   323  	for _, tc := range testcases {
   324  		fc := fakegithub.NewFakeClient()
   325  		e := &github.GenericCommentEvent{
   326  			Action:     tc.action,
   327  			Body:       tc.body,
   328  			Number:     5,
   329  			IssueState: tc.state,
   330  			IsPR:       tc.pr,
   331  		}
   332  		err := handle(fc, logrus.WithField("plugin", pluginName), e, fakeHerd("pone"))
   333  		if err != nil {
   334  			t.Errorf("For case %s, didn't expect error: %v", tc.name, err)
   335  		}
   336  
   337  		var actualPonyCount = parsePoniesFromComment(fc.IssueComments[5])
   338  		if tc.numPonies != actualPonyCount {
   339  			t.Errorf("For case '%s', #expected ponies %v, #found ponies %v", tc.name, tc.numPonies, actualPonyCount)
   340  		}
   341  	}
   342  }