fork of https://github.com/sourcegraph/zoekt
1// Copyright 2016 Google Inc. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package web
16
17import (
18 "bytes"
19 "context"
20 "encoding/json"
21 "fmt"
22 "io"
23 "log"
24 "net/http"
25 "net/http/httptest"
26 "reflect"
27 "sort"
28 "strings"
29 "testing"
30 "time"
31
32 "github.com/google/go-cmp/cmp"
33
34 "github.com/sourcegraph/zoekt"
35 "github.com/sourcegraph/zoekt/index"
36 "github.com/sourcegraph/zoekt/query"
37)
38
39// TODO(hanwen): cut & paste from ../ . Should create internal test
40// util package.
41type memSeeker struct {
42 data []byte
43}
44
45func (s *memSeeker) Close() {}
46func (s *memSeeker) Read(off, sz uint32) ([]byte, error) {
47 return s.data[off : off+sz], nil
48}
49
50func (s *memSeeker) Size() (uint32, error) {
51 return uint32(len(s.data)), nil
52}
53
54func (s *memSeeker) Name() string {
55 return "memSeeker"
56}
57
58func searcherForTest(t *testing.T, b *index.ShardBuilder) zoekt.Streamer {
59 var buf bytes.Buffer
60 if err := b.Write(&buf); err != nil {
61 t.Fatal(err)
62 }
63 f := &memSeeker{buf.Bytes()}
64
65 searcher, err := index.NewSearcher(f)
66 if err != nil {
67 t.Fatalf("NewSearcher: %v", err)
68 }
69
70 return adapter{Searcher: searcher}
71}
72
73type adapter struct {
74 zoekt.Searcher
75}
76
77func (a adapter) StreamSearch(ctx context.Context, q query.Q, opts *zoekt.SearchOptions, sender zoekt.Sender) (err error) {
78 sr, err := a.Searcher.Search(ctx, q, opts)
79 if err != nil {
80 return err
81 }
82 sender.Send(sr)
83 return nil
84}
85
86func TestBasic(t *testing.T) {
87 b, err := index.NewShardBuilder(&zoekt.Repository{
88 Name: "name",
89 URL: "repo-url",
90 CommitURLTemplate: `{{ URLJoinPath "https://github.com/org/repo/commit/" .Version}}`,
91 FileURLTemplate: `{{ URLJoinPath "https://github.com/org/repo/blob/" .Version .Path}}`,
92 LineFragmentTemplate: "#L{{.LineNumber}}",
93 Branches: []zoekt.RepositoryBranch{{Name: "master", Version: "1234"}},
94 })
95 if err != nil {
96 t.Fatalf("NewShardBuilder: %v", err)
97 }
98 if err := b.Add(index.Document{
99 // use a name which requires correct escaping. https://github.com/sourcegraph/zoekt/issues/807
100 Name: "foo/bar+baz",
101 Content: []byte("to carry water in the no later bla"),
102 // --------------0123456789012345678901234567890123
103 // --------------0 1 2 3
104 Branches: []string{"master"},
105 }); err != nil {
106 t.Fatalf("Add: %v", err)
107 }
108
109 s := searcherForTest(t, b)
110 srv := Server{
111 Searcher: s,
112 Top: Top,
113 HTML: true,
114 }
115
116 mux, err := NewMux(&srv)
117 if err != nil {
118 t.Fatalf("NewMux: %v", err)
119 }
120
121 ts := httptest.NewServer(mux)
122 defer ts.Close()
123
124 nowStr := time.Now().UTC().Format("Jan 02, 2006 15:04")
125 for req, needles := range map[string][]string{
126 "/": {"from 1 repositories"},
127 "/search?q=water": {
128 `href="https://github.com/org/repo/blob/1234/foo/bar%2Bbaz"`,
129 "carry <b>water</b>",
130 },
131 "/search?q=r:": {
132 "1234\">master",
133 "Found 1 repositories",
134 nowStr,
135 "repo-url\">name",
136 "1 files (45B)",
137 },
138 "/search?q=magic": {
139 `value=magic`,
140 },
141 "/search?q=foo+type:file": {
142 `value=foo`,
143 },
144 "/robots.txt": {
145 "disallow: /search",
146 },
147 } {
148 checkNeedles(t, ts, req, needles)
149 }
150}
151
152func TestPrint(t *testing.T) {
153 b, err := index.NewShardBuilder(&zoekt.Repository{
154 Name: "name",
155 URL: "repo-url",
156 CommitURLTemplate: "{{.Version}}",
157 FileURLTemplate: "file-url",
158 LineFragmentTemplate: "line",
159 Branches: []zoekt.RepositoryBranch{{Name: "master", Version: "1234"}},
160 })
161 if err != nil {
162 t.Fatalf("NewShardBuilder: %v", err)
163 }
164 if err := b.Add(index.Document{
165 Name: "f2",
166 Content: []byte("to carry water in the no later bla"),
167 Branches: []string{"master"},
168 }); err != nil {
169 t.Fatalf("Add: %v", err)
170 }
171
172 if err := b.Add(index.Document{
173 Name: "dir/f2",
174 Content: []byte("blabla"),
175 Branches: []string{"master"},
176 }); err != nil {
177 t.Fatalf("Add: %v", err)
178 }
179
180 s := searcherForTest(t, b)
181 srv := Server{
182 Searcher: s,
183 Top: Top,
184 HTML: true,
185 Print: true,
186 }
187
188 mux, err := NewMux(&srv)
189 if err != nil {
190 t.Fatalf("NewMux: %v", err)
191 }
192
193 ts := httptest.NewServer(mux)
194 defer ts.Close()
195
196 for req, needles := range map[string][]string{
197 "/print?q=bla&r=name&f=f2": {
198 `pre id="l1" class="inline-pre"><span class="noselect"><a href="#l1">`,
199 },
200 } {
201 checkNeedles(t, ts, req, needles)
202 }
203}
204
205func TestPrintDefault(t *testing.T) {
206 b, err := index.NewShardBuilder(&zoekt.Repository{
207 Name: "name",
208 URL: "repo-url",
209 Branches: []zoekt.RepositoryBranch{{Name: "master", Version: "1234"}},
210 })
211 if err != nil {
212 t.Fatalf("NewShardBuilder: %v", err)
213 }
214 if err := b.Add(index.Document{
215 Name: "f2",
216 Content: []byte("to carry water in the no later bla"),
217 Branches: []string{"master"},
218 }); err != nil {
219 t.Fatalf("Add: %v", err)
220 }
221 s := searcherForTest(t, b)
222 srv := Server{
223 Searcher: s,
224 Top: Top,
225 HTML: true,
226 }
227
228 mux, err := NewMux(&srv)
229 if err != nil {
230 t.Fatalf("NewMux: %v", err)
231 }
232
233 ts := httptest.NewServer(mux)
234 defer ts.Close()
235
236 for req, needles := range map[string][]string{
237 "/search?q=water": {
238 `href="print?`,
239 },
240 } {
241 checkNeedles(t, ts, req, needles)
242 }
243}
244
245func checkNeedles(t *testing.T, ts *httptest.Server, req string, needles []string) {
246 res, err := http.Get(ts.URL + req)
247 if err != nil {
248 t.Fatal(err)
249 }
250 resultBytes, err := io.ReadAll(res.Body)
251 res.Body.Close()
252 if err != nil {
253 log.Fatal(err)
254 }
255
256 result := string(resultBytes)
257 for _, want := range needles {
258 if !strings.Contains(result, want) {
259 t.Errorf("query %q: result did not have %q: %s", req, want, result)
260 }
261 }
262 if notWant := "crashed"; strings.Contains(result, notWant) {
263 t.Errorf("result has %q: %s", notWant, result)
264 }
265 if notWant := "bytes skipped)..."; strings.Contains(result, notWant) {
266 t.Errorf("result has %q: %s", notWant, result)
267 }
268}
269
270type Expectation struct {
271 title string
272 fileMatch FileMatch
273}
274
275func TestFormatJson(t *testing.T) {
276 b, err := index.NewShardBuilder(&zoekt.Repository{
277 Name: "name",
278 URL: "repo-url",
279 Branches: []zoekt.RepositoryBranch{{Name: "master", Version: "1234"}},
280 })
281 if err != nil {
282 t.Fatalf("NewShardBuilder: %v", err)
283 }
284 if err := b.Add(index.Document{
285 Name: "f2",
286 Content: []byte("to carry water in the no later bla"),
287 Branches: []string{"master"},
288 }); err != nil {
289 t.Fatalf("Add: %v", err)
290 }
291 s := searcherForTest(t, b)
292 srv := Server{
293 Searcher: s,
294 Top: Top,
295 HTML: true,
296 }
297
298 mux, err := NewMux(&srv)
299 if err != nil {
300 t.Fatalf("NewMux: %v", err)
301 }
302
303 ts := httptest.NewServer(mux)
304 defer ts.Close()
305
306 expected := Expectation{
307 "json basic test",
308 FileMatch{
309 FileName: "f2",
310 Repo: "name",
311 Matches: []Match{
312 {
313 FileName: "f2",
314 LineNum: 1,
315 Fragments: []Fragment{
316 {
317 Pre: "to carry ",
318 Match: "water",
319 Post: " in the no later bla",
320 },
321 },
322 },
323 },
324 },
325 }
326
327 checkResultMatches(t, ts, "/search?q=water&format=json", expected)
328}
329
330func TestContextLines(t *testing.T) {
331 b, err := index.NewShardBuilder(&zoekt.Repository{
332 Name: "name",
333 URL: "repo-url",
334 Branches: []zoekt.RepositoryBranch{{Name: "master", Version: "1234"}},
335 })
336 if err != nil {
337 t.Fatalf("NewShardBuilder: %v", err)
338 }
339 if err := b.Add(index.Document{
340 Name: "f2",
341 Content: []byte("one line\nsecond snippet\nthird thing\nfourth\nfifth block\nsixth example\nseventh"),
342 Branches: []string{"master"},
343 }); err != nil {
344 t.Fatalf("Add: %v", err)
345 }
346 if err := b.Add(index.Document{
347 Name: "f3",
348 Content: []byte("\n\n\n\nto carry water in the no later bla\n\n\n\n"),
349 Branches: []string{"master"},
350 }); err != nil {
351 t.Fatalf("Add: %v", err)
352 }
353 if err := b.Add(index.Document{
354 Name: "f4",
355 Content: []byte("un \n \n\ttrois\n \n\nsix\n "),
356 Branches: []string{"master"},
357 }); err != nil {
358 t.Fatalf("Add: %v", err)
359 }
360 if err := b.Add(index.Document{
361 Name: "f5",
362 Content: []byte("\ngreen\npastures\n\nhere"),
363 Branches: []string{"master"},
364 }); err != nil {
365 t.Fatalf("Add: %v", err)
366 }
367 s := searcherForTest(t, b)
368 srv := Server{
369 Searcher: s,
370 Top: Top,
371 HTML: true,
372 }
373
374 mux, err := NewMux(&srv)
375 if err != nil {
376 t.Fatalf("NewMux: %v", err)
377 }
378
379 ts := httptest.NewServer(mux)
380 defer ts.Close()
381
382 for req, expected := range map[string]Expectation{
383 "/search?q=our&format=json&ctx=0": {
384 "no context doesn't return Before or After",
385 FileMatch{
386 FileName: "f2",
387 Repo: "name",
388 Matches: []Match{
389 {
390 FileName: "f2",
391 LineNum: 4,
392 Fragments: []Fragment{
393 {
394 Pre: "f",
395 Match: "our",
396 Post: "th\n",
397 },
398 },
399 },
400 },
401 },
402 },
403 "/search?q=f:f2&format=json&ctx=2": {
404 "filename does not return Before or After",
405 FileMatch{
406 FileName: "f2",
407 Repo: "name",
408 Matches: []Match{
409 {
410 FileName: "f2",
411 LineNum: 0,
412 Fragments: []Fragment{
413 {
414 Match: "f2",
415 },
416 },
417 },
418 },
419 },
420 },
421 "/search?q=our&format=json&ctx=2": {
422 "context returns Before and After",
423 FileMatch{
424 FileName: "f2",
425 Repo: "name",
426 Matches: []Match{
427 {
428 FileName: "f2",
429 LineNum: 4,
430 Fragments: []Fragment{
431 {
432 Pre: "f",
433 Match: "our",
434 Post: "th\n",
435 },
436 },
437 Before: "second snippet\nthird thing\n",
438 After: "fifth block\nsixth example\n",
439 },
440 },
441 },
442 },
443 "/search?q=one&format=json&ctx=2": {
444 "index at start returns After but no Before",
445 FileMatch{
446 FileName: "f2",
447 Repo: "name",
448 Matches: []Match{
449 {
450 FileName: "f2",
451 LineNum: 1,
452 Fragments: []Fragment{
453 {
454 Pre: "",
455 Match: "one",
456 Post: " line\n",
457 },
458 },
459 After: "second snippet\nthird thing\n",
460 },
461 },
462 },
463 },
464 "/search?q=seventh&format=json&ctx=2": {
465 "index at end returns Before but no After",
466 FileMatch{
467 FileName: "f2",
468 Repo: "name",
469 Matches: []Match{
470 {
471 FileName: "f2",
472 LineNum: 7,
473 Fragments: []Fragment{
474 {
475 Pre: "",
476 Match: "seventh",
477 Post: "",
478 },
479 },
480 Before: "fifth block\nsixth example\n",
481 },
482 },
483 },
484 },
485 "/search?q=seventh&format=json&ctx=10": {
486 "index with large context at end returns whole document",
487 FileMatch{
488 FileName: "f2",
489 Repo: "name",
490 Matches: []Match{
491 {
492 FileName: "f2",
493 LineNum: 7,
494 Fragments: []Fragment{
495 {
496 Pre: "",
497 Match: "seventh",
498 Post: "",
499 },
500 },
501 Before: "one line\nsecond snippet\nthird thing\nfourth\nfifth block\nsixth example\n",
502 },
503 },
504 },
505 },
506 "/search?q=one&format=json&ctx=10": {
507 "index with large context at start returns whole document",
508 FileMatch{
509 FileName: "f2",
510 Repo: "name",
511 Matches: []Match{
512 {
513 FileName: "f2",
514 LineNum: 1,
515 Fragments: []Fragment{
516 {
517 Pre: "",
518 Match: "one",
519 Post: " line\n",
520 },
521 },
522 After: "second snippet\nthird thing\nfourth\nfifth block\nsixth example\nseventh",
523 },
524 },
525 },
526 },
527 "/search?q=trois&format=json&ctx=2": {
528 "context returns whitespaces lines",
529 FileMatch{
530 FileName: "f4",
531 Repo: "name",
532 Matches: []Match{
533 {
534 FileName: "f4",
535 LineNum: 3,
536 Fragments: []Fragment{
537 {
538 Pre: "\t",
539 Match: "trois",
540 Post: "\n",
541 },
542 },
543 Before: "un \n \n",
544 After: " \n\n",
545 },
546 },
547 },
548 },
549 "/search?q=water&format=json&ctx=4": {
550 "context returns new lines",
551 FileMatch{
552 FileName: "f3",
553 Repo: "name",
554 Matches: []Match{
555 {
556 FileName: "f3",
557 LineNum: 5,
558 Fragments: []Fragment{
559 {
560 Pre: "to carry ",
561 Match: "water",
562 Post: " in the no later bla\n",
563 },
564 },
565 Before: "\n\n\n\n",
566 After: "\n\n\n",
567 },
568 },
569 },
570 },
571 "/search?q=pastures&format=json&ctx=1": {
572 "context returns empty end line",
573 FileMatch{
574 FileName: "f5",
575 Repo: "name",
576 Matches: []Match{
577 {
578 FileName: "f5",
579 LineNum: 3,
580 Fragments: []Fragment{
581 {
582 Pre: "",
583 Match: "pastures",
584 Post: "\n",
585 },
586 },
587 Before: "green\n",
588 After: "\n",
589 },
590 },
591 },
592 },
593 } {
594 checkResultMatches(t, ts, req, expected)
595 }
596}
597
598func matchesPartiallyEqual(a, b []Match) bool {
599 if len(a) != len(b) {
600 return false
601 }
602 for i := range a {
603 if a[i].FileName != b[i].FileName {
604 return false
605 }
606 if a[i].LineNum != b[i].LineNum {
607 return false
608 }
609 if !reflect.DeepEqual(a[i].Before, b[i].Before) {
610 return false
611 }
612 if !reflect.DeepEqual(a[i].After, b[i].After) {
613 return false
614 }
615 if !reflect.DeepEqual(a[i].Fragments, b[i].Fragments) {
616 return false
617 }
618 }
619 return true
620}
621
622func checkResultMatches(t *testing.T, ts *httptest.Server, req string, expected Expectation) {
623 res, err := http.Get(ts.URL + req)
624 if err != nil {
625 t.Fatal(err)
626 }
627 resultBytes, err := io.ReadAll(res.Body)
628 res.Body.Close()
629 if err != nil {
630 log.Fatal(err)
631 }
632
633 var result ApiSearchResult
634 if err := json.Unmarshal(resultBytes, &result); err != nil {
635 log.Fatal(err)
636 }
637
638 if len(result.Result.FileMatches) != 1 {
639 t.Fatalf("Expected search to return just one result but it was %d", len(result.Result.FileMatches))
640 }
641 match := result.Result.FileMatches[0]
642 if match.FileName == expected.fileMatch.FileName && match.Repo == expected.fileMatch.Repo {
643 if matchesPartiallyEqual(match.Matches, expected.fileMatch.Matches) {
644 return
645 }
646 }
647
648 t.Errorf(
649 "result doesn't match case <%s>:\nDiff:\n %v",
650 expected.title,
651 cmp.Diff(expected.fileMatch.Matches, result.Result.FileMatches[0].Matches))
652}
653
654func TestContextLinesMustBeValid(t *testing.T) {
655 b, err := index.NewShardBuilder(&zoekt.Repository{
656 Name: "name",
657 URL: "repo-url",
658 Branches: []zoekt.RepositoryBranch{{Name: "master", Version: "1234"}},
659 })
660 if err != nil {
661 t.Fatalf("NewShardBuilder: %v", err)
662 }
663 if err := b.Add(index.Document{
664 Name: "f2",
665 Content: []byte("to carry water in the no later bla"),
666 Branches: []string{"master"},
667 }); err != nil {
668 t.Fatalf("Add: %v", err)
669 }
670 s := searcherForTest(t, b)
671 srv := Server{
672 Searcher: s,
673 Top: Top,
674 HTML: true,
675 }
676
677 mux, err := NewMux(&srv)
678 if err != nil {
679 t.Fatalf("NewMux: %v", err)
680 }
681
682 ts := httptest.NewServer(mux)
683 defer ts.Close()
684
685 // Don't care about ctx if format is not json
686 code := getHttpStatusCode(t, ts, "/search?q=water&ctx=10")
687 if code != 200 {
688 t.Errorf("Expected 200 but got %v", code)
689 }
690
691 // ctx must be a valid integer in the right range.
692 for _, want := range []string{"foo", "-1", "20"} {
693 code := getHttpStatusCode(t, ts, "/search?q=water&format=json&ctx="+want)
694 if code != 418 {
695 t.Errorf("Expected 418 but got %v", code)
696 }
697 }
698}
699
700func getHttpStatusCode(t *testing.T, ts *httptest.Server, req string) int {
701 res, err := http.Get(ts.URL + req)
702 if err != nil {
703 t.Fatal(err)
704 }
705 return res.StatusCode
706}
707
708type crashSearcher struct {
709 zoekt.Streamer
710}
711
712func (s *crashSearcher) Search(ctx context.Context, q query.Q, opts *zoekt.SearchOptions) (*zoekt.SearchResult, error) {
713 res := zoekt.SearchResult{}
714 res.Stats.Crashes = 1
715 return &res, nil
716}
717
718func TestCrash(t *testing.T) {
719 srv := Server{
720 Searcher: &crashSearcher{},
721 Top: Top,
722 HTML: true,
723 }
724
725 mux, err := NewMux(&srv)
726 if err != nil {
727 t.Fatalf("NewMux: %v", err)
728 }
729
730 ts := httptest.NewServer(mux)
731 defer ts.Close()
732
733 res, err := http.Get(ts.URL + "/search?q=water")
734 if err != nil {
735 t.Fatal(err)
736 }
737 resultBytes, err := io.ReadAll(res.Body)
738 res.Body.Close()
739 if err != nil {
740 t.Fatal(err)
741 }
742
743 result := string(resultBytes)
744 if want := "1 shards crashed"; !strings.Contains(result, want) {
745 t.Errorf("result did not have %q: %s", want, result)
746 }
747}
748
749func TestHostCustomization(t *testing.T) {
750 b, err := index.NewShardBuilder(&zoekt.Repository{
751 Name: "name",
752 })
753 if err != nil {
754 t.Fatalf("NewShardBuilder: %v", err)
755 }
756 if err := b.Add(index.Document{
757 Name: "file",
758 Content: []byte("bla"),
759 }); err != nil {
760 t.Fatalf("Add: %v", err)
761 }
762
763 s := searcherForTest(t, b)
764 srv := Server{
765 Searcher: s,
766 Top: Top,
767 HTML: true,
768 HostCustomQueries: map[string]string{
769 "myproject.io": "r:myproject",
770 },
771 }
772
773 mux, err := NewMux(&srv)
774 if err != nil {
775 t.Fatalf("NewMux: %v", err)
776 }
777
778 ts := httptest.NewServer(mux)
779 defer ts.Close()
780
781 req, err := http.NewRequest("GET", ts.URL, &bytes.Buffer{})
782 if err != nil {
783 t.Fatalf("NewRequest: %v", err)
784 }
785 req.Host = "myproject.io"
786 res, err := (&http.Client{}).Do(req)
787 if err != nil {
788 t.Fatalf("Do(%v): %v", req, err)
789 }
790 resultBytes, err := io.ReadAll(res.Body)
791 res.Body.Close()
792 if err != nil {
793 t.Fatalf("ReadAll: %v", err)
794 }
795
796 if got, want := string(resultBytes), "r:myproject"; !strings.Contains(got, want) {
797 t.Fatalf("got %s, want substring %q", got, want)
798 }
799}
800
801func TestDupResult(t *testing.T) {
802 b, err := index.NewShardBuilder(&zoekt.Repository{
803 Name: "name",
804 })
805 if err != nil {
806 t.Fatalf("NewShardBuilder: %v", err)
807 }
808
809 for i := range 2 {
810 if err := b.Add(index.Document{
811 Name: fmt.Sprintf("file%d", i),
812 Content: []byte("bla"),
813 }); err != nil {
814 t.Fatalf("Add: %v", err)
815 }
816 }
817 s := searcherForTest(t, b)
818 srv := Server{
819 Searcher: s,
820 Top: Top,
821 HTML: true,
822 }
823
824 mux, err := NewMux(&srv)
825 if err != nil {
826 t.Fatalf("NewMux: %v", err)
827 }
828
829 ts := httptest.NewServer(mux)
830 defer ts.Close()
831
832 req, err := http.NewRequest("GET", ts.URL+"/search?q=bla", &bytes.Buffer{})
833 if err != nil {
834 t.Fatalf("NewRequest: %v", err)
835 }
836 res, err := (&http.Client{}).Do(req)
837 if err != nil {
838 t.Fatalf("Do(%v): %v", req, err)
839 }
840 resultBytes, err := io.ReadAll(res.Body)
841 res.Body.Close()
842 if err != nil {
843 t.Fatalf("ReadAll: %v", err)
844 }
845
846 if got, want := string(resultBytes), "Duplicate result"; !strings.Contains(got, want) {
847 t.Fatalf("got %s, want substring %q", got, want)
848 }
849}
850
851func TestTruncateLine(t *testing.T) {
852 b, err := index.NewShardBuilder(&zoekt.Repository{
853 Name: "name",
854 })
855 if err != nil {
856 t.Fatalf("NewShardBuilder: %v", err)
857 }
858
859 largePadding := bytes.Repeat([]byte{'a'}, 100*1000) // 100kb
860 if err := b.Add(index.Document{
861 Name: "file",
862 Content: append(append(largePadding, []byte("helloworld")...), largePadding...),
863 }); err != nil {
864 t.Fatalf("Add: %v", err)
865 }
866 s := searcherForTest(t, b)
867 srv := Server{
868 Searcher: s,
869 Top: Top,
870 HTML: true,
871 }
872
873 mux, err := NewMux(&srv)
874 if err != nil {
875 t.Fatalf("NewMux: %v", err)
876 }
877
878 ts := httptest.NewServer(mux)
879 defer ts.Close()
880
881 req, err := http.NewRequest("GET", ts.URL+"/search?q=helloworld", &bytes.Buffer{})
882 if err != nil {
883 t.Fatalf("NewRequest: %v", err)
884 }
885 res, err := (&http.Client{}).Do(req)
886 if err != nil {
887 t.Fatalf("Do(%v): %v", req, err)
888 }
889 resultBytes, err := io.ReadAll(res.Body)
890 res.Body.Close()
891 if err != nil {
892 t.Fatalf("ReadAll: %v", err)
893 }
894
895 if got, want := len(resultBytes)/1000, 10; got > want {
896 t.Fatalf("got %dkb response, want <= %dkb", got, want)
897 }
898 result := string(resultBytes)
899 if want := "aa<b>helloworld</b>aa"; !strings.Contains(result, want) {
900 t.Fatalf("got %s, want substring %q", result, want)
901 }
902 if want := "bytes skipped)..."; !strings.Contains(result, want) {
903 t.Fatalf("got %s, want substring %q", result, want)
904 }
905}
906
907func TestHealthz(t *testing.T) {
908 b, err := index.NewShardBuilder(&zoekt.Repository{
909 Name: "name",
910 })
911 if err != nil {
912 t.Fatalf("NewShardBuilder: %v", err)
913 }
914
915 for i := range 2 {
916 if err := b.Add(index.Document{
917 Name: fmt.Sprintf("file%d", i),
918 Content: []byte("bla"),
919 }); err != nil {
920 t.Fatalf("Add: %v", err)
921 }
922 }
923 s := searcherForTest(t, b)
924 srv := Server{
925 Searcher: s,
926 Top: Top,
927 HTML: true,
928 }
929
930 mux, err := NewMux(&srv)
931 if err != nil {
932 t.Fatalf("NewMux: %v", err)
933 }
934
935 ts := httptest.NewServer(mux)
936 t.Cleanup(ts.Close)
937
938 req, err := http.NewRequest("GET", ts.URL+"/healthz", nil)
939 if err != nil {
940 t.Fatalf("NewRequest: %v", err)
941 }
942 res, err := http.DefaultClient.Do(req)
943 if err != nil {
944 t.Fatalf("Do(%v): %v", req, err)
945 }
946
947 t.Cleanup(func() {
948 res.Body.Close()
949 })
950
951 if res.StatusCode != http.StatusOK {
952 t.Fatalf("want 200 status code, got: %v", res.StatusCode)
953 }
954
955 var result zoekt.SearchResult
956 err = json.NewDecoder(res.Body).Decode(&result)
957 if err != nil {
958 t.Fatalf("json.Decode: %v", err)
959 }
960
961 if reflect.DeepEqual(result, zoekt.SearchResult{}) {
962 t.Fatal("empty result in response")
963 }
964}
965
966func assertResults(t *testing.T, files []zoekt.FileMatch, want string) {
967 t.Helper()
968
969 var lines []string
970 for _, fm := range files {
971 for _, cm := range fm.ChunkMatches {
972 lines = append(lines, fmt.Sprintf("%s: %s", fm.FileName, string(cm.Content)))
973 }
974 }
975 sort.Strings(lines)
976 got := strings.TrimSpace(strings.Join(lines, "\n"))
977 want = strings.TrimSpace(want)
978
979 if d := cmp.Diff(want, got); d != "" {
980 t.Fatalf("unexpected results (-want, +got):\n%s", d)
981 }
982}