feat: add kwallet dbus client
This commit is contained in:
parent
3f75357c89
commit
14191cae1a
6 changed files with 607 additions and 2 deletions
4
go.mod
4
go.mod
|
|
@ -1,3 +1,7 @@
|
|||
module email-mcp
|
||||
|
||||
go 1.25.0
|
||||
|
||||
require github.com/godbus/dbus/v5 v5.2.2
|
||||
|
||||
require golang.org/x/sys v0.27.0 // indirect
|
||||
|
|
|
|||
4
go.sum
Normal file
4
go.sum
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
|
||||
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
|
||||
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
|
||||
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
|
|
@ -2,11 +2,13 @@ package cli
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"email-mcp/internal/secretstore"
|
||||
"email-mcp/internal/secretstore/kwallet"
|
||||
)
|
||||
|
||||
type MCPRunner interface {
|
||||
|
|
@ -68,7 +70,7 @@ func (a *App) runSetup(ctx context.Context) error {
|
|||
return err
|
||||
}
|
||||
if err := a.store.Save(ctx, secretstore.DefaultAccountKey, cred); err != nil {
|
||||
return err
|
||||
return mapAppError(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -77,5 +79,24 @@ func (a *App) runMCP(ctx context.Context) error {
|
|||
if a.runner == nil {
|
||||
return fmt.Errorf("mcp runner is not configured")
|
||||
}
|
||||
return a.runner.Run(ctx)
|
||||
return mapAppError(a.runner.Run(ctx))
|
||||
}
|
||||
|
||||
func mapAppError(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch {
|
||||
case errors.Is(err, kwallet.ErrKWalletUnavailable):
|
||||
return fmt.Errorf("kwallet is not available; make sure KDE Wallet is installed and your session D-Bus is running")
|
||||
case errors.Is(err, kwallet.ErrKWalletDisabled):
|
||||
return fmt.Errorf("kwallet is disabled in this KDE session")
|
||||
case errors.Is(err, kwallet.ErrKWalletOpenFailed):
|
||||
return fmt.Errorf("kwallet could not be opened; unlock the wallet and try again")
|
||||
case errors.Is(err, kwallet.ErrCredentialNotFound):
|
||||
return fmt.Errorf("credentials not configured; run `email-mcp setup`")
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
|
|
|||
64
internal/cli/app_task6_test.go
Normal file
64
internal/cli/app_task6_test.go
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"email-mcp/internal/secretstore"
|
||||
"email-mcp/internal/secretstore/kwallet"
|
||||
)
|
||||
|
||||
type errorStoreStub struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (s errorStoreStub) Save(context.Context, string, secretstore.Credential) error {
|
||||
return s.err
|
||||
}
|
||||
|
||||
func (s errorStoreStub) Load(context.Context, string) (secretstore.Credential, error) {
|
||||
return secretstore.Credential{}, s.err
|
||||
}
|
||||
|
||||
func TestAppRunSetupMapsUnavailableWalletError(t *testing.T) {
|
||||
app := NewAppWithDependencies(&promptStub{
|
||||
credential: secretstore.Credential{
|
||||
Host: "imap.example.com",
|
||||
Username: "alice",
|
||||
Password: "secret",
|
||||
},
|
||||
}, errorStoreStub{
|
||||
err: fmt.Errorf("%w: session bus missing", kwallet.ErrKWalletUnavailable),
|
||||
}, nil, &bytes.Buffer{})
|
||||
|
||||
err := app.Run([]string{"setup"})
|
||||
if err == nil {
|
||||
t.Fatal("expected setup to fail")
|
||||
}
|
||||
if !strings.Contains(strings.ToLower(err.Error()), "kwallet is not available") {
|
||||
t.Fatalf("expected mapped kwallet error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMapAppErrorMapsMissingCredentialError(t *testing.T) {
|
||||
err := mapAppError(fmt.Errorf("%w: missing entry", kwallet.ErrCredentialNotFound))
|
||||
if err == nil {
|
||||
t.Fatal("expected mapped error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "run `email-mcp setup`") {
|
||||
t.Fatalf("expected setup guidance, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMapAppErrorLeavesUnknownErrorsUntouched(t *testing.T) {
|
||||
wantErr := errors.New("boom")
|
||||
|
||||
err := mapAppError(wantErr)
|
||||
if !errors.Is(err, wantErr) {
|
||||
t.Fatalf("expected original error, got %v", err)
|
||||
}
|
||||
}
|
||||
308
internal/secretstore/kwallet/client.go
Normal file
308
internal/secretstore/kwallet/client.go
Normal file
|
|
@ -0,0 +1,308 @@
|
|||
package kwallet
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
const (
|
||||
kwalletInterface = "org.kde.KWallet"
|
||||
kwalletAppID = "email-mcp"
|
||||
kwalletFolderName = "email-mcp"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrKWalletUnavailable = errors.New("kwallet is not available")
|
||||
ErrKWalletDisabled = errors.New("kwallet is disabled")
|
||||
ErrKWalletOpenFailed = errors.New("kwallet could not be opened")
|
||||
ErrCredentialNotFound = errors.New("credentials not configured")
|
||||
)
|
||||
|
||||
type ClientImpl struct {
|
||||
probe func(context.Context) error
|
||||
open func(context.Context) error
|
||||
read func(context.Context, string) ([]byte, error)
|
||||
write func(context.Context, string, []byte) error
|
||||
}
|
||||
|
||||
type dbusConnection interface {
|
||||
Object(dest string, path dbus.ObjectPath) dbusObject
|
||||
}
|
||||
|
||||
type dbusObject interface {
|
||||
CallWithContext(ctx context.Context, method string, flags dbus.Flags, args ...any) *dbus.Call
|
||||
}
|
||||
|
||||
type kwalletService struct {
|
||||
name string
|
||||
path dbus.ObjectPath
|
||||
}
|
||||
|
||||
type walletSession struct {
|
||||
connect func() (dbusConnection, error)
|
||||
|
||||
services []kwalletService
|
||||
object dbusObject
|
||||
handle int32
|
||||
}
|
||||
|
||||
type sessionBusConnection struct {
|
||||
conn *dbus.Conn
|
||||
}
|
||||
|
||||
type sessionBusObject struct {
|
||||
object dbus.BusObject
|
||||
}
|
||||
|
||||
func NewDefaultWalletClient() Client {
|
||||
return newClientImpl(newWalletSession(defaultSessionBusConnection))
|
||||
}
|
||||
|
||||
func newClientImpl(session *walletSession) ClientImpl {
|
||||
return ClientImpl{
|
||||
probe: session.probe,
|
||||
open: session.open,
|
||||
read: session.readEntry,
|
||||
write: session.writeEntry,
|
||||
}
|
||||
}
|
||||
|
||||
func newWalletSession(connect func() (dbusConnection, error)) *walletSession {
|
||||
return &walletSession{
|
||||
connect: connect,
|
||||
services: []kwalletService{
|
||||
{name: "org.kde.kwalletd6", path: "/modules/kwalletd6"},
|
||||
{name: "org.kde.kwalletd5", path: "/modules/kwalletd5"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func defaultSessionBusConnection() (dbusConnection, error) {
|
||||
conn, err := dbus.SessionBus()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %v", ErrKWalletUnavailable, err)
|
||||
}
|
||||
return sessionBusConnection{conn: conn}, nil
|
||||
}
|
||||
|
||||
func (c sessionBusConnection) Object(dest string, path dbus.ObjectPath) dbusObject {
|
||||
return sessionBusObject{object: c.conn.Object(dest, path)}
|
||||
}
|
||||
|
||||
func (o sessionBusObject) CallWithContext(ctx context.Context, method string, flags dbus.Flags, args ...any) *dbus.Call {
|
||||
return o.object.CallWithContext(ctx, method, flags, args...)
|
||||
}
|
||||
|
||||
func (c ClientImpl) IsAvailable(ctx context.Context) error {
|
||||
if c.probe == nil {
|
||||
return ErrKWalletUnavailable
|
||||
}
|
||||
return c.probe(ctx)
|
||||
}
|
||||
|
||||
func (c ClientImpl) Open(ctx context.Context) error {
|
||||
if c.open == nil {
|
||||
return nil
|
||||
}
|
||||
return c.open(ctx)
|
||||
}
|
||||
|
||||
func (c ClientImpl) WriteEntry(ctx context.Context, key string, value []byte) error {
|
||||
if c.write == nil {
|
||||
return fmt.Errorf("kwallet write operation is not configured")
|
||||
}
|
||||
return c.write(ctx, key, value)
|
||||
}
|
||||
|
||||
func (c ClientImpl) ReadEntry(ctx context.Context, key string) ([]byte, error) {
|
||||
if c.read == nil {
|
||||
return nil, ErrCredentialNotFound
|
||||
}
|
||||
return c.read(ctx, key)
|
||||
}
|
||||
|
||||
func (s *walletSession) probe(ctx context.Context) error {
|
||||
_, err := s.ensureObject(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *walletSession) open(ctx context.Context) error {
|
||||
object, err := s.ensureObject(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if s.handle != 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
walletName, err := s.walletName(ctx, object)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %v", ErrKWalletOpenFailed, err)
|
||||
}
|
||||
|
||||
handle, err := s.callInt32(ctx, object, "open", walletName, int64(0), kwalletAppID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %v", ErrKWalletOpenFailed, err)
|
||||
}
|
||||
if handle < 0 {
|
||||
return fmt.Errorf("%w: handle %d", ErrKWalletOpenFailed, handle)
|
||||
}
|
||||
|
||||
hasFolder, err := s.callBool(ctx, object, "hasFolder", handle, kwalletFolderName, kwalletAppID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %v", ErrKWalletOpenFailed, err)
|
||||
}
|
||||
if !hasFolder {
|
||||
created, err := s.callBool(ctx, object, "createFolder", handle, kwalletFolderName, kwalletAppID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %v", ErrKWalletOpenFailed, err)
|
||||
}
|
||||
if !created {
|
||||
return fmt.Errorf("%w: folder %q could not be created", ErrKWalletOpenFailed, kwalletFolderName)
|
||||
}
|
||||
}
|
||||
|
||||
s.handle = handle
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *walletSession) writeEntry(ctx context.Context, key string, value []byte) error {
|
||||
if err := s.open(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
object, err := s.ensureObject(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
code, err := s.callInt32(ctx, object, "writeEntry", s.handle, kwalletFolderName, key, value, kwalletAppID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("kwallet write failed: %w", err)
|
||||
}
|
||||
if code != 0 {
|
||||
return fmt.Errorf("kwallet write failed with code %d", code)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *walletSession) readEntry(ctx context.Context, key string) ([]byte, error) {
|
||||
if err := s.open(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
object, err := s.ensureObject(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hasEntry, err := s.callBool(ctx, object, "hasEntry", s.handle, kwalletFolderName, key, kwalletAppID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !hasEntry {
|
||||
return nil, fmt.Errorf("%w: key %q", ErrCredentialNotFound, key)
|
||||
}
|
||||
|
||||
value, err := s.callBytes(ctx, object, "readEntry", s.handle, kwalletFolderName, key, kwalletAppID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func (s *walletSession) ensureObject(ctx context.Context) (dbusObject, error) {
|
||||
if s.object != nil {
|
||||
return s.object, nil
|
||||
}
|
||||
|
||||
conn, err := s.connect()
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrKWalletUnavailable) {
|
||||
return nil, err
|
||||
}
|
||||
return nil, fmt.Errorf("%w: %v", ErrKWalletUnavailable, err)
|
||||
}
|
||||
|
||||
var lastErr error
|
||||
for _, service := range s.services {
|
||||
object := conn.Object(service.name, service.path)
|
||||
enabled, err := s.callBool(ctx, object, "isEnabled")
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
if !enabled {
|
||||
return nil, ErrKWalletDisabled
|
||||
}
|
||||
|
||||
s.object = object
|
||||
return s.object, nil
|
||||
}
|
||||
|
||||
if lastErr == nil {
|
||||
lastErr = errors.New("no kwallet service responded")
|
||||
}
|
||||
return nil, fmt.Errorf("%w: %v", ErrKWalletUnavailable, lastErr)
|
||||
}
|
||||
|
||||
func (s *walletSession) walletName(ctx context.Context, object dbusObject) (string, error) {
|
||||
name, err := s.callString(ctx, object, "networkWallet")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
name = strings.TrimSpace(name)
|
||||
if name != "" {
|
||||
return name, nil
|
||||
}
|
||||
|
||||
name, err = s.callString(ctx, object, "localWallet")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" {
|
||||
return "", errors.New("kwallet did not report a wallet name")
|
||||
}
|
||||
return name, nil
|
||||
}
|
||||
|
||||
func (s *walletSession) callBool(ctx context.Context, object dbusObject, name string, args ...any) (bool, error) {
|
||||
var value bool
|
||||
if err := object.CallWithContext(ctx, kwalletMethod(name), 0, args...).Store(&value); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func (s *walletSession) callInt32(ctx context.Context, object dbusObject, name string, args ...any) (int32, error) {
|
||||
var value int32
|
||||
if err := object.CallWithContext(ctx, kwalletMethod(name), 0, args...).Store(&value); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func (s *walletSession) callString(ctx context.Context, object dbusObject, name string, args ...any) (string, error) {
|
||||
var value string
|
||||
if err := object.CallWithContext(ctx, kwalletMethod(name), 0, args...).Store(&value); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func (s *walletSession) callBytes(ctx context.Context, object dbusObject, name string, args ...any) ([]byte, error) {
|
||||
var value []byte
|
||||
if err := object.CallWithContext(ctx, kwalletMethod(name), 0, args...).Store(&value); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func kwalletMethod(name string) string {
|
||||
return kwalletInterface + "." + name
|
||||
}
|
||||
204
internal/secretstore/kwallet/client_test.go
Normal file
204
internal/secretstore/kwallet/client_test.go
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
package kwallet
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
type stubCall struct {
|
||||
body []any
|
||||
err error
|
||||
}
|
||||
|
||||
type recordedCall struct {
|
||||
method string
|
||||
args []any
|
||||
}
|
||||
|
||||
type stubObject struct {
|
||||
responses map[string][]stubCall
|
||||
calls []recordedCall
|
||||
}
|
||||
|
||||
func (o *stubObject) CallWithContext(_ context.Context, method string, _ dbus.Flags, args ...any) *dbus.Call {
|
||||
o.calls = append(o.calls, recordedCall{method: method, args: append([]any(nil), args...)})
|
||||
|
||||
queue := o.responses[method]
|
||||
if len(queue) == 0 {
|
||||
return &dbus.Call{Err: errors.New("unexpected method: " + method)}
|
||||
}
|
||||
|
||||
response := queue[0]
|
||||
o.responses[method] = queue[1:]
|
||||
return &dbus.Call{Body: response.body, Err: response.err}
|
||||
}
|
||||
|
||||
type stubConnection struct {
|
||||
objects map[string]*stubObject
|
||||
}
|
||||
|
||||
func (c *stubConnection) Object(dest string, path dbus.ObjectPath) dbusObject {
|
||||
return c.objects[dest+"|"+string(path)]
|
||||
}
|
||||
|
||||
func TestClientIsAvailableReturnsErrorWhenServiceIsMissing(t *testing.T) {
|
||||
client := newClientImpl(newWalletSession(func() (dbusConnection, error) {
|
||||
return &stubConnection{
|
||||
objects: map[string]*stubObject{
|
||||
"org.kde.kwalletd6|/modules/kwalletd6": {
|
||||
responses: map[string][]stubCall{
|
||||
kwalletMethod("isEnabled"): {{err: errors.New("service missing")}},
|
||||
},
|
||||
},
|
||||
"org.kde.kwalletd5|/modules/kwalletd5": {
|
||||
responses: map[string][]stubCall{
|
||||
kwalletMethod("isEnabled"): {{err: errors.New("service missing")}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}))
|
||||
|
||||
err := client.IsAvailable(context.Background())
|
||||
if err == nil {
|
||||
t.Fatal("expected availability error")
|
||||
}
|
||||
if !errors.Is(err, ErrKWalletUnavailable) {
|
||||
t.Fatalf("expected unavailable error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientOpenUsesNetworkWalletAndCreatesFolder(t *testing.T) {
|
||||
walletObject := &stubObject{
|
||||
responses: map[string][]stubCall{
|
||||
kwalletMethod("isEnabled"): {{body: []any{true}}},
|
||||
kwalletMethod("networkWallet"): {{body: []any{"kdewallet"}}},
|
||||
kwalletMethod("open"): {{body: []any{int32(42)}}},
|
||||
kwalletMethod("hasFolder"): {{body: []any{false}}},
|
||||
kwalletMethod("createFolder"): {{body: []any{true}}},
|
||||
},
|
||||
}
|
||||
client := newClientImpl(newWalletSession(func() (dbusConnection, error) {
|
||||
return &stubConnection{
|
||||
objects: map[string]*stubObject{
|
||||
"org.kde.kwalletd6|/modules/kwalletd6": walletObject,
|
||||
},
|
||||
}, nil
|
||||
}))
|
||||
|
||||
if err := client.Open(context.Background()); err != nil {
|
||||
t.Fatalf("Open returned error: %v", err)
|
||||
}
|
||||
|
||||
wantMethods := []string{
|
||||
kwalletMethod("isEnabled"),
|
||||
kwalletMethod("networkWallet"),
|
||||
kwalletMethod("open"),
|
||||
kwalletMethod("hasFolder"),
|
||||
kwalletMethod("createFolder"),
|
||||
}
|
||||
gotMethods := make([]string, 0, len(walletObject.calls))
|
||||
for _, call := range walletObject.calls {
|
||||
gotMethods = append(gotMethods, call.method)
|
||||
}
|
||||
if !reflect.DeepEqual(gotMethods, wantMethods) {
|
||||
t.Fatalf("unexpected methods: got %v want %v", gotMethods, wantMethods)
|
||||
}
|
||||
openArgs := walletObject.calls[2].args
|
||||
if len(openArgs) != 3 || openArgs[0] != "kdewallet" || openArgs[1] != int64(0) || openArgs[2] != kwalletAppID {
|
||||
t.Fatalf("unexpected open args: %#v", openArgs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientWriteEntryWritesBytesToConfiguredFolder(t *testing.T) {
|
||||
walletObject := &stubObject{
|
||||
responses: map[string][]stubCall{
|
||||
kwalletMethod("isEnabled"): {{body: []any{true}}},
|
||||
kwalletMethod("networkWallet"): {{body: []any{"kdewallet"}}},
|
||||
kwalletMethod("open"): {{body: []any{int32(42)}}},
|
||||
kwalletMethod("hasFolder"): {{body: []any{true}}},
|
||||
kwalletMethod("writeEntry"): {{body: []any{int32(0)}}},
|
||||
},
|
||||
}
|
||||
client := newClientImpl(newWalletSession(func() (dbusConnection, error) {
|
||||
return &stubConnection{
|
||||
objects: map[string]*stubObject{
|
||||
"org.kde.kwalletd6|/modules/kwalletd6": walletObject,
|
||||
},
|
||||
}, nil
|
||||
}))
|
||||
|
||||
if err := client.WriteEntry(context.Background(), "default", []byte("payload")); err != nil {
|
||||
t.Fatalf("WriteEntry returned error: %v", err)
|
||||
}
|
||||
|
||||
writeArgs := walletObject.calls[len(walletObject.calls)-1].args
|
||||
wantArgs := []any{int32(42), kwalletFolderName, "default", []byte("payload"), kwalletAppID}
|
||||
if !reflect.DeepEqual(writeArgs, wantArgs) {
|
||||
t.Fatalf("unexpected write args: got %#v want %#v", writeArgs, wantArgs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientReadEntryReturnsCredentialNotFoundWhenEntryIsMissing(t *testing.T) {
|
||||
walletObject := &stubObject{
|
||||
responses: map[string][]stubCall{
|
||||
kwalletMethod("isEnabled"): {{body: []any{true}}},
|
||||
kwalletMethod("networkWallet"): {{body: []any{"kdewallet"}}},
|
||||
kwalletMethod("open"): {{body: []any{int32(42)}}},
|
||||
kwalletMethod("hasFolder"): {{body: []any{true}}},
|
||||
kwalletMethod("hasEntry"): {{body: []any{false}}},
|
||||
},
|
||||
}
|
||||
client := newClientImpl(newWalletSession(func() (dbusConnection, error) {
|
||||
return &stubConnection{
|
||||
objects: map[string]*stubObject{
|
||||
"org.kde.kwalletd6|/modules/kwalletd6": walletObject,
|
||||
},
|
||||
}, nil
|
||||
}))
|
||||
|
||||
_, err := client.ReadEntry(context.Background(), "default")
|
||||
if err == nil {
|
||||
t.Fatal("expected missing credential error")
|
||||
}
|
||||
if !errors.Is(err, ErrCredentialNotFound) {
|
||||
t.Fatalf("expected missing credential error, got %v", err)
|
||||
}
|
||||
for _, call := range walletObject.calls {
|
||||
if call.method == kwalletMethod("readEntry") {
|
||||
t.Fatal("did not expect readEntry call when key is missing")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientReadEntryReadsStoredPayload(t *testing.T) {
|
||||
walletObject := &stubObject{
|
||||
responses: map[string][]stubCall{
|
||||
kwalletMethod("isEnabled"): {{body: []any{true}}},
|
||||
kwalletMethod("networkWallet"): {{body: []any{"kdewallet"}}},
|
||||
kwalletMethod("open"): {{body: []any{int32(42)}}},
|
||||
kwalletMethod("hasFolder"): {{body: []any{true}}},
|
||||
kwalletMethod("hasEntry"): {{body: []any{true}}},
|
||||
kwalletMethod("readEntry"): {{body: []any{[]byte("payload")}}},
|
||||
},
|
||||
}
|
||||
client := newClientImpl(newWalletSession(func() (dbusConnection, error) {
|
||||
return &stubConnection{
|
||||
objects: map[string]*stubObject{
|
||||
"org.kde.kwalletd6|/modules/kwalletd6": walletObject,
|
||||
},
|
||||
}, nil
|
||||
}))
|
||||
|
||||
value, err := client.ReadEntry(context.Background(), "default")
|
||||
if err != nil {
|
||||
t.Fatalf("ReadEntry returned error: %v", err)
|
||||
}
|
||||
if got := string(value); got != "payload" {
|
||||
t.Fatalf("unexpected payload: %q", got)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue