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