//nolint:testpackage // sets the unexported fields of Instance package cli import ( "os" "path/filepath" "syscall" "testing" "github.com/spf13/afero" "github.com/spf13/cobra" "github.com/stretchr/testify/require" "sneak.berlin/go/secret/internal/vault" ) // plainText is the content of plain.txt in the directory newCryptoTest makes. const plainText = "document" // newCryptoTest returns an Instance whose current vault, in a new state // directory on the real filesystem, holds the age key secret "key", and a // new directory holding only plain.txt and plain.txt.age, its encryption // with that key. func newCryptoTest(t *testing.T) (*Instance, string) { t.Helper() fs := afero.NewOsFs() stateDir := t.TempDir() _, err := vault.CreateVault(fs, stateDir, "default", testMnemonicBuffer(t), nil) require.NoError(t, err) cli := NewCLIInstanceWithStateDir(fs, stateDir) cli.Mnemonic = testMnemonicBuffer(t) cli.cmd = &cobra.Command{} dir := t.TempDir() plain := filepath.Join(dir, "plain.txt") require.NoError(t, afero.WriteFile(fs, plain, []byte(plainText), 0o600)) require.NoError(t, cli.Encrypt("key", plain, plain+".age")) return cli, dir } // TestFailedCryptoLeavesOutputUnchanged is a regression test for // https://git.eeqj.de/sneak/secret/issues/143, where secret encrypt and // secret decrypt emptied the file named by --output before they failed. // A failed command must leave an existing file as it was and create none. func TestFailedCryptoLeavesOutputUnchanged(t *testing.T) { t.Parallel() tests := []struct { name string run func(cli *Instance, dir, output string) error }{ // Reading a directory fails once encryption has begun {"encrypt", func(cli *Instance, dir, output string) error { return cli.Encrypt("key", dir, output) }}, // plain.txt is not an age file {"decrypt", func(cli *Instance, dir, output string) error { return cli.Decrypt("key", filepath.Join(dir, "plain.txt"), output) }}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() cli, dir := newCryptoTest(t) existing := filepath.Join(dir, "existing.txt") require.NoError(t, afero.WriteFile(cli.fs, existing, []byte("precious"), 0o600)) require.Error(t, tt.run(cli, dir, existing)) require.Error(t, tt.run(cli, dir, filepath.Join(dir, "new.txt"))) content, err := afero.ReadFile(cli.fs, existing) require.NoError(t, err) require.Equal(t, "precious", string(content)) // No new.txt, and no temporary file left behind entries, err := afero.ReadDir(cli.fs, dir) require.NoError(t, err) names := make([]string, 0, len(entries)) for _, entry := range entries { names = append(names, entry.Name()) } require.Equal(t, []string{"existing.txt", "plain.txt", "plain.txt.age"}, names) }) } } // TestCryptoInPlace checks that secret encrypt and secret decrypt given // one file as both --input and --output replace it with the right result. // Before https://git.eeqj.de/sneak/secret/issues/143 they emptied the file // before reading it. func TestCryptoInPlace(t *testing.T) { t.Parallel() cli, dir := newCryptoTest(t) file := filepath.Join(dir, "plain.txt") require.NoError(t, cli.Encrypt("key", file, file)) require.NoError(t, cli.Decrypt("key", file, file)) content, err := afero.ReadFile(cli.fs, file) require.NoError(t, err) require.Equal(t, plainText, string(content)) } // TestDecryptToPipe checks that secret decrypt writes into a named pipe // given as --output, as it must into a device such as /dev/null, instead // of replacing it with a file. func TestDecryptToPipe(t *testing.T) { t.Parallel() cli, dir := newCryptoTest(t) pipe := filepath.Join(dir, "pipe") require.NoError(t, syscall.Mkfifo(pipe, 0o600)) received := make(chan []byte, 1) go func() { content, _ := afero.ReadFile(cli.fs, pipe) received <- content }() require.NoError(t, cli.Decrypt("key", filepath.Join(dir, "plain.txt.age"), pipe)) info, err := os.Lstat(pipe) require.NoError(t, err) require.Equal(t, os.ModeNamedPipe, info.Mode().Type()) require.Equal(t, plainText, string(<-received)) }