1package tests
2
3import (
4	"bytes"
5	"context"
6	"fmt"
7	"io"
8	"os"
9	"os/exec"
10	"path/filepath"
11	"runtime"
12	"strings"
13	"testing"
14
15	"github.com/gopasspw/gopass/tests/gptest"
16	shellquote "github.com/kballard/go-shellquote"
17	"github.com/stretchr/testify/require"
18)
19
20const (
21	gopassConfig = `
22exportkeys: false
23`
24	keyID = "BE73F104"
25)
26
27type tester struct {
28	t *testing.T
29
30	// Binary is the path to the gopass binary used for testing
31	Binary    string
32	sourceDir string
33	tempDir   string
34	resetFn   func()
35}
36
37func newTester(t *testing.T) *tester {
38	t.Helper()
39	sourceDir := "."
40	if d := os.Getenv("GOPASS_TEST_DIR"); d != "" {
41		sourceDir = d
42	}
43
44	gopassBin := ""
45	if b := os.Getenv("GOPASS_BINARY"); b != "" {
46		gopassBin = b
47	}
48	fi, err := os.Stat(gopassBin)
49	if err != nil {
50		t.Skipf("Failed to stat GOPASS_BINARY %s: %s", gopassBin, err)
51	}
52	if !strings.HasSuffix(gopassBin, ".exe") && fi.Mode()&0111 == 0 {
53		t.Fatalf("GOPASS_BINARY is not executeable")
54	}
55	t.Logf("Using gopass binary: %s", gopassBin)
56
57	ts := &tester{
58		t:         t,
59		sourceDir: sourceDir,
60		Binary:    gopassBin,
61	}
62	// create tempDir
63	td, err := os.MkdirTemp("", "gopass-")
64	require.NoError(t, err)
65
66	t.Logf("Tempdir: %s", td)
67	ts.tempDir = td
68
69	// prepare ENVIRONMENT
70	ts.resetFn = gptest.UnsetVars("GNUPGHOME", "GOPASS_DEBUG", "NO_COLOR", "GOPASS_CONFIG", "GOPASS_NO_NOTIFY", "GOPASS_HOMEDIR")
71	require.NoError(t, os.Setenv("GNUPGHOME", ts.gpgDir()))
72	require.NoError(t, os.Setenv("GOPASS_DEBUG", ""))
73	require.NoError(t, os.Setenv("NO_COLOR", "true"))
74	require.NoError(t, os.Setenv("GOPASS_CONFIG", ts.gopassConfig()))
75	require.NoError(t, os.Setenv("GOPASS_NO_NOTIFY", "true"))
76	require.NoError(t, os.Setenv("GOPASS_HOMEDIR", td))
77
78	// write config
79	require.NoError(t, os.MkdirAll(filepath.Dir(ts.gopassConfig()), 0700))
80	// we need to set the root path to something else than the root directory otherwise the mounts will show as regular entries
81	if err := os.WriteFile(ts.gopassConfig(), []byte(gopassConfig+"\npath: "+ts.storeDir("root")+"\n"), 0600); err != nil {
82		t.Fatalf("Failed to write gopass config to %s: %s", ts.gopassConfig(), err)
83	}
84
85	// copy gpg test files
86	files := map[string]string{
87		filepath.Join(ts.sourceDir, "can", "gnupg", "pubring.gpg"): filepath.Join(ts.gpgDir(), "pubring.gpg"),
88		filepath.Join(ts.sourceDir, "can", "gnupg", "random_seed"): filepath.Join(ts.gpgDir(), "random_seed"),
89		filepath.Join(ts.sourceDir, "can", "gnupg", "secring.gpg"): filepath.Join(ts.gpgDir(), "secring.gpg"),
90		filepath.Join(ts.sourceDir, "can", "gnupg", "trustdb.gpg"): filepath.Join(ts.gpgDir(), "trustdb.gpg"),
91	}
92	for from, to := range files {
93		buf, err := os.ReadFile(from)
94		require.NoError(t, err, "Failed to read file %s", from)
95
96		err = os.MkdirAll(filepath.Dir(to), 0700)
97		require.NoError(t, err, "Failed to create dir for %s", to)
98
99		err = os.WriteFile(to, buf, 0600)
100		require.NoError(t, err, "Failed to write file %s", to)
101	}
102
103	return ts
104}
105
106func (ts tester) gpgDir() string {
107	return filepath.Join(ts.tempDir, ".gnupg")
108}
109
110func (ts tester) gopassConfig() string {
111	return filepath.Join(ts.tempDir, ".config", "gopass", "config.yml")
112}
113
114func (ts tester) storeDir(mount string) string {
115	return filepath.Join(ts.tempDir, ".local", "share", "gopass", "stores", mount)
116}
117
118func (ts tester) workDir() string {
119	return filepath.Dir(ts.tempDir)
120}
121
122func (ts tester) teardown() {
123	ts.resetFn() // restore env vars
124	if ts.tempDir == "" {
125		return
126	}
127	err := os.RemoveAll(ts.tempDir)
128	require.NoError(ts.t, err)
129}
130
131func (ts tester) runCmd(args []string, in []byte) (string, error) {
132	if len(args) < 1 {
133		return "", fmt.Errorf("no command")
134	}
135
136	cmd := exec.CommandContext(context.Background(), args[0], args[1:]...)
137	cmd.Dir = ts.workDir()
138	cmd.Stdin = bytes.NewReader(in)
139
140	ts.t.Logf("%+v", cmd.Args)
141
142	out, err := cmd.CombinedOutput()
143	if err != nil {
144		return string(out), err
145	}
146
147	return strings.TrimSpace(string(out)), nil
148}
149
150func (ts tester) run(arg string) (string, error) {
151	if runtime.GOOS == "windows" {
152		arg = strings.Replace(arg, "\\", "\\\\", -1)
153	}
154	args, err := shellquote.Split(arg)
155	if err != nil {
156		return "", err
157	}
158
159	cmd := exec.CommandContext(context.Background(), ts.Binary, args...)
160	cmd.Dir = ts.workDir()
161
162	ts.t.Logf("%+v", cmd.Args)
163
164	out, err := cmd.CombinedOutput()
165	if err != nil {
166		return string(out), err
167	}
168
169	return strings.TrimSpace(string(out)), nil
170}
171
172func (ts tester) runWithInput(arg, input string) ([]byte, error) {
173	reader := strings.NewReader(input)
174	return ts.runWithInputReader(arg, reader)
175}
176
177func (ts tester) runWithInputReader(arg string, input io.Reader) ([]byte, error) {
178	args, err := shellquote.Split(arg)
179	if err != nil {
180		return nil, err
181	}
182
183	cmd := exec.Command(ts.Binary, args...)
184	cmd.Dir = ts.workDir()
185	cmd.Stdin = input
186
187	ts.t.Logf("%+v", cmd.Args)
188
189	return cmd.CombinedOutput()
190}
191
192func (ts *tester) initStore() {
193	out, err := ts.run("init --crypto=gpgcli --storage=fs " + keyID)
194	require.NoError(ts.t, err, "failed to init password store:\n%s", out)
195}
196
197func (ts *tester) initSecrets(prefix string) {
198	out, err := ts.run("generate -p " + prefix + "foo/bar 20")
199	require.NoError(ts.t, err, "failed to generate password:\n%s", out)
200
201	out, err = ts.run("generate -p " + prefix + "baz 40")
202	require.NoError(ts.t, err, "failed to generate password:\n%s", out)
203
204	out, err = ts.runCmd([]string{ts.Binary, "insert", prefix + "fixed/secret"}, []byte("moar"))
205	require.NoError(ts.t, err, "failed to insert password:\n%s", out)
206
207	out, err = ts.runCmd([]string{ts.Binary, "insert", prefix + "fixed/twoliner"}, []byte("and\nmore stuff"))
208	require.NoError(ts.t, err, "failed to insert password:\n%s", out)
209}
210