Release / Build and publish (push) Successful in 22s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
201 lines
6.7 KiB
Go
201 lines
6.7 KiB
Go
package app
|
|
|
|
import (
|
|
"context"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
|
|
tea "charm.land/bubbletea/v2"
|
|
|
|
"git.kebcloud.no/keb/lazysecrets/internal/provider"
|
|
"git.kebcloud.no/keb/lazysecrets/internal/provider/fake"
|
|
)
|
|
|
|
func TestGroupedSources(t *testing.T) {
|
|
t.Parallel()
|
|
d := newDriver(t)
|
|
screen := d.screen()
|
|
for _, want := range []string{"▾ Azure (2)", " platform-prod", "▾ Databricks (2)", " prod"} {
|
|
if !strings.Contains(screen, want) {
|
|
t.Errorf("missing %q:\n%s", want, screen)
|
|
}
|
|
}
|
|
if src, _, _ := d.selected(); src != "platform-prod" {
|
|
t.Errorf("startup cursor on %q, want the first source", src)
|
|
}
|
|
}
|
|
|
|
func TestFoldGroup(t *testing.T) {
|
|
t.Parallel()
|
|
d := newDriver(t)
|
|
d.keys("k", "enter") // Azure header: fold
|
|
if !strings.Contains(d.screen(), "▸ Azure (2)") || strings.Contains(d.screen(), "platform-prod") {
|
|
t.Fatalf("Azure not folded:\n%s", d.screen())
|
|
}
|
|
if d.m.focus != paneSources {
|
|
t.Error("enter on a header moved focus")
|
|
}
|
|
d.keys("space") // unfold
|
|
if !strings.Contains(d.screen(), "platform-prod") {
|
|
t.Fatalf("Azure not unfolded:\n%s", d.screen())
|
|
}
|
|
// enter on a source still drills in.
|
|
d.keys("j", "enter")
|
|
if d.m.focus != paneContainers {
|
|
t.Error("enter on a source didn't move to vaults")
|
|
}
|
|
}
|
|
|
|
func TestToggleFlatKeepsSelection(t *testing.T) {
|
|
t.Parallel()
|
|
d := newDriver(t)
|
|
d.keys("j", "j", "j") // Databricks prod
|
|
d.keys("`")
|
|
if !d.m.flatSources || strings.Contains(d.screen(), "▾") {
|
|
t.Fatalf("not flat:\n%s", d.screen())
|
|
}
|
|
if !strings.Contains(d.screen(), "Databricks prod") {
|
|
t.Errorf("flat rows should show the provider:\n%s", d.screen())
|
|
}
|
|
if src, _, _ := d.selected(); src != "prod" {
|
|
t.Errorf("selection lost: %q", src)
|
|
}
|
|
d.keys("`")
|
|
if src, _, _ := d.selected(); d.m.flatSources || src != "prod" {
|
|
t.Errorf("back to grouped: flat=%v selected=%q", d.m.flatSources, src)
|
|
}
|
|
}
|
|
|
|
func TestFlatSourcesOption(t *testing.T) {
|
|
t.Parallel()
|
|
d := newDriverOpts(t, Options{FlatSources: true})
|
|
if strings.Contains(d.screen(), "▾") || d.m.sources.Len() != 4 {
|
|
t.Errorf("FlatSources ignored:\n%s", d.screen())
|
|
}
|
|
}
|
|
|
|
func TestGroupHeaderShowsProviderError(t *testing.T) {
|
|
t.Parallel()
|
|
d := newDriver(t, failingSources{fake.Azure(0)}, fake.Databricks(0))
|
|
if !strings.Contains(d.screen(), "▾ Azure (!)") {
|
|
t.Errorf("no error marker:\n%s", d.screen())
|
|
}
|
|
if src, _, _ := d.selected(); src != "prod" {
|
|
t.Errorf("cursor should skip to the first working source, got %q", src)
|
|
}
|
|
}
|
|
|
|
// slowSources delivers Azure's sources after Databricks', like the real
|
|
// Azure provider (network) versus Databricks (a local file).
|
|
func TestStartupPrefersFirstProviderWhenItAnswersLast(t *testing.T) {
|
|
t.Parallel()
|
|
m := New([]provider.Provider{fake.Azure(0), fake.Databricks(0)}, Options{Clipboard: &fakeClipboard{}})
|
|
d := &driver{t: t, m: m}
|
|
d.send(tea.WindowSizeMsg{Width: 100, Height: 30})
|
|
d.send(sourcesMsg{gen: m.sourcesGen, provider: "Databricks", sources: mustSources(t, fake.Databricks(0))})
|
|
if src, _, _ := d.selected(); src != "prod" {
|
|
t.Fatalf("before Azure answers: %q", src)
|
|
}
|
|
d.send(sourcesMsg{gen: m.sourcesGen, provider: "Azure", sources: mustSources(t, fake.Azure(0))})
|
|
if src, _, _ := d.selected(); src != "platform-prod" {
|
|
t.Errorf("cursor not moved to the first Azure source: %q", src)
|
|
}
|
|
|
|
// Once the user has moved, late results don't move the cursor.
|
|
d2 := &driver{t: t, m: New([]provider.Provider{fake.Azure(0), fake.Databricks(0)}, Options{Clipboard: &fakeClipboard{}})}
|
|
d2.send(tea.WindowSizeMsg{Width: 100, Height: 30})
|
|
d2.send(sourcesMsg{gen: d2.m.sourcesGen, provider: "Databricks", sources: mustSources(t, fake.Databricks(0))})
|
|
d2.keys("j") // dev
|
|
d2.send(sourcesMsg{gen: d2.m.sourcesGen, provider: "Azure", sources: mustSources(t, fake.Azure(0))})
|
|
if src, _, _ := d2.selected(); src != "dev" {
|
|
t.Errorf("cursor moved after the user navigated: %q", src)
|
|
}
|
|
}
|
|
|
|
func mustSources(t *testing.T, p provider.Provider) []provider.Source {
|
|
t.Helper()
|
|
s, err := p.Sources(context.Background())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return s
|
|
}
|
|
|
|
func TestTwoAccounts(t *testing.T) {
|
|
t.Parallel()
|
|
alice := &counting{Provider: fake.Azure(0).As("Azure:alice", "Azure · alice")}
|
|
bob := &counting{Provider: fake.Azure(0).As("Azure:bob", "Azure · bob")}
|
|
d := newDriver(t, alice, bob)
|
|
|
|
screen := d.screen()
|
|
if !strings.Contains(screen, "▾ Azure · alice (2)") || !strings.Contains(screen, "▾ Azure · bob (2)") {
|
|
t.Fatalf("one group per account expected:\n%s", screen)
|
|
}
|
|
|
|
// Both accounts see platform-prod; bob's is a different row, and
|
|
// browsing it goes through bob's provider.
|
|
d.keys("j", "j", "j") // alice dev, bob header, bob prod
|
|
if src, c, s := d.selected(); src != "platform-prod" || c != "kv-app-prod" || s != "db-password" {
|
|
t.Fatalf("bob's platform-prod: %s/%s/%s", src, c, s)
|
|
}
|
|
c, _ := d.m.containers.Selected()
|
|
if c.Source.Provider != "Azure:bob" || d.m.containers.SelectedID() != "Azure:bob|"+c.ID {
|
|
t.Errorf("vault not tied to bob: provider %q, key %q", c.Source.Provider, d.m.containers.SelectedID())
|
|
}
|
|
if bob.objects != 1 {
|
|
t.Errorf("bob listed objects %d times, want 1", bob.objects)
|
|
}
|
|
before := alice.objects
|
|
d.keys("space") // reveal goes through bob too
|
|
if alice.values != 0 || bob.values != 1 || alice.objects != before {
|
|
t.Errorf("reveal routed wrong: alice values %d, bob values %d", alice.values, bob.values)
|
|
}
|
|
|
|
if !strings.Contains(d.screen(), "source Azure · bob / platform-prod") {
|
|
t.Errorf("detail should name the account:\n%s", d.screen())
|
|
}
|
|
|
|
// Flat: the account is shown on each row.
|
|
d.keys("`")
|
|
if !strings.Contains(d.screen(), "platform-prod · bob") {
|
|
t.Errorf("flat rows should name the account:\n%s", d.screen())
|
|
}
|
|
}
|
|
|
|
func TestFilterSourcesByProvider(t *testing.T) {
|
|
t.Parallel()
|
|
d := newDriver(t)
|
|
d.keys("/")
|
|
d.typeText("databr")
|
|
d.m.sources.Top()
|
|
var names []string
|
|
for range d.m.sources.Len() {
|
|
r, _ := d.m.sources.Selected()
|
|
names = append(names, r.source.Name)
|
|
d.m.sources.Move(1)
|
|
}
|
|
slices.Sort(names)
|
|
if strings.Join(names, ",") != "dev,prod" {
|
|
t.Errorf("\"databr\" matched %v, want the Databricks workspaces", names)
|
|
}
|
|
}
|
|
|
|
func TestFilteringShowsAccounts(t *testing.T) {
|
|
t.Parallel()
|
|
alice := fake.Azure(0).As("Azure:alice", "Azure · alice")
|
|
bob := fake.Azure(0).As("Azure:bob", "Azure · bob")
|
|
d := newDriver(t, alice, bob)
|
|
d.keys("k", "enter") // fold alice: filtering still finds her sources
|
|
d.keys("/")
|
|
d.typeText("dev")
|
|
screen := d.screen()
|
|
if !strings.Contains(screen, "platform-dev · ali") || !strings.Contains(screen, "platform-dev · bob") {
|
|
t.Errorf("filtered rows should name the account:\n%s", screen)
|
|
}
|
|
d.keys("esc") // clear: back to groups
|
|
if !strings.Contains(d.screen(), "▸ Azure · alice") || strings.Contains(d.screen(), "platform-dev · bob") {
|
|
t.Errorf("not grouped again after clearing:\n%s", d.screen())
|
|
}
|
|
}
|