Files
lazysecrets/internal/ui/app/sharefiles_test.go
T
kebandClaude Opus 5.5 166790d825
CI / Checks (pull_request) Successful in 41s
PR title / Conventional title (pull_request) Successful in 2s
fix: a demo repo's SOPS value can be the share key
The demo provider finds repos by their remote, as the repo provider
does, so a key in a demo repo's SOPS file can be read. Opening a
share link uses the repo's clone only when it's on disk (the demo's
aren't), else asks where to save. The demo GitHub client secret is 40
characters, like real ones, so it passes share.keyRules.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 13:17:04 +02:00

230 lines
7.8 KiB
Go

package app
import (
"net/url"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"time"
tea "charm.land/bubbletea/v2"
"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/share"
)
// scryptWait is how long the driver waits for encrypting and decrypting,
// which take about a second on purpose.
const scryptWait = 20 * time.Second
// demoRules take the demo key, made up to be remembered.
var demoRules = share.Rules{MinLength: 16, MinClasses: 1}
// cloneWith makes a clone of git.example.com/team/app holding files.
func cloneWith(t *testing.T, files map[string]string) string {
t.Helper()
root := gitRepoWith(t, "https://git.example.com/team/app.git")
os.WriteFile(filepath.Join(root, ".gitignore"), []byte(".env\n"), 0o644)
for name, data := range files {
os.MkdirAll(filepath.Dir(filepath.Join(root, name)), 0o755)
os.WriteFile(filepath.Join(root, name), []byte(data), 0o600)
}
return root
}
// shareFrom shares a repo's env files from lazysecrets started in it,
// returning the link copied.
func shareFrom(t *testing.T, root string, paste bool) string {
t.Helper()
clip := &fakeClipboard{}
d := newDriverOpts(t, Options{Clipboard: clip, ShareRules: demoRules}, fake.Azure(0), repo.New(repo.Settings{Dir: root}))
d.wait = scryptWait
if src, ok := d.m.selectedSource(); !ok || src.ID != root {
t.Fatalf("selected %v, want the repo", src.ID)
}
d.keys("S", "f")
// No share.key set: it asks which secret, in Go to, which lists
// only secrets.
v := d.m.search
if v == nil || v.pick == nil {
t.Fatalf("no secret picker: %s", d.m.flash.text)
}
for _, o := range v.shown {
if o.plain || strings.Contains(o.label, ".env") {
t.Errorf("a plain env value offered as the key: %s", o.label)
}
}
if !strings.Contains(d.screen(), "as an encrypted link") {
t.Errorf("doesn't say what picking does:\n%s", d.screen())
}
if paste {
// A reference pasted is picked as it is.
d.send(tea.PasteMsg{Content: "https://" + kv + "/secrets/db-password"})
} else {
for _, r := range "app-prod db-password" {
d.keys(string(r))
}
if len(v.shown) == 0 {
t.Fatalf("no secrets to pick:\n%s", d.screen())
}
for _, o := range v.shown {
if _, ok := pickable(o); !ok {
t.Errorf("not a secret: %s", o.label)
}
}
if !strings.Contains(d.screen(), "Share 2 env files") && !strings.Contains(d.screen(), "Share .env") {
t.Errorf("not titled as a picker:\n%s", d.screen())
}
d.keys("enter")
}
// Picking it shares: the picker said what.
if !strings.HasPrefix(clip.content, "lazysecrets://share/") {
t.Fatalf("copied %q (%s)", clip.content, d.m.flash.text)
}
return clip.content
}
func TestShareFilesRoundTrip(t *testing.T) {
if _, err := exec.LookPath("git"); err != nil {
t.Skip("no git")
}
link := shareFrom(t, cloneWith(t, map[string]string{
".env": "DB_PASSWORD=hunter2\n",
"prod.env": "TOKEN=abc\n",
}), false)
// A colleague's clone: .env is there already, prod.env isn't.
theirs := cloneWith(t, map[string]string{".env": "DB_PASSWORD=old\n"})
d := newDriverOpts(t, Options{}, fake.Azure(0), repo.New(repo.Settings{Dir: theirs}))
d.wait = scryptWait
d.send(tea.PasteMsg{Content: link})
if d.m.prompt == nil || d.m.prompt.title != "Shared env files" || !strings.Contains(d.screen(), "2 env files from git.example.com/team/app") {
t.Fatalf("not offered:\n%s", d.screen())
}
d.keys("o")
if d.m.prompt == nil || d.m.prompt.title != "Save the shared files" {
t.Fatalf("no save menu (%s)", d.m.flash.text)
}
if s := d.screen(); !strings.Contains(s, "replaces the one there") || !strings.Contains(s, "not ignored by git") {
t.Errorf("save menu doesn't say what happens:\n%s", s)
}
d.keys("n") // only the new one
if b, _ := os.ReadFile(filepath.Join(theirs, ".env")); string(b) != "DB_PASSWORD=old\n" {
t.Errorf(".env replaced: %q", b)
}
if b, _ := os.ReadFile(filepath.Join(theirs, "prod.env")); string(b) != "TOKEN=abc\n" {
t.Errorf("prod.env: %q", b)
}
// Again, replacing.
d.send(tea.PasteMsg{Content: link})
d.keys("o", "w")
if b, _ := os.ReadFile(filepath.Join(theirs, ".env")); string(b) != "DB_PASSWORD=hunter2\n" {
t.Errorf(".env not replaced: %q", b)
}
}
func TestShareWithShareKeyConfirms(t *testing.T) {
if _, err := exec.LookPath("git"); err != nil {
t.Skip("no git")
}
root := cloneWith(t, map[string]string{
".env": "A=1\n",
".lazysecrets.yaml": "share:\n key: https://" + kv + "/secrets/db-password\n",
})
clip := &fakeClipboard{}
d := newDriverOpts(t, Options{Clipboard: clip, ShareRules: demoRules}, fake.Azure(0), repo.New(repo.Settings{Dir: root}))
d.wait = scryptWait
d.keys("S", "f")
// The key wasn't picked: it shows which, before sharing.
if d.m.search != nil || d.m.prompt == nil || d.m.prompt.title != "Share .env" || !strings.Contains(d.screen(), "db-password") {
t.Fatalf("no confirmation:\n%s", d.screen())
}
d.keys("c")
if !strings.HasPrefix(clip.content, "lazysecrets://share/") {
t.Errorf("copied %q (%s)", clip.content, d.m.flash.text)
}
}
func TestShareLinkElsewhereAsksWhere(t *testing.T) {
if _, err := exec.LookPath("git"); err != nil {
t.Skip("no git")
}
link := shareFrom(t, cloneWith(t, map[string]string{".env": "A=1\n"}), true)
dir, _ := filepath.EvalSymlinks(t.TempDir())
d := newDriverOpts(t, Options{Dir: dir}, fake.Azure(0))
d.wait = scryptWait
d.send(tea.PasteMsg{Content: link})
d.keys("o")
if d.m.prompt == nil || d.m.prompt.input == nil || d.m.prompt.input.Value() != dir {
t.Fatalf("doesn't ask where, starting from where it runs:\n%s", d.screen())
}
d.keys("enter", "w")
if b, _ := os.ReadFile(filepath.Join(dir, ".env")); string(b) != "A=1\n" {
t.Errorf(".env: %q (%s)", b, d.m.flash.text)
}
}
func TestShareLinkGarbled(t *testing.T) {
t.Parallel()
d := newDriver(t)
d.send(tea.PasteMsg{Content: "lazysecrets://share/AQ" + strings.Repeat("A", 30)})
if d.m.prompt == nil || !strings.Contains(d.screen(), "garbled") {
t.Errorf("not reported:\n%s", d.screen())
}
}
// TestShareInDemo shares a demo repo's env files with the demo key and
// opens the link, as lazysecrets --demo does: the demo repos aren't on
// disk, so it asks where to save.
func TestShareInDemo(t *testing.T) {
clip := &fakeClipboard{}
d := newDriverOpts(t, Options{Clipboard: clip},
fake.Azure(0), fake.AzureOps(0).As("Azure:admin", "Azure · admin"), fake.Repos())
d.wait = scryptWait
var src provider.Source
for _, s := range d.m.sourceResults["Repos"].sources {
if s.Here {
src = s
}
}
if src.ID == "" {
t.Fatal("no demo repo")
}
src.Provider = "Repos"
d.run(d.m.shareFiles(src, nil))
if d.m.search == nil || d.m.search.pick == nil {
t.Fatalf("no picker: %s", d.m.flash.text)
}
d.send(tea.PasteMsg{Content: "https://kv-shared-prod.vault.azure.net/secrets/env-share"})
if !strings.HasPrefix(clip.content, "lazysecrets://share/") {
t.Fatalf("copied %q (%s)", clip.content, d.m.flash.text)
}
link := clip.content
// A value in a demo repo's SOPS file can be the key too, found by
// the repo's remote.
clip.content = ""
d.run(d.m.shareFiles(src, nil))
d.send(tea.PasteMsg{Content: "lazysecrets://repo/" + url.PathEscape(src.Detail) + "/config%2Foauth.enc.json/github.client_secret"})
if !strings.HasPrefix(clip.content, "lazysecrets://share/") {
t.Errorf("with the demo repo's key: copied %q (%s)", clip.content, d.m.flash.text)
}
dir, _ := filepath.EvalSymlinks(t.TempDir())
d.m.placeDir = dir
d.send(tea.PasteMsg{Content: link})
d.keys("o")
if d.m.prompt == nil || d.m.prompt.input == nil || !strings.Contains(d.screen(), "isn't among your repos") {
t.Fatalf("doesn't ask where:\n%s", d.screen())
}
d.keys("enter", "w")
entries, _ := os.ReadDir(dir)
if len(entries) == 0 {
t.Fatalf("nothing saved (%s)", d.m.flash.text)
}
}