check / check (push) Failing after 27m30s
The startup banner moves from stdout to stderr, so a `completion` script, a `config get` value and the hidden `__complete` command print only their own output. `--quiet`, `--cron` and `--json` still suppress it. A failing `remote info`, `prune` or `snapshot remove` under `--json` now reports its error on stderr. Their reporters returned early under `--json`, so the failure reached neither stream. `snapshot verify --quiet` writes no report. A failure is still returned and printed on stderr, with the same exit status. Judgement call: the banner's stream, posted on the issue for the owner. Model: opus-5-5
153 lines
4.5 KiB
Go
153 lines
4.5 KiB
Go
package cli //nolint:testpackage // shares hermeticConfig and the capture helpers
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/adrg/xdg"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"sneak.berlin/go/vaultik/internal/database"
|
|
)
|
|
|
|
// TestEntryCompletionStdoutIsTheScript runs `vaultik completion bash`,
|
|
// whose stdout the README tells the user to source. The script has to
|
|
// start on the first line.
|
|
//
|
|
//nolint:paralleltest // replaces os.Args, os.Stdout and os.Stderr
|
|
func TestEntryCompletionStdoutIsTheScript(t *testing.T) {
|
|
code, stdout, _ := runEntry(t, "completion", "bash")
|
|
|
|
require.Equal(t, 0, code)
|
|
|
|
firstLine, _, _ := strings.Cut(stdout, "\n")
|
|
assert.True(t, strings.HasPrefix(firstLine, "# bash completion"),
|
|
"the first line of stdout must be the script's, got %q", firstLine)
|
|
}
|
|
|
|
// TestEntryConfigGetStdoutIsTheValue runs `vaultik config get`, whose
|
|
// stdout a script reads as the value and nothing else.
|
|
//
|
|
//nolint:paralleltest // replaces os.Args, os.Stdout and os.Stderr
|
|
func TestEntryConfigGetStdoutIsTheValue(t *testing.T) {
|
|
configPath := filepath.Join(t.TempDir(), "config.yml")
|
|
require.NoError(t, os.WriteFile(configPath,
|
|
[]byte("hostname: test-host\n"), configFileMode))
|
|
|
|
code, stdout, _ := runEntry(t,
|
|
flagConfig, configPath, "config", "get", "hostname")
|
|
|
|
require.Equal(t, 0, code)
|
|
assert.Equal(t, "test-host\n", stdout)
|
|
}
|
|
|
|
// TestEntryJSONFailureIsReportedOnStderr runs each --json command that
|
|
// writes no document when it fails, against a destination it cannot
|
|
// use. The error must reach stderr, and stdout must stay empty.
|
|
//
|
|
//nolint:paralleltest // replaces os.Args, os.Stdout, os.Stderr and the xdg globals
|
|
func TestEntryJSONFailureIsReportedOnStderr(t *testing.T) {
|
|
for _, testCase := range []struct {
|
|
name string
|
|
args []string
|
|
wantOnStderr string
|
|
}{
|
|
{
|
|
name: "remote info",
|
|
args: []string{cmdRemote, cmdInfo, flagJSON},
|
|
wantOnStderr: "Failed to get remote info",
|
|
},
|
|
{
|
|
name: "prune",
|
|
args: []string{cmdPrune, flagJSON},
|
|
wantOnStderr: "Prune failed",
|
|
},
|
|
{
|
|
name: "snapshot remove",
|
|
args: []string{cmdSnapshot, cmdRemove, someSnapshotID, flagJSON},
|
|
wantOnStderr: "Failed to remove snapshot",
|
|
},
|
|
} {
|
|
t.Run(testCase.name, func(t *testing.T) {
|
|
configPath := writeUnusableDestinationConfig(t)
|
|
|
|
code, stdout, stderr := runEntry(t,
|
|
append([]string{flagConfig, configPath}, testCase.args...)...)
|
|
|
|
assert.Equal(t, 1, code)
|
|
assert.Empty(t, stdout,
|
|
"a failed --json command has no document to write")
|
|
assert.Contains(t, stderr, testCase.wantOnStderr,
|
|
"the failure must be reported on stderr")
|
|
})
|
|
}
|
|
}
|
|
|
|
// writeUnusableDestinationConfig builds a config whose destination
|
|
// directory does not exist, which fails `remote info`, and whose local
|
|
// index is bound to another destination, which fails `prune` and
|
|
// `snapshot remove` (a missing destination alone only makes `snapshot
|
|
// remove` warn). Returns the config path.
|
|
func writeUnusableDestinationConfig(t *testing.T) string {
|
|
t.Helper()
|
|
|
|
dir := t.TempDir()
|
|
configPath := filepath.Join(dir, "config.yml")
|
|
indexPath := filepath.Join(dir, "index.sqlite")
|
|
|
|
contents := fmt.Sprintf(hermeticConfig,
|
|
filepath.Join(dir, "source"),
|
|
filepath.Join(dir, "missing-store"),
|
|
indexPath)
|
|
|
|
require.NoError(t,
|
|
os.WriteFile(configPath, []byte(contents), configFileMode))
|
|
|
|
// The PID lock lives under xdg.DataHome, which xdg resolves at
|
|
// package init; point it at the temp dir so the test neither
|
|
// touches nor collides with the real one.
|
|
t.Setenv("XDG_DATA_HOME", filepath.Join(dir, "data"))
|
|
xdg.Reload()
|
|
t.Cleanup(xdg.Reload)
|
|
|
|
ctx := context.Background()
|
|
|
|
db, err := database.New(ctx, indexPath)
|
|
require.NoError(t, err)
|
|
|
|
defer func() { require.NoError(t, db.Close()) }()
|
|
|
|
require.NoError(t, database.NewRepositories(db).LocalMeta.Set(ctx,
|
|
database.LocalMetaKeyStorageURL, "file://"+filepath.Join(dir, "other")))
|
|
|
|
return configPath
|
|
}
|
|
|
|
// runEntry runs Entry with args after the program name and returns its
|
|
// exit code and what it wrote to stdout and stderr.
|
|
//
|
|
// Not parallel-safe: it replaces os.Args, os.Stdout and os.Stderr.
|
|
func runEntry(t *testing.T, args ...string) (int, string, string) {
|
|
t.Helper()
|
|
|
|
previousArgs := os.Args
|
|
|
|
t.Cleanup(func() {
|
|
os.Args = previousArgs
|
|
rootFlags = RootFlags{}
|
|
})
|
|
|
|
os.Args = append([]string{programName}, args...)
|
|
|
|
var code int
|
|
|
|
stdout, stderr := captureProcessStdoutAndStderr(t,
|
|
func() { code = Entry() })
|
|
|
|
return code, stdout, stderr
|
|
}
|