cloudflare/pint

Public

mirrored from https://github.com/cloudflare/pintAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v0.64.0

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

internal/reporter/github_test.go

663lines · modecode

1package reporter_test
2
3import (
4 "context"
5 "fmt"
6 "io"
7 "log/slog"
8 "net/http"
9 "net/http/httptest"
10 "strings"
11 "testing"
12 "time"
13
14 "github.com/neilotoole/slogt"
15 "github.com/stretchr/testify/require"
16
17 "github.com/cloudflare/pint/internal/checks"
18 "github.com/cloudflare/pint/internal/discovery"
19 "github.com/cloudflare/pint/internal/parser"
20 "github.com/cloudflare/pint/internal/reporter"
21)
22
23func TestGithubReporter(t *testing.T) {
24 type testCaseT struct {
25 httpHandler http.Handler
26 error func(uri string) string
27
28 description string
29
30 owner string
31 repo string
32 token string
33 reports []reporter.Report
34 prNum int
35 maxComments int
36 timeout time.Duration
37 }
38
39 p := parser.NewParser(false)
40 mockRules, _ := p.Parse([]byte(`
41- record: target is down
42 expr: up == 0
43- record: sum errors
44 expr: sum(errors) by (job)
45`))
46
47 for _, tc := range []testCaseT{
48 {
49 description: "list pull requests timeout",
50 owner: "foo",
51 repo: "bar",
52 token: "something",
53 prNum: 123,
54 maxComments: 50,
55 httpHandler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
56 time.Sleep(1 * time.Second)
57 _, _ = w.Write([]byte("OK"))
58 }),
59 timeout: 100 * time.Millisecond,
60 error: func(_ string) string {
61 return "failed to list pull request reviews: context deadline exceeded"
62 },
63 reports: []reporter.Report{
64 {
65 Path: discovery.Path{
66 Name: "$1",
67 SymlinkTarget: "$1",
68 },
69
70 ModifiedLines: []int{2},
71 Rule: mockRules[1],
72 Problem: checks.Problem{
73 Lines: parser.LineRange{
74 First: 2,
75 Last: 2,
76 },
77 Reporter: "mock",
78 Text: "syntax error",
79 Severity: checks.Fatal,
80 },
81 },
82 },
83 },
84 {
85 description: "list reviews error",
86 owner: "foo",
87 repo: "bar",
88 token: "something",
89 prNum: 123,
90 maxComments: 50,
91 timeout: time.Second,
92 httpHandler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
93 if r.Method == http.MethodGet && r.URL.Path == "/api/v3/repos/foo/bar/pulls/123/reviews" {
94 w.WriteHeader(http.StatusBadRequest)
95 _, _ = w.Write([]byte("Error"))
96 return
97 }
98 _, _ = w.Write([]byte(""))
99 }),
100 error: func(uri string) string {
101 return fmt.Sprintf("failed to list pull request reviews: GET %s/api/v3/repos/foo/bar/pulls/123/reviews: 400 []", uri)
102 },
103 reports: []reporter.Report{
104 {
105 Path: discovery.Path{
106 Name: "foo.txt",
107 SymlinkTarget: "foo.txt",
108 },
109
110 ModifiedLines: []int{2},
111 Rule: mockRules[1],
112 Problem: checks.Problem{
113 Lines: parser.LineRange{
114 First: 2,
115 Last: 2,
116 },
117 Reporter: "mock",
118 Text: "syntax error",
119 Details: "syntax details",
120 Severity: checks.Fatal,
121 },
122 },
123 },
124 },
125 {
126 description: "no problems",
127 owner: "foo",
128 repo: "bar",
129 token: "something",
130 prNum: 123,
131 maxComments: 50,
132 timeout: time.Second,
133 reports: []reporter.Report{},
134 },
135 {
136 description: "happy path",
137 owner: "foo",
138 repo: "bar",
139 token: "something",
140 prNum: 123,
141 maxComments: 50,
142 timeout: time.Second,
143 reports: []reporter.Report{
144 {
145 Path: discovery.Path{
146 Name: "foo.txt",
147 SymlinkTarget: "foo.txt",
148 },
149
150 ModifiedLines: []int{2},
151 Rule: mockRules[1],
152 Problem: checks.Problem{
153 Lines: parser.LineRange{
154 First: 2,
155 Last: 2,
156 },
157 Reporter: "mock",
158 Text: "syntax error",
159 Details: "syntax details",
160 Severity: checks.Fatal,
161 },
162 },
163 },
164 },
165 {
166 description: "error crating review",
167 owner: "foo",
168 repo: "bar",
169 token: "something",
170 prNum: 123,
171 maxComments: 50,
172 timeout: time.Second,
173 httpHandler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
174 if r.Method == http.MethodPost && r.URL.Path == "/api/v3/repos/foo/bar/pulls/123/reviews" {
175 w.WriteHeader(http.StatusBadGateway)
176 _, _ = w.Write([]byte("Error"))
177 return
178 }
179 _, _ = w.Write([]byte(""))
180 }),
181 error: func(uri string) string {
182 return fmt.Sprintf("failed to create pull request review: POST %s/api/v3/repos/foo/bar/pulls/123/reviews: 502 []", uri)
183 },
184 reports: []reporter.Report{
185 {
186 Path: discovery.Path{
187 Name: "foo.txt",
188 SymlinkTarget: "foo.txt",
189 },
190
191 ModifiedLines: []int{2},
192 Rule: mockRules[1],
193 Problem: checks.Problem{
194 Lines: parser.LineRange{
195 First: 2,
196 Last: 2,
197 },
198 Reporter: "mock",
199 Text: "syntax error",
200 Details: "syntax details",
201 Severity: checks.Fatal,
202 },
203 },
204 },
205 },
206 {
207 description: "error updating existing review",
208 owner: "foo",
209 repo: "bar",
210 token: "something",
211 prNum: 123,
212 maxComments: 50,
213 timeout: time.Second,
214 httpHandler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
215 if r.Method == http.MethodGet && r.URL.Path == "/api/v3/repos/foo/bar/pulls/123/reviews" {
216 _, _ = w.Write([]byte(`[{"id":1,"body":"### This pull request was validated by [pint](https://github.com/cloudflare/pint).\nxxxx"}]`))
217 return
218 }
219 if r.Method == http.MethodPut && r.URL.Path == "/api/v3/repos/foo/bar/pulls/123/reviews/1" {
220 w.WriteHeader(http.StatusBadGateway)
221 _, _ = w.Write([]byte("Error"))
222 return
223 }
224 _, _ = w.Write([]byte(""))
225 }),
226 error: func(uri string) string {
227 return fmt.Sprintf("failed to update pull request review: PUT %s/api/v3/repos/foo/bar/pulls/123/reviews/1: 502 []", uri)
228 },
229 reports: []reporter.Report{
230 {
231 Path: discovery.Path{
232 Name: "foo.txt",
233 SymlinkTarget: "foo.txt",
234 },
235
236 ModifiedLines: []int{2},
237 Rule: mockRules[1],
238 Problem: checks.Problem{
239 Lines: parser.LineRange{
240 First: 2,
241 Last: 2,
242 },
243 Reporter: "mock",
244 Text: "syntax error",
245 Details: "syntax details",
246 Severity: checks.Fatal,
247 },
248 },
249 },
250 },
251 {
252 description: "update existing review",
253 owner: "foo",
254 repo: "bar",
255 token: "something",
256 prNum: 123,
257 maxComments: 50,
258 timeout: time.Second,
259 httpHandler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
260 if r.Method == http.MethodGet && r.URL.Path == "/api/v3/repos/foo/bar/pulls/123/reviews" {
261 _, _ = w.Write([]byte(`[{"id":1,"body":"### This pull request was validated by [pint](https://github.com/cloudflare/pint).\nxxxx"}]`))
262 return
263 }
264 if r.Method == http.MethodGet && r.URL.Path == "/api/v3/repos/foo/bar/pulls/123/comments" {
265 _, _ = w.Write([]byte(`[{"id":1,"commit_id":"fake-commit-id","path":"foo.txt","line":2,"body":":stop_sign: [mock](https://cloudflare.github.io/pint/checks/mock.html): syntax error\n\nsyntax details"}]`))
266 return
267 }
268 _, _ = w.Write([]byte(""))
269 }),
270 reports: []reporter.Report{
271 {
272 Path: discovery.Path{
273 Name: "foo.txt",
274 SymlinkTarget: "foo.txt",
275 },
276
277 ModifiedLines: []int{2},
278 Rule: mockRules[1],
279 Problem: checks.Problem{
280 Lines: parser.LineRange{
281 First: 2,
282 Last: 2,
283 },
284 Reporter: "mock",
285 Text: "syntax error",
286 Details: "syntax details",
287 Severity: checks.Fatal,
288 },
289 },
290 },
291 },
292 {
293 description: "maxComments 2",
294 owner: "foo",
295 repo: "bar",
296 token: "something",
297 prNum: 123,
298 maxComments: 2,
299 timeout: time.Second,
300 httpHandler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
301 if r.Method == http.MethodGet && r.URL.Path == "/api/v3/repos/foo/bar/pulls/123/reviews" {
302 _, _ = w.Write([]byte(`[{"id":1,"body":"### This pull request was validated by [pint](https://github.com/cloudflare/pint).\nxxxx"}]`))
303 return
304 }
305 if r.Method == http.MethodGet && r.URL.Path == "/api/v3/repos/foo/bar/pulls/123/comments" {
306 _, _ = w.Write([]byte(`[{"id":1,"commit_id":"fake-commit-id","path":"foo.txt","line":2,"body":":stop_sign: [mock](https://cloudflare.github.io/pint/checks/mock.html): syntax error\n\nsyntax details"}]`))
307 return
308 }
309 if r.Method == http.MethodPost && r.URL.Path == "/api/v3/repos/foo/bar/pulls/123/comments" {
310 body, _ := io.ReadAll(r.Body)
311 b := strings.TrimSpace(strings.TrimRight(string(body), "\n\t\r"))
312 switch b {
313 case `{"body":":stop_sign: **Bug** reported by [pint](https://cloudflare.github.io/pint/) **mock1** check.\n\n------\n\nsyntax error1\n\nsyntax details1\n\n------\n\n:information_source: To see documentation covering this check and instructions on how to resolve it [click here](https://cloudflare.github.io/pint/checks/mock1.html).\n","path":"foo.txt","line":2,"side":"RIGHT","commit_id":"HEAD"}`:
314 case `{"body":":stop_sign: **Bug** reported by [pint](https://cloudflare.github.io/pint/) **mock2** check.\n\n------\n\nsyntax error2\n\nsyntax details2\n\n------\n\n:information_source: To see documentation covering this check and instructions on how to resolve it [click here](https://cloudflare.github.io/pint/checks/mock2.html).\n","path":"foo.txt","line":2,"side":"RIGHT","commit_id":"HEAD"}`:
315 case `{"body":"This pint run would create 4 comment(s), which is more than 2 limit configured for pint.\n2 comments were skipped and won't be visibile on this PR."}`:
316 default:
317 t.Errorf("Unexpected comment: %s", b)
318 }
319 }
320 _, _ = w.Write([]byte(""))
321 }),
322 reports: []reporter.Report{
323 {
324 Path: discovery.Path{
325 Name: "foo.txt",
326 SymlinkTarget: "foo.txt",
327 },
328
329 ModifiedLines: []int{2},
330 Rule: mockRules[1],
331 Problem: checks.Problem{
332 Lines: parser.LineRange{
333 First: 2,
334 Last: 2,
335 },
336 Reporter: "mock1",
337 Text: "syntax error1",
338 Details: "syntax details1",
339 Severity: checks.Bug,
340 },
341 },
342 {
343 Path: discovery.Path{
344 Name: "foo.txt",
345 SymlinkTarget: "foo.txt",
346 },
347
348 ModifiedLines: []int{2},
349 Rule: mockRules[1],
350 Problem: checks.Problem{
351 Lines: parser.LineRange{
352 First: 2,
353 Last: 2,
354 },
355 Reporter: "mock2",
356 Text: "syntax error2",
357 Details: "syntax details2",
358 Severity: checks.Bug,
359 },
360 },
361 {
362 Path: discovery.Path{
363 Name: "foo.txt",
364 SymlinkTarget: "foo.txt",
365 },
366
367 ModifiedLines: []int{2},
368 Rule: mockRules[1],
369 Problem: checks.Problem{
370 Lines: parser.LineRange{
371 First: 2,
372 Last: 2,
373 },
374 Reporter: "mock3",
375 Text: "syntax error3",
376 Details: "syntax details3",
377 Severity: checks.Fatal,
378 },
379 },
380 {
381 Path: discovery.Path{
382 Name: "foo.txt",
383 SymlinkTarget: "foo.txt",
384 },
385
386 ModifiedLines: []int{2},
387 Rule: mockRules[1],
388 Problem: checks.Problem{
389 Lines: parser.LineRange{
390 First: 2,
391 Last: 2,
392 },
393 Reporter: "mock4",
394 Text: "syntax error4",
395 Details: "syntax details4",
396 Severity: checks.Fatal,
397 },
398 },
399 },
400 },
401 {
402 description: "maxComments 2, too many comments comment error",
403 owner: "foo",
404 repo: "bar",
405 token: "something",
406 prNum: 123,
407 maxComments: 2,
408 timeout: time.Second,
409 httpHandler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
410 if r.Method == http.MethodGet && r.URL.Path == "/api/v3/repos/foo/bar/pulls/123/reviews" {
411 _, _ = w.Write([]byte(`[{"id":1,"body":"### This pull request was validated by [pint](https://github.com/cloudflare/pint).\nxxxx"}]`))
412 return
413 }
414 if r.Method == http.MethodGet && r.URL.Path == "/api/v3/repos/foo/bar/pulls/123/comments" {
415 _, _ = w.Write([]byte(`[{"id":1,"commit_id":"fake-commit-id","path":"foo.txt","line":2,"body":":stop_sign: [mock](https://cloudflare.github.io/pint/checks/mock.html): syntax error\n\nsyntax details"}]`))
416 return
417 }
418 if r.Method == http.MethodPost && r.URL.Path == "/api/v3/repos/foo/bar/issues/123/comments" {
419 body, _ := io.ReadAll(r.Body)
420 b := strings.TrimSpace(strings.TrimRight(string(body), "\n\t\r"))
421 if b == `{"body":"This pint run would create 4 comment(s), which is more than the limit configured for pint (2).\n2 comment(s) were skipped and won't be visibile on this PR."}` {
422 w.WriteHeader(http.StatusInternalServerError)
423 _, _ = w.Write([]byte("Cannot create issue comment"))
424 return
425 }
426 }
427 _, _ = w.Write([]byte(""))
428 }),
429 reports: []reporter.Report{
430 {
431 Path: discovery.Path{
432 Name: "foo.txt",
433 SymlinkTarget: "foo.txt",
434 },
435
436 ModifiedLines: []int{2},
437 Rule: mockRules[1],
438 Problem: checks.Problem{
439 Lines: parser.LineRange{
440 First: 2,
441 Last: 2,
442 },
443 Reporter: "mock1",
444 Text: "syntax error1",
445 Details: "syntax details1",
446 Severity: checks.Bug,
447 },
448 },
449 {
450 Path: discovery.Path{
451 Name: "foo.txt",
452 SymlinkTarget: "foo.txt",
453 },
454
455 ModifiedLines: []int{2},
456 Rule: mockRules[1],
457 Problem: checks.Problem{
458 Lines: parser.LineRange{
459 First: 2,
460 Last: 2,
461 },
462 Reporter: "mock2",
463 Text: "syntax error2",
464 Details: "syntax details2",
465 Severity: checks.Bug,
466 },
467 },
468 {
469 Path: discovery.Path{
470 Name: "foo.txt",
471 SymlinkTarget: "foo.txt",
472 },
473
474 ModifiedLines: []int{2},
475 Rule: mockRules[1],
476 Problem: checks.Problem{
477 Lines: parser.LineRange{
478 First: 2,
479 Last: 2,
480 },
481 Reporter: "mock3",
482 Text: "syntax error3",
483 Details: "syntax details3",
484 Severity: checks.Fatal,
485 },
486 },
487 {
488 Path: discovery.Path{
489 Name: "foo.txt",
490 SymlinkTarget: "foo.txt",
491 },
492
493 ModifiedLines: []int{2},
494 Rule: mockRules[1],
495 Problem: checks.Problem{
496 Lines: parser.LineRange{
497 First: 2,
498 Last: 2,
499 },
500 Reporter: "mock4",
501 Text: "syntax error4",
502 Details: "syntax details4",
503 Severity: checks.Fatal,
504 },
505 },
506 },
507 },
508 {
509 description: "general comment error",
510 owner: "foo",
511 repo: "bar",
512 token: "something",
513 prNum: 123,
514 maxComments: 2,
515 timeout: time.Second,
516 httpHandler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
517 if r.Method == http.MethodGet && r.URL.Path == "/api/v3/repos/foo/bar/pulls/123/reviews" {
518 _, _ = w.Write([]byte(`[{"id":1,"body":"### This pull request was validated by [pint](https://github.com/cloudflare/pint).\nxxxx"}]`))
519 return
520 }
521 if r.Method == http.MethodGet && r.URL.Path == "/api/v3/repos/foo/bar/pulls/123/comments" {
522 _, _ = w.Write([]byte(`[{"id":1,"commit_id":"fake-commit-id","path":"foo.txt","line":2,"body":":stop_sign: [mock](https://cloudflare.github.io/pint/checks/mock.html): syntax error\n\nsyntax details"}]`))
523 return
524 }
525 if r.Method == http.MethodPost && r.URL.Path == "/api/v3/repos/foo/bar/issues/123/comments" {
526 w.WriteHeader(http.StatusInternalServerError)
527 _, _ = w.Write([]byte("Cannot create issue comment"))
528 return
529 }
530 _, _ = w.Write([]byte(""))
531 }),
532 error: func(uri string) string {
533 return fmt.Sprintf("failed to create general comment: POST %s/api/v3/repos/foo/bar/issues/123/comments: 500 []", uri)
534 },
535 reports: []reporter.Report{
536 {
537 Path: discovery.Path{
538 Name: "foo.txt",
539 SymlinkTarget: "foo.txt",
540 },
541
542 ModifiedLines: []int{2},
543 Rule: mockRules[1],
544 Problem: checks.Problem{
545 Lines: parser.LineRange{
546 First: 2,
547 Last: 2,
548 },
549 Reporter: "mock1",
550 Text: "syntax error1",
551 Details: "syntax details1",
552 Severity: checks.Bug,
553 },
554 },
555 {
556 Path: discovery.Path{
557 Name: "foo.txt",
558 SymlinkTarget: "foo.txt",
559 },
560
561 ModifiedLines: []int{2},
562 Rule: mockRules[1],
563 Problem: checks.Problem{
564 Lines: parser.LineRange{
565 First: 2,
566 Last: 2,
567 },
568 Reporter: "mock2",
569 Text: "syntax error2",
570 Details: "syntax details2",
571 Severity: checks.Bug,
572 },
573 },
574 {
575 Path: discovery.Path{
576 Name: "foo.txt",
577 SymlinkTarget: "foo.txt",
578 },
579
580 ModifiedLines: []int{2},
581 Rule: mockRules[1],
582 Problem: checks.Problem{
583 Lines: parser.LineRange{
584 First: 2,
585 Last: 2,
586 },
587 Reporter: "mock3",
588 Text: "syntax error3",
589 Details: "syntax details3",
590 Severity: checks.Fatal,
591 },
592 },
593 {
594 Path: discovery.Path{
595 Name: "foo.txt",
596 SymlinkTarget: "foo.txt",
597 },
598
599 ModifiedLines: []int{2},
600 Rule: mockRules[1],
601 Problem: checks.Problem{
602 Lines: parser.LineRange{
603 First: 2,
604 Last: 2,
605 },
606 Reporter: "mock4",
607 Text: "syntax error4",
608 Details: "syntax details4",
609 Severity: checks.Fatal,
610 },
611 },
612 },
613 },
614 } {
615 t.Run(tc.description, func(t *testing.T) {
616 slog.SetDefault(slogt.New(t))
617
618 var handler http.Handler
619 if tc.httpHandler != nil {
620 handler = tc.httpHandler
621 } else {
622 // Handler that checks for token.
623 handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
624 auth := r.Header["Authorization"]
625 if len(auth) == 0 {
626 w.WriteHeader(500)
627 _, _ = w.Write([]byte("No token"))
628 t.Fatal("got a request with no token")
629 return
630 }
631 token := auth[0]
632 if token != "Bearer "+tc.token {
633 w.WriteHeader(500)
634 _, _ = w.Write([]byte("Invalid token"))
635 t.Fatalf("got a request with invalid token (got %s)", token)
636 }
637 })
638 }
639 srv := httptest.NewServer(handler)
640 defer srv.Close()
641 r, err := reporter.NewGithubReporter(
642 "v0.0.0",
643 srv.URL,
644 srv.URL,
645 tc.timeout,
646 tc.token,
647 tc.owner,
648 tc.repo,
649 tc.prNum,
650 tc.maxComments,
651 "HEAD",
652 )
653 require.NoError(t, err)
654
655 err = reporter.Submit(context.Background(), reporter.NewSummary(tc.reports), r)
656 if tc.error == nil {
657 require.NoError(t, err)
658 } else {
659 require.EqualError(t, err, tc.error(srv.URL))
660 }
661 })
662 }
663}
664