Release / Build and publish (push) Successful in 22s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
241 lines
8.2 KiB
Go
241 lines
8.2 KiB
Go
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())
|
|
}
|
|
}
|