The scope list follows the Sources pane: All, then each account (or Databricks, or Repos) with its sources under it, in place of All Azure, All Databricks and All repos. An account scope searches and loads only that account. ctrl+s on a group header narrows to its group. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
469 lines
14 KiB
Go
469 lines
14 KiB
Go
package app
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
tea "charm.land/bubbletea/v2"
|
|
|
|
"git.kebcloud.no/keb/lazysecrets/internal/provider/fake"
|
|
"github.com/charmbracelet/x/ansi"
|
|
)
|
|
|
|
func (d *driver) openSearch() {
|
|
d.t.Helper()
|
|
d.send(tea.KeyPressMsg{Code: 's', Mod: tea.ModCtrl})
|
|
if d.m.search == nil {
|
|
d.t.Fatalf("no search popup:\n%s", d.screen())
|
|
}
|
|
}
|
|
|
|
func ctrl(r rune) tea.KeyPressMsg { return tea.KeyPressMsg{Code: r, Mod: tea.ModCtrl} }
|
|
|
|
// searchAll picks All on the left and goes back to the results.
|
|
func (d *driver) searchAll() {
|
|
d.send(ctrl('h'))
|
|
for d.m.search.src > 0 {
|
|
d.send(tea.KeyPressMsg{Code: tea.KeyUp})
|
|
}
|
|
for d.m.search.scope().id != allSources {
|
|
d.send(tea.KeyPressMsg{Code: tea.KeyDown})
|
|
}
|
|
d.send(ctrl('l'))
|
|
}
|
|
|
|
func TestSearchLoaded(t *testing.T) {
|
|
t.Parallel()
|
|
d := newDriver(t)
|
|
d.keys("j") // platform-dev: away from what's loaded
|
|
d.openSearch()
|
|
for _, want := range []string{"Sources", "Results", "All", "platform-dev"} {
|
|
if !strings.Contains(d.screen(), want) {
|
|
t.Fatalf("missing %q:\n%s", want, d.screen())
|
|
}
|
|
}
|
|
d.typeText("api-key") // on All: found at once
|
|
if !strings.Contains(d.screen(), "ctrl+s only platform-dev") {
|
|
t.Errorf("no hint to narrow to the source:\n%s", d.screen())
|
|
}
|
|
d.send(ctrl('s'))
|
|
if !strings.Contains(d.screen(), "no matches here · All searches everything") || !strings.Contains(d.screen(), "ctrl+s searches all") {
|
|
t.Errorf("no hint to search everything:\n%s", d.screen())
|
|
}
|
|
d.send(ctrl('s'))
|
|
if !strings.Contains(d.screen(), "platform-prod / kv-app-prod / api-key") {
|
|
t.Fatalf("loaded secret not found:\n%s", d.screen())
|
|
}
|
|
d.keys("enter")
|
|
if src, c, s := d.selected(); src != "platform-prod" || c != "kv-app-prod" || s != "api-key" {
|
|
t.Errorf("jumped to %s/%s/%s", src, c, s)
|
|
}
|
|
if d.m.search != nil {
|
|
t.Error("popup still open")
|
|
}
|
|
}
|
|
|
|
func TestSearchLoadsAsYouType(t *testing.T) {
|
|
t.Parallel()
|
|
d := newDriver(t)
|
|
d.openSearch()
|
|
d.send(ctrl('s')) // only the source the cursor is on: platform-prod
|
|
// Typing loads the scope: that source, without ctrl+a.
|
|
d.typeText("smtp")
|
|
if !strings.Contains(d.screen(), "platform-prod / kv-shared-prod / smtp-password") {
|
|
t.Fatalf("not found in a vault that wasn't loaded:\n%s", d.screen())
|
|
}
|
|
if _, ok := d.m.cache.objects["Azure|sub-prod/kv-shared-prod|Secret"]; !ok {
|
|
t.Error("loaded list not in the panes' cache")
|
|
}
|
|
if _, ok := d.m.cache.containers[sourceRowKey("Azure", "sub-dev")]; ok {
|
|
t.Error("another source loaded too")
|
|
}
|
|
|
|
// On All: everything.
|
|
d.send(ctrl('s'))
|
|
d.typeText("-")
|
|
if _, ok := d.m.cache.containers[sourceRowKey("Azure", "sub-dev")]; !ok {
|
|
t.Errorf("typing on All didn't load the other sources:\n%s", d.screen())
|
|
}
|
|
if d.m.search.input.Value() != "smtp-" {
|
|
t.Error("query lost while loading")
|
|
}
|
|
if !strings.Contains(d.screen(), "unreadable") { // kv-restricted refuses
|
|
t.Errorf("unreadable vaults not counted:\n%s", d.screen())
|
|
}
|
|
// Once: the next key doesn't ask for the list that failed again;
|
|
// ctrl+a still does, on purpose.
|
|
d.typeText("p")
|
|
if d.m.crawl.running {
|
|
t.Error("typing on reloads what failed")
|
|
}
|
|
}
|
|
|
|
func TestSearchSourceNarrowsAndLoads(t *testing.T) {
|
|
t.Parallel()
|
|
d := newDriver(t)
|
|
d.openSearch()
|
|
d.keys("tab") // to Sources
|
|
sel := func() string { return d.m.search.scopes[d.m.search.src].id }
|
|
for d.m.search.src > 0 {
|
|
d.send(tea.KeyPressMsg{Code: tea.KeyUp})
|
|
}
|
|
for !strings.HasPrefix(sel(), "platform-dev") {
|
|
d.send(tea.KeyPressMsg{Code: tea.KeyDown})
|
|
if d.m.search.src == len(d.m.search.scopes)-1 {
|
|
t.Fatal("platform-dev not in sources")
|
|
}
|
|
}
|
|
if !strings.Contains(ansiStrip(d.m.search.scopes[d.m.search.src].mark), "○") {
|
|
t.Errorf("unloaded source mark %q", d.m.search.scopes[d.m.search.src].mark)
|
|
}
|
|
if len(d.m.search.shown) != 0 {
|
|
t.Errorf("results not narrowed to platform-dev: %d", len(d.m.search.shown))
|
|
}
|
|
d.send(ctrl('r'))
|
|
if !strings.Contains(ansiStrip(d.m.search.scopes[d.m.search.src].mark), "●") {
|
|
t.Errorf("loaded source mark %q", d.m.search.scopes[d.m.search.src].mark)
|
|
}
|
|
for _, o := range d.m.search.shown {
|
|
if !strings.Contains(ansiStrip(o.label), "platform-dev") {
|
|
t.Errorf("result from elsewhere: %q", ansiStrip(o.label))
|
|
}
|
|
}
|
|
if len(d.m.search.shown) == 0 {
|
|
t.Error("nothing shown after loading platform-dev")
|
|
}
|
|
if _, ok := d.m.cache.objects["Azure|sub-prod/kv-shared-prod|Secret"]; ok {
|
|
t.Error("loaded another subscription too")
|
|
}
|
|
}
|
|
|
|
func TestSearchResultsStayPut(t *testing.T) {
|
|
t.Parallel()
|
|
d := newDriver(t)
|
|
d.openSearch()
|
|
d.send(tea.KeyPressMsg{Code: tea.KeyDown})
|
|
ids := func() []string {
|
|
var out []string
|
|
for _, o := range d.m.search.shown {
|
|
out = append(out, o.id)
|
|
}
|
|
return out
|
|
}
|
|
before, at := ids(), d.m.search.shown[d.m.search.res].id
|
|
d.send(ctrl('a'))
|
|
after := ids()
|
|
if len(after) <= len(before) {
|
|
t.Fatalf("nothing loaded: %d → %d", len(before), len(after))
|
|
}
|
|
for i, id := range before {
|
|
if after[i] != id {
|
|
t.Errorf("row %d changed from %q to %q", i, id, after[i])
|
|
}
|
|
}
|
|
if got := d.m.search.shown[d.m.search.res].id; got != at {
|
|
t.Errorf("cursor moved from %q to %q", at, got)
|
|
}
|
|
}
|
|
|
|
func TestStarFromSearch(t *testing.T) {
|
|
t.Parallel()
|
|
d := newDriver(t)
|
|
d.openSearch()
|
|
d.typeText("api-key")
|
|
d.send(ctrl('f'))
|
|
if !d.m.store.Node(kvKey + "/secrets/api-key").StarredFor("Azure") {
|
|
t.Errorf("ctrl+f in search didn't star:\n%s", d.screen())
|
|
}
|
|
}
|
|
|
|
func ansiStrip(s string) string { return ansi.Strip(s) }
|
|
|
|
func TestSearchStartsWhereYouAre(t *testing.T) {
|
|
t.Parallel()
|
|
d := newDriver(t)
|
|
d.keys("2", "j") // platform-prod / kv-shared-prod
|
|
d.openSearch()
|
|
v := d.m.search
|
|
scope := func() string { return ansiStrip(v.scopes[v.src].label) }
|
|
if scope() != "All" {
|
|
t.Errorf("opened on %q, want All", scope())
|
|
}
|
|
if v.res >= len(v.shown) || !strings.Contains(ansiStrip(v.shown[v.res].label), "kv-shared-prod") {
|
|
t.Errorf("vault not under the cursor:\n%s", d.screen())
|
|
}
|
|
d.send(ctrl('s')) // narrows to the source, and back
|
|
if !strings.Contains(scope(), "platform-prod") {
|
|
t.Errorf("ctrl+s narrowed to %q", scope())
|
|
}
|
|
d.send(ctrl('s'))
|
|
if scope() != "All" {
|
|
t.Errorf("second ctrl+s on %q, want All", scope())
|
|
}
|
|
|
|
// From Favourites: to Search, on All.
|
|
d.keys("esc", "]", "]") // stop typing; Favourites
|
|
d.send(ctrl('s'))
|
|
if v.tab != tabSearch || scope() != "All" {
|
|
t.Errorf("ctrl+s from another tab: tab %d, %q", v.tab, scope())
|
|
}
|
|
|
|
// From the Sources pane: the source.
|
|
d.keys("esc", "1", "j") // close; platform-dev
|
|
d.openSearch()
|
|
v = d.m.search
|
|
d.send(ctrl('s'))
|
|
if !strings.Contains(scope(), "platform-dev") {
|
|
t.Errorf("ctrl+s narrowed to %q", scope())
|
|
}
|
|
}
|
|
|
|
func TestSearchNarrowsToAccount(t *testing.T) {
|
|
t.Parallel()
|
|
d := newDriver(t)
|
|
d.keys("1", "g") // the first group header
|
|
header, _ := d.m.sources.Selected()
|
|
d.openSearch()
|
|
d.send(ctrl('s'))
|
|
v := d.m.search
|
|
if v.scope().id != accountScope+header.provider {
|
|
t.Fatalf("ctrl+s on %q narrowed to %q", header.label, v.scope().id)
|
|
}
|
|
if len(v.shown) == 0 || !strings.Contains(d.screen(), "Results in "+header.label) {
|
|
t.Errorf("no results in the account:\n%s", d.screen())
|
|
}
|
|
for _, o := range v.shown {
|
|
if providerOf(o.group) != header.provider {
|
|
t.Errorf("result from another account: %q", ansiStrip(o.label))
|
|
}
|
|
}
|
|
// The account's sources are under it, indented.
|
|
if next := v.scopes[v.src+1]; next.group == "" || providerOf(next.group) != header.provider || !strings.HasPrefix(ansiStrip(next.label), " ") {
|
|
t.Errorf("after the account: %q", ansiStrip(next.label))
|
|
}
|
|
}
|
|
|
|
func TestSearchHint(t *testing.T) {
|
|
t.Parallel()
|
|
d := newDriver(t)
|
|
d.openSearch()
|
|
d.searchAll()
|
|
if !strings.Contains(d.screen(), "vaults loaded · ctrl+a loads everything") {
|
|
t.Errorf("no hint on All:\n%s", d.screen())
|
|
}
|
|
d.send(ctrl('a'))
|
|
if strings.Contains(d.screen(), "ctrl+a loads everything") {
|
|
t.Errorf("hint after loading everything:\n%s", d.screen())
|
|
}
|
|
}
|
|
|
|
func TestGoToTabsAndModes(t *testing.T) {
|
|
t.Parallel()
|
|
d := newDriver(t)
|
|
d.keys("2", "f", "j", "f") // two favourites
|
|
d.keys("F")
|
|
v := d.m.search
|
|
if v.tab != tabFavs || v.typing {
|
|
t.Fatalf("F: tab %d, typing %v", v.tab, v.typing)
|
|
}
|
|
d.keys("j") // browsing: j moves, doesn't type
|
|
if v.res != 1 || v.input.Value() != "" {
|
|
t.Errorf("j: cursor %d, input %q", v.res, v.input.Value())
|
|
}
|
|
d.keys("]")
|
|
if v.tab != tabRecent {
|
|
t.Errorf("] → tab %d, want Recent", v.tab)
|
|
}
|
|
d.keys("]", "]")
|
|
if v.tab != tabFavs {
|
|
t.Errorf("] wraps to tab %d, want Favourites", v.tab)
|
|
}
|
|
d.keys("[")
|
|
if v.tab != tabSearch || !strings.Contains(d.screen(), "Scope") {
|
|
t.Errorf("[ → tab %d, want Search:\n%s", v.tab, d.screen())
|
|
}
|
|
|
|
// g and G: top and bottom of the list with the cursor.
|
|
d.keys("G")
|
|
if v.res != len(v.shown)-1 || v.res == 0 {
|
|
t.Errorf("G on results: %d of %d", v.res, len(v.shown))
|
|
}
|
|
d.keys("g")
|
|
if v.res != 0 {
|
|
t.Errorf("g on results: %d", v.res)
|
|
}
|
|
d.keys("h", "G")
|
|
if v.src != len(v.scopes)-1 {
|
|
t.Errorf("G on scopes: %d of %d", v.src, len(v.scopes))
|
|
}
|
|
d.keys("g", "l")
|
|
if v.src != 0 {
|
|
t.Errorf("g on scopes: %d", v.src)
|
|
}
|
|
|
|
d.keys("/")
|
|
d.typeText("shared")
|
|
if !v.typing || len(v.shown) == 0 {
|
|
t.Fatalf("typing: %v, %d results", v.typing, len(v.shown))
|
|
}
|
|
d.keys("esc") // clears, back to browsing
|
|
if v.typing || v.input.Value() != "" || d.m.search == nil {
|
|
t.Errorf("esc while typing: typing %v, input %q, open %v", v.typing, v.input.Value(), d.m.search != nil)
|
|
}
|
|
d.keys("esc") // closes
|
|
if d.m.search != nil {
|
|
t.Error("esc while browsing didn't close")
|
|
}
|
|
}
|
|
|
|
func TestSearchHighlightsMatches(t *testing.T) {
|
|
t.Parallel()
|
|
// Icons: labels start with a multi-byte glyph, where byte offsets and
|
|
// characters differ.
|
|
d := newDriverOpts(t, Options{Icons: true})
|
|
d.openSearch()
|
|
d.typeText("dbpw")
|
|
if len(d.m.search.shown) == 0 {
|
|
t.Fatal("no results")
|
|
}
|
|
o := d.m.search.shown[0]
|
|
plain := []rune(ansiStrip(o.label))
|
|
var got string
|
|
for _, h := range o.hits {
|
|
got += string(plain[h])
|
|
}
|
|
if got != "dbpw" {
|
|
t.Errorf("hits %v spell %q in %q", o.hits, got, string(plain))
|
|
}
|
|
with := d.m.searchViewBox()
|
|
for i := range d.m.search.shown {
|
|
d.m.search.shown[i].hits = nil
|
|
}
|
|
if with == d.m.searchViewBox() {
|
|
t.Error("matched characters not styled")
|
|
}
|
|
}
|
|
|
|
func TestHelpBarFollowsPopup(t *testing.T) {
|
|
t.Parallel()
|
|
d := newDriver(t)
|
|
d.keys("F")
|
|
bar := lastLine(d.screen())
|
|
for _, want := range []string{"[ prev tab", "] next tab", "/ filter", "f star"} {
|
|
if !strings.Contains(bar, want) {
|
|
t.Errorf("browsing: help bar %q lacks %q", bar, want)
|
|
}
|
|
}
|
|
d.keys("/")
|
|
if bar := lastLine(d.screen()); !strings.Contains(bar, "ctrl+f star") || !strings.Contains(bar, "esc stop typing") {
|
|
t.Errorf("typing: help bar %q", bar)
|
|
}
|
|
}
|
|
|
|
func TestHelpFromPopups(t *testing.T) {
|
|
t.Parallel()
|
|
d := newDriver(t)
|
|
d.keys("F", "?")
|
|
if d.m.prompt == nil || d.m.prompt.title != "Keybindings" || d.m.prompt.options[0].label != "Go to" {
|
|
t.Fatalf("? in Go to:\n%s", d.screen())
|
|
}
|
|
if !strings.Contains(d.screen(), "switch list") { // tab: not on the help bar
|
|
t.Errorf("Go to's keys missing:\n%s", d.screen())
|
|
}
|
|
var sections []string
|
|
for _, o := range d.m.prompt.options {
|
|
if o.header {
|
|
sections = append(sections, o.label)
|
|
}
|
|
if strings.Contains(ansiStrip(o.label), "copy value") {
|
|
t.Errorf("pane key listed in Go to's help: %q", ansiStrip(o.label))
|
|
}
|
|
}
|
|
if strings.Join(sections, ",") != "Go to,While typing, after /" {
|
|
t.Errorf("sections %v", sections)
|
|
}
|
|
d.keys("esc") // back to Go to, where it was
|
|
if d.m.search == nil || d.m.search.tab != tabFavs || d.m.prompt != nil {
|
|
t.Errorf("not back in Go to: search %v, prompt %v", d.m.search != nil, d.m.prompt != nil)
|
|
}
|
|
|
|
// Picking a key in the list runs it in Go to.
|
|
d.keys("?")
|
|
d.typeText("next tab")
|
|
d.keys("enter")
|
|
if d.m.search == nil || d.m.search.tab != tabRecent {
|
|
t.Errorf("running ] from ? didn't switch Go to's tab")
|
|
}
|
|
|
|
// Menus too.
|
|
d.keys("esc", "3", "S", "?")
|
|
if d.m.prompt == nil || d.m.prompt.options[0].label != "This menu" {
|
|
t.Fatalf("? in a menu:\n%s", d.screen())
|
|
}
|
|
for _, o := range d.m.prompt.options {
|
|
if o.header && o.label != "This menu" {
|
|
t.Errorf("menu help has section %q", o.label)
|
|
}
|
|
}
|
|
d.keys("esc")
|
|
if d.m.prompt == nil || !strings.HasPrefix(d.m.prompt.title, "Share") {
|
|
t.Errorf("not back in the share menu:\n%s", d.screen())
|
|
}
|
|
}
|
|
|
|
func TestHelpRunsTypingKeysInGoTo(t *testing.T) {
|
|
t.Parallel()
|
|
d := newDriver(t)
|
|
d.keys("ctrl+s", "esc") // Go to on Search, browsing
|
|
before := d.m.search.results
|
|
d.keys("?")
|
|
d.typeText("ctrl+h") // "sources", a typing key
|
|
d.keys("enter")
|
|
if d.m.search == nil || !d.m.search.typing || d.m.search.results || !before {
|
|
t.Errorf("typing key from ? didn't run: typing %v, results %v", d.m.search != nil && d.m.search.typing, d.m.search.results)
|
|
}
|
|
}
|
|
|
|
func TestGoToHelpBarEndsWithHelp(t *testing.T) {
|
|
t.Parallel()
|
|
d := newDriver(t) // 100 columns: Go to's keys don't all fit
|
|
d.keys("ctrl+s", "esc")
|
|
bar := strings.TrimSpace(lastLine(d.screen()))
|
|
if !strings.HasSuffix(bar, "? help") || !strings.Contains(bar, "…") {
|
|
t.Errorf("help bar %q", bar)
|
|
}
|
|
}
|
|
|
|
func TestLoadWithEmptyResults(t *testing.T) {
|
|
t.Parallel()
|
|
c := &counting{Provider: fake.Azure(0)}
|
|
d := newDriverOpts(t, Options{}, c)
|
|
d.openSearch()
|
|
d.send(ctrl('h')) // pick platform-dev, never opened, in the scopes
|
|
for d.m.search.src > 0 {
|
|
d.send(tea.KeyPressMsg{Code: tea.KeyUp})
|
|
}
|
|
for i := 0; !strings.Contains(ansiStrip(d.m.search.scope().label), "platform-dev"); i++ {
|
|
if i > len(d.m.search.scopes) {
|
|
var labels []string
|
|
for _, o := range d.m.search.scopes {
|
|
labels = append(labels, ansiStrip(o.label))
|
|
}
|
|
t.Fatalf("no platform-dev scope: %q (at %d, results %v)", labels, d.m.search.src, d.m.search.results)
|
|
}
|
|
d.send(tea.KeyPressMsg{Code: tea.KeyDown})
|
|
}
|
|
d.send(ctrl('l'))
|
|
if len(d.m.search.shown) != 0 || !d.m.search.results {
|
|
t.Fatalf("want empty results with the cursor there: %d shown", len(d.m.search.shown))
|
|
}
|
|
before := c.containers
|
|
d.send(ctrl('r')) // in the results, nothing selected: the source
|
|
if c.containers == before {
|
|
t.Error("ctrl+r with empty results didn't load the source")
|
|
}
|
|
if len(d.m.search.shown) == 0 {
|
|
t.Errorf("nothing shown after loading:\n%s", d.screen())
|
|
}
|
|
}
|