Files
lazysecrets/internal/share/share_test.go
T
kebandClaude Opus 5.5 bc691d786a
CI / Checks (pull_request) Successful in 38s
PR title / Conventional title (pull_request) Successful in 1s
feat: pick the share key in Go to, or by reference
Sharing without share.key opens Go to as a picker of secrets instead
of asking for a link: it lists only secrets, enter picks one, and a
pasted reference picks it too. The key, there and in share.key or
--key, can be a secret's lazysecrets:// link, Key Vault URL or
@Microsoft.KeyVault(...) reference.

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

214 lines
6.9 KiB
Go

package share
import (
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"git.kebcloud.no/keb/lazysecrets/internal/provider"
"git.kebcloud.no/keb/lazysecrets/internal/ref"
)
func init() { workFactor = 10 } // fast; opening accepts any up to maxWorkFactor
var (
key = ref.Ref{Kind: provider.KindAzure, Host: "kv-team.vault.azure.net", Type: provider.TypeSecret, Name: "env-share", Version: "91d3e0"}
pass = []byte("0123456789abcdef0123456789abcdef")
)
func TestSealOpen(t *testing.T) {
files := []File{
{Path: ".env", Data: []byte("DB_PASSWORD=hunter2\n")},
{Path: "apps/api/.env.local", Data: []byte("TOKEN=\"a b\" # quoted\n")},
}
link, err := Seal(Header{Key: key, Repo: "git.example.com/team/app"}, pass, files)
if err != nil {
t.Fatal(err)
}
if strings.Contains(link.String(), "hunter2") || strings.Contains(link.String(), "apps") {
t.Errorf("link shows what it shares: %s", link)
}
// It goes through ref.Parse like any link.
r, err := ref.Parse(link.String())
if err != nil || !r.IsShare() {
t.Fatalf("parse: %v, %+v", err, r)
}
s, err := Read(r)
if err != nil {
t.Fatal(err)
}
if s.Key != key || s.Repo != "git.example.com/team/app" || s.Files != 2 {
t.Errorf("header %+v", s.Header)
}
got, err := s.Open(pass)
if err != nil {
t.Fatal(err)
}
if len(got) != 2 || got[1].Path != files[1].Path || string(got[1].Data) != string(files[1].Data) {
t.Errorf("opened %+v", got)
}
if _, err := s.Open([]byte("another key, long enough to use")); err == nil || !strings.Contains(err.Error(), "doesn't decrypt") {
t.Errorf("wrong key: %v", err)
}
}
func TestSealRefuses(t *testing.T) {
ok := []File{{Path: ".env", Data: []byte("A=1")}}
for name, c := range map[string]struct {
h Header
pass []byte
files []File
want string
}{
"short key": {Header{Key: key}, []byte("short"), ok, "at least 16"},
"vault as key": {Header{Key: ref.Ref{Kind: provider.KindAzure, Host: "kv.vault.azure.net"}}, pass, ok, "isn't a secret"},
"no files": {Header{Key: key}, pass, nil, "no files"},
"not env": {Header{Key: key}, pass, []File{{Path: "main.go", Data: []byte("x")}}, "isn't an env file"},
"escapes": {Header{Key: key}, pass, []File{{Path: "../.env", Data: []byte("x")}}, "inside a repo"},
"binary": {Header{Key: key}, pass, []File{{Path: ".env", Data: []byte{0xff, 0xfe}}}, "isn't text"},
"too big": {Header{Key: key}, pass, []File{{Path: ".env", Data: []byte(random(2 * MaxLink))}}, "more than the 16 KB"},
} {
if _, err := Seal(c.h, c.pass, c.files); err == nil || !strings.Contains(err.Error(), c.want) {
t.Errorf("%s: %v, want %q", name, err, c.want)
}
}
}
// random is text that doesn't compress.
func random(n int) string {
var b strings.Builder
x := uint32(1)
for b.Len() < n {
x ^= x << 13
x ^= x >> 17
x ^= x << 5
b.WriteByte("abcdefghijklmnopqrstuvwxyz0123456789"[x%36])
}
return b.String()
}
func TestReadGarbled(t *testing.T) {
link, err := Seal(Header{Key: key}, pass, []File{{Path: ".env", Data: []byte("A=1")}})
if err != nil {
t.Fatal(err)
}
cut := ref.Ref{Share: link.Share[:len(link.Share)/2]}
s, err := Read(cut)
if err == nil {
_, err = s.Open(pass)
}
if err == nil {
t.Error("a link cut in half opened")
}
if _, err := Read(ref.Ref{Share: "AgAAAAAAAAAAAAAAAAAAAA"}); err == nil || !strings.Contains(err.Error(), "newer lazysecrets") {
t.Errorf("version 2: %v", err)
}
}
func TestCheckPath(t *testing.T) {
for _, p := range []string{".env", "apps/api/.env.local", "prod.env"} {
if err := CheckPath(p); err != nil {
t.Errorf("%s: %v", p, err)
}
}
for _, p := range []string{"", "/etc/.env", "../.env", "a/../.env", ".git/.env", "a/.git/x.env", `a\.env`, ".env.example", "node_modules/x/.env", "Makefile"} {
if err := CheckPath(p); err == nil {
t.Errorf("%q accepted", p)
}
}
}
func TestPlanWrite(t *testing.T) {
dir := t.TempDir()
if out, err := exec.Command("git", "-C", dir, "init", "-q").CombinedOutput(); err != nil {
t.Skip("git:", string(out))
}
os.WriteFile(filepath.Join(dir, ".gitignore"), []byte(".env\n"), 0o644)
os.WriteFile(filepath.Join(dir, ".env"), []byte("OLD=1\n"), 0o644)
targets, err := Plan(dir, []File{
{Path: ".env", Data: []byte("NEW=1\n")},
{Path: "apps/api/.env.local", Data: []byte("B=2\n")},
})
if err != nil {
t.Fatal(err)
}
if !targets[0].Exists || targets[0].Exposed {
t.Errorf(".env: %+v", targets[0])
}
if targets[1].Exists || !targets[1].Exposed {
t.Errorf("apps/api/.env.local: %+v", targets[1])
}
for _, tg := range targets {
if err := tg.Write(); err != nil {
t.Fatal(err)
}
}
if b, _ := os.ReadFile(filepath.Join(dir, ".env")); string(b) != "NEW=1\n" {
t.Errorf(".env is %q", b)
}
info, err := os.Stat(filepath.Join(dir, "apps", "api", ".env.local"))
if err != nil || info.Mode().Perm() != 0o600 {
t.Errorf("new file: %v, %v", info.Mode(), err)
}
}
func TestPlanSymlinks(t *testing.T) {
dir, outside := t.TempDir(), t.TempDir()
if err := os.Symlink(outside, filepath.Join(dir, "apps")); err != nil {
t.Skip(err)
}
if _, err := Plan(dir, []File{{Path: "apps/.env"}}); err == nil || !strings.Contains(err.Error(), "out of the folder") {
t.Errorf("through a linked folder: %v", err)
}
os.Symlink(filepath.Join(outside, "x"), filepath.Join(dir, ".env"))
if _, err := Plan(dir, []File{{Path: ".env"}}); err == nil || !strings.Contains(err.Error(), "symlink") {
t.Errorf("onto a symlink: %v", err)
}
}
func TestParseKey(t *testing.T) {
for _, s := range []string{
"lazysecrets://azure/kv-team.vault.azure.net/secrets/env-share",
"https://kv-team.vault.azure.net/secrets/env-share",
"@Microsoft.KeyVault(VaultName=kv-team;SecretName=env-share)",
"lazysecrets://databricks/adb-1.1.azuredatabricks.net/team/env-share",
} {
if k, err := ParseKey(s); err != nil || k.Name != "env-share" {
t.Errorf("%s: %+v, %v", s, k, err)
}
}
for s, want := range map[string]string{
"{{secrets/team/env-share}}": "names no workspace",
"https://kv-team.vault.azure.net/keys/sops": "isn't a secret",
"https://kv-team.vault.azure.net/": "isn't a secret",
"lazysecrets://azure/subscriptions/0000-1111": "isn't a secret",
} {
if _, err := ParseKey(s); err == nil || !strings.Contains(err.Error(), want) {
t.Errorf("%s: %v, want %q", s, err, want)
}
}
}
func TestDefaultKey(t *testing.T) {
dir := t.TempDir()
general := "lazysecrets://azure/kv-team.vault.azure.net/secrets/env-share"
k, ok, err := DefaultKey(dir, general)
if err != nil || !ok || k.Name != "env-share" {
t.Errorf("general: %+v %v %v", k, ok, err)
}
os.WriteFile(filepath.Join(dir, ".lazysecrets.yaml"), []byte("share:\n key: lazysecrets://azure/kv-app.vault.azure.net/secrets/app-share\n"), 0o644)
if k, _, _ := DefaultKey(dir, general); k.Name != "app-share" {
t.Errorf("the repo's should win: %+v", k)
}
if _, ok, _ := DefaultKey(t.TempDir(), ""); ok {
t.Error("a key with none set")
}
unpinned := key
unpinned.Version = ""
if p := Pin(unpinned, "abc"); p.Version != "abc" {
t.Errorf("pin: %+v", p)
}
}