261 lines
8.4 KiB
Go
261 lines
8.4 KiB
Go
package main
|
|
|
|
import (
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.unkin.net/unkin/vault-tools/shared"
|
|
)
|
|
|
|
// setupVctx points XDG dirs at temp locations, writes a config file and the
|
|
// given cached tokens, and puts a fake `vault` binary on PATH. It returns the
|
|
// fake vault's absolute path.
|
|
func setupVctx(t *testing.T, cfg string, tokens map[string]shared.Token) string {
|
|
t.Helper()
|
|
cfgHome := t.TempDir()
|
|
t.Setenv("XDG_CONFIG_HOME", cfgHome)
|
|
t.Setenv("XDG_CACHE_HOME", t.TempDir())
|
|
|
|
cfgDir := filepath.Join(cfgHome, "vcm")
|
|
if err := os.MkdirAll(cfgDir, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(cfgDir, "config.yaml"), []byte(cfg), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for name, tok := range tokens {
|
|
tk := tok
|
|
tk.Context = name
|
|
if err := shared.SaveToken(&tk); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
binDir := t.TempDir()
|
|
vault := filepath.Join(binDir, "vault")
|
|
if err := os.WriteFile(vault, []byte("#!/bin/sh\nexit 0\n"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Setenv("PATH", binDir+string(os.PathListSeparator)+os.Getenv("PATH"))
|
|
return vault
|
|
}
|
|
|
|
const vctxConfig = `
|
|
contexts:
|
|
sydney:
|
|
address: https://vault.syd1.au.unkin.net
|
|
staging/sydney:
|
|
address: https://vault-staging.syd1.au.unkin.net
|
|
namespace: staging
|
|
`
|
|
|
|
// envVal returns key's value from an env slice, failing the test if the key
|
|
// appears more than once (Go's os/exec children honour the first occurrence).
|
|
func envVal(t *testing.T, env []string, key string) (string, bool) {
|
|
t.Helper()
|
|
val, n := "", 0
|
|
for _, e := range env {
|
|
if strings.HasPrefix(e, key+"=") {
|
|
val, n = e[len(key)+1:], n+1
|
|
}
|
|
}
|
|
if n > 1 {
|
|
t.Errorf("%s appears %d times in env %v", key, n, env)
|
|
}
|
|
return val, n > 0
|
|
}
|
|
|
|
func TestBuildInvocationArgsAndEnvOverride(t *testing.T) {
|
|
vault := setupVctx(t, vctxConfig, map[string]shared.Token{
|
|
"sydney": {Address: "https://vault.syd1.au.unkin.net", Token: "s.SYDTOK"},
|
|
})
|
|
|
|
// Ambient VAULT_ADDR/VAULT_TOKEN must be overridden by the context's values.
|
|
baseEnv := []string{"HOME=/home/x", "VAULT_ADDR=ambient", "VAULT_TOKEN=ambient", "VAULT_NAMESPACE=ambient"}
|
|
args := []string{"kv", "get", "-field=foo", "secret/x"}
|
|
|
|
bin, argv, env, err := buildInvocation("sydney", "", args, baseEnv)
|
|
if err != nil {
|
|
t.Fatalf("buildInvocation: %v", err)
|
|
}
|
|
if bin != vault {
|
|
t.Errorf("bin = %q, want %q", bin, vault)
|
|
}
|
|
// argv is vault + the untouched args (vault flags like -field pass through).
|
|
wantArgv := append([]string{vault}, args...)
|
|
if !reflect.DeepEqual(argv, wantArgv) {
|
|
t.Errorf("argv = %v, want %v", argv, wantArgv)
|
|
}
|
|
if v, _ := envVal(t, env, "VAULT_ADDR"); v != "https://vault.syd1.au.unkin.net" {
|
|
t.Errorf("VAULT_ADDR = %q, want context override to win", v)
|
|
}
|
|
if v, _ := envVal(t, env, "VAULT_TOKEN"); v != "s.SYDTOK" {
|
|
t.Errorf("VAULT_TOKEN = %q, want s.SYDTOK", v)
|
|
}
|
|
if _, ok := envVal(t, env, "VAULT_NAMESPACE"); ok {
|
|
t.Error("VAULT_NAMESPACE should be unset for a context with no namespace")
|
|
}
|
|
// The ambient HOME must be preserved.
|
|
if v, _ := envVal(t, env, "HOME"); v != "/home/x" {
|
|
t.Errorf("HOME = %q, base env not preserved", v)
|
|
}
|
|
}
|
|
|
|
func TestBuildInvocationNamespaceFromContext(t *testing.T) {
|
|
setupVctx(t, vctxConfig, map[string]shared.Token{
|
|
"staging/sydney": {Address: "https://vault-staging.syd1.au.unkin.net", Namespace: "staging", Token: "s.X"},
|
|
})
|
|
_, _, env, err := buildInvocation("staging/sydney", "", []string{"token", "lookup"}, nil)
|
|
if err != nil {
|
|
t.Fatalf("buildInvocation: %v", err)
|
|
}
|
|
if v, _ := envVal(t, env, "VAULT_NAMESPACE"); v != "staging" {
|
|
t.Errorf("VAULT_NAMESPACE = %q, want staging", v)
|
|
}
|
|
}
|
|
|
|
func TestBuildInvocationNamespaceFallsBackToToken(t *testing.T) {
|
|
// Context has no namespace, but the cached token records one — vctx should
|
|
// fall back to the token's namespace.
|
|
setupVctx(t, vctxConfig, map[string]shared.Token{
|
|
"sydney": {Address: "https://vault.syd1.au.unkin.net", Namespace: "from-token", Token: "s.X"},
|
|
})
|
|
_, _, env, err := buildInvocation("sydney", "", []string{"status"}, nil)
|
|
if err != nil {
|
|
t.Fatalf("buildInvocation: %v", err)
|
|
}
|
|
if v, _ := envVal(t, env, "VAULT_NAMESPACE"); v != "from-token" {
|
|
t.Errorf("VAULT_NAMESPACE = %q, want from-token fallback", v)
|
|
}
|
|
}
|
|
|
|
func TestBuildInvocationMissingToken(t *testing.T) {
|
|
setupVctx(t, vctxConfig, nil) // no cached tokens
|
|
_, _, _, err := buildInvocation("sydney", "", []string{"status"}, nil)
|
|
if err == nil {
|
|
t.Fatal("expected error for missing cached token")
|
|
}
|
|
if !errors.Is(err, os.ErrNotExist) {
|
|
t.Errorf("error should wrap os.ErrNotExist, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestBuildInvocationUnknownContext(t *testing.T) {
|
|
setupVctx(t, vctxConfig, nil)
|
|
if _, _, _, err := buildInvocation("nope", "", []string{"status"}, nil); err == nil {
|
|
t.Error("expected error for unknown context")
|
|
}
|
|
}
|
|
|
|
func TestRunVctxValidation(t *testing.T) {
|
|
if err := runVctx("", "", []string{"status"}); err == nil || !strings.Contains(err.Error(), "--context is required") {
|
|
t.Errorf("empty context: got %v", err)
|
|
}
|
|
if err := runVctx("sydney", "", nil); err == nil || !strings.Contains(err.Error(), "no vault arguments") {
|
|
t.Errorf("no args: got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRunVctxExecsWithBuiltInvocation(t *testing.T) {
|
|
vault := setupVctx(t, vctxConfig, map[string]shared.Token{
|
|
"sydney": {Address: "https://vault.syd1.au.unkin.net", Token: "s.SYDTOK"},
|
|
})
|
|
|
|
var gotBin string
|
|
var gotArgv, gotEnv []string
|
|
orig := execVault
|
|
execVault = func(bin string, argv, env []string) error {
|
|
gotBin, gotArgv, gotEnv = bin, argv, env
|
|
return nil
|
|
}
|
|
t.Cleanup(func() { execVault = orig })
|
|
|
|
if err := runVctx("sydney", "", []string{"kv", "list", "kv/"}); err != nil {
|
|
t.Fatalf("runVctx: %v", err)
|
|
}
|
|
if gotBin != vault {
|
|
t.Errorf("exec bin = %q, want %q", gotBin, vault)
|
|
}
|
|
if !reflect.DeepEqual(gotArgv, []string{vault, "kv", "list", "kv/"}) {
|
|
t.Errorf("exec argv = %v", gotArgv)
|
|
}
|
|
if v, _ := envVal(t, gotEnv, "VAULT_TOKEN"); v != "s.SYDTOK" {
|
|
t.Errorf("exec env VAULT_TOKEN = %q", v)
|
|
}
|
|
}
|
|
|
|
func TestRunVctxSurfacesExecError(t *testing.T) {
|
|
setupVctx(t, vctxConfig, map[string]shared.Token{
|
|
"sydney": {Address: "https://vault.syd1.au.unkin.net", Token: "s.X"},
|
|
})
|
|
orig := execVault
|
|
execVault = func(bin string, argv, env []string) error { return errors.New("boom") }
|
|
t.Cleanup(func() { execVault = orig })
|
|
|
|
err := runVctx("sydney", "", []string{"status"})
|
|
if err == nil || !strings.Contains(err.Error(), "exec vault") {
|
|
t.Errorf("expected wrapped exec error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestAutoFormat(t *testing.T) {
|
|
cases := []struct {
|
|
args []string
|
|
env []string
|
|
want string
|
|
}{
|
|
{[]string{"kv", "get", "-mount=secret", "foo"}, nil, "yaml"},
|
|
{[]string{"read", "aws/creds/role"}, nil, "yaml"},
|
|
{[]string{"write", "db/creds/x", "ttl=1h"}, nil, "yaml"},
|
|
{[]string{"kv", "list"}, nil, "table"},
|
|
{[]string{"kv", "list", "aws/creds/"}, nil, "table"},
|
|
{[]string{"status"}, nil, ""},
|
|
{[]string{"kv", "put", "kv/foo", "a=b"}, nil, ""},
|
|
{[]string{"kv", "get", "-format=json", "kv/foo"}, nil, ""},
|
|
{[]string{"read", "--format", "json", "aws/creds/role"}, nil, ""},
|
|
{[]string{"kv", "get", "-field=password", "kv/foo"}, nil, ""},
|
|
{[]string{"kv", "get", "--", "-format=json"}, nil, "yaml"},
|
|
{[]string{"kv", "get", "kv/foo"}, []string{"VAULT_FORMAT=json"}, ""},
|
|
{[]string{"kv", "get", "kv/foo"}, []string{"VAULT_FORMAT="}, "yaml"},
|
|
}
|
|
for _, c := range cases {
|
|
if got := autoFormat(c.args, c.env); got != c.want {
|
|
t.Errorf("autoFormat(%v, %v) = %q, want %q", c.args, c.env, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBuildInvocationFormat(t *testing.T) {
|
|
setupVctx(t, vctxConfig, map[string]shared.Token{
|
|
"sydney": {Address: "https://vault.syd1.au.unkin.net", Token: "s.X"},
|
|
})
|
|
ambient := []string{"VAULT_FORMAT=table"}
|
|
cases := []struct {
|
|
flag string
|
|
args []string
|
|
base []string
|
|
want string
|
|
set bool
|
|
}{
|
|
{"", []string{"kv", "get", "kv/foo"}, nil, "yaml", true},
|
|
{"", []string{"status"}, nil, "", false},
|
|
{"json", []string{"kv", "get", "kv/foo"}, nil, "json", true},
|
|
{"json", []string{"status"}, nil, "json", true},
|
|
{"", []string{"kv", "get", "kv/foo"}, ambient, "table", true},
|
|
{"json", []string{"kv", "get", "kv/foo"}, ambient, "json", true},
|
|
}
|
|
for _, c := range cases {
|
|
_, _, env, err := buildInvocation("sydney", c.flag, c.args, c.base)
|
|
if err != nil {
|
|
t.Fatalf("buildInvocation: %v", err)
|
|
}
|
|
if v, ok := envVal(t, env, "VAULT_FORMAT"); v != c.want || ok != c.set {
|
|
t.Errorf("flag %q args %v base %v: VAULT_FORMAT = %q (set %v), want %q (set %v)", c.flag, c.args, c.base, v, ok, c.want, c.set)
|
|
}
|
|
}
|
|
}
|