Files
lazysecrets/internal/ui/app/links_test.go
T
kebandClaude Opus 5.5 f64ec47d71
Release / Build and publish (push) Successful in 35s
Hello world
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 14:57:17 +02:00

601 lines
20 KiB
Go

package app
import (
"context"
"strings"
"testing"
"time"
tea "charm.land/bubbletea/v2"
"github.com/charmbracelet/x/ansi"
"git.kebcloud.no/keb/lazysecrets/internal/provider"
"git.kebcloud.no/keb/lazysecrets/internal/provider/fake"
"git.kebcloud.no/keb/lazysecrets/internal/ref"
"git.kebcloud.no/keb/lazysecrets/internal/ui/panes"
)
const kv = "kv-app-prod.vault.azure.net"
// open pastes a reference and, for an object, chooses to go to it.
func (d *driver) open(link string) {
d.t.Helper()
d.openWith(link, "o")
}
// openWith pastes a reference and answers the "Open link" menu with k.
func (d *driver) openWith(link, k string) {
d.t.Helper()
d.send(tea.PasteMsg{Content: link})
if d.m.prompt != nil && d.m.prompt.title == "Open link" {
d.keys(k)
}
}
func TestOpenSecretLink(t *testing.T) {
t.Parallel()
d := newDriver(t)
d.keys("1", "j") // start elsewhere: platform-dev
d.open("lazysecrets://azure/" + kv + "/secrets/api-key")
if src, c, s := d.selected(); src != "platform-prod" || c != "kv-app-prod" || s != "api-key" {
t.Fatalf("opened %s/%s/%s", src, c, s)
}
if d.m.focus != paneSecrets || d.m.detail.State() != panes.ValueHidden {
t.Errorf("focus %d, value state %d", d.m.focus, d.m.detail.State())
}
}
func TestOpenAcceptsOtherFormats(t *testing.T) {
t.Parallel()
for _, in := range []string{
"https://" + kv + "/secrets/api-key",
"@Microsoft.KeyVault(SecretUri=https://" + kv + "/secrets/api-key)",
"@Microsoft.KeyVault(VaultName=kv-app-prod;SecretName=api-key)",
} {
d := newDriver(t)
d.open(in)
if _, _, s := d.selected(); s != "api-key" {
t.Errorf("%s: selected %q", in, s)
}
}
}
func TestOpenVaultLinkOnTab(t *testing.T) {
t.Parallel()
d := newDriver(t)
d.keys("1", "j")
d.open("lazysecrets://azure/" + kv + "/certificates")
if _, c, s := d.selected(); c != "kv-app-prod" || d.m.objType != provider.TypeCertificate || s != "app-tls" {
t.Errorf("vault link: %q tab %s selected %q", c, d.m.objType, s)
}
if d.m.focus != paneContainers {
t.Errorf("focus = %d, want the vaults pane", d.m.focus)
}
}
func TestOpenVersionLink(t *testing.T) {
t.Parallel()
d := newDriver(t)
d.open("lazysecrets://azure/" + kv + "/secrets/db-password/91d3e0")
if d.m.versionsOf == nil {
t.Fatal("versions view not opened")
}
if o, _ := d.m.secrets.Selected(); o.Version != "91d3e0" {
t.Errorf("selected version %q", o.Version)
}
}
func TestOpeningAsksWhatToDo(t *testing.T) {
t.Parallel()
d := newDriver(t)
d.send(tea.PasteMsg{Content: "lazysecrets://azure/" + kv + "/secrets/db-password"})
screen := d.screen()
for _, want := range []string{"Open link", "platform-prod / kv-app-prod / db-password", "o Go to it",
"c Go to it and copy its value", "r Go to it and show its value", "x Dismiss"} {
if !strings.Contains(screen, want) {
t.Fatalf("missing %q:\n%s", want, screen)
}
}
if d.clip().content != "" {
t.Fatal("did something before being asked")
}
d.keys("c")
if d.clip().content != "correct-horse-battery-staple" || strings.Contains(d.screen(), "correct-horse") {
t.Errorf("copy: clipboard %q, revealed %v", d.clip().content, strings.Contains(d.screen(), "correct-horse"))
}
d2 := newDriver(t)
d2.keys("1", "j")
d2.openWith("lazysecrets://azure/"+kv+"/secrets/api-key", "r")
if _, _, s := d2.selected(); s != "api-key" || !strings.Contains(d2.screen(), "sk_live_51HxDEMO") {
t.Errorf("show: selected %q\n%s", s, d2.screen())
}
// Old links carrying ?action= still open, and still ask.
d3 := newDriver(t)
d3.send(tea.PasteMsg{Content: "lazysecrets://azure/" + kv + "/secrets/db-password?action=copy"})
if d3.m.prompt == nil || d3.clip().content != "" {
t.Error("an old ?action=copy link acted without asking")
}
}
func TestOpenDatabricksLinks(t *testing.T) {
t.Parallel()
d := newDriver(t)
d.open("lazysecrets://databricks/adb-2222222222222222.2.azuredatabricks.net/app/storage-key")
if src, c, s := d.selected(); src != "dev" || c != "app" || s != "storage-key" {
t.Errorf("databricks link: %s/%s/%s", src, c, s)
}
// A bare {{secrets/…}} reference: the app scope is in both workspaces.
d.open("{{secrets/app/storage-key}}")
screen := d.screen()
if d.m.prompt == nil || !strings.Contains(screen, "1 Open in prod") || !strings.Contains(screen, "2 Open in dev") ||
!strings.Contains(screen, "x Dismiss") {
t.Fatalf("no choice:\n%s", screen)
}
d.keys("1")
if src, _, s := d.selected(); src != "prod" || s != "storage-key" {
t.Errorf("after choosing prod: %s/%s", src, s)
}
}
// unlocatable hides Locate, like an account that can read a vault's
// secrets but not see the vault resource.
type unlocatable struct{ provider.Provider }
func TestOpenUnlocatableVaultUsesOpenedLinks(t *testing.T) {
t.Parallel()
d := newDriver(t, unlocatable{fake.Azure(0)}, fake.Databricks(0))
d.open("lazysecrets://azure/" + kv + "/secrets/api-key")
if !strings.Contains(d.screen(), "↗ Opened links (1)") {
t.Fatalf("no Opened links row:\n%s", d.screen())
}
if _, c, s := d.selected(); c != "kv-app-prod" || s != "api-key" {
t.Errorf("opened %s/%s", c, s)
}
if c, _ := d.m.containers.Selected(); c.Source.Provider == "" {
t.Error("the account that could read it wasn't recorded")
}
}
func TestOpenErrors(t *testing.T) {
t.Parallel()
d := newDriver(t)
d.open("hello")
if !strings.Contains(d.screen(), "open: not a lazysecrets link") {
t.Errorf("bad paste:\n%s", d.screen())
}
d.open("lazysecrets://azure/" + kv + "/secrets/nope")
if !strings.Contains(d.screen(), "open: nope not found in kv-app-prod") {
t.Errorf("missing secret:\n%s", d.screen())
}
d.open("lazysecrets://databricks/adb-9.9.azuredatabricks.net/app")
if !strings.Contains(d.screen(), "no Databricks profile for workspace adb-9.9") {
t.Errorf("unknown workspace:\n%s", d.screen())
}
}
func TestPasteWhileFilteringFilters(t *testing.T) {
t.Parallel()
d := newDriver(t)
d.keys("3", "/")
d.send(tea.PasteMsg{Content: "api"})
if d.m.secrets.Len() != 1 || d.m.nav != nil {
t.Errorf("paste in filter: visible %d", d.m.secrets.Len())
}
}
func TestOpenPromptAndStartupOpen(t *testing.T) {
t.Parallel()
d := newDriver(t)
d.send(tea.KeyPressMsg{Code: 'o', Mod: tea.ModCtrl})
d.send(tea.PasteMsg{Content: "lazysecrets://azure/" + kv + "/secrets/api-key"})
d.keys("enter", "o")
if _, _, s := d.selected(); s != "api-key" {
t.Errorf("ctrl+o: selected %q", s)
}
r, _ := ref.Parse("lazysecrets://azure/" + kv + "/secrets/api-key")
d2 := newDriverOpts(t, Options{Open: &r})
if d2.m.prompt == nil || d2.m.prompt.title != "Open link" {
t.Fatal("open at startup didn't ask what to do")
}
d2.keys("o")
if _, _, s := d2.selected(); s != "api-key" {
t.Errorf("open at startup: selected %q", s)
}
}
func TestShareLinks(t *testing.T) {
t.Parallel()
d := newDriver(t)
d.keys("3", "s") // s: the plain link, straight away
if want := "lazysecrets://azure/" + kv + "/secrets/db-password"; d.clip().content != want {
t.Errorf("s = %q", d.clip().content)
}
if strings.Contains(d.screen(), "copies its value") {
t.Error("share menu still offers action links")
}
for k, want := range map[string]string{
"o": "lazysecrets://azure/" + kv + "/secrets/db-password",
"u": "https://" + kv + "/secrets/db-password",
"y": "@Microsoft.KeyVault(SecretUri=https://" + kv + "/secrets/db-password)",
} {
d.keys("S", k)
if d.clip().content != want {
t.Errorf("S %s = %q, want %q", k, d.clip().content, want)
}
}
d.keys("2", "S", "u") // vault menu: plain vault URL
if want := "https://" + kv + "/"; d.clip().content != want {
t.Errorf("vault URL = %q", d.clip().content)
}
d.keys("]", "S", "t") // link that opens on the Keys tab
if want := "lazysecrets://azure/" + kv + "/keys"; d.clip().content != want {
t.Errorf("tab link = %q", d.clip().content)
}
d.keys("[", "S", "b")
if !strings.HasPrefix(d.clip().content, "https://portal.azure.com/") {
t.Errorf("portal link = %q", d.clip().content)
}
d.keys("2", "s")
if want := "lazysecrets://azure/" + kv; d.clip().content != want {
t.Errorf("vault link = %q", d.clip().content)
}
d.keys("3", "v", "j", "s") // from the versions view: that version
if want := "lazysecrets://azure/" + kv + "/secrets/db-password/91d3e0"; d.clip().content != want {
t.Errorf("version link = %q", d.clip().content)
}
d.keys("esc", "1", "j", "j", "j", "3", "S")
if strings.Contains(d.screen(), "Key Vault URL") {
t.Error("Databricks offered a Key Vault URL")
}
d.keys("o")
if want := "lazysecrets://databricks/adb-1111111111111111.11.azuredatabricks.net/app/storage-key"; d.clip().content != want {
t.Errorf("databricks link = %q", d.clip().content)
}
// Every shared link opens what was shared.
d2 := newDriver(t)
d2.open(d.clip().content)
if src, c, s := d2.selected(); src != "prod" || c != "app" || s != "storage-key" {
t.Errorf("round trip: %s/%s/%s", src, c, s)
}
}
// blind sees vaults (Locate) but may not read them, like an account with
// Reader on the subscription but no Key Vault data-plane role.
type blind struct{ *fake.Provider }
func (blind) Objects(context.Context, provider.Container, provider.ObjectType) ([]provider.Object, error) {
return nil, fake.ErrForbidden
}
func TestOpenAsksWhichAccount(t *testing.T) {
t.Parallel()
// IDs like the real ones: they contain a "/".
alice := blind{fake.Azure(0).As("Azure:alice@example.com/t1", "Azure · alice")}
bob := fake.Azure(0).As("Azure:bob@example.com/t1", "Azure · bob")
d := newDriver(t, alice, bob)
d.open("lazysecrets://azure/" + kv + "/secrets/api-key")
screen := d.screen()
for _, want := range []string{"Several accounts can see kv-app-prod",
"1 Open with Azure · alice / platform-prod", "2 Open with Azure · bob / platform-prod", "x Dismiss"} {
if !strings.Contains(screen, want) {
t.Fatalf("missing %q:\n%s", want, screen)
}
}
d.keys("2")
c, _ := d.m.containers.Selected()
if _, _, s := d.selected(); s != "api-key" || c.Source.Provider != "Azure:bob@example.com/t1" {
t.Errorf("opened %q via %q, want api-key via bob", s, c.Source.Provider)
}
// j/k and enter work too; x dismisses.
d.open("lazysecrets://azure/" + kv + "/secrets/api-key")
d.keys("x")
if d.m.prompt != nil || d.m.nav != nil {
t.Error("x didn't dismiss")
}
d.open("lazysecrets://azure/" + kv + "/secrets/legacy-token")
d.keys("j", "enter")
if c, _ := d.m.containers.Selected(); c.Source.Provider != "Azure:bob@example.com/t1" {
t.Errorf("j enter picked %q", c.Source.Provider)
}
// Opening into a folded account unfolds it.
d.keys("1", "g") // alice's header
d.keys("enter") // fold it
d.open("lazysecrets://azure/" + kv + "/secrets/api-key")
d.keys("1")
if c, _ := d.m.containers.Selected(); c.Source.Provider != "Azure:alice@example.com/t1" {
t.Errorf("folded account not opened: %q", c.Source.Provider)
}
}
// offered reports whether a "Link on clipboard" popup is showing.
func (d *driver) offered() bool {
return d.m.prompt != nil && strings.HasPrefix(d.m.prompt.title, "Link on clipboard")
}
func TestLinkOnClipboardPopsUp(t *testing.T) {
t.Parallel()
link := "lazysecrets://azure/" + kv + "/secrets/api-key"
d := newDriverOpts(t, Options{Clipboard: &fakeClipboard{content: link}})
screen := d.screen()
for _, want := range []string{"Link on clipboard", "platform-prod / kv-app-prod / api-key", "o Go to it", "c Go to it and copy its value", "x Dismiss"} {
if !strings.Contains(screen, want) {
t.Fatalf("popup missing %q:\n%s", want, screen)
}
}
d.keys("o")
if _, _, s := d.selected(); s != "api-key" || d.offered() {
t.Errorf("o opened %q, popup still shown: %v", s, d.offered())
}
d.send(tea.FocusMsg{}) // same link: offered once only
if d.offered() {
t.Error("offered again after opening")
}
}
func TestClipboardLinkOfferedOnce(t *testing.T) {
t.Parallel()
d := newDriver(t)
if d.offered() {
t.Fatal("empty clipboard offered a link")
}
d.clip().content = "lazysecrets://azure/" + kv + "/secrets/api-key" // copied in another app
d.send(tea.FocusMsg{})
if !d.offered() {
t.Fatal("not offered on focus")
}
d.keys("esc")
d.clip().content = "something else"
d.send(tea.FocusMsg{})
d.clip().content = "lazysecrets://azure/" + kv + "/secrets/api-key" // copied again later
d.send(tea.FocusMsg{})
if d.offered() {
t.Error("link offered a second time")
}
d.clip().content = "lazysecrets://azure/" + kv + "/secrets/legacy-token" // a different one
d.send(tea.FocusMsg{})
if !d.offered() {
t.Error("new link not offered")
}
}
func TestClipboardIgnoresOtherText(t *testing.T) {
t.Parallel()
d := newDriverOpts(t, Options{Clipboard: &fakeClipboard{content: "hunter2"}})
if d.offered() || d.m.dismissedLink != "" {
t.Error("non-link clipboard text used")
}
// A Key Vault URL isn't offered: only lazysecrets:// links are.
d.clip().content = "https://" + kv + "/secrets/api-key"
d.send(tea.FocusMsg{})
if d.offered() {
t.Error("Key Vault URL offered")
}
}
func TestLinkToSelectionNotOffered(t *testing.T) {
t.Parallel()
d := newDriver(t)
d.keys("3", "s") // share db-password: it's what's selected
d.send(tea.FocusMsg{})
if d.offered() {
t.Fatal("offered a link to the selected secret")
}
d.keys("1", "j") // elsewhere now: the same link is worth offering
d.send(tea.FocusMsg{})
if !d.offered() || !strings.Contains(d.screen(), "platform-prod / kv-app-prod / db-password") {
t.Errorf("own link not offered after moving away:\n%s", d.screen())
}
}
func TestNoClipboardPopupOverAnotherPopup(t *testing.T) {
t.Parallel()
d := newDriver(t)
d.keys("3", "S") // share menu open
d.clip().content = "lazysecrets://azure/" + kv + "/secrets/api-key"
d.send(tea.FocusMsg{})
if d.offered() || !strings.HasPrefix(d.m.prompt.title, "Share") {
t.Error("clipboard popup replaced the share menu")
}
d.keys("esc")
d.send(tea.FocusMsg{}) // offered once the other popup is gone
if !d.offered() {
t.Error("link not offered after the share menu closed")
}
}
func TestInvalidClipboardLinkIsReported(t *testing.T) {
t.Parallel()
d := newDriverOpts(t, Options{Clipboard: &fakeClipboard{content: "lazysecrets://azure/" + kv + "/secret/api-key"}})
if !strings.Contains(d.screen(), "Link on clipboard can't be read") || !strings.Contains(d.screen(), `unknown object type "secret"`) {
t.Fatalf("typo not reported:\n%s", d.screen())
}
d.keys("x")
if d.m.prompt != nil {
t.Error("x didn't dismiss the report")
}
}
func TestOpenPromptPrefilledFromClipboard(t *testing.T) {
t.Parallel()
link := "lazysecrets://azure/" + kv + "/secrets/api-key"
d := newDriverOpts(t, Options{Clipboard: &fakeClipboard{content: link}})
d.keys("x") // the clipboard popup comes first
d.send(tea.KeyPressMsg{Code: 'o', Mod: tea.ModCtrl})
d.keys("enter", "o")
if _, _, s := d.selected(); s != "api-key" {
t.Errorf("prefilled ctrl+o opened %q", s)
}
}
func TestOpenPromptReadsClipboardFresh(t *testing.T) {
t.Parallel()
d := newDriver(t)
// Copied in another app, no focus event: not offered, but ctrl+o
// still picks it up, and so it does for a plain Key Vault URL.
d.clip().content = "https://" + kv + "/secrets/api-key"
d.send(tea.KeyPressMsg{Code: 'o', Mod: tea.ModCtrl})
d.keys("enter", "o")
if _, _, s := d.selected(); s != "api-key" {
t.Errorf("ctrl+o didn't use the clipboard: %q", s)
}
// Not prefilled with text that isn't a reference.
d.clip().content = "hunter2"
d.send(tea.KeyPressMsg{Code: 'o', Mod: tea.ModCtrl})
if v := d.m.prompt.input.Value(); v != "" {
t.Errorf("prefilled with %q", v)
}
}
func TestClipboardCheckedOnKeysWithoutFocusEvents(t *testing.T) {
t.Parallel()
d := newDriver(t)
clock := time.Now()
d.m.now = func() time.Time { return clock }
d.clip().content = "lazysecrets://azure/" + kv + "/secrets/api-key"
clock = clock.Add(clipCheckEvery + time.Second)
d.keys("j") // any key, once the throttle allows
if !d.offered() {
t.Fatal("not offered after a key press")
}
d.keys("x")
d.clip().content = "lazysecrets://azure/" + kv + "/secrets/legacy-token"
d.keys("k") // within the throttle window: not checked yet
if d.offered() {
t.Error("checked again within the throttle window")
}
clock = clock.Add(clipCheckEvery)
d.keys("j")
if !d.offered() {
t.Error("new link not offered after the window")
}
}
func TestOpenSourceLinks(t *testing.T) {
t.Parallel()
d := newDriver(t)
d.send(tea.PasteMsg{Content: "lazysecrets://azure/subscriptions/sub-dev"})
if src, _, _ := d.selected(); src != "platform-dev" || d.m.focus != paneContainers {
t.Errorf("subscription link: %q, focus %d", src, d.m.focus)
}
d.send(tea.PasteMsg{Content: "lazysecrets://databricks/adb-2222222222222222.2.azuredatabricks.net"})
if src, _, _ := d.selected(); src != "dev" {
t.Errorf("workspace link: %q", src)
}
two := newDriver(t, fake.Azure(0).As("Azure:alice", "Azure · alice"), fake.Azure(0).As("Azure:bob", "Azure · bob"))
two.send(tea.PasteMsg{Content: "lazysecrets://azure/subscriptions/sub-dev"})
if !strings.Contains(two.screen(), "2 Open with Azure · bob / platform-dev") {
t.Fatalf("no account choice:\n%s", two.screen())
}
two.keys("2")
if r, _ := two.m.sources.Selected(); r.provider != "Azure:bob" || r.source.Name != "platform-dev" {
t.Errorf("chose %s/%s", r.provider, r.source.Name)
}
}
func TestHandedOverLinkAndActivity(t *testing.T) {
t.Parallel()
touched := 0
d := newDriverOpts(t, Options{OnActive: func() { touched++ }})
d.keys("j")
d.send(tea.FocusMsg{})
if touched != 2 {
t.Errorf("OnActive called %d times, want 2", touched)
}
r, _ := ref.Parse("lazysecrets://azure/kv-shared-prod.vault.azure.net/secrets/smtp-password")
d.send(OpenLink(r))
if d.m.prompt == nil || !strings.Contains(d.screen(), "Open link") {
t.Fatalf("handed-over link not offered:\n%s", d.screen())
}
d.keys("o")
if _, c, s := d.selected(); c != "kv-shared-prod" || s != "smtp-password" {
t.Errorf("went to %s/%s", c, s)
}
}
func TestRefLabel(t *testing.T) {
t.Parallel()
d := newDriver(t)
for link, want := range map[string]string{
// The subscription from lists already loaded.
"lazysecrets://azure/kv-app-prod.vault.azure.net/keys/signing/abc123": "platform-prod / kv-app-prod / signing · key · version abc123",
"lazysecrets://azure/kv-app-prod.vault.azure.net/certificates": "platform-prod / kv-app-prod · certificates tab",
// Not loaded: the vault alone.
"lazysecrets://azure/kv-elsewhere.vault.azure.net/secrets/x": "kv-elsewhere / x",
} {
r, err := ref.Parse(link)
if err != nil {
t.Fatal(err)
}
if got := ansi.Strip(d.m.refLabel(r)); !strings.HasSuffix(got, want) {
t.Errorf("%s: %q, want …%q", link, got, want)
}
}
}
func TestCopyFromLinkStaysPut(t *testing.T) {
t.Parallel()
alice := &counting{Provider: fake.Azure(0).As("Azure:alice", "Azure · alice")}
bob := &counting{Provider: fake.Azure(0).As("Azure:bob", "Azure · bob")}
d := newDriverOpts(t, Options{}, denyingValues{alice}, bob, fake.Databricks(0))
d.keys("j") // platform-dev
before := d.m.sources.SelectedID()
r, _ := ref.Parse("lazysecrets://azure/kv-app-prod.vault.azure.net/secrets/db-password")
d.send(OpenLink(r))
d.keys("y")
if got := d.clip().content; got != "correct-horse-battery-staple" {
t.Fatalf("clipboard = %q (alice denies, bob reads)\n%s", got, d.screen())
}
if d.m.sources.SelectedID() != before || d.m.nav != nil || strings.Contains(d.screen(), "correct-horse") {
t.Errorf("moved or revealed:\n%s", d.screen())
}
// A version, and a Databricks secret.
r, _ = ref.Parse("lazysecrets://azure/kv-app-prod.vault.azure.net/secrets/db-password/91d3e0")
d.send(OpenLink(r))
d.keys("y")
if got := d.clip().content; got != "previous-password" {
t.Errorf("version: clipboard = %q", got)
}
r, _ = ref.Parse("lazysecrets://databricks/adb-1111111111111111.11.azuredatabricks.net/app/storage-key")
d.send(OpenLink(r))
d.keys("y")
if got := d.clip().content; got == "" || got == "previous-password" {
t.Errorf("databricks: clipboard = %q\n%s", got, d.screen())
}
}
// denyingValues refuses to read values, like an account without access.
type denyingValues struct{ *counting }
func (d denyingValues) Value(context.Context, provider.Object) (provider.SecretValue, error) {
return provider.SecretValue{}, fake.ErrForbidden
}
func TestShareWebPages(t *testing.T) {
t.Parallel()
d := newDriver(t)
d.keys("S") // a subscription
if !strings.Contains(d.screen(), "o Lazysecrets link") || !strings.Contains(d.screen(), "b Web page (Azure portal)") {
t.Fatalf("subscription share menu:\n%s", d.screen())
}
d.keys("o")
if got := d.clip().content; got != "lazysecrets://azure/subscriptions/sub-prod" {
t.Errorf("subscription link %q", got)
}
d.keys("3", "S", "b") // a secret's web page
if got := d.clip().content; !strings.HasPrefix(got, "https://portal.azure.com/") {
t.Errorf("secret web page %q", got)
}
}