@ 261101.7
current
package main

import (
	"net/http"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"testing"
)

// A missing path's 404 says where git saw it go: deleted, moved, or
// deleted only in the work tree, with a link to its last version.
func TestNotFoundSaysWhereItWent(t *testing.T) {
	s, dir := repoTree(t)
	git := func(args ...string) {
		t.Helper()
		if out, err := exec.Command("git", append([]string{"-C", dir}, args...)...).CombinedOutput(); err != nil {
			t.Fatalf("git %v: %v\n%s", args, err, out)
		}
	}
	git("rm", "-q", "index.md")
	git("mv", "bin.dat", "data.bin")
	commitAll(t, dir, "Tidy the top")
	git("mv", "a", "lib")
	commitAll(t, dir, "Move a to lib")
	os.Remove(filepath.Join(dir, "lib", "page.html"))
	h := s.handler()
	for p, want := range map[string][]string{
		"/index.md":       {"<th>deleted</th><td><a href=\"/?commit=", "Tidy the top", "<th>last version</th><td><a href=\"/@", "/index.md\">", "Add the docs"},
		"/bin.dat":        {"<th>moved</th>", "Tidy the top", `<th>to</th><td><a href="/data.bin"><span class="seg">data.bin</span></a>`, "/bin.dat\">"},
		"/a/b.go":         {"<th>moved</th>", "Move a to lib", `<th>to</th><td><a href="/lib/b.go"><span class="seg">lib/</span><wbr><span class="seg">b.go</span></a>`, "/a/b.go\">", "Rename the package"},
		"/a/":             {`<th>to</th><td><a href="/lib"><span class="seg">lib</span></a>`, "/a/\">"},
		"/lib/page.html":  {"<th>deleted</th><td>since the last commit", "<th>last version</th><td><a href=\"/@", "/lib/page.html\">", "Move a to lib"},
		"/untracked/y.md": {},
	} {
		rec := get(h, p)
		body := rec.Body.String()
		if rec.Code != http.StatusNotFound {
			t.Errorf("%s: status %d, want 404", p, rec.Code)
		}
		for _, w := range want {
			if !strings.Contains(body, w) {
				t.Errorf("%s: lacks %q:\n%s", p, w, body)
			}
		}
		if len(want) == 0 && strings.Contains(body, "last version") {
			t.Errorf("%s: a path git never held has a last version:\n%s", p, body)
		}
	}
	// Each link leads somewhere.
	body := get(h, "/index.md").Body.String()
	i := strings.Index(body, "<th>last version</th><td><a href=\"") + len("<th>last version</th><td><a href=\"")
	if u := body[i : i+strings.Index(body[i:], `"`)]; get(h, u).Code != http.StatusOK {
		t.Errorf("last version %s: status %d", u, get(h, u).Code)
	}
}

// Names close to a missing one are listed: a slip of letters, another
// extension, a deeper path whose rest the tree holds. Far names are not.
func TestNotFoundCloseNames(t *testing.T) {
	s, _, _ := serveTree(t)
	h := s.handler()
	for p, want := range map[string]string{
		"/indx.md":      `<a href="/index.md"><span class="seg">index.md</span></a>`,
		"/index.txt":    `<a href="/index.md"><span class="seg">index.md</span></a>`,
		"/a/b.og":       `<a href="/a/b.go"><span class="seg">a/</span><wbr><span class="seg">b.go</span></a>`,
		"/A/README.md":  `<a href="/a/README.md"><span class="seg">a/</span><wbr><span class="seg">README.md</span></a>`,
		"/a/READ":       `<a href="/a/README.md"><span class="seg">a/</span><wbr><span class="seg">README.md</span></a>`,
		"/b/README.md":  `<a href="/a/README.md"><span class="seg">a/</span><wbr><span class="seg">README.md</span></a>`,
		"/a/REAMDE.txt": `<a href="/a/README.md"><span class="seg">a/</span><wbr><span class="seg">README.md</span></a>`,
		"/a/nothing.md": "",
	} {
		body := get(h, p).Body.String()
		if want == "" {
			if strings.Contains(body, "<th>close</th>") {
				t.Errorf("%s: lists far names:\n%s", p, body)
			}
			continue
		}
		if !strings.Contains(body, want) {
			t.Errorf("%s: lacks %s:\n%s", p, want, body)
		}
	}
	if body := get(h, "/127.31.1.1-8080.loc").Body.String(); strings.Contains(body, ".lock") {
		t.Errorf("a lock is offered as a close name:\n%s", body)
	}
}

func TestEditDistance(t *testing.T) {
	for _, c := range []struct {
		a, b string
		d    int
	}{{"", "", 0}, {"abc", "", 3}, {"kitten", "sitting", 3}, {"b.og", "b.go", 2}, {"åäö", "aäö", 1}} {
		if d := editDistance(c.a, c.b); d != c.d {
			t.Errorf("%q %q: %d, want %d", c.a, c.b, d, c.d)
		}
	}
}

// A path outside every tree gets a clean 404: the URL and, as help, the
// trees nearest it, or every tree when none is near. No index, no button.
func TestStrayPath(t *testing.T) {
	s, tr, dir := serveTree(t)
	tr.prefix, tr.label = "/notes/", "~/notes"
	addTree(t, s, filepath.Join(dir, "a"), "/other/", "~/other")
	h := s.handler()
	for p, want := range map[string][]string{
		"/notez/x.md": {"<th>close</th><td><a href=\"/notes/\">", "<th>not found</th>"},
		"/zzz":        {"<th>served</th><td><a href=\"/notes/\">", "<td><a href=\"/other/\">"},
	} {
		rec := get(h, p)
		body := rec.Body.String()
		if rec.Code != http.StatusNotFound {
			t.Errorf("%s: status %d, want 404", p, rec.Code)
		}
		for _, w := range want {
			if !strings.Contains(body, w) {
				t.Errorf("%s: lacks %q:\n%s", p, w, body)
			}
		}
		if strings.Contains(body, "Kill server") || strings.Contains(body, "Delete lock") || strings.Contains(body, ", this page") {
			t.Errorf("%s: a stray path shows the index or a stop button:\n%s", p, body)
		}
	}
	if body := get(h, "/notez/").Body.String(); strings.Contains(body, "/other/") {
		t.Errorf("a near tree is shown with a far one:\n%s", body)
	}
}