lockbox

password manager
Log | Files | Refs | README | LICENSE

commit 9852add9ff5131ab12a504844208e5accf6e80bc
parent 6690ef1b8a3b84331c07353312ca1ea4b36a2711
Author: Sean Enck <sean@ttypty.com>
Date:   Sun,  8 Dec 2024 10:09:22 -0500

all tests pass for toml configuration only

Diffstat:
Minternal/config/core.go | 15++++++++++-----
Mjustfile | 4++--
Mtests/expected.log | 2+-
Atests/main.go | 329+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dtests/run.sh | 257-------------------------------------------------------------------------------
5 files changed, 342 insertions(+), 265 deletions(-)

diff --git a/internal/config/core.go b/internal/config/core.go @@ -36,14 +36,19 @@ const ( ) var ( - YesValue = strconv.FormatBool(true) - NoValue = strconv.FormatBool(false) exampleColorWindows = []string{fmt.Sprintf("[%s]", strings.Join([]string{exampleColorWindow, exampleColorWindow, exampleColorWindow + "..."}, util.TimeWindowDelimiter))} configDirFile = filepath.Join("lockbox", "config.toml") - ConfigXDG = configDirFile - ConfigHome = filepath.Join(".config", configDirFile) - ConfigEnv = environmentPrefix + "CONFIG_TOML" registry = map[string]printer{} + // ConfigXDG is the offset to the config for XDG + ConfigXDG = configDirFile + // ConfigHome is the offset to the config HOME + ConfigHome = filepath.Join(".config", configDirFile) + // ConfigEnv allows overriding the config detection + ConfigEnv = environmentPrefix + "CONFIG_TOML" + // YesValue is the string variant of 'Yes' (or true) items + YesValue = strconv.FormatBool(true) + // NoValue is the string variant of 'No' (or false) items + NoValue = strconv.FormatBool(false) // TOTPDefaultColorWindow is the default coloring rules for totp TOTPDefaultColorWindow = []util.TimeWindow{{Start: 0, End: 5}, {Start: 30, End: 35}} // TOTPDefaultBetween is the default color window as a string diff --git a/justfile b/justfile @@ -11,12 +11,12 @@ build: go build {{goflags}} -ldflags "{{ldflags}} -X main.version={{version}}" -o "{{object}}" cmd/main.go unittest: - LOCKBOX_CONFIG_TOML= go test ./... + LOCKBOX_CONFIG_TOML=fake go test ./... check: unittest tests tests: build - cd tests && LOCKBOX_CONFIG_TOML= ./run.sh + cd tests && LOCKBOX_CONFIG_TOML=fake go run main.go clean: rm -f "{{object}}" diff --git a/tests/expected.log b/tests/expected.log @@ -161,7 +161,7 @@ test2 "data": "6d2" } } -clipboard will clear in 5 seconds +clipboard will clear in 3 seconds Wrong password? HMAC-SHA256 of header mismatching no store set keys/k/one2 diff --git a/tests/main.go b/tests/main.go @@ -0,0 +1,329 @@ +// Package main runs tests for all of lb +package main + +import ( + "errors" + "fmt" + "os" + "os/exec" + "path/filepath" + "sort" + "strings" + "time" + + "github.com/seanenck/lockbox/internal/platform" +) + +const ( + bothProfile = "both" + passProfile = "password" + keyFileProfile = "keyfile" + testPass = "testingpass" + testKeyData = "testkey" + reKeyPass = "rekeying" + reKeyKeyData = "rekeyfile" + clipWait = 1 + clipTries = 6 +) + +var binary = filepath.Join("..", "target", "lb") + +type ( + conf map[string]string + runner struct { + log string + testDir string + config string + store string + } +) + +func newRunner(profile string) (runner, error) { + t := filepath.Join("testdata", profile) + l := filepath.Join(t, "actual.log") + if err := os.RemoveAll(t); err != nil { + return runner{}, err + } + if err := os.MkdirAll(t, 0o755); err != nil { + return runner{}, err + } + return runner{l, t, filepath.Join(t, "config.toml"), filepath.Join(t, "pass.kdbx")}, nil +} + +func main() { + if err := run(); err != nil { + fmt.Fprintf(os.Stderr, "tests failed: %v\n", err) + os.Exit(1) + } +} + +func run() error { + args := os.Args + set := []string{passProfile, keyFileProfile, bothProfile} + if len(args) > 1 { + set = args[1:] + } + var failures []string + for _, item := range set { + fmt.Printf("%-10s -> ", item) + status := "passed" + if err := test(item); err != nil { + status = "failed" + failures = append(failures, item) + } + fmt.Println(status) + } + if len(failures) > 0 { + return fmt.Errorf("profiles failed: %v", failures) + } + return nil +} + +func setConfig(config string) { + os.Setenv("LOCKBOX_CONFIG_TOML", config) +} + +func (r runner) writeConfig(c conf) { + var set []string + for k, v := range c { + set = append(set, fmt.Sprintf("%s = %s", k, v)) + } + sort.Strings(set) + os.WriteFile(r.config, []byte(strings.Join(set, "\n")), 0o644) +} + +func (r runner) run(pipeIn, command string) error { + return r.raw(pipeIn, command, r.log, "&1") +} + +func (r runner) raw(pipeIn, command, stdout, stderr string) error { + text := fmt.Sprintf("%s %s %s >> %s 2>%s", pipeIn, binary, command, stdout, stderr) + cmd := exec.Command("/bin/sh", "-c", text) + return cmd.Run() +} + +func (r runner) logAppend(command string) error { + return exec.Command("/bin/sh", "-c", fmt.Sprintf("%s >> %s", command, r.log)).Run() +} + +func (r runner) newConf() conf { + c := make(conf) + c["store"] = c.quoteString(r.store) + return c +} + +func (c conf) makePass(pass string) string { + return fmt.Sprintf("[\"%s\"]", pass) +} + +func (c conf) quoteString(s string) string { + return fmt.Sprintf("\"%s\"", s) +} + +func test(profile string) error { + r, err := newRunner(profile) + if err != nil { + return err + } + setConfig(filepath.Join(r.testDir, "invalid")) + if err := r.raw("", "help", "/dev/null", "/dev/null"); err != nil { + return err + } + setConfig(r.config) + c := r.newConf() + c["interactive"] = "false" + keyFile := filepath.Join(r.testDir, "key.file") + hasPass := profile == passProfile || profile == bothProfile + hasFile := profile == keyFileProfile || profile == bothProfile + if hasPass { + c["credentials.password"] = c.makePass(testPass) + c["credentials.password_mode"] = c.quoteString("plaintext") + } + if hasFile { + os.WriteFile(keyFile, []byte(testKeyData), 0o644) + c["credentials.key_file"] = c.quoteString(keyFile) + if !hasPass { + c["credentials.password_mode"] = c.quoteString("none") + } + } + r.writeConfig(c) + r.run("echo test2 |", "insert keys/k/one2") + if hasPass { + delete(c, "credentials.password") + c["interactive"] = "true" + c["credentials.password_mode"] = c.quoteString("ask") + r.writeConfig(c) + } else { + r.logAppend("printf \"password: \"") + } + r.raw(fmt.Sprintf("echo %s |", testPass), "ls", r.log, "/dev/null") + c = r.newConf() + c["interactive"] = "false" + if hasPass { + c["credentials.password_mode"] = c.quoteString("plaintext") + c["credentials.password"] = c.makePass(testPass) + } + if hasFile { + c["credentials.key_file"] = c.quoteString(keyFile) + if !hasPass { + c["credentials.password_mode"] = c.quoteString("none") + } + } + r.writeConfig(c) + for _, k := range []string{"keys/k/one", "key/a/one", "keys/k/one", "keys/k/one/", "/keys/k/one", "keys/aa/b//s////e"} { + r.run("echo test |", fmt.Sprintf("insert %s", k)) + } + for _, k := range []string{"insert keys2/k/three", "multiline keys2/k/three"} { + r.run(`printf "test3\ntest4\n" |`, k) + } + r.run("", "ls") + r.run("echo y |", "rm keys/k/one") + r.logAppend("echo") + r.run("", "ls") + r.run("", "ls | grep e") + r.run("", "json") + r.logAppend("echo") + r.run("", "show keys/k/one2") + r.run("", "show keys2/k/three") + r.run("", "json keys2/k/three") + r.logAppend("echo") + r.run("echo 5ae472abqdekjqykoyxk7hvc2leklq5n |", "totp insert test/k") + r.run("echo 5ae472abqdekjqykoyxk7hvc2leklq5n |", "totp insert test/k/totp") + r.run("", "totp ls") + r.run("", "totp show test/k") + r.run("", "totp once test/k") + r.run("", "totp minimal test/k") + r.run("", fmt.Sprintf("conv \"%s\"", r.store)) + r.run("echo y |", "rm keys2/k/three") + r.logAppend("echo") + r.run("echo y |", "rm test/k/totp") + r.logAppend("echo") + r.run("echo y |", "rm test/k/one") + r.logAppend("echo") + r.logAppend("echo") + r.run("echo test2 |", "insert move/m/ka/abc") + r.run("echo test |", "insert move/m/ka/xyz") + r.run("echo test2 |", "insert move/ma/ka/yyy") + r.run("echo test |", "insert move/ma/ka/zzz") + r.run("echo test |", "insert move/ma/ka2/zzz") + r.run("echo test |", "insert move/ma/ka3/yyy") + r.run("echo test |", "insert move/ma/ka3/zzz") + r.run("", "mv move/m/* move/mac/") + r.run("", "mv move/ma/ka/* move/mac/") + r.run("", "mv move/ma/ka2/* move/mac/") + r.run("", "mv move/ma/ka3/* move/mac/") + r.run("", "mv key/a/one keyx/d/e") + r.run("", "ls") + r.run("echo y |", "rm move/*") + r.run("echo y |", "rm keyx/d/e") + r.logAppend("echo") + r.run("", "ls") + r.run("echo test2 |", "insert keys/k2/one2") + r.run("echo test |", "insert keys/k2/one") + r.run("echo test2 |", "insert keys/k2/t1/one2") + r.run("echo test |", "insert keys/k2/t1/one") + r.run("echo test2 |", "insert keys/k2/t2/one2") + + wd, err := os.Getwd() + if err != nil { + return err + } + + // test hooks + c["hooks.directory"] = c.quoteString(filepath.Join(wd, "hooks")) + r.writeConfig(c) + r.run("echo test |", "insert keys/k2/t2/one") + r.logAppend("echo") + r.run("", "ls") + r.run("echo y |", "rm keys/k2/t1/*") + r.logAppend("echo") + r.run("", "ls") + r.run("echo y |", "rm keys/k2/*") + r.logAppend("echo") + r.run("", "ls") + r.logAppend("echo") + delete(c, "hooks.directory") + + // test rekeying + reKeyArgs := []string{} + reKeyFile := filepath.Join(r.testDir, "rekey.file") + if hasFile { + os.WriteFile(reKeyFile, []byte(reKeyKeyData), 0o644) + reKeyArgs = append(reKeyArgs, fmt.Sprintf("-keyfile %s", reKeyFile)) + if !hasPass { + reKeyArgs = append(reKeyArgs, "-nokey") + } + } + r.run(fmt.Sprintf("echo %s |", reKeyPass), fmt.Sprintf("rekey %s", strings.Join(reKeyArgs, " "))) + if hasPass { + c["credentials.password"] = c.makePass(reKeyPass) + } + if hasFile { + c["credentials.key_file"] = c.quoteString(reKeyFile) + } + r.writeConfig(c) + r.logAppend("echo") + r.run("", "ls") + r.run("", "show keys/k/one2") + c["json.mode"] = c.quoteString("plaintext") + r.writeConfig(c) + r.run("", "json k") + c["json.mode"] = c.quoteString("empty") + r.writeConfig(c) + r.run("", "json k") + c["json.mode"] = c.quoteString("hash") + c["json.hash_length"] = "3" + r.writeConfig(c) + r.run("", "json k") + + // clipboard + copyFile := filepath.Join(r.testDir, "clip.copy") + pasteFile := filepath.Join(r.testDir, "clip.paste") + c["clip.copy_command"] = fmt.Sprintf("[\"touch\", \"%s\"]", copyFile) + c["clip.paste_command"] = fmt.Sprintf("[\"touch\", \"%s\"]", pasteFile) + c["clip.timeout"] = "3" + r.writeConfig(c) + r.run("", "clip keys/k/one2") + clipPassed := false + tries := 0 + for tries < clipTries { + if platform.PathExists(copyFile) && platform.PathExists(pasteFile) { + clipPassed = true + break + } + time.Sleep(500 * clipWait * time.Millisecond) + tries++ + } + if !clipPassed { + return errors.New("clipboard test failed unexpectedly") + } + + invalid := r.newConf() + for k, v := range c { + invalid[k] = v + } + if hasPass { + invalid["credentials.password"] = c.makePass("garbage") + } + if hasFile { + invalidFile := filepath.Join(r.testDir, "bad.file") + os.WriteFile(invalidFile, []byte{}, 0o644) + invalid["credentials.key_file"] = c.quoteString(invalidFile) + } + r.writeConfig(invalid) + r.run("", "ls") + r.writeConfig(c) + setConfig(filepath.Join(r.testDir, "invalid")) + r.run("", "ls") + setConfig(r.config) + r.run("", "ls") + tmpFile := fmt.Sprintf("%s.tmp", r.log) + for _, item := range []string{"'s/\"modtime\": \"[0-9].*$/\"modtime\": \"XXXX-XX-XX\",/g'", "'s/^[0-9][0-9][0-9][0-9][0-9][0-9]$/XXXXXX/g'"} { + exec.Command("/bin/sh", "-c", fmt.Sprintf("sed %s %s > %s", item, r.log, tmpFile)).Run() + exec.Command("mv", tmpFile, r.log).Run() + } + diff := exec.Command("diff", "-u", "expected.log", r.log) + diff.Stdout = os.Stdout + diff.Stderr = os.Stderr + return diff.Run() +} diff --git a/tests/run.sh b/tests/run.sh @@ -1,257 +0,0 @@ -#!/bin/sh -LB_BINARY=../target/lb -DATA="testdata/$1" -TOML="$DATA/config.toml" -CLIP_WAIT=1 -CLIP_TRIES=3 -CLIP_COPY="$DATA/clip.copy" -CLIP_PASTE="$DATA/clip.paste" -PASS_TEST="password" -KEYF_TEST="keyfile" -BOTH_TEST="both" - -_unset() { - # shellcheck disable=SC2046 - unset $(env | grep '^LOCKBOX' | cut -d "=" -f 1) -} - -if [ -z "$1" ]; then - CODE=0 - for TEST_RUN in "$PASS_TEST" "$KEYF_TEST" "$BOTH_TEST"; do - if ! "$0" "$TEST_RUN"; then - CODE=1 - fi - done - exit $CODE -fi - -_unset -export LOCKBOX_CONFIG_TOML="none" -if [ ! -x "${LB_BINARY}" ]; then - echo "binary missing?" - exit 1 -fi -if ! ${LB_BINARY} help >/dev/null; then - echo "help unavailable by default...fatal" - exit 1 -fi -mkdir -p "$DATA" -find "$DATA" -type f -delete - -export LOCKBOX_CREDENTIALS_KEY_FILE="" -export LOCKBOX_CREDENTIALS_PASSWORD="" -VALID=0 -if [ "$1" = "$PASS_TEST" ] || [ "$1" = "$BOTH_TEST" ]; then - VALID=1 - export LOCKBOX_CREDENTIALS_PASSWORD="testingkey" -fi -if [ "$1" = "$KEYF_TEST" ] || [ "$1" = "$BOTH_TEST" ]; then - VALID=1 - KEYFILE="$DATA/test.key" - echo "thisisatest" > "$KEYFILE" - export LOCKBOX_CREDENTIALS_KEY_FILE="$KEYFILE" -fi -if [ "$VALID" -eq 0 ]; then - echo "invalid test" - exit 1 -fi - -LOGFILE="$DATA/actual.log" -printf "%-10s ... " "$1" -{ - export LOCKBOX_HOOKS_DIRECTORY="" - export LOCKBOX_STORE="${DATA}/passwords.kdbx" - export LOCKBOX_TOTP_ENTRY=totp - export LOCKBOX_INTERACTIVE=false - export LOCKBOX_READONLY=false - if [ "$LOCKBOX_CREDENTIALS_PASSWORD" = "" ]; then - export LOCKBOX_CREDENTIALS_PASSWORD_MODE=none - else - export LOCKBOX_CREDENTIALS_PASSWORD_MODE=plaintext - fi - export LOCKBOX_JSON_HASH_LENGTH=0 - echo test2 |${LB_BINARY} insert keys/k/one2 - OLDMODE="$LOCKBOX_CREDENTIALS_PASSWORD_MODE" - OLDKEY="$LOCKBOX_CREDENTIALS_PASSWORD" - if [ "$OLDKEY" != "" ]; then - export LOCKBOX_INTERACTIVE=true - export LOCKBOX_CREDENTIALS_PASSWORD_MODE=ask - export LOCKBOX_CREDENTIALS_PASSWORD="" - else - printf "password: " - fi - echo "$OLDKEY" | ${LB_BINARY} ls 2>/dev/null - if [ "$OLDKEY" != "" ]; then - export LOCKBOX_INTERACTIVE=false - export LOCKBOX_CREDENTIALS_PASSWORD_MODE="$OLDMODE" - export LOCKBOX_CREDENTIALS_PASSWORD="$OLDKEY" - fi - echo test |${LB_BINARY} insert keys/k/one - echo test |${LB_BINARY} insert key/a/one - echo test |${LB_BINARY} insert keys/k/one - echo test |${LB_BINARY} insert keys/k/one/ - echo test |${LB_BINARY} insert /keys/k/one - echo test |${LB_BINARY} insert keys/aa/b//s///e - printf "test3\ntest4\n" |${LB_BINARY} insert keys2/k/three - printf "test3\ntest4\n" |${LB_BINARY} multiline keys2/k/three - ${LB_BINARY} ls - echo y |${LB_BINARY} rm keys/k/one - echo - ${LB_BINARY} ls - ${LB_BINARY} ls | grep e - ${LB_BINARY} json - echo - ${LB_BINARY} show keys/k/one2 - ${LB_BINARY} show keys2/k/three - ${LB_BINARY} json keys2/k/three - echo - echo 5ae472abqdekjqykoyxk7hvc2leklq5n |${LB_BINARY} totp insert test/k - echo 5ae472abqdekjqykoyxk7hvc2leklq5n |${LB_BINARY} totp insert test/k/totp - ${LB_BINARY} totp ls - ${LB_BINARY} totp show test/k - ${LB_BINARY} totp once test/k - ${LB_BINARY} totp minimal test/k - ${LB_BINARY} conv "$LOCKBOX_STORE" - echo y |${LB_BINARY} rm keys2/k/three - echo - echo y |${LB_BINARY} rm test/k/totp - echo - echo y |${LB_BINARY} rm test/k/one - echo - echo - echo test2 |${LB_BINARY} insert move/m/ka/abc - echo test |${LB_BINARY} insert move/m/ka/xyz - echo test2 |${LB_BINARY} insert move/ma/ka/yyy - echo test |${LB_BINARY} insert move/ma/ka/zzz - echo test |${LB_BINARY} insert move/ma/ka2/zzz - echo test |${LB_BINARY} insert move/ma/ka3/yyy - echo test |${LB_BINARY} insert move/ma/ka3/zzz - ${LB_BINARY} mv move/m/* move/mac/ - ${LB_BINARY} mv move/ma/ka/* move/mac/ - ${LB_BINARY} mv move/ma/ka2/* move/mac/ - ${LB_BINARY} mv move/ma/ka3/* move/mac/ - ${LB_BINARY} mv key/a/one keyx/d/e - ${LB_BINARY} ls - echo y |${LB_BINARY} rm move/* - echo y |${LB_BINARY} rm keyx/d/e - echo - ${LB_BINARY} ls - echo test2 |${LB_BINARY} insert keys/k2/one2 - echo test |${LB_BINARY} insert keys/k2/one - echo test2 |${LB_BINARY} insert keys/k2/t1/one2 - echo test |${LB_BINARY} insert keys/k2/t1/one - echo test2 |${LB_BINARY} insert keys/k2/t2/one2 - export LOCKBOX_HOOKS_DIRECTORY="$PWD/hooks" - echo test |${LB_BINARY} insert keys/k2/t2/one - echo - ${LB_BINARY} ls - echo y |${LB_BINARY} rm keys/k2/t1/* - echo - ${LB_BINARY} ls - echo y |${LB_BINARY} rm keys/k2/* - echo - ${LB_BINARY} ls - echo - # rekeying - REKEY_ARGS="" - NEWKEY="newkey$1" - export LOCKBOX_HOOKS_DIRECTORY="" - if [ -n "$LOCKBOX_CREDENTIALS_KEY_FILE" ]; then - REKEYFILE="$DATA/newkeyfile" - REKEY_ARGS="-keyfile $REKEYFILE" - echo "thisisanewkey" > "$REKEYFILE" - if [ -z "$LOCKBOX_CREDENTIALS_PASSWORD" ]; then - REKEY_ARGS="$REKEY_ARGS -nokey" - NEWKEY="" - fi - fi - # shellcheck disable=SC2086 - echo "$NEWKEY" | ${LB_BINARY} rekey $REKEY_ARGS - export LOCKBOX_CREDENTIALS_PASSWORD="$NEWKEY" - export LOCKBOX_CREDENTIALS_KEY_FILE="$REKEYFILE" - echo - ${LB_BINARY} ls - ${LB_BINARY} show keys/k/one2 - export LOCKBOX_JSON_MODE=plaintext - ${LB_BINARY} json k - export LOCKBOX_JSON_MODE=empty - ${LB_BINARY} json k - export LOCKBOX_JSON_MODE=hash - export LOCKBOX_JSON_HASH_LENGTH=3 - ${LB_BINARY} json k - # clipboard - export LOCKBOX_CLIP_COPY_COMMAND="touch $CLIP_COPY" - export LOCKBOX_CLIP_PASTE_COMMAND="touch $CLIP_PASTE" - export LOCKBOX_CLIP_TIMEOUT=5 - ${LB_BINARY} clip keys/k/one2 - CLIP_PASSED=0 - while [ "$CLIP_TRIES" -gt 0 ] ; do - if [ -e "$CLIP_COPY" ] && [ -e "$CLIP_PASTE" ]; then - CLIP_PASSED=1 - break - fi - sleep "$CLIP_WAIT" - CLIP_TRIES=$((CLIP_TRIES-1)) - done - if [ $CLIP_PASSED -eq 0 ]; then - echo "clipboard test failed" - fi - # invalid settings - OLDKEY="$LOCKBOX_CREDENTIALS_PASSWORD" - OLDMODE="$LOCKBOX_CREDENTIALS_PASSWORD_MODE" - OLDKEYFILE="$LOCKBOX_CREDENTIALS_KEY_FILE" - if [ -n "$LOCKBOX_CREDENTIALS_KEY_FILE" ]; then - export LOCKBOX_CREDENTIALS_KEY_FILE="" - if [ -z "$LOCKBOX_CREDENTIALS_PASSWORD" ]; then - export LOCKBOX_CREDENTIALS_PASSWORD="garbage" - fi - else - KEYFILE="$DATA/invalid.key" - echo "invalid" > "$KEYFILE" - export LOCKBOX_CREDENTIALS_KEY_FILE="$KEYFILE" - fi - if [ "$OLDMODE" = "none" ]; then - export LOCKBOX_CREDENTIALS_PASSWORD_MODE="plaintext" - fi - ${LB_BINARY} ls - export LOCKBOX_CREDENTIALS_KEY_FILE="$OLDKEYFILE" - export LOCKBOX_CREDENTIALS_PASSWORD="$OLDKEY" - export LOCKBOX_CREDENTIALS_PASSWORD_MODE="$OLDMODE" - # configuration - { - cat << EOF -store = "$LOCKBOX_STORE" -interactive = false - -[clip] -copy_command = [$(echo "$LOCKBOX_CLIP_COPY_COMMAND" | sed 's/ /", "/g;s/^/"/g;s/$/"/g')] -copy_command = [$(echo "$LOCKBOX_CLIP_PASTE_COMMAND" | sed 's/ /", "/g;s/^/"/g;s/$/"/g')] -timeout = $LOCKBOX_CLIP_TIMEOUT - -[json] -mode = "$LOCKBOX_JSON_MODE" -hash_length = $LOCKBOX_JSON_HASH_LENGTH - -[credentials] -key_file = "$LOCKBOX_CREDENTIALS_KEY_FILE" -password_mode = "$LOCKBOX_CREDENTIALS_PASSWORD_MODE" -password = ["$LOCKBOX_CREDENTIALS_PASSWORD"] -EOF - } > "$TOML" - _unset - export LOCKBOX_FAKE_TEST=plain - export LOCKBOX_CONFIG_TOML="none" - ${LB_BINARY} ls - export LOCKBOX_CONFIG_TOML="$TOML" - ${LB_BINARY} ls -} 2>&1 | \ - sed 's/"modtime": "[0-9].*$/"modtime": "XXXX-XX-XX",/g' | \ - sed 's/^[0-9][0-9][0-9][0-9][0-9][0-9]$/XXXXXX/g' > "$LOGFILE" -STATE=0 -RESULT="passed" -if ! diff -u "$LOGFILE" "expected.log"; then - RESULT="failed" - STATE=1 -fi -printf "[%s]\n" "$RESULT" -exit "$STATE"