lockbox

password manager
Log | Files | Refs | README | LICENSE

commit d63ff393612e66398ce8442281aeb2e8e069ef12
parent cb0c04f912d3403a769151c787ea3afde5316d5a
Author: Sean Enck <sean@ttypty.com>
Date:   Fri,  3 Mar 2023 18:45:30 -0500

restructure app/commands

Diffstat:
Mcmd/main.go | 20++++++++++----------
Ainternal/app/core.go | 7+++++++
Ainternal/app/hash.go | 31+++++++++++++++++++++++++++++++
Ainternal/app/hash_test.go | 25+++++++++++++++++++++++++
Ainternal/app/info.go | 83+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/app/info_test.go | 96+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/app/insert.go | 83+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/app/listfind.go | 34++++++++++++++++++++++++++++++++++
Ainternal/app/listfind_test.go | 68++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/app/move.go | 33+++++++++++++++++++++++++++++++++
Ainternal/app/move_test.go | 39+++++++++++++++++++++++++++++++++++++++
Ainternal/app/remove.go | 38++++++++++++++++++++++++++++++++++++++
Ainternal/app/remove_test.go | 30++++++++++++++++++++++++++++++
Ainternal/app/showclip.go | 42++++++++++++++++++++++++++++++++++++++++++
Ainternal/app/showclip_test.go | 38++++++++++++++++++++++++++++++++++++++
Ainternal/app/stats.go | 26++++++++++++++++++++++++++
Ainternal/app/stats_test.go | 33+++++++++++++++++++++++++++++++++
Dinternal/commands/core.go | 7-------
Dinternal/commands/hash.go | 31-------------------------------
Dinternal/commands/hash_test.go | 25-------------------------
Dinternal/commands/info.go | 83-------------------------------------------------------------------------------
Dinternal/commands/info_test.go | 96-------------------------------------------------------------------------------
Dinternal/commands/insert.go | 83-------------------------------------------------------------------------------
Dinternal/commands/listfind.go | 34----------------------------------
Dinternal/commands/listfind_test.go | 68--------------------------------------------------------------------
Dinternal/commands/move.go | 33---------------------------------
Dinternal/commands/move_test.go | 39---------------------------------------
Dinternal/commands/remove.go | 38--------------------------------------
Dinternal/commands/remove_test.go | 30------------------------------
Dinternal/commands/showclip.go | 42------------------------------------------
Dinternal/commands/showclip_test.go | 38--------------------------------------
Dinternal/commands/stats.go | 26--------------------------
Dinternal/commands/stats_test.go | 33---------------------------------
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") - } -}