package app import ( tea "charm.land/bubbletea/v2" "context" "os" "os/exec" "path/filepath" "slices" "strings" "testing" "git.kebcloud.no/keb/lazysecrets/internal/provider" "git.kebcloud.no/keb/lazysecrets/internal/provider/fake" "git.kebcloud.no/keb/lazysecrets/internal/provider/repo" "git.kebcloud.no/keb/lazysecrets/internal/ref" "git.kebcloud.no/keb/lazysecrets/internal/store" ) const sopsFile = `db: password: ENC[AES256_GCM,data:x,iv:x,tag:x,type:str] sops: age: - recipient: age1xxxx lastmodified: "2026-10-09T10:05:29Z" mac: ENC[AES256_GCM,data:x,iv:x,tag:x,type:str] ` // gitRepoWith makes a git repository holding a SOPS file, with a remote. func gitRepoWith(t *testing.T, remote string) string { t.Helper() root, _ := filepath.EvalSymlinks(t.TempDir()) os.MkdirAll(filepath.Join(root, "secrets"), 0o755) os.WriteFile(filepath.Join(root, "secrets", "app.yaml"), []byte(sopsFile), 0o644) for _, args := range [][]string{{"init", "-q"}, {"add", "."}, {"-c", "user.name=t", "-c", "user.email=t@t", "commit", "-qm", "x"}, {"remote", "add", "origin", remote}} { if out, err := exec.Command("git", append([]string{"-C", root}, args...)...).CombinedOutput(); err != nil { t.Fatalf("%v %s", err, out) } } return root } func TestCurrentRepoFirst(t *testing.T) { // Not parallel: git, run by the repo provider, is slow under load. if _, err := exec.LookPath("git"); err != nil { t.Skip("no git") } here := gitRepoWith(t, "https://git.example.com/team/here.git") other := gitRepoWith(t, "https://git.example.com/team/aaa-other.git") mem, _ := store.Load("") mem.Update("lazysecrets://repo/git.example.com%2Fteam%2Faaa-other", func(n *store.Node) { n.Source, n.StarredBy = other, []string{"Repos"} }) d := newDriverOpts(t, Options{Store: mem}, repo.New(repo.Settings{Dir: here, Starred: []string{other}})) // Grouped: the repo it started in on a row of its own on top, // selected, then the other repos under Repos, without it. var rows []string for _, r := range d.m.sources.Items() { switch r.kind { case rowGroup: rows = append(rows, "group "+r.label) case rowSource: rows = append(rows, r.source.ID) } } if want := []string{here, "group Repos", other}; !slices.Equal(rows, want) { t.Errorf("rows %v, want %v", rows, want) } if !strings.Contains(d.screen(), "Repo · "+filepath.Base(here)) { t.Errorf("no Repo · name row:\n%s", d.screen()) } if src, ok := d.m.selectedSource(); !ok || src.ID != here { t.Errorf("selected %v, want the repo it started in", src.ID) } // Flat: it sorts first, labelled. d.keys("`") if !strings.Contains(d.screen(), filepath.Base(here)+" · here") { t.Errorf("no here label in the flat list:\n%s", d.screen()) } } func TestRepoSource(t *testing.T) { // Not parallel: git, run by the repo provider, is slow under load. if _, err := exec.LookPath("git"); err != nil { t.Skip("no git") } root, _ := filepath.EvalSymlinks(t.TempDir()) os.MkdirAll(filepath.Join(root, "secrets"), 0o755) os.WriteFile(filepath.Join(root, "secrets", "app.yaml"), []byte(sopsFile), 0o644) for _, args := range [][]string{{"init", "-q"}, {"add", "."}, {"-c", "user.name=t", "-c", "user.email=t@t", "commit", "-qm", "x"}, {"remote", "add", "origin", "https://git.example.com/team/infra.git"}} { if out, err := exec.Command("git", append([]string{"-C", root}, args...)...).CombinedOutput(); err != nil { t.Fatalf("%v %s", err, out) } } d := newDriverOpts(t, Options{}, []provider.Provider{repo.New(repo.Settings{Dir: root})}...) scr := d.screen() for _, want := range []string{"Repos", filepath.Base(root), "[2] Files", "secrets/app.yaml sops · age", "db.password"} { if !strings.Contains(scr, want) { t.Fatalf("missing %q:\n%s", want, scr) } } d.keys("3", "f") key := "lazysecrets://repo/git.example.com%2Fteam%2Finfra/secrets%2Fapp.yaml/db.password" if !d.m.store.Node(key).StarredFor("Repos") { t.Errorf("star not stored under the repo link; nodes: %v", d.m.store.Nodes()) } d.keys("s") if got := d.clip().content; got != key { t.Errorf("shared link %q", got) } // Opening the link finds the repo by its remote and goes there. d.keys("1") r, _ := ref.Parse(key) d.send(OpenLink(r)) d.keys("o") if src, c, s := d.selected(); src != filepath.Base(root) || c != "secrets/app.yaml" || s != "db.password" { t.Errorf("opened %s/%s/%s\n%s", src, c, s, d.screen()) } } func TestReposLoadThemselves(t *testing.T) { // Not parallel: git, run by the repo provider, is slow under load. if _, err := exec.LookPath("git"); err != nil { t.Skip("no git") } root := gitRepoWith(t, "https://git.example.com/team/x.git") // Started elsewhere, so the repo isn't the selected source. d := newDriverOpts(t, Options{}, fake.Azure(0), repo.New(repo.Settings{Dir: t.TempDir(), Paths: []string{root}})) d.openSearch() d.searchAll() d.typeText("db.password") if !strings.Contains(d.screen(), "secrets/app.yaml / db.password") { t.Errorf("repo keys not searchable without loading them:\n%s", d.screen()) } } func TestGroupsStartCollapsed(t *testing.T) { // Not parallel: git, run by the repo provider, is slow under load. if _, err := exec.LookPath("git"); err != nil { t.Skip("no git") } here := gitRepoWith(t, "https://git.example.com/team/here.git") d := newDriverOpts(t, Options{CollapseGroups: true}, fake.Azure(0), fake.Databricks(0), repo.New(repo.Settings{Dir: here})) var rows []string for _, r := range d.m.sources.Items() { switch r.kind { case rowGroup: rows = append(rows, "group "+r.label) case rowSource: rows = append(rows, r.source.Name) } } // Folded groups, and the repo it started in on top, selected, so its // files show. if want := []string{filepath.Base(here), "group Azure", "group Databricks"}; !slices.Equal(rows, want) { t.Errorf("rows %v, want %v", rows, want) } if src, ok := d.m.selectedSource(); !ok || src.ID != here { t.Errorf("selected %v, want the repo it started in", src.ID) } d.keys("j", "enter") // unfold Azure if d.sourceCount() < 2 { t.Errorf("enter on a folded group didn't unfold it:\n%s", d.screen()) } } func TestCurrentRepoBeforeTheRest(t *testing.T) { // Not parallel: git, run by the repo provider, is slow under load. if _, err := exec.LookPath("git"); err != nil { t.Skip("no git") } here := gitRepoWith(t, "https://git.example.com/team/here.git") p := repo.New(repo.Settings{Dir: here}) d := newDriverOpts(t, Options{}, fake.Azure(0), p) // Sources asked again, nothing answered yet but the started-in repo // on its own (provider.Current): it shows on top already. d.m.loadSources() src, ok := p.Current(context.Background()) if !ok { t.Fatal("no current repo") } src.Provider, src.ProviderLabel = p.ID(), p.Label() d.send(currentMsg{gen: d.m.sourcesGen, source: src}) if !strings.Contains(d.screen(), "Repo · "+filepath.Base(here)) { t.Errorf("the started-in repo waits for the others:\n%s", d.screen()) } } func TestSearchAllRepos(t *testing.T) { // Not parallel: git, run by the repo provider, is slow under load. if _, err := exec.LookPath("git"); err != nil { t.Skip("no git") } a := gitRepoWith(t, "https://git.example.com/team/alpha.git") b := gitRepoWith(t, "https://git.example.com/team/beta.git") d := newDriverOpts(t, Options{}, fake.Azure(0), repo.New(repo.Settings{Paths: []string{a, b}})) d.openSearch() d.send(ctrl('h')) for d.m.search.src > 0 { d.send(tea.KeyPressMsg{Code: tea.KeyUp}) } for d.m.search.scope().id != kindScope+string(provider.KindRepo) { if d.m.search.src == len(d.m.search.scopes)-1 { t.Fatalf("no All repos scope: %v", d.m.search.scopes) } d.send(tea.KeyPressMsg{Code: tea.KeyDown}) } azure := func() (n int) { for row := range d.m.cache.containers { if p, _ := splitSourceRow(row); p != "Repos" { n++ } } return n } azureBefore := azure() d.typeText("password") var got []string for _, o := range d.m.search.shown { if o.kind != provider.KindRepo { t.Errorf("not a repo: %s", ansiStrip(o.label)) } got = append(got, ansiStrip(o.label)) } if len(got) != 2 { t.Errorf("want db.password in both repos: %v", got) } if azure() != azureBefore || d.m.crawl.running { t.Errorf("searching all repos loaded Azure: %d → %d lists", azureBefore, azure()) } if !strings.Contains(d.screen(), "Results in all repos") { t.Errorf("title:\n%s", d.screen()) } }