feat: l for details; details for vaults, scopes and files; getting started; the age of cached lists (#17)
- `l` goes to the details of what is selected; `enter` still drills in - vaults, scopes and files show their details (vault settings from the listing, no extra requests) - with nothing to show, the right pane says how to get started - a list from the cache says how old it is while it reloads - what is expiring, in the status pane and `lazysecrets expiring` - when access is refused, what to check, for Azure and Databricks - no group list in an account's details, and no Microsoft Graph call for it Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit was merged in pull request #17.
This commit is contained in:
1 parent
9b0329e9da
commit
09bacd8438
32 files changed
+1321
-254
No files matched your search
+85
-26
@@ -758,7 +758,9 @@ func TestFocusKeys(t *testing.T) {
|
||||
}{
|
||||
{"tab", paneContainers}, {"enter", paneSecrets}, {"enter", paneDetail},
|
||||
{"enter", paneDetail}, {"esc", paneSecrets}, {"shift+tab", paneContainers},
|
||||
{"4", paneDetail}, {"l", paneSources}, {"h", paneDetail},
|
||||
{"4", paneDetail}, {"tab", paneSources}, {"shift+tab", paneDetail},
|
||||
// l: what's selected, on the right; h: back to it.
|
||||
{"2", paneContainers}, {"l", paneDetail}, {"h", paneContainers}, {"h", paneSources}, {"l", paneDetail},
|
||||
} {
|
||||
d.keys(tc.key)
|
||||
if d.m.focus != tc.want {
|
||||
@@ -1104,25 +1106,16 @@ func TestAccountInDetails(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// grouped is an account in two groups.
|
||||
type grouped struct{ described }
|
||||
|
||||
func (grouped) Groups(context.Context) ([]string, error) {
|
||||
return []string{"Platform team", "Vault readers"}, nil
|
||||
}
|
||||
|
||||
func TestAccountPane(t *testing.T) {
|
||||
t.Parallel()
|
||||
d := newDriver(t, grouped{described{fake.Azure(0)}})
|
||||
d.keys("g", "tab") // the account's row, then straight to its pane
|
||||
d := newDriver(t, described{fake.Azure(0)})
|
||||
d.keys("g", "l") // the account's row, then its pane
|
||||
if d.m.focus != paneDetail {
|
||||
t.Fatalf("focus %v, want the fourth pane", d.m.focus)
|
||||
}
|
||||
scr := d.screen()
|
||||
for _, want := range []string{"Alice Example", "groups 2", "Platform team", "Vault readers"} {
|
||||
if !strings.Contains(scr, want) {
|
||||
t.Errorf("missing %q:\n%s", want, scr)
|
||||
}
|
||||
if !strings.Contains(scr, "Alice Example") {
|
||||
t.Errorf("account not shown:\n%s", scr)
|
||||
}
|
||||
bar := lastLine(scr)
|
||||
if !strings.Contains(bar, "copy") || strings.Contains(bar, "reveal") || strings.Contains(bar, "versions") {
|
||||
@@ -1132,28 +1125,20 @@ func TestAccountPane(t *testing.T) {
|
||||
if got := d.clip().content; got != "oid-123" {
|
||||
t.Errorf("y on the object ID copied %q", got)
|
||||
}
|
||||
d.keys("G", "y")
|
||||
if got := d.clip().content; got != "Vault readers" {
|
||||
t.Errorf("y on a group copied %q", got)
|
||||
}
|
||||
|
||||
// / filters it, labels too.
|
||||
d.keys("/")
|
||||
d.typeText("vault")
|
||||
d.typeText("object")
|
||||
d.keys("enter")
|
||||
if scr := d.screen(); !strings.Contains(scr, "Vault readers") || strings.Contains(scr, "Platform team") {
|
||||
if scr := d.screen(); !strings.Contains(scr, "oid-123") || strings.Contains(scr, "Alice Example") {
|
||||
t.Errorf("filtered:\n%s", scr)
|
||||
}
|
||||
d.keys("y")
|
||||
if got := d.clip().content; got != "Vault readers" {
|
||||
t.Errorf("y in the filtered pane copied %q", got)
|
||||
}
|
||||
d.keys("esc")
|
||||
if !strings.Contains(d.screen(), "Platform team") {
|
||||
if !strings.Contains(d.screen(), "Alice Example") {
|
||||
t.Errorf("filter not cleared:\n%s", d.screen())
|
||||
}
|
||||
|
||||
d.keys("shift+tab")
|
||||
d.keys("h")
|
||||
if d.m.focus != paneSources {
|
||||
t.Errorf("back to %v, want Sources", d.m.focus)
|
||||
}
|
||||
@@ -1236,3 +1221,77 @@ func TestStatusPane(t *testing.T) {
|
||||
t.Errorf("back to %v", d.m.focus)
|
||||
}
|
||||
}
|
||||
|
||||
// empty is an account with nothing in it, like Databricks without profiles.
|
||||
type empty struct{ provider.Provider }
|
||||
|
||||
func (empty) Sources(context.Context) ([]provider.Source, error) { return nil, nil }
|
||||
|
||||
func TestGetStarted(t *testing.T) {
|
||||
t.Parallel()
|
||||
d := newDriverOpts(t, Options{}, signedOut{fake.Azure(0)}, empty{fake.Databricks(0)})
|
||||
scr := d.screen()
|
||||
for _, want := range []string{"get started", "Azure az login", "L runs it here",
|
||||
"databricks auth login --host", "lazysecrets --demo"} {
|
||||
if !strings.Contains(scr, want) {
|
||||
t.Errorf("missing %q:\n%s", want, scr)
|
||||
}
|
||||
}
|
||||
d.keys("l", "j")
|
||||
if a := d.m.loginFor(); a == nil || a.Account != "Azure · admin" {
|
||||
t.Errorf("L signs in to %+v", a)
|
||||
}
|
||||
d.keys("y")
|
||||
if got := d.clip().content; got != "az login" {
|
||||
t.Errorf("y copied %q", got)
|
||||
}
|
||||
|
||||
// Once an account has something, an empty one says what it needs.
|
||||
d = newDriverOpts(t, Options{}, fake.Azure(0), empty{fake.Databricks(0)})
|
||||
d.m.sources.SelectWhere(func(r sourceRow) bool { return r.kind == rowGroup && r.provider == fake.Databricks(0).ID() })
|
||||
d.send(nil)
|
||||
if scr := d.screen(); strings.Contains(scr, "get started") || !strings.Contains(scr, "databricks auth login") {
|
||||
t.Errorf("empty Databricks:\n%s", scr)
|
||||
}
|
||||
}
|
||||
|
||||
// detailed is an account whose vaults have details.
|
||||
type detailed struct{ provider.Provider }
|
||||
|
||||
func (detailed) Details(_ context.Context, c provider.Container) ([]provider.Attr, error) {
|
||||
return []provider.Attr{{Label: "access", Value: "Azure roles (RBAC)"}, {Label: "git", Value: "tracked", Warn: true}}, nil
|
||||
}
|
||||
|
||||
func TestContainerDetails(t *testing.T) {
|
||||
t.Parallel()
|
||||
d := newDriverOpts(t, Options{}, detailed{fake.Azure(0)})
|
||||
d.keys("2")
|
||||
if scr := d.screen(); !strings.Contains(scr, "access Azure roles (RBAC)") || !strings.Contains(scr, "URI") {
|
||||
t.Errorf("details:\n%s", scr)
|
||||
}
|
||||
if st := d.m.details[d.m.containers.SelectedID()]; st.loading || len(st.attrs) != 2 {
|
||||
t.Errorf("state %+v", st)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpiringInStatus(t *testing.T) {
|
||||
t.Parallel()
|
||||
d := newDriverOpts(t, Options{ExpiryWarning: 30 * 24 * time.Hour}, fake.Azure(0))
|
||||
d.keys("3", "]", "]") // load kv-app-prod's certificates
|
||||
if !strings.Contains(d.screen(), "⚠ 1 expiring") {
|
||||
t.Errorf("status line:\n%s", d.screen())
|
||||
}
|
||||
d.keys("0")
|
||||
scr := d.screen()
|
||||
for _, want := range []string{"expiring ✗ expired old-tls · kv-app-prod", "in 1 of 3 vaults (ctrl+a in search loads all)"} {
|
||||
if !strings.Contains(scr, want) {
|
||||
t.Errorf("missing %q:\n%s", want, scr)
|
||||
}
|
||||
}
|
||||
d.keys("3", "[", "[", "0", "enter") // elsewhere, then to it from the status pane
|
||||
d.m.fields.SelectWhere(func(f panes.Field) bool { return f.Label == "expiring" })
|
||||
d.keys("enter")
|
||||
if _, v, o := d.selected(); v != "kv-app-prod" || o != "old-tls" {
|
||||
t.Errorf("enter went to %s/%s\n%s", v, o, d.screen())
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user