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
|
module email-mcp
|
||||||
|
|
||||||
go 1.25.0
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"email-mcp/internal/secretstore"
|
"email-mcp/internal/secretstore"
|
||||||
|
"email-mcp/internal/secretstore/kwallet"
|
||||||
)
|
)
|
||||||
|
|
||||||
type MCPRunner interface {
|
type MCPRunner interface {
|
||||||
|
|
@ -68,7 +70,7 @@ func (a *App) runSetup(ctx context.Context) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := a.store.Save(ctx, secretstore.DefaultAccountKey, cred); err != nil {
|
if err := a.store.Save(ctx, secretstore.DefaultAccountKey, cred); err != nil {
|
||||||
return err
|
return mapAppError(err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -77,5 +79,24 @@ func (a *App) runMCP(ctx context.Context) error {
|
||||||
if a.runner == nil {
|
if a.runner == nil {
|
||||||
return fmt.Errorf("mcp runner is not configured")
|
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