commit 842971fe5f4ec89e574c1923e9149ed8cb3e4c03
parent e10bf75c17eb69901bc7f7ff439199a5a18dcca0
Author: Sean Enck <sean@ttypty.com>
Date: Fri, 18 Jul 2025 21:56:35 -0400
add a basic 'health' command
Diffstat:
8 files changed, 160 insertions(+), 4 deletions(-)
diff --git a/cmd/lb/main.go b/cmd/lb/main.go
@@ -78,6 +78,8 @@ func run() error {
return fmt.Errorf("%s is not allowed in read-only", command)
}
switch command {
+ case commands.Health:
+ return app.Health(p)
case commands.ReKey:
return app.ReKey(p)
case commands.List, commands.Groups:
diff --git a/cmd/lb/main_test.go b/cmd/lb/main_test.go
@@ -344,6 +344,9 @@ func test(profile string) error {
setConfig(r.config)
r.run("", "ls")
+ r.section("health")
+ r.run("", "health")
+
r.section("env")
r.run("", fmt.Sprintf("vars | sed 's#/%s#/datadir#g' | grep -v CREDENTIALS | sort", profile))
diff --git a/cmd/lb/tests/expected.log b/cmd/lb/tests/expected.log
@@ -321,6 +321,15 @@ test5/multiline/notes
test6/multiline/notes
test6/multiline/otp
test6/multiline/password
+health
+key
+ -> ok
+keyfile
+ -> ok
+clipboard
+ -> ok
+store
+ -> ok
env
LOCKBOX_CLIP_COPY=[touch testdata/datadir/clip.copy]
LOCKBOX_JSON_HASH_LENGTH=3
diff --git a/internal/app/commands/core.go b/internal/app/commands/core.go
@@ -64,8 +64,8 @@ const (
TOTPURL = "url"
// TOTPSeed will display the seed for the TOTP tokens
TOTPSeed = "seed"
- // ClipManagerStop will stop the clipboard manager
- ClipManagerStop = "-kill"
+ // Health will show health information (for debugging/troubleshooting)
+ Health = "health"
)
var (
diff --git a/internal/app/health.go b/internal/app/health.go
@@ -0,0 +1,52 @@
+// Package app can insert
+package app
+
+import (
+ "errors"
+ "fmt"
+ "io"
+
+ "git.sr.ht/~enckse/lockbox/internal/config"
+ "git.sr.ht/~enckse/lockbox/internal/platform"
+)
+
+func report(w io.Writer, cat string, err error) {
+ msg := "ok"
+ if err != nil {
+ msg = fmt.Sprintf("error: %v", err)
+ }
+ text := fmt.Sprintf("%s\n -> %s\n", cat, msg)
+ w.Write([]byte(text))
+}
+
+// Health will display configuration/system health
+func Health(cmd CommandOptions) error {
+ key, err := config.NewKey(config.DefaultKeyMode)
+ w := cmd.Writer()
+ if err == nil {
+ _, err = key.Read()
+ }
+ report(w, "key", err)
+ err = nil
+ file := config.EnvKeyFile.Get()
+ if file != "" {
+ err = errors.New("key file set, does not exist")
+
+ if platform.PathExists(file) {
+ err = nil
+ }
+ }
+ report(w, "keyfile", err)
+ _, err = platform.NewClipboard(platform.DefaultClipboardLoader{})
+ report(w, "clipboard", err)
+ store := config.EnvStore.Get()
+ err = errors.New("store not set")
+ if store != "" {
+ err = errors.New("store does not exist")
+ if platform.PathExists(store) {
+ err = nil
+ }
+ }
+ report(w, "store", err)
+ return nil
+}
diff --git a/internal/app/health_test.go b/internal/app/health_test.go
@@ -0,0 +1,89 @@
+package app_test
+
+import (
+ "strings"
+ "testing"
+
+ "git.sr.ht/~enckse/lockbox/internal/app"
+ "git.sr.ht/~enckse/lockbox/internal/config/store"
+)
+
+func TestHealth(t *testing.T) {
+ m := newMockCommand(t)
+ database, _ := store.GetString("LOCKBOX_STORE")
+ store.Clear()
+ store.SetBool("LOCKBOX_FEATURE_CLIP", false)
+ if err := app.Health(m); err != nil {
+ t.Errorf("invalid error: %v", err)
+ }
+ s := m.buf.String()
+ if strings.Count(s, "ok") != 1 || !strings.Contains(s, "key MUST be set in this key mode") || !strings.Contains(s, "clip feature is disabled") || !strings.Contains(s, "store not set") {
+ t.Errorf("invalid health: %s", s)
+ }
+ m.buf.Reset()
+ store.SetBool("LOCKBOX_FEATURE_CLIP", true)
+ store.SetArray("LOCKBOX_CLIP_COPY", []string{"x"})
+ if err := app.Health(m); err != nil {
+ t.Errorf("invalid error: %v", err)
+ }
+ s = m.buf.String()
+ if strings.Count(s, "ok") != 2 || !strings.Contains(s, "key MUST be set in this key mode") || !strings.Contains(s, "store not set") {
+ t.Errorf("invalid health: %s", s)
+ }
+ m.buf.Reset()
+ store.SetString("LOCKBOX_STORE", "xxxxxx")
+ if err := app.Health(m); err != nil {
+ t.Errorf("invalid error: %v", err)
+ }
+ s = m.buf.String()
+ if strings.Count(s, "ok") != 2 || !strings.Contains(s, "key MUST be set in this key mode") || !strings.Contains(s, "store does not exist") {
+ t.Errorf("invalid health: %s", s)
+ }
+ m.buf.Reset()
+ store.SetString("LOCKBOX_STORE", database)
+ if err := app.Health(m); err != nil {
+ t.Errorf("invalid error: %v", err)
+ }
+ s = m.buf.String()
+ if strings.Count(s, "ok") != 3 || !strings.Contains(s, "key MUST be set in this key mode") {
+ t.Errorf("invalid health: %s", s)
+ }
+ m.buf.Reset()
+ store.SetString("LOCKBOX_CREDENTIALS_KEY_FILE", "xxxxx")
+ if err := app.Health(m); err != nil {
+ t.Errorf("invalid error: %v", err)
+ }
+ s = m.buf.String()
+ if strings.Count(s, "ok") != 2 || !strings.Contains(s, "key MUST be set in this key mode") || !strings.Contains(s, "key file set, does not exist") {
+ t.Errorf("invalid health: %s", s)
+ }
+ m.buf.Reset()
+ store.SetString("LOCKBOX_CREDENTIALS_KEY_FILE", database)
+ if err := app.Health(m); err != nil {
+ t.Errorf("invalid error: %v", err)
+ }
+ s = m.buf.String()
+ if strings.Count(s, "ok") != 3 || !strings.Contains(s, "key MUST be set in this key mode") {
+ t.Errorf("invalid health: %s", s)
+ }
+ m.buf.Reset()
+ store.SetString("LOCKBOX_CREDENTIALS_PASSWORD_MODE", "command")
+ store.SetArray("LOCKBOX_CREDENTIALS_PASSWORD", []string{"zoiajfoaijfoeaijo1j091"})
+ if err := app.Health(m); err != nil {
+ t.Errorf("invalid error: %v", err)
+ }
+ s = m.buf.String()
+ if strings.Count(s, "ok") != 3 || !strings.Contains(s, "key command failed") {
+ t.Errorf("invalid health: %s", s)
+ }
+ m.buf.Reset()
+ store.SetString("LOCKBOX_CREDENTIALS_PASSWORD_MODE", "plaintext")
+ store.SetArray("LOCKBOX_CREDENTIALS_PASSWORD", []string{"pass"})
+ if err := app.Health(m); err != nil {
+ t.Errorf("invalid error: %v", err)
+ }
+ s = m.buf.String()
+ if strings.Count(s, "ok") != 4 {
+ t.Errorf("invalid health: %s", s)
+ }
+}
diff --git a/internal/app/help/core.go b/internal/app/help/core.go
@@ -92,6 +92,7 @@ func Usage(verbose bool, exe string) ([]string, error) {
results = append(results, command(commands.Move, fmt.Sprintf("%s %s", isGroup, isGroup), "move a group from source to destination"))
results = append(results, command(commands.ReKey, "", "rekey/reinitialize the database credentials"))
results = append(results, command(commands.Remove, isGroup, "remove an entry from the store"))
+ results = append(results, command(commands.Health, "", "display configuration health"))
results = append(results, command(commands.JSON, isFilter, "display detailed information"))
results = append(results, command(commands.List, isFilter, "list entries"))
results = append(results, command(commands.Groups, isFilter, "list groups"))
diff --git a/internal/app/help/core_test.go b/internal/app/help/core_test.go
@@ -9,11 +9,11 @@ import (
func TestUsage(t *testing.T) {
u, _ := help.Usage(false, "lb")
- if len(u) != 27 {
+ if len(u) != 28 {
t.Errorf("invalid usage, out of date? %d", len(u))
}
u, _ = help.Usage(true, "lb")
- if len(u) != 128 {
+ if len(u) != 129 {
t.Errorf("invalid verbose usage, out of date? %d", len(u))
}
for _, usage := range u {