internal/httpd/symbols_test.go
139 lines · 4783 bytes
1package httpd
2
3import (
4 "context"
5 "os"
6 "os/exec"
7 "path/filepath"
8 "strings"
9 "testing"
10
11 "gitbay.org/gitbay/internal/config"
12 "gitbay.org/gitbay/internal/control"
13 "gitbay.org/gitbay/internal/store"
14 "gitbay.org/gitbay/internal/symbols"
15)
16
17func symbolGit(t *testing.T, dir string, args ...string) {
18 t.Helper()
19 cmd := exec.Command("git", args...)
20 cmd.Dir = dir
21 cmd.Env = append(os.Environ(),
22 "GIT_CONFIG_NOSYSTEM=1", "GIT_CONFIG_GLOBAL=/dev/null",
23 "GIT_AUTHOR_NAME=t", "GIT_AUTHOR_EMAIL=t@example.test",
24 "GIT_COMMITTER_NAME=t", "GIT_COMMITTER_EMAIL=t@example.test")
25 if out, err := cmd.CombinedOutput(); err != nil {
26 t.Fatalf("git %v: %v\n%s", args, err, out)
27 }
28}
29
30// symbolServer serves alice/app (public) and alice/secret (private), each
31// with main indexed and a side branch whose tree differs.
32func symbolServer(t *testing.T) *Server {
33 t.Helper()
34 st, err := store.Open(":memory:")
35 if err != nil {
36 t.Fatal(err)
37 }
38 t.Cleanup(func() { st.Close() })
39 if err := st.MigrateUp(); err != nil {
40 t.Fatal(err)
41 }
42 uid, err := st.CreateUser("alice", false)
43 if err != nil {
44 t.Fatal(err)
45 }
46 root := t.TempDir()
47 src := filepath.Join(root, "src")
48 symbolGit(t, root, "init", "-q", "-b", "main", "src")
49 files := map[string]string{
50 "main.go": "package main\n\nfunc main() { helper(); dup(); unknown() }\n\nfunc helper() {}\n",
51 "a/dup.go": "package a\n\nfunc dup() {}\n",
52 "b/dup.go": "package b\n\nfunc dup() {}\n",
53 }
54 for name, body := range files {
55 os.MkdirAll(filepath.Join(src, filepath.Dir(name)), 0o755)
56 os.WriteFile(filepath.Join(src, name), []byte(body), 0o644)
57 }
58 symbolGit(t, src, "add", ".")
59 symbolGit(t, src, "commit", "-q", "-m", "base")
60 symbolGit(t, src, "checkout", "-q", "-b", "side")
61 os.WriteFile(filepath.Join(src, "side.go"), []byte("package main\n"), 0o644)
62 symbolGit(t, src, "add", ".")
63 symbolGit(t, src, "commit", "-q", "-m", "side")
64
65 w := symbols.NewWith(st, func(owner, name string) string { return control.RepoDir(root, owner, name) }, 0)
66 for _, r := range []struct{ name, vis string }{{"app", "public"}, {"secret", "private"}} {
67 id, err := st.CreateRepo("user", uid, r.name, r.vis)
68 if err != nil {
69 t.Fatal(err)
70 }
71 dir := control.RepoDir(root, "alice", r.name)
72 os.MkdirAll(filepath.Dir(dir), 0o755)
73 symbolGit(t, root, "init", "-q", "--bare", dir)
74 symbolGit(t, src, "push", "-q", dir, "main", "side")
75 if _, err := w.Index(context.Background(), id, false); err != nil {
76 t.Fatal(err)
77 }
78 }
79 cfg := config.Default()
80 cfg.Server.Root = root
81 cfg.Server.SiteURL = "https://forge.test/"
82 return New(cfg, st, nil)
83}
84
85func TestBlobLinksIndexedNames(t *testing.T) {
86 h := symbolServer(t).Handler()
87 w := get(t, h, "/alice/app/blob/main/main.go", nil)
88 if w.Code != 200 {
89 t.Fatalf("blob: %d", w.Code)
90 }
91 body := w.Body.String()
92 for _, want := range []string{
93 // defined once: straight to the line, at the ref being viewed
94 `<a class="sym" href="/alice/app/blob/main/main.go#L5"><span class="nf">helper</span></a>`,
95 // defined twice: to the results page
96 `<a class="sym" href="/alice/app/symbols?q=dup"><span class="nf">dup</span></a>`,
97 // the per-file list
98 `2 symbols in this file`,
99 } {
100 if !strings.Contains(body, want) {
101 t.Errorf("blob page lacks %s", want)
102 }
103 }
104 if strings.Contains(body, `>unknown</span></a>`) {
105 t.Error("a name the index does not hold is linked")
106 }
107
108 // A branch at another tree is not what the index describes.
109 w = get(t, h, "/alice/app/blob/side/main.go", nil)
110 if w.Code != 200 || strings.Contains(w.Body.String(), `class="sym"`) || strings.Contains(w.Body.String(), "in this file") {
111 t.Errorf("side branch: %d, links or list present", w.Code)
112 }
113}
114
115func TestSymbolsPage(t *testing.T) {
116 h := symbolServer(t).Handler()
117 w := get(t, h, "/alice/app/symbols?q=dup", nil)
118 body := w.Body.String()
119 if w.Code != 200 || !strings.Contains(body, "a/dup.go:3") || !strings.Contains(body, "b/dup.go:3") {
120 t.Fatalf("results page: %d\n%s", w.Code, body)
121 }
122 w = get(t, h, "/alice/app/symbols?q=dup&kind=type", nil)
123 if strings.Contains(w.Body.String(), "a/dup.go:3") {
124 t.Error("kind filter ignored")
125 }
126 if w := get(t, h, "/alice/app/symbols?q=d", nil); !strings.Contains(w.Body.String(), "query must be 2 to 200 characters") {
127 t.Error("a one-character query was searched")
128 }
129 // A cursor from an index that has been replaced says so rather than
130 // showing an empty page.
131 if w := get(t, h, "/alice/app/symbols?q=dup&after=999999.1", nil); !strings.Contains(w.Body.String(), "rebuilt since that cursor") {
132 t.Errorf("stale cursor:\n%s", w.Body.String())
133 }
134 for _, p := range []string{"/alice/secret/symbols?q=dup", "/alice/secret/blob/main/main.go"} {
135 if w := get(t, h, p, nil); w.Code != 404 {
136 t.Errorf("%s anonymously: %d, want 404", p, w.Code)
137 }
138 }
139}