Sharing refuses a key shorter than minLength characters (default 32, never under 16) or of fewer than minClasses kinds of lowercase, uppercase, digits and symbols (default 2, so random hex passes), saying so without showing it. Opening doesn't check. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
159 lines
5.2 KiB
Go
159 lines
5.2 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"
|
|
"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(), "Encrypt with which secret?") {
|
|
t.Errorf("not titled as a picker:\n%s", d.screen())
|
|
}
|
|
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",
|
|
}), 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 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())
|
|
}
|
|
}
|