Add backend resolver interface with file and env backends plus phase-3 stubs
Introduce the single Backend interface every credential source implements.
v0 ships two working backends: file (0600 KEY=VALUE env files, parsed in
pure Go, never sourced; looser permission masks refused before read) and
env (process-environment indirection). Bitwarden Secrets Manager and
HashiCorp Vault ship as explicit not-implemented stubs behind the same
interface so the phase-3 connectors are drop-ins. All failures are typed
ResolveErrors carrying only the ref, backend, and a fixed reason enum —
never material.
💘 Generated with Crush
Assisted-by: Crush:glm-5.2
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
package backend
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEnvResolve(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
set func(t *testing.T)
|
||||
ref Ref
|
||||
want string
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "set variable resolves",
|
||||
set: func(t *testing.T) { t.Setenv("KEYPROXY_TEST_VAR", testMaterial) },
|
||||
ref: Ref{Name: "mpk-env", Backend: "env", Source: "KEYPROXY_TEST_VAR"},
|
||||
want: testMaterial,
|
||||
},
|
||||
{
|
||||
name: "unset variable",
|
||||
set: func(t *testing.T) {},
|
||||
ref: Ref{Name: "mpk-env", Backend: "env", Source: "KEYPROXY_TEST_UNSET"},
|
||||
wantErr: "missing_key",
|
||||
},
|
||||
{
|
||||
name: "empty variable",
|
||||
set: func(t *testing.T) { t.Setenv("KEYPROXY_TEST_EMPTY", "") },
|
||||
ref: Ref{Name: "mpk-env", Backend: "env", Source: "KEYPROXY_TEST_EMPTY"},
|
||||
wantErr: "empty_value",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tt.set(t)
|
||||
e := NewEnv()
|
||||
got, err := e.Resolve(context.Background(), tt.ref)
|
||||
if tt.wantErr != "" {
|
||||
if err == nil {
|
||||
t.Fatalf("want error %q, got value", tt.wantErr)
|
||||
}
|
||||
msg := err.Error()
|
||||
if !strings.Contains(msg, tt.wantErr) {
|
||||
t.Fatalf("error %q does not contain %q", msg, tt.wantErr)
|
||||
}
|
||||
assertRedactedError(t, msg, tt.ref.Name, "env", testMaterial)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Fatalf("got %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStubsNotImplemented(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
backend Backend
|
||||
bname string
|
||||
}{
|
||||
{"bitwarden", NewBitwarden(), "bitwarden"},
|
||||
{"vault", NewVault(), "vault"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ref := Ref{Name: "mpk-stub", Backend: tt.bname, Source: "sm://x", Key: "K"}
|
||||
got, err := tt.backend.Resolve(context.Background(), ref)
|
||||
if err == nil {
|
||||
t.Fatalf("stub resolved to %q; want explicit not-implemented error", got)
|
||||
}
|
||||
if got != "" {
|
||||
t.Fatalf("stub returned non-empty value %q", got)
|
||||
}
|
||||
msg := err.Error()
|
||||
for _, want := range []string{"mpk-stub", tt.bname, "not_implemented"} {
|
||||
if !strings.Contains(msg, want) {
|
||||
t.Fatalf("stub error %q must contain %q", msg, want)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegistry(t *testing.T) {
|
||||
r := NewRegistry(NewFile(), NewEnv(), NewBitwarden(), NewVault())
|
||||
if got := r.Names(); strings.Join(got, ",") != "bitwarden,env,file,vault" {
|
||||
t.Fatalf("names = %v", got)
|
||||
}
|
||||
if _, ok := r.Get("nope"); ok {
|
||||
t.Fatal("unknown backend must not resolve")
|
||||
}
|
||||
if b, ok := r.Get("file"); !ok || b.Name() != "file" {
|
||||
t.Fatal("file backend must resolve")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user