gopass/internal/action/generate_test.go
Dominik Schulz 54603d4e2c
[chore] Add tests and comments for hasPwRuleForSecret (#3162)
Mostly to remind myself how those are supposed to operate
and document it for my future self.

The lookup part is a little bit error prone, i.e. if the rules for
these domains change the test will break. But I promise to take
care of that once that happens for the first time.

Signed-off-by: Dominik Schulz <dominik.schulz@gauner.org>
2025-05-22 21:05:18 +02:00

446 lines
12 KiB
Go

package action
import (
"bytes"
"flag"
"fmt"
"os"
"regexp"
"runtime"
"strings"
"testing"
"github.com/fatih/color"
"github.com/gopasspw/gopass/internal/config"
"github.com/gopasspw/gopass/internal/out"
"github.com/gopasspw/gopass/pkg/ctxutil"
"github.com/gopasspw/gopass/pkg/pwgen/pwrules"
"github.com/gopasspw/gopass/tests/gptest"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v2"
)
func TestRuleLookup(t *testing.T) {
domain, _ := hasPwRuleForSecret(config.NewContextInMemory(), "foo/gopass.pw")
assert.Empty(t, domain)
}
func TestGenerate(t *testing.T) {
u := gptest.NewUnitTester(t)
ctx := t.Context()
ctx = ctxutil.WithAlwaysYes(ctx, true)
ctx = ctxutil.WithInteractive(ctx, false)
act, err := newMock(ctx, u.StoreDir(""))
require.NoError(t, err)
require.NotNil(t, act)
ctx = act.cfg.WithConfig(ctx)
require.NoError(t, act.cfg.Set("", "generate.autoclip", "false"))
buf := &bytes.Buffer{}
out.Stdout = buf
out.Stderr = buf
defer func() {
out.Stdout = os.Stdout
out.Stderr = os.Stderr
}()
color.NoColor = true
// generate
t.Run("generate", func(t *testing.T) {
require.Error(t, act.Generate(gptest.CliCtx(ctx, t)))
buf.Reset()
})
// generate foobar
t.Run("generate foobar", func(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode.")
}
require.NoError(t, act.Generate(gptest.CliCtx(ctx, t, "foobar")))
buf.Reset()
})
// generate foobar
// should succeed because of always yes
t.Run("generate foobar again", func(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode.")
}
require.NoError(t, act.Generate(gptest.CliCtx(ctx, t, "foobar")))
buf.Reset()
})
// generate --edit foobar
t.Run("generate --edit foobar", func(t *testing.T) {
if testing.Short() || runtime.GOOS == "windows" {
t.Skip("skipping test in short mode.")
}
require.NoError(t, act.Generate(gptest.CliCtxWithFlags(ctx, t, map[string]string{"edit": "true", "editor": "/usr/bin/env cat"}, "foobar")))
buf.Reset()
})
// generate --force foobar
t.Run("generate --force foobar", func(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode.")
}
require.NoError(t, act.Generate(gptest.CliCtxWithFlags(ctx, t, map[string]string{"force": "true"}, "foobar")))
buf.Reset()
})
// generate --force foobar 32
t.Run("generate --force foobar 32", func(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode.")
}
require.NoError(t, act.Generate(gptest.CliCtxWithFlags(ctx, t, map[string]string{"force": "true"}, "foobar", "32")))
buf.Reset()
})
// generate --force --symbols foobar 32
t.Run("generate --force --symbols foobar 32", func(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode.")
}
require.NoError(t, act.Generate(gptest.CliCtxWithFlags(ctx, t, map[string]string{"force": "true", "print": "true", "symbols": "true"}, "foobar", "32")))
passIsAlphaNum(t, buf.String(), false)
buf.Reset()
})
// generate --force --symbols=false foobar 32
t.Run("generate --force --symbols=False foobar 32", func(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode.")
}
require.NoError(t, act.Generate(gptest.CliCtxWithFlags(ctx, t, map[string]string{"force": "true", "print": "true", "symbols": "false"}, "foobar", "32")))
passIsAlphaNum(t, buf.String(), true)
buf.Reset()
})
// generate --force --xkcd foobar 32
t.Run("generate --force --xkcd foobar 32", func(t *testing.T) {
require.NoError(t, act.Generate(gptest.CliCtxWithFlags(ctx, t, map[string]string{"force": "true", "xkcd": "true", "lang": "en"}, "foobar", "32")))
buf.Reset()
})
// generate --force --xkcd foobar baz 32
t.Run("generate --force --xkcd foobar baz 32", func(t *testing.T) {
require.NoError(t, act.Generate(gptest.CliCtxWithFlags(ctx, t, map[string]string{"force": "true", "xkcd": "true", "lang": "en"}, "foobar", "baz", "32")))
buf.Reset()
})
// generate --force --xkcd foobar baz
t.Run("generate --force --xkcd foobar baz", func(t *testing.T) {
require.NoError(t, act.Generate(gptest.CliCtxWithFlags(ctx, t, map[string]string{"force": "true", "xkcd": "true", "lang": "en"}, "foobar", "baz")))
buf.Reset()
})
// generate --force --xkcd --print foobar baz
t.Run("generate --force --xkcd --print foobar baz", func(t *testing.T) {
require.NoError(t, act.Generate(gptest.CliCtxWithFlags(ctx, t, map[string]string{"force": "true", "xkcd": "true", "print": "true", "lang": "en"}, "foobar", "baz")))
buf.Reset()
})
// generate --force foobar 24 w/ autoclip and output redirection
t.Run("generate --force foobar 24", func(t *testing.T) {
ov := act.cfg.Get("generate.autoclip")
defer func() {
require.NoError(t, act.cfg.Set("", "generate.autoclip", ov))
}()
require.NoError(t, act.cfg.Set("", "generate.autoclip", "true"))
ctx := ctxutil.WithTerminal(ctx, false)
require.NoError(t, act.Generate(gptest.CliCtxWithFlags(ctx, t, map[string]string{"force": "true"}, "foobar", "24")))
assert.Contains(t, buf.String(), "Not printing secrets by default")
buf.Reset()
})
// generate --force foobar 24 w/ autoclip and no output redirection
t.Run("generate --force foobar 24", func(t *testing.T) {
ov := act.cfg.Get("generate.autoclip")
defer func() {
require.NoError(t, act.cfg.Set("", "generate.autoclip", ov))
}()
require.NoError(t, act.cfg.Set("", "generate.autoclip", "true"))
ctx := ctxutil.WithTerminal(ctx, true)
require.NoError(t, act.Generate(gptest.CliCtxWithFlags(ctx, t, map[string]string{"force": "true"}, "foobar", "24")))
assert.Contains(t, buf.String(), "Copied to clipboard")
buf.Reset()
})
// generate --force foobar w/ pw length set via env variable (42 chars)
t.Run("generate --force foobar", func(t *testing.T) {
t.Setenv("GOPASS_PW_DEFAULT_LENGTH", "42")
require.NoError(t, act.Generate(gptest.CliCtxWithFlags(ctx, t, map[string]string{"force": "true", "print": "true", "symbols": "false"}, "foobar")))
lines := strings.Split(strings.TrimSpace(buf.String()), "\n")
assert.Len(t, lines[3], 42)
buf.Reset()
})
// generate --force foobar w/ pw length set via env variable to invalid value, fallback mechanism
t.Run("generate --force foobar", func(t *testing.T) {
t.Setenv("GOPASS_PW_DEFAULT_LENGTH", "0")
if testing.Short() {
t.Skip("skipping test in short mode.")
}
require.NoError(t, act.Generate(gptest.CliCtxWithFlags(ctx, t, map[string]string{"force": "true", "print": "true", "symbols": "false"}, "foobar")))
lines := strings.Split(strings.TrimSpace(buf.String()), "\n")
assert.Len(t, lines[3], 24) // 24 = default value used as fallback
buf.Reset()
})
}
func passIsAlphaNum(t *testing.T, buf string, want bool) {
t.Helper()
reAlphaNum := regexp.MustCompile(`^[A-Za-z0-9]+$`)
lines := strings.Split(strings.TrimSpace(buf), "\n")
if len(lines) < 1 {
t.Errorf("buffer empty (no lines)")
}
line := strings.TrimSpace(lines[len(lines)-1])
if reAlphaNum.MatchString(line) != want {
t.Errorf("buffer did not match alpha num re: %s (%s)", line, buf)
}
}
func TestKeyAndLength(t *testing.T) {
for _, tc := range []struct {
in []string
key string
length string
}{
{
in: []string{"32"},
key: "",
length: "32",
},
{
in: []string{"baz"},
key: "baz",
length: "",
},
{
in: []string{"baz", "32"},
key: "baz",
length: "32",
},
{
in: []string{},
key: "",
length: "",
},
} {
t.Run(fmt.Sprintf("%v", tc.in), func(t *testing.T) {
app := cli.NewApp()
fs := flag.NewFlagSet("default", flag.ContinueOnError)
require.NoError(t, fs.Parse(append([]string{"foobar"}, tc.in...)))
c := cli.NewContext(app, fs, nil)
args, _ := parseArgs(c)
k, l := keyAndLength(args)
assert.Equal(t, tc.key, k, "Key from %+v", tc.in)
assert.Equal(t, tc.length, l, "Length from %+v", tc.in)
})
}
}
func TestExtractEmails(t *testing.T) {
for _, tc := range []struct {
in []string
out []string
}{
{
out: []string{},
},
{
in: []string{"some/mount/gmail.com/john.doe@example.org", "example.com/user@example.org"},
out: []string{"john.doe@example.org", "user@example.org"},
},
} {
t.Run(fmt.Sprintf("%v", tc.in), func(t *testing.T) {
assert.Equal(t, tc.out, extractEmails(tc.in))
})
}
}
func TestExtractDomains(t *testing.T) {
for _, tc := range []struct {
in []string
out []string
}{
{
out: []string{},
},
{
in: []string{"websites/gmail.com", "live.com", "some/mount/websites/web.de"},
out: []string{"gmail.com", "live.com", "web.de"},
},
} {
t.Run(fmt.Sprintf("%v", tc.in), func(t *testing.T) {
assert.Equal(t, tc.out, extractDomains(tc.in))
})
}
}
func TestUniq(t *testing.T) {
for _, tc := range []struct {
in []string
out []string
}{
{
out: []string{},
},
{
in: []string{"foo", "foo", "bar"},
out: []string{"bar", "foo"},
},
} {
t.Run(fmt.Sprintf("%v", tc.in), func(t *testing.T) {
assert.Equal(t, tc.out, uniq(tc.in))
})
}
}
func TestFilterPrefix(t *testing.T) {
for _, tc := range []struct {
in []string
prefix string
out []string
}{
{
out: []string{},
},
{
in: []string{"foo", "bar", "baz"},
prefix: "foo",
out: []string{"foo"},
},
{
in: []string{"foo/bar", "foo/baz", "bar/foo"},
prefix: "foo/",
out: []string{"foo/bar", "foo/baz"},
},
} {
t.Run(fmt.Sprintf("%v", tc.in), func(t *testing.T) {
assert.Equal(t, tc.out, filterPrefix(tc.in, tc.prefix))
})
}
}
// NOTE: Do not use t.Parallel because environment variables are being used
// which can leak into other tests that run in parallel.
func TestDefaultLengthFromEnv(t *testing.T) {
const pwLengthEnvName = "GOPASS_PW_DEFAULT_LENGTH"
ctx := config.NewContextInMemory()
t.Run("use default value if no environment variable is set", func(t *testing.T) {
actual, isCustom := config.DefaultPasswordLengthFromEnv(ctx)
expected := config.DefaultPasswordLength
assert.Equal(t, expected, actual)
assert.False(t, isCustom)
})
t.Run("interpretetion of various inputs for environment variable", func(t *testing.T) {
for _, tc := range []struct {
in string
expected int
custom bool
}{
{in: "42", expected: 42, custom: true},
{in: "1", expected: 1, custom: true},
{in: "0", expected: config.DefaultPasswordLength, custom: false},
{in: "abc", expected: config.DefaultPasswordLength, custom: false},
{in: "-1", expected: config.DefaultPasswordLength, custom: false},
} {
t.Setenv(pwLengthEnvName, tc.in)
actual, isCustom := config.DefaultPasswordLengthFromEnv(ctx)
assert.Equal(t, tc.expected, actual)
assert.Equal(t, isCustom, tc.custom)
}
})
}
func TestHasPwRuleForSecret(t *testing.T) {
ctx := t.Context()
wantRule := pwrules.Rule{
Minlen: 8,
Maxlen: 63,
Required: []string{"digit", "lower", "upper"},
Allowed: []string{"ascii-printable"},
Exact: false,
}
for _, tc := range []struct {
name string
input string
domain string
want pwrules.Rule
}{
{
name: "domain only",
input: "websites/apple.com",
domain: "apple.com",
want: wantRule,
},
{
name: "domain and username",
input: "websites/apple.com/gopass",
domain: "apple.com",
want: wantRule,
},
{
name: "domain and email",
input: "websites/apple.com/gopass@gopass.pw",
domain: "apple.com",
want: wantRule,
},
{
name: "domain and user that looks like a domain",
input: "websites/apple.com/gopass.pw",
domain: "apple.com",
want: wantRule,
},
{
name: "empty input",
input: "",
domain: "",
want: pwrules.Rule{},
},
{
// domains are search for starting from the end of the string, so
// the the further right the domain is, the more specific it is.
name: "double domains",
input: "websites/apple.com/google.com",
domain: "google.com",
want: pwrules.Rule{
Minlen: 8,
Maxlen: 0,
Required: []string{},
Allowed: []string{"", "digit", "lower", "upper"},
Maxconsec: 0,
Exact: false,
},
},
} {
t.Run(tc.name, func(t *testing.T) {
domain, rule := hasPwRuleForSecret(ctx, tc.input)
assert.Equal(t, tc.domain, domain, tc.name)
assert.Equal(t, tc.want, rule, tc.name)
})
}
}