commit d63ff393612e66398ce8442281aeb2e8e069ef12
parent cb0c04f912d3403a769151c787ea3afde5316d5a
Author: Sean Enck <sean@ttypty.com>
Date: Fri, 3 Mar 2023 18:45:30 -0500
restructure app/commands
Diffstat:
33 files changed, 716 insertions(+), 716 deletions(-)
diff --git a/cmd/main.go b/cmd/main.go
@@ -1,4 +1,4 @@
-// provides the binary runs or calls lockbox commands.
+// provides the binary runs or calls lockbox app.
package main
import (
@@ -10,9 +10,9 @@ import (
"strings"
"time"
+ "github.com/enckse/lockbox/internal/app"
"github.com/enckse/lockbox/internal/backend"
"github.com/enckse/lockbox/internal/cli"
- "github.com/enckse/lockbox/internal/commands"
"github.com/enckse/lockbox/internal/inputs"
"github.com/enckse/lockbox/internal/platform"
"github.com/enckse/lockbox/internal/totp"
@@ -29,7 +29,7 @@ func main() {
}
func handleEarly(command string, args []string) (bool, error) {
- ok, err := commands.Info(os.Stdout, command, args)
+ ok, err := app.Info(os.Stdout, command, args)
if err != nil {
return false, err
}
@@ -43,7 +43,7 @@ func handleEarly(command string, args []string) (bool, error) {
case cli.TOTPCommand:
return true, totp.Call(args)
case cli.HashCommand:
- return true, commands.Hash(os.Stdout, args)
+ return true, app.Hash(os.Stdout, args)
case cli.ClearCommand:
return true, clearClipboard(args)
}
@@ -74,17 +74,17 @@ func run() error {
return t.ReKey()
}
case cli.ListCommand, cli.FindCommand:
- return commands.ListFind(t, os.Stdout, command == cli.FindCommand, sub)
+ return app.ListFind(t, os.Stdout, command == cli.FindCommand, sub)
case cli.MoveCommand:
- return commands.Move(t, sub, confirm)
+ return app.Move(t, sub, confirm)
case cli.InsertCommand:
- return commands.Insert(os.Stdout, t, sub, confirm)
+ return app.Insert(os.Stdout, t, sub, confirm)
case cli.RemoveCommand:
- return commands.Remove(os.Stdout, t, sub, confirm)
+ return app.Remove(os.Stdout, t, sub, confirm)
case cli.StatsCommand:
- return commands.Stats(os.Stdout, t, sub)
+ return app.Stats(os.Stdout, t, sub)
case cli.ShowCommand, cli.ClipCommand:
- return commands.ShowClip(os.Stdout, t, command == cli.ShowCommand, sub)
+ return app.ShowClip(os.Stdout, t, command == cli.ShowCommand, sub)
default:
return fmt.Errorf("unknown command: %s", command)
}
diff --git a/internal/app/core.go b/internal/app/core.go
@@ -0,0 +1,7 @@
+// Package app common objects
+package app
+
+type (
+ // Confirm user inputs
+ Confirm func(string) bool
+)
diff --git a/internal/app/hash.go b/internal/app/hash.go
@@ -0,0 +1,31 @@
+package app
+
+import (
+ "errors"
+ "fmt"
+ "io"
+ "strings"
+
+ "github.com/enckse/lockbox/internal/backend"
+)
+
+// Hash will hash 1-N files
+func Hash(w io.Writer, args []string) error {
+ if len(args) == 0 {
+ return errors.New("hash requires a file")
+ }
+ for _, a := range args {
+ t, err := backend.Load(a)
+ if err != nil {
+ return err
+ }
+ e, err := t.QueryCallback(backend.QueryOptions{Mode: backend.ListMode, Values: backend.HashedValue})
+ if err != nil {
+ return err
+ }
+ for _, item := range e {
+ fmt.Fprintf(w, "%s:\n %s\n\n", item.Path, strings.ReplaceAll(item.Value, "\n", "\n "))
+ }
+ }
+ return nil
+}
diff --git a/internal/app/hash_test.go b/internal/app/hash_test.go
@@ -0,0 +1,25 @@
+package app_test
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/enckse/lockbox/internal/app"
+ "github.com/enckse/lockbox/internal/backend"
+)
+
+func TestHash(t *testing.T) {
+ var buf bytes.Buffer
+ if err := app.Hash(&buf, []string{}); err.Error() != "hash requires a file" {
+ t.Errorf("invalid error: %v", err)
+ }
+ setup(t)
+ fullSetup(t, true).Insert(backend.NewPath("test", "test2", "test1"), "pass")
+ fullSetup(t, true).Insert(backend.NewPath("test", "test2", "test3"), "pass")
+ if err := app.Hash(&buf, []string{"test.kdbx"}); err != nil {
+ t.Errorf("invalid error: %v", err)
+ }
+ if buf.String() == "" {
+ t.Error("nothing hashed")
+ }
+}
diff --git a/internal/app/info.go b/internal/app/info.go
@@ -0,0 +1,83 @@
+// Package app handles informational requests
+package app
+
+import (
+ "errors"
+ "fmt"
+ "io"
+ "strings"
+
+ "github.com/enckse/lockbox/internal/cli"
+ "github.com/enckse/lockbox/internal/inputs"
+)
+
+// Info will report help/bash/env details
+func Info(w io.Writer, command string, args []string) (bool, error) {
+ i, err := info(command, args)
+ if err != nil {
+ return false, err
+ }
+ if len(i) > 0 {
+ fmt.Fprintf(w, "%s\n", strings.Join(i, "\n"))
+ return true, nil
+ }
+ return false, nil
+}
+
+func info(command string, args []string) ([]string, error) {
+ switch command {
+ case cli.HelpCommand:
+ if len(args) > 1 {
+ return nil, errors.New("invalid help command")
+ }
+ isAdvanced := false
+ if len(args) == 1 {
+ if args[0] == cli.HelpAdvancedCommand {
+ isAdvanced = true
+ } else {
+ return nil, errors.New("invalid help option")
+ }
+ }
+ results, err := cli.Usage(isAdvanced)
+ if err != nil {
+ return nil, err
+ }
+ return results, nil
+ case cli.EnvCommand, cli.BashCommand:
+ defaultFlag := cli.BashDefaultsCommand
+ isEnv := command == cli.EnvCommand
+ if isEnv {
+ defaultFlag = cli.EnvDefaultsCommand
+ }
+ defaults, err := getInfoDefault(args, defaultFlag)
+ if err != nil {
+ return nil, err
+ }
+ if isEnv {
+ return inputs.ListEnvironmentVariables(!defaults), nil
+ }
+ return cli.BashCompletions(defaults)
+ }
+ return nil, nil
+}
+
+func getInfoDefault(args []string, possibleArg string) (bool, error) {
+ defaults := false
+ invalid := false
+ switch len(args) {
+ case 0:
+ break
+ case 1:
+ if args[0] == possibleArg {
+ defaults = true
+ } else {
+ invalid = true
+ }
+ default:
+ invalid = true
+ }
+ if invalid {
+ return false, errors.New("invalid argument")
+ }
+ return defaults, nil
+}
diff --git a/internal/app/info_test.go b/internal/app/info_test.go
@@ -0,0 +1,96 @@
+package app_test
+
+import (
+ "bytes"
+ "os"
+ "testing"
+
+ "github.com/enckse/lockbox/internal/app"
+)
+
+func TestNoInfo(t *testing.T) {
+ var buf bytes.Buffer
+ ok, err := app.Info(&buf, "", []string{})
+ if ok || err != nil {
+ t.Errorf("invalid error: %v", err)
+ }
+}
+
+func TestHelpInfo(t *testing.T) {
+ os.Clearenv()
+ var buf bytes.Buffer
+ ok, err := app.Info(&buf, "help", []string{})
+ if !ok || err != nil {
+ t.Errorf("invalid error: %v", err)
+ }
+ if buf.String() == "" {
+ t.Error("nothing written")
+ }
+ old := buf.String()
+ buf = bytes.Buffer{}
+ ok, err = app.Info(&buf, "help", []string{"-verbose"})
+ if !ok || err != nil {
+ t.Errorf("invalid error: %v", err)
+ }
+ if buf.String() == "" || old == buf.String() {
+ t.Error("nothing written")
+ }
+ if _, err = app.Info(&buf, "help", []string{"-verb"}); err.Error() != "invalid help option" {
+ t.Errorf("invalid error: %v", err)
+ }
+ if _, err = app.Info(&buf, "help", []string{"-verbose", "A"}); err.Error() != "invalid help command" {
+ t.Errorf("invalid error: %v", err)
+ }
+}
+
+func TestBashInfo(t *testing.T) {
+ os.Clearenv()
+ var buf bytes.Buffer
+ ok, err := app.Info(&buf, "bash", []string{})
+ if !ok || err != nil {
+ t.Errorf("invalid error: %v", err)
+ }
+ if buf.String() == "" {
+ t.Error("nothing written")
+ }
+ buf = bytes.Buffer{}
+ ok, err = app.Info(&buf, "bash", []string{"-defaults"})
+ if !ok || err != nil {
+ t.Errorf("invalid error: %v", err)
+ }
+ if buf.String() == "" {
+ t.Error("nothing written")
+ }
+ if _, err = app.Info(&buf, "bash", []string{"-default"}); err.Error() != "invalid argument" {
+ t.Errorf("invalid error: %v", err)
+ }
+ if _, err = app.Info(&buf, "bash", []string{"test", "-default"}); err.Error() != "invalid argument" {
+ t.Errorf("invalid error: %v", err)
+ }
+}
+
+func TestEnvInfo(t *testing.T) {
+ os.Clearenv()
+ var buf bytes.Buffer
+ ok, err := app.Info(&buf, "env", []string{})
+ if !ok || err != nil {
+ t.Errorf("invalid error: %v", err)
+ }
+ if buf.String() == "" {
+ t.Error("nothing written")
+ }
+ buf = bytes.Buffer{}
+ ok, err = app.Info(&buf, "env", []string{"-defaults"})
+ if !ok || err != nil {
+ t.Errorf("invalid error: %v", err)
+ }
+ if buf.String() == "" {
+ t.Error("nothing written")
+ }
+ if _, err = app.Info(&buf, "env", []string{"-default"}); err.Error() != "invalid argument" {
+ t.Errorf("invalid error: %v", err)
+ }
+ if _, err = app.Info(&buf, "env", []string{"test", "-default"}); err.Error() != "invalid argument" {
+ t.Errorf("invalid error: %v", err)
+ }
+}
diff --git a/internal/app/insert.go b/internal/app/insert.go
@@ -0,0 +1,83 @@
+// Package app can insert
+package app
+
+import (
+ "errors"
+ "fmt"
+ "io"
+ "strings"
+
+ "github.com/enckse/lockbox/internal/backend"
+ "github.com/enckse/lockbox/internal/cli"
+ "github.com/enckse/lockbox/internal/inputs"
+ "github.com/enckse/lockbox/internal/totp"
+)
+
+func insertError(message string, err error) error {
+ return fmt.Errorf("%s (%w)", message, err)
+}
+
+// Insert will insert new entries
+// NOTE: almost entirely tested via regresssion due to complexities around piping/inputs
+func Insert(w io.Writer, t *backend.Transaction, args []string, confirm Confirm) error {
+ multi := false
+ isTOTP := false
+ idx := 0
+ switch len(args) {
+ case 0:
+ return errors.New("insert requires an entry")
+ case 1:
+ case 2:
+ opt := args[0]
+ switch opt {
+ case cli.InsertMultiCommand:
+ multi = true
+ case cli.InsertTOTPCommand:
+ off, err := inputs.IsNoTOTP()
+ if err != nil {
+ return err
+ }
+ if off {
+ return totp.ErrNoTOTP
+ }
+ isTOTP = true
+ default:
+ return errors.New("unknown argument")
+ }
+ multi = true
+ idx = 1
+ default:
+ return errors.New("too many arguments")
+ }
+ isPipe := inputs.IsInputFromPipe()
+ entry := args[idx]
+ if isTOTP {
+ totpToken := inputs.TOTPToken()
+ if !strings.HasSuffix(entry, backend.NewSuffix(totpToken)) {
+ entry = backend.NewPath(entry, totpToken)
+ }
+ }
+ existing, err := t.Get(entry, backend.BlankValue)
+ if err != nil {
+ return insertError("unable to check for existing entry", err)
+ }
+ if existing != nil {
+ if !isPipe {
+ if !confirm("overwrite existing") {
+ return nil
+ }
+ }
+ }
+ password, err := inputs.GetUserInputPassword(isPipe, multi)
+ if err != nil {
+ return insertError("invalid input", err)
+ }
+ p := strings.TrimSpace(string(password))
+ if err := t.Insert(entry, p); err != nil {
+ return insertError("failed to insert", err)
+ }
+ if !isPipe {
+ fmt.Println()
+ }
+ return nil
+}
diff --git a/internal/app/listfind.go b/internal/app/listfind.go
@@ -0,0 +1,34 @@
+package app
+
+import (
+ "errors"
+ "fmt"
+ "io"
+
+ "github.com/enckse/lockbox/internal/backend"
+)
+
+// ListFind will list/find entries
+func ListFind(t *backend.Transaction, w io.Writer, isFind bool, args []string) error {
+ opts := backend.QueryOptions{}
+ opts.Mode = backend.ListMode
+ if isFind {
+ opts.Mode = backend.FindMode
+ if len(args) < 1 {
+ return errors.New("find requires search term")
+ }
+ opts.Criteria = args[0]
+ } else {
+ if len(args) != 0 {
+ return errors.New("list does not support any arguments")
+ }
+ }
+ e, err := t.QueryCallback(opts)
+ if err != nil {
+ return err
+ }
+ for _, f := range e {
+ fmt.Fprintf(w, "%s\n", f.Path)
+ }
+ return nil
+}
diff --git a/internal/app/listfind_test.go b/internal/app/listfind_test.go
@@ -0,0 +1,68 @@
+package app_test
+
+import (
+ "bytes"
+ "os"
+ "strings"
+ "testing"
+
+ "github.com/enckse/lockbox/internal/app"
+ "github.com/enckse/lockbox/internal/backend"
+)
+
+func fullSetup(t *testing.T, keep bool) *backend.Transaction {
+ if !keep {
+ os.Remove("test.kdbx")
+ }
+ os.Setenv("LOCKBOX_READONLY", "no")
+ os.Setenv("LOCKBOX_STORE", "test.kdbx")
+ os.Setenv("LOCKBOX_KEY", "test")
+ os.Setenv("LOCKBOX_KEYFILE", "")
+ os.Setenv("LOCKBOX_KEYMODE", "plaintext")
+ os.Setenv("LOCKBOX_TOTP", "totp")
+ os.Setenv("LOCKBOX_HOOKDIR", "")
+ os.Setenv("LOCKBOX_SET_MODTIME", "")
+ tr, err := backend.NewTransaction()
+ if err != nil {
+ t.Errorf("failed: %v", err)
+ }
+ return tr
+}
+
+func setup(t *testing.T) *backend.Transaction {
+ return fullSetup(t, false)
+}
+
+func TestList(t *testing.T) {
+ setup(t)
+ fullSetup(t, true).Insert(backend.NewPath("test", "test2", "test1"), "pass")
+ fullSetup(t, true).Insert(backend.NewPath("test", "test2", "test3"), "pass")
+ tx := fullSetup(t, true)
+ var buf bytes.Buffer
+ if err := app.ListFind(tx, &buf, false, []string{}); err != nil {
+ t.Errorf("invalid error: %v", err)
+ }
+ if buf.String() == "" {
+ t.Error("nothing listed")
+ }
+ if err := app.ListFind(tx, &buf, false, []string{"test"}); err.Error() != "list does not support any arguments" {
+ t.Errorf("invalid error: %v", err)
+ }
+}
+
+func TestFind(t *testing.T) {
+ setup(t)
+ fullSetup(t, true).Insert(backend.NewPath("test", "test2", "test1"), "pass")
+ fullSetup(t, true).Insert(backend.NewPath("test", "test2", "test3"), "pass")
+ tx := fullSetup(t, true)
+ var buf bytes.Buffer
+ if err := app.ListFind(tx, &buf, true, []string{}); err.Error() != "find requires search term" {
+ t.Errorf("invalid error: %v", err)
+ }
+ if err := app.ListFind(tx, &buf, true, []string{"test1"}); err != nil {
+ t.Errorf("invalid error: %v", err)
+ }
+ if buf.String() == "" || strings.Contains(buf.String(), "test3") {
+ t.Error("wrong find")
+ }
+}
diff --git a/internal/app/move.go b/internal/app/move.go
@@ -0,0 +1,33 @@
+package app
+
+import (
+ "errors"
+
+ "github.com/enckse/lockbox/internal/backend"
+)
+
+// Move is the CLI command to move entries
+func Move(t *backend.Transaction, args []string, confirm Confirm) error {
+ if len(args) != 2 {
+ return errors.New("src/dst required for move")
+ }
+ src := args[0]
+ dst := args[1]
+ srcExists, err := t.Get(src, backend.SecretValue)
+ if err != nil {
+ return errors.New("unable to get source entry")
+ }
+ if srcExists == nil {
+ return errors.New("no source object found")
+ }
+ dstExists, err := t.Get(dst, backend.BlankValue)
+ if err != nil {
+ return errors.New("unable to get destination object")
+ }
+ if dstExists != nil {
+ if !confirm("overwrite destination") {
+ return nil
+ }
+ }
+ return t.Move(*srcExists, dst)
+}
diff --git a/internal/app/move_test.go b/internal/app/move_test.go
@@ -0,0 +1,39 @@
+package app_test
+
+import (
+ "testing"
+
+ "github.com/enckse/lockbox/internal/app"
+ "github.com/enckse/lockbox/internal/backend"
+)
+
+type (
+ mockConfirm struct {
+ called bool
+ }
+)
+
+func (m *mockConfirm) prompt(string) bool {
+ m.called = true
+ return true
+}
+
+func TestMove(t *testing.T) {
+ setup(t)
+ fullSetup(t, true).Insert(backend.NewPath("test", "test2", "test1"), "pass")
+ fullSetup(t, true).Insert(backend.NewPath("test", "test2", "test3"), "pass")
+ m := mockConfirm{}
+ if err := app.Move(fullSetup(t, true), []string{}, m.prompt); err.Error() != "src/dst required for move" {
+ t.Errorf("invalid error: %v", err)
+ }
+ if err := app.Move(fullSetup(t, true), []string{"a", "b"}, m.prompt); err.Error() != "unable to get source entry" {
+ t.Errorf("invalid error: %v", err)
+ }
+ m.called = false
+ if err := app.Move(fullSetup(t, true), []string{"test/test2/test1", "test/test2/test3"}, m.prompt); err != nil {
+ t.Errorf("invalid error: %v", err)
+ }
+ if !m.called {
+ t.Error("no move")
+ }
+}
diff --git a/internal/app/remove.go b/internal/app/remove.go
@@ -0,0 +1,38 @@
+// Package app can remove an entry
+package app
+
+import (
+ "errors"
+ "fmt"
+ "io"
+
+ "github.com/enckse/lockbox/internal/backend"
+)
+
+// Remove will remove an entry
+func Remove(w io.Writer, t *backend.Transaction, args []string, confirm Confirm) error {
+ if len(args) != 1 {
+ return errors.New("remove requires an entry")
+ }
+ deleting := args[0]
+ postfixRemove := "y"
+ existings, err := t.MatchPath(deleting)
+ if err != nil {
+ return err
+ }
+
+ if len(existings) > 1 {
+ postfixRemove = "ies"
+ fmt.Fprintln(w, "selected entities:")
+ for _, e := range existings {
+ fmt.Fprintf(w, " %s\n", e.Path)
+ }
+ fmt.Fprintln(w, "")
+ }
+ if confirm(fmt.Sprintf("delete entr%s", postfixRemove)) {
+ if err := t.RemoveAll(existings); err != nil {
+ return fmt.Errorf("unable to remove: %w", err)
+ }
+ }
+ return nil
+}
diff --git a/internal/app/remove_test.go b/internal/app/remove_test.go
@@ -0,0 +1,30 @@
+package app_test
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/enckse/lockbox/internal/app"
+ "github.com/enckse/lockbox/internal/backend"
+)
+
+func TestRemove(t *testing.T) {
+ setup(t)
+ fullSetup(t, true).Insert(backend.NewPath("test", "test2", "test1"), "pass")
+ fullSetup(t, true).Insert(backend.NewPath("test", "test2", "test3"), "pass")
+ m := mockConfirm{}
+ var buf bytes.Buffer
+ if err := app.Remove(&buf, fullSetup(t, true), []string{}, m.prompt); err.Error() != "remove requires an entry" {
+ t.Errorf("invalid error: %v", err)
+ }
+ if err := app.Remove(&buf, fullSetup(t, true), []string{"a", "b"}, m.prompt); err.Error() != "remove requires an entry" {
+ t.Errorf("invalid error: %v", err)
+ }
+ m.called = false
+ if err := app.Remove(&buf, fullSetup(t, true), []string{"tzzzest/test2/test1"}, m.prompt); err.Error() != "unable to remove: no entities given" {
+ t.Errorf("invalid error: %v", err)
+ }
+ if !m.called {
+ t.Error("no remove")
+ }
+}
diff --git a/internal/app/showclip.go b/internal/app/showclip.go
@@ -0,0 +1,42 @@
+// Package app can show/clip an entry
+package app
+
+import (
+ "errors"
+ "fmt"
+ "io"
+
+ "github.com/enckse/lockbox/internal/backend"
+ "github.com/enckse/lockbox/internal/platform"
+)
+
+// ShowClip will handle showing/clipping an entry
+func ShowClip(w io.Writer, t *backend.Transaction, isShow bool, args []string) error {
+ if len(args) != 1 {
+ return errors.New("entry required")
+ }
+ entry := args[0]
+ clipboard := platform.Clipboard{}
+ if !isShow {
+ var err error
+ clipboard, err = platform.NewClipboard()
+ if err != nil {
+ return fmt.Errorf("unable to get clipboard: %w", err)
+ }
+ }
+ existing, err := t.Get(entry, backend.SecretValue)
+ if err != nil {
+ return err
+ }
+ if existing == nil {
+ return nil
+ }
+ if isShow {
+ fmt.Fprintln(w, existing.Value)
+ return nil
+ }
+ if err := clipboard.CopyTo(existing.Value); err != nil {
+ return fmt.Errorf("clipboard operation failed: %w", err)
+ }
+ return nil
+}
diff --git a/internal/app/showclip_test.go b/internal/app/showclip_test.go
@@ -0,0 +1,38 @@
+package app_test
+
+import (
+ "bytes"
+ "os"
+ "testing"
+
+ "github.com/enckse/lockbox/internal/app"
+ "github.com/enckse/lockbox/internal/backend"
+)
+
+func TestShowClip(t *testing.T) {
+ setup(t)
+ fullSetup(t, true).Insert(backend.NewPath("test", "test2", "test1"), "pass")
+ fullSetup(t, true).Insert(backend.NewPath("test", "test2", "test3"), "pass")
+ tx := fullSetup(t, true)
+ var b bytes.Buffer
+ if err := app.ShowClip(&b, tx, true, []string{}); err.Error() != "entry required" {
+ t.Errorf("invalid error: %v", err)
+ }
+ if err := app.ShowClip(&b, tx, true, []string{"test/test2/test1"}); err != nil {
+ t.Errorf("invalid error: %v", err)
+ }
+ if b.String() == "" {
+ t.Error("no show")
+ }
+ b = bytes.Buffer{}
+ if err := app.ShowClip(&b, tx, true, []string{"tsest/test2/test1"}); err != nil {
+ t.Errorf("invalid error: %v", err)
+ }
+ if b.String() != "" {
+ t.Error("no show")
+ }
+ os.Clearenv()
+ if err := app.ShowClip(&b, tx, false, []string{"tsest/test2/test1"}); err == nil {
+ t.Errorf("invalid error: %v", err)
+ }
+}
diff --git a/internal/app/stats.go b/internal/app/stats.go
@@ -0,0 +1,26 @@
+// Package app can get stats
+package app
+
+import (
+ "errors"
+ "fmt"
+ "io"
+
+ "github.com/enckse/lockbox/internal/backend"
+)
+
+// Stats will retrieve entry stats
+func Stats(w io.Writer, t *backend.Transaction, args []string) error {
+ if len(args) != 1 {
+ return errors.New("entry required")
+ }
+ entry := args[0]
+ v, err := t.Get(entry, backend.StatsValue)
+ if err != nil {
+ return fmt.Errorf("unable to get stats: %w", err)
+ }
+ if v != nil {
+ fmt.Fprintln(w, v.Value)
+ }
+ return nil
+}
diff --git a/internal/app/stats_test.go b/internal/app/stats_test.go
@@ -0,0 +1,33 @@
+package app_test
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/enckse/lockbox/internal/app"
+ "github.com/enckse/lockbox/internal/backend"
+)
+
+func TestStats(t *testing.T) {
+ setup(t)
+ fullSetup(t, true).Insert(backend.NewPath("test", "test2", "test1"), "pass")
+ fullSetup(t, true).Insert(backend.NewPath("test", "test2", "test3"), "pass")
+ tx := fullSetup(t, true)
+ var b bytes.Buffer
+ if err := app.Stats(&b, tx, []string{}); err.Error() != "entry required" {
+ t.Errorf("invalid error: %v", err)
+ }
+ if err := app.Stats(&b, tx, []string{"test/test2/test1"}); err != nil {
+ t.Errorf("invalid error: %v", err)
+ }
+ if b.String() == "" {
+ t.Error("no stats")
+ }
+ b = bytes.Buffer{}
+ if err := app.Stats(&b, tx, []string{"tsest/test2/test1"}); err != nil {
+ t.Errorf("invalid error: %v", err)
+ }
+ if b.String() != "" {
+ t.Error("no stats")
+ }
+}
diff --git a/internal/commands/core.go b/internal/commands/core.go
@@ -1,7 +0,0 @@
-// Package commands common objects
-package commands
-
-type (
- // Confirm user inputs
- Confirm func(string) bool
-)
diff --git a/internal/commands/hash.go b/internal/commands/hash.go
@@ -1,31 +0,0 @@
-package commands
-
-import (
- "errors"
- "fmt"
- "io"
- "strings"
-
- "github.com/enckse/lockbox/internal/backend"
-)
-
-// Hash will hash 1-N files
-func Hash(w io.Writer, args []string) error {
- if len(args) == 0 {
- return errors.New("hash requires a file")
- }
- for _, a := range args {
- t, err := backend.Load(a)
- if err != nil {
- return err
- }
- e, err := t.QueryCallback(backend.QueryOptions{Mode: backend.ListMode, Values: backend.HashedValue})
- if err != nil {
- return err
- }
- for _, item := range e {
- fmt.Fprintf(w, "%s:\n %s\n\n", item.Path, strings.ReplaceAll(item.Value, "\n", "\n "))
- }
- }
- return nil
-}
diff --git a/internal/commands/hash_test.go b/internal/commands/hash_test.go
@@ -1,25 +0,0 @@
-package commands_test
-
-import (
- "bytes"
- "testing"
-
- "github.com/enckse/lockbox/internal/backend"
- "github.com/enckse/lockbox/internal/commands"
-)
-
-func TestHash(t *testing.T) {
- var buf bytes.Buffer
- if err := commands.Hash(&buf, []string{}); err.Error() != "hash requires a file" {
- t.Errorf("invalid error: %v", err)
- }
- setup(t)
- fullSetup(t, true).Insert(backend.NewPath("test", "test2", "test1"), "pass")
- fullSetup(t, true).Insert(backend.NewPath("test", "test2", "test3"), "pass")
- if err := commands.Hash(&buf, []string{"test.kdbx"}); err != nil {
- t.Errorf("invalid error: %v", err)
- }
- if buf.String() == "" {
- t.Error("nothing hashed")
- }
-}
diff --git a/internal/commands/info.go b/internal/commands/info.go
@@ -1,83 +0,0 @@
-// Package commands handles informational requests
-package commands
-
-import (
- "errors"
- "fmt"
- "io"
- "strings"
-
- "github.com/enckse/lockbox/internal/cli"
- "github.com/enckse/lockbox/internal/inputs"
-)
-
-// Info will report help/bash/env details
-func Info(w io.Writer, command string, args []string) (bool, error) {
- i, err := info(command, args)
- if err != nil {
- return false, err
- }
- if len(i) > 0 {
- fmt.Fprintf(w, "%s\n", strings.Join(i, "\n"))
- return true, nil
- }
- return false, nil
-}
-
-func info(command string, args []string) ([]string, error) {
- switch command {
- case cli.HelpCommand:
- if len(args) > 1 {
- return nil, errors.New("invalid help command")
- }
- isAdvanced := false
- if len(args) == 1 {
- if args[0] == cli.HelpAdvancedCommand {
- isAdvanced = true
- } else {
- return nil, errors.New("invalid help option")
- }
- }
- results, err := cli.Usage(isAdvanced)
- if err != nil {
- return nil, err
- }
- return results, nil
- case cli.EnvCommand, cli.BashCommand:
- defaultFlag := cli.BashDefaultsCommand
- isEnv := command == cli.EnvCommand
- if isEnv {
- defaultFlag = cli.EnvDefaultsCommand
- }
- defaults, err := getInfoDefault(args, defaultFlag)
- if err != nil {
- return nil, err
- }
- if isEnv {
- return inputs.ListEnvironmentVariables(!defaults), nil
- }
- return cli.BashCompletions(defaults)
- }
- return nil, nil
-}
-
-func getInfoDefault(args []string, possibleArg string) (bool, error) {
- defaults := false
- invalid := false
- switch len(args) {
- case 0:
- break
- case 1:
- if args[0] == possibleArg {
- defaults = true
- } else {
- invalid = true
- }
- default:
- invalid = true
- }
- if invalid {
- return false, errors.New("invalid argument")
- }
- return defaults, nil
-}
diff --git a/internal/commands/info_test.go b/internal/commands/info_test.go
@@ -1,96 +0,0 @@
-package commands_test
-
-import (
- "bytes"
- "os"
- "testing"
-
- "github.com/enckse/lockbox/internal/commands"
-)
-
-func TestNoInfo(t *testing.T) {
- var buf bytes.Buffer
- ok, err := commands.Info(&buf, "", []string{})
- if ok || err != nil {
- t.Errorf("invalid error: %v", err)
- }
-}
-
-func TestHelpInfo(t *testing.T) {
- os.Clearenv()
- var buf bytes.Buffer
- ok, err := commands.Info(&buf, "help", []string{})
- if !ok || err != nil {
- t.Errorf("invalid error: %v", err)
- }
- if buf.String() == "" {
- t.Error("nothing written")
- }
- old := buf.String()
- buf = bytes.Buffer{}
- ok, err = commands.Info(&buf, "help", []string{"-verbose"})
- if !ok || err != nil {
- t.Errorf("invalid error: %v", err)
- }
- if buf.String() == "" || old == buf.String() {
- t.Error("nothing written")
- }
- if _, err = commands.Info(&buf, "help", []string{"-verb"}); err.Error() != "invalid help option" {
- t.Errorf("invalid error: %v", err)
- }
- if _, err = commands.Info(&buf, "help", []string{"-verbose", "A"}); err.Error() != "invalid help command" {
- t.Errorf("invalid error: %v", err)
- }
-}
-
-func TestBashInfo(t *testing.T) {
- os.Clearenv()
- var buf bytes.Buffer
- ok, err := commands.Info(&buf, "bash", []string{})
- if !ok || err != nil {
- t.Errorf("invalid error: %v", err)
- }
- if buf.String() == "" {
- t.Error("nothing written")
- }
- buf = bytes.Buffer{}
- ok, err = commands.Info(&buf, "bash", []string{"-defaults"})
- if !ok || err != nil {
- t.Errorf("invalid error: %v", err)
- }
- if buf.String() == "" {
- t.Error("nothing written")
- }
- if _, err = commands.Info(&buf, "bash", []string{"-default"}); err.Error() != "invalid argument" {
- t.Errorf("invalid error: %v", err)
- }
- if _, err = commands.Info(&buf, "bash", []string{"test", "-default"}); err.Error() != "invalid argument" {
- t.Errorf("invalid error: %v", err)
- }
-}
-
-func TestEnvInfo(t *testing.T) {
- os.Clearenv()
- var buf bytes.Buffer
- ok, err := commands.Info(&buf, "env", []string{})
- if !ok || err != nil {
- t.Errorf("invalid error: %v", err)
- }
- if buf.String() == "" {
- t.Error("nothing written")
- }
- buf = bytes.Buffer{}
- ok, err = commands.Info(&buf, "env", []string{"-defaults"})
- if !ok || err != nil {
- t.Errorf("invalid error: %v", err)
- }
- if buf.String() == "" {
- t.Error("nothing written")
- }
- if _, err = commands.Info(&buf, "env", []string{"-default"}); err.Error() != "invalid argument" {
- t.Errorf("invalid error: %v", err)
- }
- if _, err = commands.Info(&buf, "env", []string{"test", "-default"}); err.Error() != "invalid argument" {
- t.Errorf("invalid error: %v", err)
- }
-}
diff --git a/internal/commands/insert.go b/internal/commands/insert.go
@@ -1,83 +0,0 @@
-// Package commands can insert
-package commands
-
-import (
- "errors"
- "fmt"
- "io"
- "strings"
-
- "github.com/enckse/lockbox/internal/backend"
- "github.com/enckse/lockbox/internal/cli"
- "github.com/enckse/lockbox/internal/inputs"
- "github.com/enckse/lockbox/internal/totp"
-)
-
-func insertError(message string, err error) error {
- return fmt.Errorf("%s (%w)", message, err)
-}
-
-// Insert will insert new entries
-// NOTE: almost entirely tested via regresssion due to complexities around piping/inputs
-func Insert(w io.Writer, t *backend.Transaction, args []string, confirm Confirm) error {
- multi := false
- isTOTP := false
- idx := 0
- switch len(args) {
- case 0:
- return errors.New("insert requires an entry")
- case 1:
- case 2:
- opt := args[0]
- switch opt {
- case cli.InsertMultiCommand:
- multi = true
- case cli.InsertTOTPCommand:
- off, err := inputs.IsNoTOTP()
- if err != nil {
- return err
- }
- if off {
- return totp.ErrNoTOTP
- }
- isTOTP = true
- default:
- return errors.New("unknown argument")
- }
- multi = true
- idx = 1
- default:
- return errors.New("too many arguments")
- }
- isPipe := inputs.IsInputFromPipe()
- entry := args[idx]
- if isTOTP {
- totpToken := inputs.TOTPToken()
- if !strings.HasSuffix(entry, backend.NewSuffix(totpToken)) {
- entry = backend.NewPath(entry, totpToken)
- }
- }
- existing, err := t.Get(entry, backend.BlankValue)
- if err != nil {
- return insertError("unable to check for existing entry", err)
- }
- if existing != nil {
- if !isPipe {
- if !confirm("overwrite existing") {
- return nil
- }
- }
- }
- password, err := inputs.GetUserInputPassword(isPipe, multi)
- if err != nil {
- return insertError("invalid input", err)
- }
- p := strings.TrimSpace(string(password))
- if err := t.Insert(entry, p); err != nil {
- return insertError("failed to insert", err)
- }
- if !isPipe {
- fmt.Println()
- }
- return nil
-}
diff --git a/internal/commands/listfind.go b/internal/commands/listfind.go
@@ -1,34 +0,0 @@
-package commands
-
-import (
- "errors"
- "fmt"
- "io"
-
- "github.com/enckse/lockbox/internal/backend"
-)
-
-// ListFind will list/find entries
-func ListFind(t *backend.Transaction, w io.Writer, isFind bool, args []string) error {
- opts := backend.QueryOptions{}
- opts.Mode = backend.ListMode
- if isFind {
- opts.Mode = backend.FindMode
- if len(args) < 1 {
- return errors.New("find requires search term")
- }
- opts.Criteria = args[0]
- } else {
- if len(args) != 0 {
- return errors.New("list does not support any arguments")
- }
- }
- e, err := t.QueryCallback(opts)
- if err != nil {
- return err
- }
- for _, f := range e {
- fmt.Fprintf(w, "%s\n", f.Path)
- }
- return nil
-}
diff --git a/internal/commands/listfind_test.go b/internal/commands/listfind_test.go
@@ -1,68 +0,0 @@
-package commands_test
-
-import (
- "bytes"
- "os"
- "strings"
- "testing"
-
- "github.com/enckse/lockbox/internal/backend"
- "github.com/enckse/lockbox/internal/commands"
-)
-
-func fullSetup(t *testing.T, keep bool) *backend.Transaction {
- if !keep {
- os.Remove("test.kdbx")
- }
- os.Setenv("LOCKBOX_READONLY", "no")
- os.Setenv("LOCKBOX_STORE", "test.kdbx")
- os.Setenv("LOCKBOX_KEY", "test")
- os.Setenv("LOCKBOX_KEYFILE", "")
- os.Setenv("LOCKBOX_KEYMODE", "plaintext")
- os.Setenv("LOCKBOX_TOTP", "totp")
- os.Setenv("LOCKBOX_HOOKDIR", "")
- os.Setenv("LOCKBOX_SET_MODTIME", "")
- tr, err := backend.NewTransaction()
- if err != nil {
- t.Errorf("failed: %v", err)
- }
- return tr
-}
-
-func setup(t *testing.T) *backend.Transaction {
- return fullSetup(t, false)
-}
-
-func TestList(t *testing.T) {
- setup(t)
- fullSetup(t, true).Insert(backend.NewPath("test", "test2", "test1"), "pass")
- fullSetup(t, true).Insert(backend.NewPath("test", "test2", "test3"), "pass")
- tx := fullSetup(t, true)
- var buf bytes.Buffer
- if err := commands.ListFind(tx, &buf, false, []string{}); err != nil {
- t.Errorf("invalid error: %v", err)
- }
- if buf.String() == "" {
- t.Error("nothing listed")
- }
- if err := commands.ListFind(tx, &buf, false, []string{"test"}); err.Error() != "list does not support any arguments" {
- t.Errorf("invalid error: %v", err)
- }
-}
-
-func TestFind(t *testing.T) {
- setup(t)
- fullSetup(t, true).Insert(backend.NewPath("test", "test2", "test1"), "pass")
- fullSetup(t, true).Insert(backend.NewPath("test", "test2", "test3"), "pass")
- tx := fullSetup(t, true)
- var buf bytes.Buffer
- if err := commands.ListFind(tx, &buf, true, []string{}); err.Error() != "find requires search term" {
- t.Errorf("invalid error: %v", err)
- }
- if err := commands.ListFind(tx, &buf, true, []string{"test1"}); err != nil {
- t.Errorf("invalid error: %v", err)
- }
- if buf.String() == "" || strings.Contains(buf.String(), "test3") {
- t.Error("wrong find")
- }
-}
diff --git a/internal/commands/move.go b/internal/commands/move.go
@@ -1,33 +0,0 @@
-package commands
-
-import (
- "errors"
-
- "github.com/enckse/lockbox/internal/backend"
-)
-
-// Move is the CLI command to move entries
-func Move(t *backend.Transaction, args []string, confirm Confirm) error {
- if len(args) != 2 {
- return errors.New("src/dst required for move")
- }
- src := args[0]
- dst := args[1]
- srcExists, err := t.Get(src, backend.SecretValue)
- if err != nil {
- return errors.New("unable to get source entry")
- }
- if srcExists == nil {
- return errors.New("no source object found")
- }
- dstExists, err := t.Get(dst, backend.BlankValue)
- if err != nil {
- return errors.New("unable to get destination object")
- }
- if dstExists != nil {
- if !confirm("overwrite destination") {
- return nil
- }
- }
- return t.Move(*srcExists, dst)
-}
diff --git a/internal/commands/move_test.go b/internal/commands/move_test.go
@@ -1,39 +0,0 @@
-package commands_test
-
-import (
- "testing"
-
- "github.com/enckse/lockbox/internal/backend"
- "github.com/enckse/lockbox/internal/commands"
-)
-
-type (
- mockConfirm struct {
- called bool
- }
-)
-
-func (m *mockConfirm) prompt(string) bool {
- m.called = true
- return true
-}
-
-func TestMove(t *testing.T) {
- setup(t)
- fullSetup(t, true).Insert(backend.NewPath("test", "test2", "test1"), "pass")
- fullSetup(t, true).Insert(backend.NewPath("test", "test2", "test3"), "pass")
- m := mockConfirm{}
- if err := commands.Move(fullSetup(t, true), []string{}, m.prompt); err.Error() != "src/dst required for move" {
- t.Errorf("invalid error: %v", err)
- }
- if err := commands.Move(fullSetup(t, true), []string{"a", "b"}, m.prompt); err.Error() != "unable to get source entry" {
- t.Errorf("invalid error: %v", err)
- }
- m.called = false
- if err := commands.Move(fullSetup(t, true), []string{"test/test2/test1", "test/test2/test3"}, m.prompt); err != nil {
- t.Errorf("invalid error: %v", err)
- }
- if !m.called {
- t.Error("no move")
- }
-}
diff --git a/internal/commands/remove.go b/internal/commands/remove.go
@@ -1,38 +0,0 @@
-// Package commands can remove an entry
-package commands
-
-import (
- "errors"
- "fmt"
- "io"
-
- "github.com/enckse/lockbox/internal/backend"
-)
-
-// Remove will remove an entry
-func Remove(w io.Writer, t *backend.Transaction, args []string, confirm Confirm) error {
- if len(args) != 1 {
- return errors.New("remove requires an entry")
- }
- deleting := args[0]
- postfixRemove := "y"
- existings, err := t.MatchPath(deleting)
- if err != nil {
- return err
- }
-
- if len(existings) > 1 {
- postfixRemove = "ies"
- fmt.Fprintln(w, "selected entities:")
- for _, e := range existings {
- fmt.Fprintf(w, " %s\n", e.Path)
- }
- fmt.Fprintln(w, "")
- }
- if confirm(fmt.Sprintf("delete entr%s", postfixRemove)) {
- if err := t.RemoveAll(existings); err != nil {
- return fmt.Errorf("unable to remove: %w", err)
- }
- }
- return nil
-}
diff --git a/internal/commands/remove_test.go b/internal/commands/remove_test.go
@@ -1,30 +0,0 @@
-package commands_test
-
-import (
- "bytes"
- "testing"
-
- "github.com/enckse/lockbox/internal/backend"
- "github.com/enckse/lockbox/internal/commands"
-)
-
-func TestRemove(t *testing.T) {
- setup(t)
- fullSetup(t, true).Insert(backend.NewPath("test", "test2", "test1"), "pass")
- fullSetup(t, true).Insert(backend.NewPath("test", "test2", "test3"), "pass")
- m := mockConfirm{}
- var buf bytes.Buffer
- if err := commands.Remove(&buf, fullSetup(t, true), []string{}, m.prompt); err.Error() != "remove requires an entry" {
- t.Errorf("invalid error: %v", err)
- }
- if err := commands.Remove(&buf, fullSetup(t, true), []string{"a", "b"}, m.prompt); err.Error() != "remove requires an entry" {
- t.Errorf("invalid error: %v", err)
- }
- m.called = false
- if err := commands.Remove(&buf, fullSetup(t, true), []string{"tzzzest/test2/test1"}, m.prompt); err.Error() != "unable to remove: no entities given" {
- t.Errorf("invalid error: %v", err)
- }
- if !m.called {
- t.Error("no remove")
- }
-}
diff --git a/internal/commands/showclip.go b/internal/commands/showclip.go
@@ -1,42 +0,0 @@
-// Package commands can show/clip an entry
-package commands
-
-import (
- "errors"
- "fmt"
- "io"
-
- "github.com/enckse/lockbox/internal/backend"
- "github.com/enckse/lockbox/internal/platform"
-)
-
-// ShowClip will handle showing/clipping an entry
-func ShowClip(w io.Writer, t *backend.Transaction, isShow bool, args []string) error {
- if len(args) != 1 {
- return errors.New("entry required")
- }
- entry := args[0]
- clipboard := platform.Clipboard{}
- if !isShow {
- var err error
- clipboard, err = platform.NewClipboard()
- if err != nil {
- return fmt.Errorf("unable to get clipboard: %w", err)
- }
- }
- existing, err := t.Get(entry, backend.SecretValue)
- if err != nil {
- return err
- }
- if existing == nil {
- return nil
- }
- if isShow {
- fmt.Fprintln(w, existing.Value)
- return nil
- }
- if err := clipboard.CopyTo(existing.Value); err != nil {
- return fmt.Errorf("clipboard operation failed: %w", err)
- }
- return nil
-}
diff --git a/internal/commands/showclip_test.go b/internal/commands/showclip_test.go
@@ -1,38 +0,0 @@
-package commands_test
-
-import (
- "bytes"
- "os"
- "testing"
-
- "github.com/enckse/lockbox/internal/backend"
- "github.com/enckse/lockbox/internal/commands"
-)
-
-func TestShowClip(t *testing.T) {
- setup(t)
- fullSetup(t, true).Insert(backend.NewPath("test", "test2", "test1"), "pass")
- fullSetup(t, true).Insert(backend.NewPath("test", "test2", "test3"), "pass")
- tx := fullSetup(t, true)
- var b bytes.Buffer
- if err := commands.ShowClip(&b, tx, true, []string{}); err.Error() != "entry required" {
- t.Errorf("invalid error: %v", err)
- }
- if err := commands.ShowClip(&b, tx, true, []string{"test/test2/test1"}); err != nil {
- t.Errorf("invalid error: %v", err)
- }
- if b.String() == "" {
- t.Error("no show")
- }
- b = bytes.Buffer{}
- if err := commands.ShowClip(&b, tx, true, []string{"tsest/test2/test1"}); err != nil {
- t.Errorf("invalid error: %v", err)
- }
- if b.String() != "" {
- t.Error("no show")
- }
- os.Clearenv()
- if err := commands.ShowClip(&b, tx, false, []string{"tsest/test2/test1"}); err == nil {
- t.Errorf("invalid error: %v", err)
- }
-}
diff --git a/internal/commands/stats.go b/internal/commands/stats.go
@@ -1,26 +0,0 @@
-// Package commands can get stats
-package commands
-
-import (
- "errors"
- "fmt"
- "io"
-
- "github.com/enckse/lockbox/internal/backend"
-)
-
-// Stats will retrieve entry stats
-func Stats(w io.Writer, t *backend.Transaction, args []string) error {
- if len(args) != 1 {
- return errors.New("entry required")
- }
- entry := args[0]
- v, err := t.Get(entry, backend.StatsValue)
- if err != nil {
- return fmt.Errorf("unable to get stats: %w", err)
- }
- if v != nil {
- fmt.Fprintln(w, v.Value)
- }
- return nil
-}
diff --git a/internal/commands/stats_test.go b/internal/commands/stats_test.go
@@ -1,33 +0,0 @@
-package commands_test
-
-import (
- "bytes"
- "testing"
-
- "github.com/enckse/lockbox/internal/backend"
- "github.com/enckse/lockbox/internal/commands"
-)
-
-func TestStats(t *testing.T) {
- setup(t)
- fullSetup(t, true).Insert(backend.NewPath("test", "test2", "test1"), "pass")
- fullSetup(t, true).Insert(backend.NewPath("test", "test2", "test3"), "pass")
- tx := fullSetup(t, true)
- var b bytes.Buffer
- if err := commands.Stats(&b, tx, []string{}); err.Error() != "entry required" {
- t.Errorf("invalid error: %v", err)
- }
- if err := commands.Stats(&b, tx, []string{"test/test2/test1"}); err != nil {
- t.Errorf("invalid error: %v", err)
- }
- if b.String() == "" {
- t.Error("no stats")
- }
- b = bytes.Buffer{}
- if err := commands.Stats(&b, tx, []string{"tsest/test2/test1"}); err != nil {
- t.Errorf("invalid error: %v", err)
- }
- if b.String() != "" {
- t.Error("no stats")
- }
-}