package e2e import ( "encoding/json" "os" "path/filepath" "strings" "testing" ) // Saved queries over stock ssh: a query spans the repositories the // caller reads, never another user's private one, pages with the // cursor, and a pinned one reaches the dashboard and its web page (#292). func TestSavedQueries(t *testing.T) { t.Parallel() inst := startInstanceWith(t, "[web]\nmode = \"accounts\"\n") aliceKey := inst.newKey(t, "alice") bobKey := inst.newKey(t, "bob") inst.admin(t, "admin", "user", "create", "alice", "--key", aliceKey+".pub", "--email", "alice@example.test", "--verified") inst.admin(t, "admin", "user", "create", "bob", "--key", bobKey+".pub") ssh := func(key string, want int, args ...string) string { t.Helper() out, errOut, code := inst.ssh(t, key, "", args...) if code != want { t.Fatalf("%v: exit %d, want %d\n%s%s", args, code, want, out, errOut) } return out + errOut } ssh(aliceKey, 0, "repo", "create", "alice/app") ssh(aliceKey, 0, "repo", "create", "alice/lib") ssh(bobKey, 0, "repo", "create", "bob/secret", "--private") work := t.TempDir() env := inst.gitEnv(aliceKey) mustGit(t, work, env, "clone", inst.sshURL("alice/app"), "w") dir := filepath.Join(work, "w") os.WriteFile(filepath.Join(dir, "a.txt"), []byte("a\n"), 0o644) mustGit(t, dir, env, "checkout", "-q", "-b", "main") mustGit(t, dir, env, "add", ".") mustGit(t, dir, env, "commit", "-q", "-m", "base") mustGit(t, dir, env, "push", "-q", "origin", "main") mustGit(t, dir, env, "checkout", "-q", "-b", "feat") os.WriteFile(filepath.Join(dir, "a.txt"), []byte("a\nb\n"), 0o644) mustGit(t, dir, env, "commit", "-q", "-am", "feat") mustGit(t, dir, env, "push", "-q", "origin", "feat") ssh(aliceKey, 0, "mr", "create", "alice/app", "--source", "feat", "--target", "main", "--title", "feature") ssh(aliceKey, 0, "issue", "create", "alice/app", "--title", "app-bug", "--label", "bug") ssh(aliceKey, 0, "issue", "create", "alice/lib", "--title", "lib-bug", "--label", "bug") ssh(bobKey, 0, "issue", "create", "bob/secret", "--title", "hidden-bug", "--label", "bug") if out := ssh(aliceKey, 2, "query", "save", "bad", "is:open", "bogus:x"); !strings.Contains(out, "bogus:x") { t.Errorf("a bad term is not named: %s", out) } ssh(aliceKey, 0, "query", "save", "bugs", "'repo:alice/*'", "label:bug", "is:open") ssh(aliceKey, 1, "query", "save", "bugs", "is:open") ssh(aliceKey, 0, "query", "save", "all", "is:open") ssh(bobKey, 0, "query", "save", "all", "is:open") type page struct { Data struct { Items []struct { Kind string `json:"kind"` Repo string `json:"repo"` Title string `json:"title"` } `json:"items"` Next string `json:"next"` } `json:"data"` } run := func(key string, args ...string) page { t.Helper() var p page if err := json.Unmarshal([]byte(ssh(key, 0, append(args, "--json")...)), &p); err != nil { t.Fatal(err) } return p } titles := func(p page) string { var out []string for _, it := range p.Data.Items { out = append(out, it.Repo+":"+it.Title) } return strings.Join(out, " ") } if got := titles(run(aliceKey, "issue", "list", "--query", "bugs")); got != "alice/lib:lib-bug alice/app:app-bug" { t.Errorf("issue list --query bugs = %s", got) } if got := titles(run(aliceKey, "mr", "list", "--query", "all")); got != "alice/app:feature" { t.Errorf("mr list --query all = %s", got) } if got := titles(run(bobKey, "query", "run", "all")); !strings.Contains(got, "hidden-bug") { t.Errorf("bob's own private issue is missing from his query: %s", got) } // alice pages through everything she reads, one row at a time. var seen []string cursor := "" for i := 0; i < 6; i++ { args := []string{"query", "run", "all", "--limit", "1"} if cursor != "" { args = append(args, "--cursor", cursor) } p := run(aliceKey, args...) seen = append(seen, titles(p)) if cursor = p.Data.Next; cursor == "" { break } } if got := strings.Join(seen, " "); got != "alice/lib:lib-bug alice/app:app-bug alice/app:feature" { t.Errorf("paged run = %s", got) } ssh(aliceKey, 0, "query", "pin", "all") var dash struct { Data struct { Queries []struct { Name string `json:"name"` Count int `json:"count"` } `json:"queries"` } `json:"data"` } json.Unmarshal([]byte(ssh(aliceKey, 0, "dashboard", "--json")), &dash) if len(dash.Data.Queries) != 1 || dash.Data.Queries[0].Name != "all" || dash.Data.Queries[0].Count != 3 { t.Errorf("dashboard queries = %+v; want all with 3, bob's private issue uncounted", dash.Data.Queries) } out := ssh(aliceKey, 0, "web", "login", "--json") var login struct { Data struct { URL string `json:"url"` } `json:"data"` } json.Unmarshal([]byte(out), &login) browser := newBrowser(t) if status, _ := browserGet(t, browser, inst.base()+login.Data.URL[strings.Index(login.Data.URL, "/login"):]); status != 200 { t.Fatalf("login: %d", status) } _, body := browserGet(t, browser, inst.base()+"/") if !strings.Contains(body, `all 3`) { t.Errorf("dashboard lacks the pinned query:\n%s", body) } status, body := browserGet(t, browser, inst.base()+"/alice/-/queries/bugs") if status != 200 || !strings.Contains(body, "lib-bug") || strings.Contains(body, "hidden-bug") { t.Errorf("/alice/-/queries/bugs: %d\n%s", status, body) } if status, _ := browserGet(t, browser, inst.base()+"/alice/-/queries/nosuch"); status != 404 { t.Errorf("/alice/-/queries/nosuch: %d, want 404", status) } if status, _ := browserGet(t, browser, inst.base()+"/bob/-/queries/all"); status != 404 { t.Errorf("alice reached bob's query page: %d", status) } if _, body := inst.get(t, "/alice/-/queries/bugs"); strings.Contains(body, "lib-bug") { t.Error("an anonymous visitor reached a saved query page") } }