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