lockbox

password manager
Log | Files | Refs | README | LICENSE

commit 21a35b4f33e65c7472d80ed7f0539cfe0490d573
parent e2e2c5656c58542322525dc6e4cd34a0560b08bf
Author: Sean Enck <sean@ttypty.com>
Date:   Wed,  8 Mar 2023 19:14:31 -0500

use unit test framework for integration tests

Diffstat:
MMakefile | 5+----
Dinternal/scripts/testing/Makefile | 15---------------
Dinternal/scripts/testing/check.go | 224-------------------------------------------------------------------------------
Ainternal/test/Makefile | 6++++++
Rinternal/scripts/testing/expected.log -> internal/test/expected.log | 0
Rinternal/scripts/testing/hooks/all.sh -> internal/test/hooks/all.sh | 0
Rinternal/scripts/testing/hooks/test.sh -> internal/test/hooks/test.sh | 0
Ainternal/test/integration.go | 229+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/test/integration_test.go | 46++++++++++++++++++++++++++++++++++++++++++++++
9 files changed, 282 insertions(+), 243 deletions(-)

diff --git a/Makefile b/Makefile @@ -14,12 +14,9 @@ $(TARGET): cmd/main.go internal/**/*.go go.* internal/cli/completions* @git describe --tags --abbrev=0 | sed 's/^/version:/g' @git tag --points-at HEAD | grep -q '' || echo "version:-1" -unittest: +check: $(TARGET) go test -v ./... -check: $(TARGET) unittest - LB_BUILD=$(PWD)/$(TARGET) make -C $(TESTS) - clean: rm -rf $(BUILD) make -C $(TESTS) clean diff --git a/internal/scripts/testing/Makefile b/internal/scripts/testing/Makefile @@ -1,15 +0,0 @@ -BIN := bin -ACTUAL := $(BIN)/actual.log -RUNS := -keyfile=true -keyfile=false - -all: $(RUNS) - -$(RUNS): - mkdir -p $(BIN) - rm -f $(BIN)/*.kdbx - LB_BUILD=$(LB_BUILD) go run check.go $@ -run=true -data $(BIN) > $(ACTUAL) 2>&1 - go run check.go -run=false -data $(ACTUAL) - diff -u $(ACTUAL) expected.log - -clean: - rm -rf $(BIN) diff --git a/internal/scripts/testing/check.go b/internal/scripts/testing/check.go @@ -1,224 +0,0 @@ -// package main runs the tests -package main - -import ( - "bytes" - "flag" - "fmt" - "os" - "os/exec" - "path/filepath" - "regexp" - "strings" - "time" - - "github.com/enckse/pgl/os/exit" - "github.com/enckse/pgl/os/paths" -) - -var yes = []string{"y"} - -const ( - testKey = "plaintextkey" -) - -func runCommand(args []string, data []string) { - p := exec.Command(os.Getenv("LB_BUILD"), args...) - var buf bytes.Buffer - for _, d := range data { - if _, err := buf.WriteString(fmt.Sprintf("%s\n", d)); err != nil { - exit.Dief("failed to write stdin: %v", err) - } - } - p.Stdout = os.Stdout - p.Stderr = os.Stderr - p.Stdin = &buf - if err := p.Run(); err != nil { - fmt.Println(err) - } -} - -func ls() { - runCommand([]string{"ls"}, nil) -} - -func rm(k string) { - runCommand([]string{"rm", k}, yes) -} - -func show(k string) { - runCommand([]string{"show", k}, nil) -} - -func insert(k string, d []string) { - runCommand([]string{"insert", k}, d) -} - -func totpList() { - runCommand([]string{"totp", "-list"}, nil) -} - -func main() { - if err := execute(); err != nil { - exit.Die(err) - } -} - -func replace(input string, re *regexp.Regexp, to string) string { - matches := re.FindAllStringSubmatch(input, -1) - res := input - for _, match := range matches { - for _, m := range match { - res = strings.ReplaceAll(res, m, to) - } - } - return res -} - -func cleanup(dataFile, workDir string) error { - data, err := os.ReadFile(dataFile) - if err != nil { - return err - } - totp, err := regexp.Compile("^[0-9][0-9][0-9][0-9][0-9][0-9]$") - if err != nil { - return err - } - date := fmt.Sprintf("modtime: %s", time.Now().Format("2006-01-02")) - var results []string - for _, l := range strings.Split(string(data), "\n") { - payload := l - payload = replace(payload, totp, "XXXXXX") - if strings.Contains(payload, date) { - prefix := "" - if strings.HasPrefix(payload, " ") { - prefix = " " - } - payload = fmt.Sprintf("%s%s", prefix, "modtime: XXXX-XX-XX") - } - results = append(results, payload) - } - return os.WriteFile(dataFile, []byte(strings.Join(results, "\n")), 0o644) -} - -func execute() error { - keyFile := flag.Bool("keyfile", false, "enable keyfile") - dataPath := flag.String("data", "", "data area") - runMode := flag.Bool("run", true, "execute tests") - clipRetry := flag.Uint("clipboard-retry", 3, "number of tries for the clipboard test") - clipWait := flag.Uint("clip-wait", 1000, "number of milliseconds to wait for the clipboard test") - flag.Parse() - path := *dataPath - cwd, err := os.Getwd() - if err != nil { - return err - } - if !*runMode { - return cleanup(path, cwd) - } - useKeyFile := "" - if *keyFile { - useKeyFile = filepath.Join(path, "test.key") - if err := os.WriteFile(useKeyFile, []byte("thisisatest"), 0o644); err != nil { - return err - } - } - store := filepath.Join(path, fmt.Sprintf("%s.kdbx", time.Now().Format("20060102150405"))) - os.Setenv("LOCKBOX_HOOKDIR", "") - os.Setenv("LOCKBOX_STORE", store) - os.Setenv("LOCKBOX_KEY", testKey) - os.Setenv("LOCKBOX_TOTP", "totp") - os.Setenv("LOCKBOX_INTERACTIVE", "no") - os.Setenv("LOCKBOX_READONLY", "no") - os.Setenv("LOCKBOX_KEYMODE", "plaintext") - os.Setenv("LOCKBOX_KEYFILE", useKeyFile) - insert("keys/k/one2", []string{"test2"}) - for _, k := range []string{"keys/k/one", "key/a/one", "keys/k/one", "keys/k/one/", "/keys/k/one", "keys/aa/b//s///e"} { - insert(k, []string{"test"}) - } - insert("keys2/k/three", []string{"test3", "test4"}) - ls() - rm("keys/k/one") - fmt.Println() - ls() - runCommand([]string{"find", "e"}, nil) - show("keys/k/one2") - show("keys2/k/three") - runCommand([]string{"stats", "keys2/k/three"}, nil) - for _, k := range []string{"test/k", "test/k/totp"} { - runCommand([]string{"insert", "-totp", k}, []string{"5ae472abqdekjqykoyxk7hvc2leklq5n"}) - } - totpList() - runCommand([]string{"totp", "test/k"}, nil) - runCommand([]string{"hash", store}, nil) - rm("keys2/k/three") - fmt.Println() - rm("test/k/totp") - fmt.Println() - rm("test/k/one") - fmt.Println() - fmt.Println() - runCommand([]string{"mv", "key/a/one", "keyx/d/e"}, nil) - ls() - rm("keyx/d/e") - fmt.Println() - ls() - insert("keys/k2/one2", []string{"test2"}) - insert("keys/k2/one", []string{"test"}) - insert("keys/k2/t1/one2", []string{"test2"}) - insert("keys/k2/t1/one", []string{"test"}) - insert("keys/k2/t2/one2", []string{"test2"}) - os.Setenv("LOCKBOX_HOOKDIR", filepath.Join(cwd, "hooks")) - insert("keys/k2/t2/one", []string{"test"}) - fmt.Println() - ls() - rm("keys/k2/t1/*") - fmt.Println() - ls() - rm("keys/k2/*") - fmt.Println() - ls() - fmt.Println() - reKeyStore := fmt.Sprintf("%s.rekey.kdbx", store) - reKey := "rekey" - os.Setenv("LOCKBOX_STORE_NEW", reKeyStore) - os.Setenv("LOCKBOX_KEY_NEW", reKey) - os.Setenv("LOCKBOX_KEYMODE_NEW", "plaintext") - os.Setenv("LOCKBOX_KEYFILE_NEW", "") - runCommand([]string{"rekey"}, yes) - os.Setenv("LOCKBOX_STORE", reKeyStore) - os.Setenv("LOCKBOX_KEYFILE", "") - os.Setenv("LOCKBOX_KEY", reKey) - fmt.Println() - ls() - testClipboard(path, *clipRetry, *clipWait) - return nil -} - -func testClipboard(dataPath string, tries uint, wait uint) { - clipCopyFile := filepath.Join(dataPath, "clipboard") - clipPasteFile := clipCopyFile + ".paste" - clipFiles := []string{clipCopyFile, clipPasteFile} - os.Setenv("LOCKBOX_CLIP_COPY", fmt.Sprintf("touch %s", clipCopyFile)) - os.Setenv("LOCKBOX_CLIP_PASTE", fmt.Sprintf("touch %s", clipPasteFile)) - os.Setenv("LOCKBOX_CLIP_MAX", "5") - runCommand([]string{"clip", "keys/k/one2"}, nil) - clipDur := time.Duration(wait) * time.Millisecond - for { - if tries == 0 { - fmt.Println("missing clipboard files") - break - } - foundClipCount := 0 - for _, f := range clipFiles { - if paths.Exist(f) { - foundClipCount++ - } - } - if foundClipCount == len(clipFiles) { - break - } - time.Sleep(clipDur) - tries-- - } -} diff --git a/internal/test/Makefile b/internal/test/Makefile @@ -0,0 +1,6 @@ +$(RUNS): + mkdir -p $(BIN) + rm -f $(BIN)/*.kdbx + LB_BUILD=$(LB_BUILD) go run check.go $@ -run=true -data $(BIN) > $(ACTUAL) 2>&1 + go run check.go -run=false -data $(ACTUAL) + diff -u $(ACTUAL) expected.log diff --git a/internal/scripts/testing/expected.log b/internal/test/expected.log diff --git a/internal/scripts/testing/hooks/all.sh b/internal/test/hooks/all.sh diff --git a/internal/scripts/testing/hooks/test.sh b/internal/test/hooks/test.sh diff --git a/internal/test/integration.go b/internal/test/integration.go @@ -0,0 +1,229 @@ +// Package test has some integration tests for the binary lb variant +package test + +import ( + "bytes" + "errors" + "fmt" + "os" + "os/exec" + "path/filepath" + "regexp" + "strings" + "time" + + "github.com/enckse/pgl/os/exit" + "github.com/enckse/pgl/os/paths" +) + +var yes = []string{"y"} + +const ( + testKey = "plaintextkey" + clipRetry = 3 + clipWait = 1000 +) + +type ( + testRunner struct { + file *os.File + exe string + } +) + +func (r testRunner) runCommand(args []string, data []string) { + p := exec.Command(r.exe, args...) + var buf bytes.Buffer + for _, d := range data { + if _, err := buf.WriteString(fmt.Sprintf("%s\n", d)); err != nil { + exit.Dief("failed to write stdin: %v", err) + } + } + p.Stdout = r.file + p.Stderr = r.file + p.Stdin = &buf + if err := p.Run(); err != nil { + fmt.Fprintf(r.file, "%v\n", err) + } +} + +func (r testRunner) ls() { + r.runCommand([]string{"ls"}, nil) +} + +func (r testRunner) rm(k string) { + r.runCommand([]string{"rm", k}, yes) +} + +func (r testRunner) show(k string) { + r.runCommand([]string{"show", k}, nil) +} + +func (r testRunner) insert(k string, d []string) { + r.runCommand([]string{"insert", k}, d) +} + +func (r testRunner) totpList() { + r.runCommand([]string{"totp", "-list"}, nil) +} + +func (r testRunner) ln() { + r.file.Write([]byte("\n")) +} + +func replace(input string, re *regexp.Regexp, to string) string { + matches := re.FindAllStringSubmatch(input, -1) + res := input + for _, match := range matches { + for _, m := range match { + res = strings.ReplaceAll(res, m, to) + } + } + return res +} + +// Cleanup will cleanup the data log outputs +func Cleanup(dataFile string) error { + data, err := os.ReadFile(dataFile) + if err != nil { + return err + } + totp, err := regexp.Compile("^[0-9][0-9][0-9][0-9][0-9][0-9]$") + if err != nil { + return err + } + date := fmt.Sprintf("modtime: %s", time.Now().Format("2006-01-02")) + var results []string + for _, l := range strings.Split(string(data), "\n") { + payload := l + payload = replace(payload, totp, "XXXXXX") + if strings.Contains(payload, date) { + prefix := "" + if strings.HasPrefix(payload, " ") { + prefix = " " + } + payload = fmt.Sprintf("%s%s", prefix, "modtime: XXXX-XX-XX") + } + results = append(results, payload) + } + return os.WriteFile(dataFile, []byte(strings.Join(results, "\n")), 0o644) +} + +// Execute will run a test +func Execute(keyFile bool, exe, dataPath, logFile string) error { + f, err := os.Create(logFile) + if err != nil { + return err + } + defer f.Close() + cwd, err := os.Getwd() + if err != nil { + return err + } + useKeyFile := "" + if keyFile { + useKeyFile = filepath.Join(dataPath, "test.key") + if err := os.WriteFile(useKeyFile, []byte("thisisatest"), 0o644); err != nil { + return err + } + } + store := filepath.Join(dataPath, fmt.Sprintf("%s.kdbx", time.Now().Format("20060102150405"))) + os.Setenv("LOCKBOX_HOOKDIR", "") + os.Setenv("LOCKBOX_STORE", store) + os.Setenv("LOCKBOX_KEY", testKey) + os.Setenv("LOCKBOX_TOTP", "totp") + os.Setenv("LOCKBOX_INTERACTIVE", "no") + os.Setenv("LOCKBOX_READONLY", "no") + os.Setenv("LOCKBOX_KEYMODE", "plaintext") + os.Setenv("LOCKBOX_KEYFILE", useKeyFile) + runner := testRunner{file: f, exe: exe} + runner.insert("keys/k/one2", []string{"test2"}) + for _, k := range []string{"keys/k/one", "key/a/one", "keys/k/one", "keys/k/one/", "/keys/k/one", "keys/aa/b//s///e"} { + runner.insert(k, []string{"test"}) + } + runner.insert("keys2/k/three", []string{"test3", "test4"}) + runner.ls() + runner.rm("keys/k/one") + runner.ln() + runner.ls() + runner.runCommand([]string{"find", "e"}, nil) + runner.show("keys/k/one2") + runner.show("keys2/k/three") + runner.runCommand([]string{"stats", "keys2/k/three"}, nil) + for _, k := range []string{"test/k", "test/k/totp"} { + runner.runCommand([]string{"insert", "-totp", k}, []string{"5ae472abqdekjqykoyxk7hvc2leklq5n"}) + } + runner.totpList() + runner.runCommand([]string{"totp", "test/k"}, nil) + runner.runCommand([]string{"hash", store}, nil) + runner.rm("keys2/k/three") + runner.ln() + runner.rm("test/k/totp") + runner.ln() + runner.rm("test/k/one") + runner.ln() + runner.ln() + runner.runCommand([]string{"mv", "key/a/one", "keyx/d/e"}, nil) + runner.ls() + runner.rm("keyx/d/e") + runner.ln() + runner.ls() + runner.insert("keys/k2/one2", []string{"test2"}) + runner.insert("keys/k2/one", []string{"test"}) + runner.insert("keys/k2/t1/one2", []string{"test2"}) + runner.insert("keys/k2/t1/one", []string{"test"}) + runner.insert("keys/k2/t2/one2", []string{"test2"}) + os.Setenv("LOCKBOX_HOOKDIR", filepath.Join(cwd, "hooks")) + runner.insert("keys/k2/t2/one", []string{"test"}) + runner.ln() + runner.ls() + runner.rm("keys/k2/t1/*") + runner.ln() + runner.ls() + runner.rm("keys/k2/*") + runner.ln() + runner.ls() + runner.ln() + reKeyStore := fmt.Sprintf("%s.rekey.kdbx", store) + reKey := "rekey" + os.Setenv("LOCKBOX_STORE_NEW", reKeyStore) + os.Setenv("LOCKBOX_KEY_NEW", reKey) + os.Setenv("LOCKBOX_KEYMODE_NEW", "plaintext") + os.Setenv("LOCKBOX_KEYFILE_NEW", "") + runner.runCommand([]string{"rekey"}, yes) + os.Setenv("LOCKBOX_STORE", reKeyStore) + os.Setenv("LOCKBOX_KEYFILE", "") + os.Setenv("LOCKBOX_KEY", reKey) + runner.ln() + runner.ls() + return runner.clipboard(dataPath) +} + +func (r testRunner) clipboard(dataPath string) error { + clipCopyFile := filepath.Join(dataPath, "clipboard") + clipPasteFile := clipCopyFile + ".paste" + clipFiles := []string{clipCopyFile, clipPasteFile} + os.Setenv("LOCKBOX_CLIP_COPY", fmt.Sprintf("touch %s", clipCopyFile)) + os.Setenv("LOCKBOX_CLIP_PASTE", fmt.Sprintf("touch %s", clipPasteFile)) + os.Setenv("LOCKBOX_CLIP_MAX", "5") + r.runCommand([]string{"clip", "keys/k/one2"}, nil) + clipDur := time.Duration(clipWait) * time.Millisecond + tries := clipRetry + for { + if tries == 0 { + return errors.New("missing clipboard files") + } + foundClipCount := 0 + for _, f := range clipFiles { + if paths.Exist(f) { + foundClipCount++ + } + } + if foundClipCount == len(clipFiles) { + break + } + time.Sleep(clipDur) + tries-- + } + return nil +} diff --git a/internal/test/integration_test.go b/internal/test/integration_test.go @@ -0,0 +1,46 @@ +package test_test + +import ( + "os" + "os/exec" + "path/filepath" + "testing" + + "github.com/enckse/lockbox/internal/test" + "github.com/enckse/pgl/os/paths" +) + +const ( + testDir = "bin" +) + +func runTest(t *testing.T, keyFile bool) { + os.RemoveAll(testDir) + os.Mkdir(testDir, 0o755) + binary := filepath.Join("..", "..", testDir, "lb") + if !paths.Exist(binary) { + t.Error("no binary to test") + return + } + logFile := filepath.Join(testDir, "actual.log") + if err := test.Execute(keyFile, binary, testDir, logFile); err != nil { + t.Errorf("unexpected error: %v", err) + } + if err := test.Cleanup(logFile); err != nil { + t.Errorf("cleanup failed: %v", err) + } + diff := exec.Command("diff", "-u", logFile, "expected.log") + diff.Stdout = os.Stdout + diff.Stderr = os.Stderr + if err := diff.Run(); err != nil { + t.Errorf("diff failed: %v", err) + } +} + +func TestKeyFile(t *testing.T) { + runTest(t, true) +} + +func TestPasswordOnly(t *testing.T) { + runTest(t, false) +}