416 lines
13 KiB
Go
416 lines
13 KiB
Go
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)]
|
|
}
|
|
|
|
type rotatingConnection struct {
|
|
objects []dbusObject
|
|
index int
|
|
}
|
|
|
|
func (c *rotatingConnection) Object(string, dbus.ObjectPath) dbusObject {
|
|
if len(c.objects) == 0 {
|
|
return nil
|
|
}
|
|
object := c.objects[c.index]
|
|
if c.index < len(c.objects)-1 {
|
|
c.index++
|
|
}
|
|
return object
|
|
}
|
|
|
|
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 TestClientOpenDoesNotReopenWhenHandleZeroIsValid(t *testing.T) {
|
|
walletObject := &stubObject{
|
|
responses: map[string][]stubCall{
|
|
kwalletMethod("isEnabled"): {{body: []any{true}}},
|
|
kwalletMethod("networkWallet"): {{body: []any{"kdewallet"}}},
|
|
kwalletMethod("open"): {{body: []any{int32(0)}}},
|
|
kwalletMethod("hasFolder"): {{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("first Open returned error: %v", err)
|
|
}
|
|
if err := client.Open(context.Background()); err != nil {
|
|
t.Fatalf("second Open returned error: %v", err)
|
|
}
|
|
|
|
openCalls := 0
|
|
for _, call := range walletObject.calls {
|
|
if call.method == kwalletMethod("open") {
|
|
openCalls++
|
|
}
|
|
}
|
|
if openCalls != 1 {
|
|
t.Fatalf("expected one open call, got %d", openCalls)
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
func TestClientWriteEntryMapsTransportFailuresToUnavailable(t *testing.T) {
|
|
wantErr := errors.New("transport closed")
|
|
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"): {{err: wantErr}},
|
|
},
|
|
}
|
|
client := newClientImpl(newWalletSession(func() (dbusConnection, error) {
|
|
return &stubConnection{
|
|
objects: map[string]*stubObject{
|
|
"org.kde.kwalletd6|/modules/kwalletd6": walletObject,
|
|
},
|
|
}, nil
|
|
}))
|
|
|
|
err := client.WriteEntry(context.Background(), "default", []byte("payload"))
|
|
if !errors.Is(err, ErrKWalletUnavailable) {
|
|
t.Fatalf("expected unavailable error, got %v", err)
|
|
}
|
|
if !errors.Is(err, wantErr) {
|
|
t.Fatalf("expected wrapped transport error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestClientReadEntryMapsTransportFailuresToUnavailable(t *testing.T) {
|
|
wantErr := errors.New("transport closed")
|
|
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"): {{err: wantErr}},
|
|
},
|
|
}
|
|
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 !errors.Is(err, ErrKWalletUnavailable) {
|
|
t.Fatalf("expected unavailable error, got %v", err)
|
|
}
|
|
if !errors.Is(err, wantErr) {
|
|
t.Fatalf("expected wrapped transport error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestClientWriteEntryReopensAfterMappedTransportFailure(t *testing.T) {
|
|
firstObject := &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"): {{err: errors.New("transport closed")}},
|
|
},
|
|
}
|
|
secondObject := &stubObject{
|
|
responses: map[string][]stubCall{
|
|
kwalletMethod("isEnabled"): {{body: []any{true}}},
|
|
kwalletMethod("networkWallet"): {{body: []any{"kdewallet"}}},
|
|
kwalletMethod("open"): {{body: []any{int32(43)}}},
|
|
kwalletMethod("hasFolder"): {{body: []any{true}}},
|
|
kwalletMethod("writeEntry"): {{body: []any{int32(0)}}},
|
|
},
|
|
}
|
|
client := newClientImpl(newWalletSession(func() (dbusConnection, error) {
|
|
return &rotatingConnection{objects: []dbusObject{firstObject, secondObject}}, nil
|
|
}))
|
|
|
|
err := client.WriteEntry(context.Background(), "default", []byte("payload"))
|
|
if !errors.Is(err, ErrKWalletUnavailable) {
|
|
t.Fatalf("expected unavailable error, got %v", err)
|
|
}
|
|
|
|
if err := client.WriteEntry(context.Background(), "default", []byte("payload")); err != nil {
|
|
t.Fatalf("expected retry to succeed, got %v", err)
|
|
}
|
|
|
|
firstOpenCalls := 0
|
|
for _, call := range firstObject.calls {
|
|
if call.method == kwalletMethod("open") {
|
|
firstOpenCalls++
|
|
}
|
|
}
|
|
secondOpenCalls := 0
|
|
for _, call := range secondObject.calls {
|
|
if call.method == kwalletMethod("open") {
|
|
secondOpenCalls++
|
|
}
|
|
}
|
|
if firstOpenCalls != 1 || secondOpenCalls != 1 {
|
|
t.Fatalf("expected reopen on retry, got first=%d second=%d", firstOpenCalls, secondOpenCalls)
|
|
}
|
|
}
|
|
|
|
func TestClientReadEntryReopensAfterMappedTransportFailure(t *testing.T) {
|
|
firstObject := &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"): {{err: errors.New("transport closed")}},
|
|
},
|
|
}
|
|
secondObject := &stubObject{
|
|
responses: map[string][]stubCall{
|
|
kwalletMethod("isEnabled"): {{body: []any{true}}},
|
|
kwalletMethod("networkWallet"): {{body: []any{"kdewallet"}}},
|
|
kwalletMethod("open"): {{body: []any{int32(43)}}},
|
|
kwalletMethod("hasFolder"): {{body: []any{true}}},
|
|
kwalletMethod("hasEntry"): {{body: []any{true}}},
|
|
kwalletMethod("readEntry"): {{body: []any{[]byte("payload")}}},
|
|
},
|
|
}
|
|
client := newClientImpl(newWalletSession(func() (dbusConnection, error) {
|
|
return &rotatingConnection{objects: []dbusObject{firstObject, secondObject}}, nil
|
|
}))
|
|
|
|
_, err := client.ReadEntry(context.Background(), "default")
|
|
if !errors.Is(err, ErrKWalletUnavailable) {
|
|
t.Fatalf("expected unavailable error, got %v", err)
|
|
}
|
|
|
|
value, err := client.ReadEntry(context.Background(), "default")
|
|
if err != nil {
|
|
t.Fatalf("expected retry to succeed, got %v", err)
|
|
}
|
|
if string(value) != "payload" {
|
|
t.Fatalf("unexpected retry payload: %q", value)
|
|
}
|
|
|
|
firstOpenCalls := 0
|
|
for _, call := range firstObject.calls {
|
|
if call.method == kwalletMethod("open") {
|
|
firstOpenCalls++
|
|
}
|
|
}
|
|
secondOpenCalls := 0
|
|
for _, call := range secondObject.calls {
|
|
if call.method == kwalletMethod("open") {
|
|
secondOpenCalls++
|
|
}
|
|
}
|
|
if firstOpenCalls != 1 || secondOpenCalls != 1 {
|
|
t.Fatalf("expected reopen on retry, got first=%d second=%d", firstOpenCalls, secondOpenCalls)
|
|
}
|
|
}
|