package secretstore import ( "errors" "testing" "github.com/99designs/keyring" ) type stubKeyring struct { items map[string]keyring.Item } func (s *stubKeyring) Get(key string) (keyring.Item, error) { item, ok := s.items[key] if !ok { return keyring.Item{}, keyring.ErrKeyNotFound } return item, nil } func (s *stubKeyring) GetMetadata(key string) (keyring.Metadata, error) { item, err := s.Get(key) if err != nil { return keyring.Metadata{}, err } return keyring.Metadata{Item: &item}, nil } func (s *stubKeyring) Set(item keyring.Item) error { if s.items == nil { s.items = map[string]keyring.Item{} } s.items[item.Key] = item return nil } func (s *stubKeyring) Remove(key string) error { delete(s.items, key) return nil } func (s *stubKeyring) Keys() ([]string, error) { keys := make([]string, 0, len(s.items)) for key := range s.items { keys = append(keys, key) } return keys, nil } func withKeyringHooks( t *testing.T, available []keyring.BackendType, opener func(cfg keyring.Config) (keyring.Keyring, error), ) { t.Helper() prevAvailable := availableKeyringPolicy prevOpen := openKeyring availableKeyringPolicy = func() []keyring.BackendType { return available } openKeyring = opener t.Cleanup(func() { availableKeyringPolicy = prevAvailable openKeyring = prevOpen }) } func TestOpenRejectsInvalidPolicy(t *testing.T) { _, err := Open(Options{ ServiceName: "mcp-framework-test", BackendPolicy: BackendPolicy("invalid"), }) if err == nil { t.Fatal("expected error") } if !errors.Is(err, ErrInvalidBackendPolicy) { t.Fatalf("error = %v, want ErrInvalidBackendPolicy", err) } } func TestOpenAutoUsesAvailableKeyringBackends(t *testing.T) { var gotAllowed []keyring.BackendType ring := &stubKeyring{} withKeyringHooks(t, []keyring.BackendType{ keyring.SecretServiceBackend, keyring.KWalletBackend, }, func(cfg keyring.Config) (keyring.Keyring, error) { gotAllowed = append([]keyring.BackendType(nil), cfg.AllowedBackends...) return ring, nil }) store, err := Open(Options{ ServiceName: "mcp-framework-test", BackendPolicy: BackendAuto, }) if err != nil { t.Fatalf("Open returned error: %v", err) } if err := store.SetSecret("token", "API token", "secret-value"); err != nil { t.Fatalf("SetSecret returned error: %v", err) } value, err := store.GetSecret("token") if err != nil { t.Fatalf("GetSecret returned error: %v", err) } if value != "secret-value" { t.Fatalf("GetSecret = %q, want secret-value", value) } if len(gotAllowed) != 2 { t.Fatalf("allowed backends = %v, want two entries", gotAllowed) } } func TestOpenAutoFallsBackToEnvironmentWhenNoKeyringBackendIsAvailable(t *testing.T) { withKeyringHooks(t, nil, func(cfg keyring.Config) (keyring.Keyring, error) { t.Fatal("unexpected keyring open call") return nil, nil }) store, err := Open(Options{ ServiceName: "mcp-framework-test", BackendPolicy: BackendAuto, LookupEnv: func(name string) (string, bool) { if name == "EMAIL_CREDENTIALS" { return `{"username":"alice"}`, true } return "", false }, }) if err != nil { t.Fatalf("Open returned error: %v", err) } value, err := store.GetSecret("EMAIL_CREDENTIALS") if err != nil { t.Fatalf("GetSecret returned error: %v", err) } if value != `{"username":"alice"}` { t.Fatalf("GetSecret = %q", value) } } func TestOpenKeyringAnyReturnsExplicitUnavailableError(t *testing.T) { withKeyringHooks(t, []keyring.BackendType{keyring.SecretServiceBackend}, func(cfg keyring.Config) (keyring.Keyring, error) { t.Fatal("unexpected keyring open call") return nil, nil }) _, err := Open(Options{ ServiceName: "mcp-framework-test", BackendPolicy: BackendKWalletOnly, }) if err == nil { t.Fatal("expected error") } var backendErr *BackendUnavailableError if !errors.As(err, &backendErr) { t.Fatalf("error = %v, want BackendUnavailableError", err) } if !errors.Is(err, ErrBackendUnavailable) { t.Fatalf("error = %v, want ErrBackendUnavailable", err) } if backendErr.Required != "kwallet" { t.Fatalf("required backend = %q, want kwallet", backendErr.Required) } } func TestOpenEnvOnlyIsReadOnlyAndUsesLookupEnv(t *testing.T) { store, err := Open(Options{ BackendPolicy: BackendEnvOnly, LookupEnv: func(name string) (string, bool) { if name == "API_TOKEN" { return "from-env", true } return "", false }, }) if err != nil { t.Fatalf("Open returned error: %v", err) } value, err := store.GetSecret("API_TOKEN") if err != nil { t.Fatalf("GetSecret returned error: %v", err) } if value != "from-env" { t.Fatalf("GetSecret = %q, want from-env", value) } err = store.SetSecret("API_TOKEN", "API token", "new-value") if !errors.Is(err, ErrReadOnly) { t.Fatalf("SetSecret error = %v, want ErrReadOnly", err) } err = store.DeleteSecret("API_TOKEN") if !errors.Is(err, ErrReadOnly) { t.Fatalf("DeleteSecret error = %v, want ErrReadOnly", err) } } func TestJSONHelpersRoundTrip(t *testing.T) { ring := &stubKeyring{} withKeyringHooks(t, []keyring.BackendType{keyring.SecretServiceBackend}, func(cfg keyring.Config) (keyring.Keyring, error) { return ring, nil }) store, err := Open(Options{ ServiceName: "mcp-framework-test", }) if err != nil { t.Fatalf("Open returned error: %v", err) } type credentials struct { Host string `json:"host"` Username string `json:"username"` Password string `json:"password"` } input := credentials{ Host: "imap.example.com", Username: "alice", Password: "s3cr3t", } if err := SetJSON(store, "imap-credentials", "IMAP credentials", input); err != nil { t.Fatalf("SetJSON returned error: %v", err) } output, err := GetJSON[credentials](store, "imap-credentials") if err != nil { t.Fatalf("GetJSON returned error: %v", err) } if output != input { t.Fatalf("GetJSON = %#v, want %#v", output, input) } } func TestEffectiveBackendPolicyReportsConcreteBackend(t *testing.T) { t.Run("env-only", func(t *testing.T) { store, err := Open(Options{ BackendPolicy: BackendEnvOnly, LookupEnv: func(string) (string, bool) { return "", false }, }) if err != nil { t.Fatalf("Open returned error: %v", err) } if got := EffectiveBackendPolicy(store); got != BackendEnvOnly { t.Fatalf("EffectiveBackendPolicy = %q, want %q", got, BackendEnvOnly) } }) t.Run("keyring", func(t *testing.T) { withKeyringHooks(t, []keyring.BackendType{keyring.SecretServiceBackend}, func(cfg keyring.Config) (keyring.Keyring, error) { return &stubKeyring{}, nil }) store, err := Open(Options{ ServiceName: "mcp-framework-test", BackendPolicy: BackendAuto, }) if err != nil { t.Fatalf("Open returned error: %v", err) } if got := EffectiveBackendPolicy(store); got != BackendKeyringAny { t.Fatalf("EffectiveBackendPolicy = %q, want %q", got, BackendKeyringAny) } }) } func TestSetSecretVerifiedWritesThenReadsBack(t *testing.T) { ring := &stubKeyring{} withKeyringHooks(t, []keyring.BackendType{keyring.SecretServiceBackend}, func(cfg keyring.Config) (keyring.Keyring, error) { return ring, nil }) store, err := Open(Options{ ServiceName: "mcp-framework-test", BackendPolicy: BackendAuto, }) if err != nil { t.Fatalf("Open returned error: %v", err) } if err := SetSecretVerified(store, "token", "API token", "secret-value"); err != nil { t.Fatalf("SetSecretVerified returned error: %v", err) } } func TestSetSecretVerifiedFailsOnReadBackMismatch(t *testing.T) { store := &mismatchSecretStore{} err := SetSecretVerified(store, "token", "API token", "secret-value") if err == nil { t.Fatal("expected error") } if !errors.Is(err, ErrNotFound) { t.Fatalf("error = %v, want wrapped ErrNotFound", err) } if store.setCalls != 1 { t.Fatalf("setCalls = %d, want 1", store.setCalls) } } type mismatchSecretStore struct { setCalls int } func (s *mismatchSecretStore) SetSecret(name, label, secret string) error { s.setCalls++ return nil } func (s *mismatchSecretStore) GetSecret(name string) (string, error) { return "", ErrNotFound } func (s *mismatchSecretStore) DeleteSecret(name string) error { return nil }