internal/control/symbols_test.go
144 lines · 5052 bytes
1package control
2
3import (
4 "bytes"
5 "encoding/json"
6 "strings"
7 "testing"
8
9 "gitbay.org/gitbay/internal/protocol"
10 "gitbay.org/gitbay/internal/store"
11)
12
13func symbolsFixture(t *testing.T) (*store.Store, store.Repo, store.User, store.User) {
14 t.Helper()
15 st, err := store.Open(":memory:")
16 if err != nil {
17 t.Fatal(err)
18 }
19 t.Cleanup(func() { st.Close() })
20 if err := st.MigrateUp(); err != nil {
21 t.Fatal(err)
22 }
23 aliceID, _ := st.CreateUser("alice", false)
24 bobID, _ := st.CreateUser("bob", false)
25 id, err := st.CreateRepo("user", aliceID, "secret", "private")
26 if err != nil {
27 t.Fatal(err)
28 }
29 repo, _ := st.RepoByID(id)
30 var rows []store.SymbolRow
31 for _, n := range []string{"Parse", "ParseAll", "ParseArgs", "Parser", "parse"} {
32 rows = append(rows, store.SymbolRow{Name: n, Key: n, Kind: "function", Path: "p.go", Line: len(rows) + 1})
33 }
34 rows = append(rows, store.SymbolRow{Name: "Parser.Run", Key: "Run", Kind: "method", Path: "p.go", Line: 40})
35 if _, err := st.ReplaceSymbolIndex(store.SymbolIndex{RepoID: id, Commit: "c0", Tree: "t0", State: "ok"}, rows); err != nil {
36 t.Fatal(err)
37 }
38 return st, repo,
39 store.User{ID: aliceID, Username: "alice"},
40 store.User{ID: bobID, Username: "bob"}
41}
42
43type symbolPage struct {
44 Data struct {
45 Items []symbolOut `json:"items"`
46 Next string `json:"next"`
47 } `json:"data"`
48}
49
50func runSymbols(t *testing.T, st *store.Store, u store.User, argv ...string) (int, string, string) {
51 t.Helper()
52 var out, errOut bytes.Buffer
53 c := &Ctx{User: u, Scope: "full", Store: st, Stdout: &out, Stderr: &errOut, JSON: true}
54 c.Cfg.Server.Root = t.TempDir()
55 code := Dispatch(c, argv)
56 if code != protocol.ExitOK {
57 // In JSON mode a refusal is the output's error field.
58 return code, out.String(), out.String() + errOut.String()
59 }
60 return code, out.String(), errOut.String()
61}
62
63func TestRepoSymbolsPages(t *testing.T) {
64 st, repo, alice, _ := symbolsFixture(t)
65 var names []string
66 cursor := ""
67 for pages := 0; ; pages++ {
68 argv := []string{"repo", "symbols", repo.Path(), "Parse", "--limit", "2"}
69 if cursor != "" {
70 argv = append(argv, "--cursor", cursor)
71 }
72 code, out, errOut := runSymbols(t, st, alice, argv...)
73 if code != protocol.ExitOK {
74 t.Fatalf("exit %d: %s", code, errOut)
75 }
76 var p symbolPage
77 if err := json.Unmarshal([]byte(out), &p); err != nil {
78 t.Fatalf("%v: %s", err, out)
79 }
80 for _, it := range p.Data.Items {
81 names = append(names, it.Name)
82 }
83 if p.Data.Next == "" {
84 break
85 }
86 if pages > 5 {
87 t.Fatal("paging does not end")
88 }
89 cursor = p.Data.Next
90 }
91 if got, want := strings.Join(names, " "), "Parse ParseAll ParseArgs Parser Parser.Run parse"; got != want {
92 t.Fatalf("got %q, want %q", got, want)
93 }
94
95 // A cursor from before a rebuild is refused rather than followed.
96 st.ReplaceSymbolIndex(store.SymbolIndex{RepoID: repo.ID, Commit: "c1", Tree: "t1", State: "ok"},
97 []store.SymbolRow{{Name: "Parse", Key: "Parse", Kind: "function", Path: "p.go", Line: 1}})
98 code, _, errOut := runSymbols(t, st, alice, "repo", "symbols", repo.Path(), "Parse", "--cursor", cursor)
99 if code != protocol.ExitUsage || !strings.Contains(errOut, "rebuilt") {
100 t.Fatalf("stale cursor: exit %d: %s", code, errOut)
101 }
102}
103
104func TestRepoSymbolsRefusals(t *testing.T) {
105 st, repo, alice, bob := symbolsFixture(t)
106 for _, tc := range []struct {
107 name string
108 user store.User
109 argv []string
110 code int
111 msg string
112 }{
113 {"outsider sees no repository", bob, []string{"repo", "symbols", repo.Path(), "Parse"}, protocol.ExitNotFound, "not found"},
114 {"another ref", alice, []string{"repo", "symbols", repo.Path(), "--ref", "feature", "Parse"}, protocol.ExitNotFound, "only the default branch"},
115 {"unknown kind", alice, []string{"repo", "symbols", repo.Path(), "--kind", "widget", "Parse"}, protocol.ExitUsage, "--kind"},
116 {"no query", alice, []string{"repo", "symbols", repo.Path()}, protocol.ExitUsage, ""},
117 {"one-character query", alice, []string{"repo", "symbols", repo.Path(), "P"}, protocol.ExitUsage, "2 to"},
118 {"reindex needs an admin", alice, []string{"admin", "symbols", "reindex", repo.Path()}, protocol.ExitDenied, "admin"},
119 } {
120 t.Run(tc.name, func(t *testing.T) {
121 code, _, errOut := runSymbols(t, st, tc.user, tc.argv...)
122 if code != tc.code || !strings.Contains(errOut, tc.msg) {
123 t.Fatalf("exit %d, want %d with %q: %s", code, tc.code, tc.msg, errOut)
124 }
125 })
126 }
127
128 code, _, errOut := runSymbols(t, st, alice, "repo", "symbols", repo.Path(), "--ref", repo.DefaultBranch, "--kind", "method", "Run")
129 if code != protocol.ExitOK {
130 t.Fatalf("default branch by name: exit %d: %s", code, errOut)
131 }
132}
133
134func TestAdminSymbolsReindexQueuesAForcedBuild(t *testing.T) {
135 st, repo, _, _ := symbolsFixture(t)
136 code, _, errOut := runSymbols(t, st, rootUser(t, st), "admin", "symbols", "reindex", repo.Path())
137 if code != protocol.ExitOK {
138 t.Fatalf("exit %d: %s", code, errOut)
139 }
140 reqs, err := st.SymbolRequests()
141 if err != nil || len(reqs) != 1 || reqs[0].RepoID != repo.ID || !reqs[0].Force {
142 t.Fatalf("requests = %+v, %v", reqs, err)
143 }
144}