S → f on a repo, an env file or a key in one copies a lazysecrets://share link holding the env files, encrypted (age) with a secret's value: whoever can read that secret can open it. The key is share.key in the repo's .lazysecrets.yaml, else in the config, else picked when sharing; on Azure the link pins its version. Opening one says what it holds, decrypts when asked, and saves the files in the clone of the repo (or a folder chosen), replacing existing ones only when asked. lazysecrets share prints a link from the command line. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
130 lines
4.4 KiB
Go
130 lines
4.4 KiB
Go
package app
|
|
|
|
import (
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
tea "charm.land/bubbletea/v2"
|
|
|
|
"git.kebcloud.no/keb/lazysecrets/internal/provider/fake"
|
|
"git.kebcloud.no/keb/lazysecrets/internal/provider/repo"
|
|
)
|
|
|
|
// scryptWait is how long the driver waits for encrypting and decrypting,
|
|
// which take about a second on purpose.
|
|
const scryptWait = 20 * time.Second
|
|
|
|
// shareKeyLink is a demo secret, long enough to be a key.
|
|
const shareKeyLink = "lazysecrets://azure/" + kv + "/secrets/db-password"
|
|
|
|
// 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) string {
|
|
t.Helper()
|
|
clip := &fakeClipboard{}
|
|
d := newDriverOpts(t, Options{Clipboard: clip}, 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.
|
|
if d.m.prompt == nil || d.m.prompt.input == nil {
|
|
t.Fatalf("no key prompt: %s", d.m.flash.text)
|
|
}
|
|
d.send(tea.PasteMsg{Content: shareKeyLink})
|
|
d.keys("enter")
|
|
if !strings.Contains(d.screen(), ".env") || !strings.Contains(d.screen(), "db-password") {
|
|
t.Errorf("the files and key aren't shown before sharing:\n%s", d.screen())
|
|
}
|
|
d.keys("c")
|
|
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",
|
|
}))
|
|
|
|
// 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 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"}))
|
|
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())
|
|
}
|
|
}
|