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,116 @@
|
||||
package backend
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseEnvFile(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
in string
|
||||
want map[string]string
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "simple lines",
|
||||
in: "A=1\nB=two words\n",
|
||||
want: map[string]string{"A": "1", "B": "two words"},
|
||||
},
|
||||
{
|
||||
name: "comments and blanks",
|
||||
in: "# header\n\nA=1\n # indented comment\nB=2\n",
|
||||
want: map[string]string{"A": "1", "B": "2"},
|
||||
},
|
||||
{
|
||||
name: "export prefix",
|
||||
in: "export A=1\nexport\tB=2\n",
|
||||
want: map[string]string{"A": "1", "B": "2"},
|
||||
},
|
||||
{
|
||||
name: "quoted values stripped",
|
||||
in: "A=\"1 2\"\nB='3 4'\n",
|
||||
want: map[string]string{"A": "1 2", "B": "3 4"},
|
||||
},
|
||||
{
|
||||
name: "spaces around key and value trimmed",
|
||||
in: " A = 1 \n",
|
||||
want: map[string]string{"A": "1"},
|
||||
},
|
||||
{
|
||||
name: "inline comment after value",
|
||||
in: "A=1 # trailing\nB=\"2 # kept\"\n",
|
||||
want: map[string]string{"A": "1", "B": "2 # kept"},
|
||||
},
|
||||
{
|
||||
name: "equals inside value",
|
||||
in: "A=b=c\n",
|
||||
want: map[string]string{"A": "b=c"},
|
||||
},
|
||||
{
|
||||
name: "crlf line endings",
|
||||
in: "A=1\r\nB=2\r\n",
|
||||
want: map[string]string{"A": "1", "B": "2"},
|
||||
},
|
||||
{
|
||||
name: "later duplicate wins",
|
||||
in: "A=first\nA=second\n",
|
||||
want: map[string]string{"A": "second"},
|
||||
},
|
||||
{
|
||||
name: "line without equals",
|
||||
in: "SECRET_MATERIAL_NO_EQUALS\n",
|
||||
wantErr: "line 1: malformed",
|
||||
},
|
||||
{
|
||||
name: "illegal key character",
|
||||
in: "A-B=supersecret\n",
|
||||
wantErr: "line 1: malformed",
|
||||
},
|
||||
{
|
||||
name: "key starting with digit",
|
||||
in: "1KEY=supersecret\n",
|
||||
wantErr: "line 1: malformed",
|
||||
},
|
||||
{
|
||||
name: "empty key",
|
||||
in: "=value\n",
|
||||
wantErr: "line 1: malformed",
|
||||
},
|
||||
{
|
||||
name: "line number reported",
|
||||
in: "A=1\nB=2\nbroken line here\n",
|
||||
wantErr: "line 3: malformed",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := parseEnvFile([]byte(tt.in))
|
||||
if tt.wantErr != "" {
|
||||
if err == nil {
|
||||
t.Fatalf("want error %q, got nil (parsed %v)", tt.wantErr, got)
|
||||
}
|
||||
if !strings.Contains(err.Error(), tt.wantErr) {
|
||||
t.Fatalf("error %q does not contain %q", err, tt.wantErr)
|
||||
}
|
||||
// Parse errors carry line numbers only — never line
|
||||
// contents (contents may be material).
|
||||
if strings.Contains(err.Error(), "supersecret") || strings.Contains(err.Error(), "SECRET_MATERIAL") {
|
||||
t.Fatalf("parse error leaks line contents: %q", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(got) != len(tt.want) {
|
||||
t.Fatalf("got %v, want %v", got, tt.want)
|
||||
}
|
||||
for k, v := range tt.want {
|
||||
if got[k] != v {
|
||||
t.Fatalf("key %s: got %q, want %q", k, got[k], v)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user