@ 261101.7
current
package main

import (
	"context"
	"encoding/json"
	"net/url"
	"os"
	"path/filepath"
	"strings"
	"testing"
)

// A search lists matching names, then matching lines, ignoring case, and
// leaves out locks, .git and binary files.
func TestSearch(t *testing.T) {
	s, _, dir := serveTree(t)
	os.MkdirAll(filepath.Join(dir, ".git"), 0o755)
	os.WriteFile(filepath.Join(dir, ".git", "readme.md"), []byte("readme\n"), 0o644)
	w := get(s.handler(), "/?search=README")
	if w.Code != 200 {
		t.Fatalf("status %d: %s", w.Code, w.Body)
	}
	var res found
	if err := json.Unmarshal(w.Body.Bytes(), &res); err != nil {
		t.Fatal(err)
	}
	if len(res.Names) != 1 || res.Names[0].Path != "a/README.md" || res.Names[0].Href != "/a/README.md" {
		t.Errorf("names: %+v", res.Names)
	}
	if len(res.Lines) != 1 || res.Lines[0].Text != "Readme of a" || res.Lines[0].Href != "/a/README.md" {
		t.Errorf("lines: %+v", res.Lines)
	}
	w = get(s.handler(), "/?search=func")
	json.Unmarshal(w.Body.Bytes(), &res)
	if len(res.Lines) != 1 || res.Lines[0].Href != "/a/b.go#L3" || res.Lines[0].Line != 3 {
		t.Errorf("code line: %+v", res.Lines)
	}
	w = get(s.handler(), "/?search=8080")
	json.Unmarshal(w.Body.Bytes(), &res)
	if len(res.Names)+len(res.Lines) != 0 {
		t.Errorf("lock found: %+v %+v", res.Names, res.Lines)
	}
}

// The search box searches the directory a page is in, and a page with
// ?q= is the page itself, for the script to fill the box from.
func TestSearchBoxScope(t *testing.T) {
	s, _, _ := serveTree(t)
	for _, c := range []struct{ url, want string }{
		{"/", "/"},
		{"/a/", "/a/"},
		{"/a/b.go", "/a/"},
		{"/a/b.go?q=func", "/a/"},
		{"/a/missing", "/"},
	} {
		w := get(s.handler(), c.url)
		if !strings.Contains(w.Body.String(), `data-search="`+c.want+`"`) {
			t.Errorf("%s: want data-search=%q, status %d", c.url, c.want, w.Code)
		}
	}
}

// Recent pages stay within their tree: each page names its tree for the
// script, a version names the tree it is a version of, and a tree at /
// names itself, not the trees whose paths lie below it.
func TestRecentsStayInTheirTree(t *testing.T) {
	s, dir := repoTree(t)
	tr := s.list()[0]
	tr.prefix, tr.label = "/notes/", "~/notes"
	other := t.TempDir()
	os.WriteFile(filepath.Join(other, "x.md"), []byte("# x\n"), 0o644)
	addTree(t, s, other, "/", "~")
	h := s.handler()
	star := stardatesIn(context.Background(), dir).byHash[head(t, dir, "HEAD~1")]
	for _, c := range []struct{ url, want string }{
		{"/notes/", "/notes/"},
		{"/notes/a/b.go", "/notes/"},
		{"/notes@" + star + "/a/b.go", "/notes/"},
		{"/x.md", "/"},
	} {
		w := get(h, c.url)
		if !strings.Contains(w.Body.String(), `data-recents="`+c.want+`"`) {
			t.Errorf("%s: want data-recents=%q, status %d", c.url, c.want, w.Code)
		}
	}
}

// A directory's own entries come first and are marked here; deeper ones
// follow unmarked.
func TestSearchHereFirst(t *testing.T) {
	s, _, dir := serveTree(t)
	os.WriteFile(filepath.Join(dir, "a", "deep.txt"), []byte("needle\n"), 0o644)
	os.WriteFile(filepath.Join(dir, "z.txt"), []byte("needle\n"), 0o644)
	var res found
	json.Unmarshal(get(s.handler(), "/?search=needle").Body.Bytes(), &res)
	if len(res.Lines) != 2 || res.Lines[0].Path != "z.txt" || !res.Lines[0].Here || res.Lines[1].Path != "a/deep.txt" || res.Lines[1].Here {
		t.Errorf("lines: %+v", res.Lines)
	}
	json.Unmarshal(get(s.handler(), "/a/?search=needle").Body.Bytes(), &res)
	if len(res.Lines) != 1 || res.Lines[0].Path != "a/deep.txt" || !res.Lines[0].Here {
		t.Errorf("lines below a: %+v", res.Lines)
	}
}

// Hidden files and directories are searched, but not the stores in
// unsearched.
func TestSearchHidden(t *testing.T) {
	s, _, dir := serveTree(t)
	os.MkdirAll(filepath.Join(dir, ".github"), 0o755)
	os.MkdirAll(filepath.Join(dir, ".venv"), 0o755)
	for _, p := range []string{".gitignore", ".github/ci.yml", ".venv/x.py"} {
		os.WriteFile(filepath.Join(dir, p), []byte("needle\n"), 0o644)
	}
	var res found
	json.Unmarshal(get(s.handler(), "/?search=needle").Body.Bytes(), &res)
	var paths []string
	for _, h := range res.Lines {
		paths = append(paths, h.Path)
	}
	if strings.Join(paths, " ") != ".gitignore .github/ci.yml" {
		t.Errorf("lines: %v", paths)
	}
}

// A query that is a forge's URL, with or without its scheme, comes back as
// a link to the web address, and only where a web address is served.
func TestSearchWeb(t *testing.T) {
	s, _, _ := serveTree(t)
	var res found
	w := get(s.handler(), "/?search="+url.QueryEscape("github.com/junegunn/fzf"))
	json.Unmarshal(w.Body.Bytes(), &res)
	if res.Web != nil {
		t.Errorf("web hit without a web address: %+v", res.Web)
	}
	s.webURL = "http://127.31.1.2:8080"
	for q, want := range map[string]string{
		"github.com/junegunn/fzf":                               "http://127.31.1.2:8080/github.com/junegunn/fzf/",
		" https://github.com/junegunn/fzf/blob/master/main.go ": "http://127.31.1.2:8080/github.com/junegunn/fzf/main.go",
	} {
		res = found{}
		w = get(s.handler(), "/?search="+url.QueryEscape(q))
		json.Unmarshal(w.Body.Bytes(), &res)
		if res.Web == nil || res.Web.Href != want {
			t.Errorf("%q: %+v, want %s", q, res.Web, want)
		}
	}
	res = found{}
	w = get(s.handler(), "/?search=readme")
	json.Unmarshal(w.Body.Bytes(), &res)
	if res.Web != nil {
		t.Errorf("web hit for a word: %+v", res.Web)
	}
}