package e2e import ( "io" "net/http" "net/url" "os/exec" "strings" "testing" ) // find runs a search as one account, or as nobody when account is empty. func find(t *testing.T, in *instance, account, query string) string { t.Helper() req, err := http.NewRequest(http.MethodGet, in.http.URL+"/search?q="+url.QueryEscape(query), nil) if err != nil { t.Fatal(err) } if account != "" { token, err := in.db.NewSession(t.Context(), account) if err != nil { t.Fatal(err) } req.AddCookie(&http.Cookie{Name: "barerepo_session", Value: token}) } resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { t.Fatal(err) } return string(body) } // The highest severity mistake available here is a private match reaching somebody else. 17, BUILD.md. func TestSearchNeverShowsAPrivateRepositoryToAStranger(t *testing.T) { if _, err := exec.LookPath("git"); err != nil { t.Skip("git is not installed") } in := newInstance(t) john := in.account("john") in.account("lisa") work := t.TempDir() run(t, work, "git", "init", "-q", "-b", "master", work) write(t, work, "secret.go", "package main\n\nconst launchCodeZebra = 1\n") write(t, work, ".barerepo/config", "[repo]\nvisibility = \"private\"\n") run(t, work, "git", "add", "-A") run(t, work, "git", "commit", "-qm", "first") run(t, work, "git", "push", "-q", in.url(john, "/john/vault"), "master") if body := find(t, in, "john", "launchCodeZebra"); !strings.Contains(body, "secret.go") { t.Error("the owner cannot find their own private code, which makes search useless to them") } for _, who := range []string{"", "lisa"} { // The query itself is echoed into the search box, so the file name is what proves a leak. body := find(t, in, who, "launchCodeZebra") if strings.Contains(body, "secret.go") { t.Errorf("a private match leaked to %q:\n%s", who, body) } if !strings.Contains(body, "nothing matched") { t.Errorf("%q was not told the search found nothing:\n%s", who, body) } } // [access] push grants read, per chapter 18, so lisa sees it once she is listed. write(t, work, ".barerepo/config", "[repo]\nvisibility = \"private\"\n\n[access]\npush = [\"lisa\"]\n") run(t, work, "git", "commit", "-qam", "let lisa in") run(t, work, "git", "push", "-q", in.url(john, "/john/vault"), "master") if body := find(t, in, "lisa", "launchCodeZebra"); !strings.Contains(body, "secret.go") { t.Error("a named reader still cannot find the code they may read") } if body := find(t, in, "", "launchCodeZebra"); strings.Contains(body, "secret.go") { t.Error("granting one reader published the repository") } } // A path outside ascii has two spellings in git's output, and the index must hold the real one. func TestTheIndexHoldsANameOutsideAscii(t *testing.T) { if _, err := exec.LookPath("git"); err != nil { t.Skip("git is not installed") } in := newInstance(t) john := in.account("john") work := seed(t, in, john, "john", "johnbot") write(t, work, "café.md", "# une note\n\nzircon is in here\n") run(t, work, "git", "add", "-A") run(t, work, "git", "commit", "-qm", "add a note with an accent") run(t, work, "git", "push", "-q", in.url(john, "/john/johnbot"), "master") body := find(t, in, "", "zircon") if !strings.Contains(body, "café.md") { t.Errorf("the index spelled the path some other way:\n%s", body) } if strings.Contains(body, `\303`) { t.Errorf("the index kept git's octal spelling of the name:\n%s", body) } } // Chapter 17 indexes on push, so what a push changed is what a search finds afterwards. func TestTheIndexFollowsThePush(t *testing.T) { if _, err := exec.LookPath("git"); err != nil { t.Skip("git is not installed") } in := newInstance(t) john := in.account("john") work := seed(t, in, john, "john", "johnbot") write(t, work, "retry.go", "package main\n\nfunc backoff(n int) int { return n * 2 }\n") run(t, work, "git", "add", "-A") run(t, work, "git", "commit", "-qm", "add a backoff") run(t, work, "git", "push", "-q", in.url(john, "/john/johnbot"), "master") if body := find(t, in, "", "backoff"); !strings.Contains(body, "retry.go") { t.Fatalf("a pushed file is not in the index:\n%s", body) } // An edit must remove what was there, or search answers with a line that no longer exists. write(t, work, "retry.go", "package main\n\nfunc linger(n int) int { return n * 3 }\n") run(t, work, "git", "commit", "-qam", "rename it") run(t, work, "git", "push", "-q", in.url(john, "/john/johnbot"), "master") if body := find(t, in, "", "backoff"); strings.Contains(body, "retry.go") { t.Errorf("the index still holds a line the push replaced:\n%s", body) } if body := find(t, in, "", "linger"); !strings.Contains(body, "retry.go") { t.Errorf("the index did not take the new line:\n%s", body) } // A deleted file must leave, or every search result is a link to a 404. run(t, work, "git", "rm", "-q", "retry.go") run(t, work, "git", "commit", "-qm", "drop it") run(t, work, "git", "push", "-q", in.url(john, "/john/johnbot"), "master") if body := find(t, in, "", "linger"); strings.Contains(body, "retry.go") { t.Errorf("a deleted file is still in the index:\n%s", body) } } // Chapter 17 indexes thread comments on note write, and a comment written on the web is a note write. func TestACommentWrittenOnTheWebIsSearchable(t *testing.T) { if _, err := exec.LookPath("git"); err != nil { t.Skip("git is not installed") } in := newInstance(t) john := in.account("john") seed(t, in, john, "john", "johnbot") resp := post(t, in, "john", "/john/johnbot/threads", url.Values{ "title": {"the reconnect loop spins"}, "body": {"it retries with no delay at all, which pins a core"}, }) if resp.StatusCode != http.StatusFound { t.Fatalf("opening a thread answered %d", resp.StatusCode) } body := find(t, in, "", "pins a core") if !strings.Contains(body, "the reconnect loop spins") { t.Errorf("a comment written on the web is not in the index:\n%s", body) } if !strings.Contains(body, "thread") { t.Errorf("the match did not come back as a thread:\n%s", body) } } // The index is derived from git, so appendix E's reindex must rebuild it from nothing. func TestReindexRebuildsTheWholeIndex(t *testing.T) { if _, err := exec.LookPath("git"); err != nil { t.Skip("git is not installed") } in := newInstance(t) john := in.account("john") work := seed(t, in, john, "john", "johnbot") write(t, work, "retry.go", "package main\n\nfunc backoff(n int) int { return n * 2 }\n") run(t, work, "git", "add", "-A") run(t, work, "git", "commit", "-qm", "add a backoff") run(t, work, "git", "push", "-q", in.url(john, "/john/johnbot"), "master") if _, err := in.db.ExecContext(t.Context(), `DELETE FROM search_docs`); err != nil { t.Fatal(err) } if body := find(t, in, "", "backoff"); strings.Contains(body, "retry.go") { t.Fatal("the index was not actually emptied, so this test proves nothing") } out := run(t, "", binary, "doctor", "--config", in.cfg.Path, "--reindex") if !strings.Contains(out, "indexed 1 repositories") { t.Errorf("reindex said %q", out) } if body := find(t, in, "", "backoff"); !strings.Contains(body, "retry.go") { t.Errorf("reindex did not rebuild the code index:\n%s", body) } } // Chapter 44.4 keeps a deleted repository for 30 days, and it must not be searchable while it waits. func TestADeletedRepositoryLeavesTheIndex(t *testing.T) { if _, err := exec.LookPath("git"); err != nil { t.Skip("git is not installed") } in := newInstance(t) john := in.account("john") work := seed(t, in, john, "john", "johnbot") write(t, work, "retry.go", "package main\n\nfunc backoff(n int) int { return n * 2 }\n") run(t, work, "git", "add", "-A") run(t, work, "git", "commit", "-qm", "add a backoff") run(t, work, "git", "push", "-q", in.url(john, "/john/johnbot"), "master") if body := find(t, in, "", "backoff"); !strings.Contains(body, "retry.go") { t.Fatal("the push did not index, so this test proves nothing") } post(t, in, "john", "/john/johnbot/delete", url.Values{"confirm": {"johnbot"}}) if body := find(t, in, "", "backoff"); strings.Contains(body, "retry.go") { t.Errorf("a deleted repository is still searchable:\n%s", body) } // A rebuild must not put it back, because the directory is still on disk under trash. run(t, "", binary, "doctor", "--config", in.cfg.Path, "--reindex") if body := find(t, in, "", "backoff"); strings.Contains(body, "retry.go") { t.Errorf("reindex walked the trash directory and put a deleted repository back:\n%s", body) } } // A transfer changes who may read a private repository, so the index has to change with it. 44.3. func TestATransferMovesWhoMaySearchIt(t *testing.T) { if _, err := exec.LookPath("git"); err != nil { t.Skip("git is not installed") } in := newInstance(t) john := in.account("john") in.account("lisa") work := t.TempDir() run(t, work, "git", "init", "-q", "-b", "master", work) write(t, work, "secret.go", "package main\n\nconst launchCodeZebra = 1\n") write(t, work, ".barerepo/config", "[repo]\nvisibility = \"private\"\n") run(t, work, "git", "add", "-A") run(t, work, "git", "commit", "-qm", "first") run(t, work, "git", "push", "-q", in.url(john, "/john/vault"), "master") resp := post(t, in, "john", "/john/vault/transfer", url.Values{"owner": {"lisa"}, "confirm": {"vault"}}) if resp.StatusCode != http.StatusFound { t.Fatalf("the transfer answered %d", resp.StatusCode) } if body := find(t, in, "lisa", "launchCodeZebra"); !strings.Contains(body, "secret.go") { t.Errorf("the new owner cannot search the repository she now owns:\n%s", body) } if body := find(t, in, "john", "launchCodeZebra"); strings.Contains(body, "secret.go") { t.Errorf("the old owner can still search a private repository he gave away:\n%s", body) } } // A file holds a symbol more than once, and one row per file would hide where the rest of them are. func TestASearchShowsMoreThanOneLineOfAFile(t *testing.T) { if _, err := exec.LookPath("git"); err != nil { t.Skip("git is not installed") } in := newInstance(t) john := in.account("john") work := seed(t, in, john, "john", "johnbot") write(t, work, "retry.go", "package main\n\nfunc backoff(n int) int {\n\treturn n\n}\n\nvar b = backoff(2)\n") run(t, work, "git", "add", "-A") run(t, work, "git", "commit", "-qm", "two mentions") run(t, work, "git", "push", "-q", in.url(john, "/john/johnbot"), "master") body := find(t, in, "", "backoff") if n := strings.Count(body, "retry.go:"); n < 2 { t.Errorf("retry.go appeared %d times for a word it holds twice:\n%s", n, body) } if !strings.Contains(body, "retry.go:3") || !strings.Contains(body, "retry.go:7") { t.Errorf("the two lines are not both named:\n%s", body) } } // search.html gives the box to the matched source line, so the handler must not hand one to a thread. func TestOnlyCodeGetsABoxOnTheSearchPage(t *testing.T) { if _, err := exec.LookPath("git"); err != nil { t.Skip("git is not installed") } in := newInstance(t) john := in.account("john") seed(t, in, john, "john", "johnbot") // A phrase that exists in a thread and in no file, so only the thread row can answer. resp := post(t, in, "john", "/john/johnbot/threads", url.Values{ "title": {"the reconnect loop spins"}, "body": {"it retries with a zircon delay"}, }) if resp.StatusCode != http.StatusFound { t.Fatalf("opening a thread answered %d", resp.StatusCode) } body := find(t, in, "", "zircon") if !strings.Contains(body, "reconnect loop spins") { t.Fatalf("the thread was not found, so this proves nothing:\n%s", body) } if strings.Contains(body, "