diff --git a/login1/dbus.go b/login1/dbus.go index 206c58d4..b03b3f0f 100644 --- a/login1/dbus.go +++ b/login1/dbus.go @@ -32,8 +32,34 @@ const ( // Conn is a connection to systemds dbus endpoint. type Conn struct { - conn *dbus.Conn - object dbus.BusObject + conn Connection + connManager connectionManager + object Caller +} + +// Objector describes functionality required from a given D-Bus connection. +type Connection interface { + Object(string, dbus.ObjectPath) dbus.BusObject + Signal(ch chan<- *dbus.Signal) + Connected() bool + // TODO: This should be replaced with AddMatchSignal. + // See https://github.com/coreos/go-systemd/issues/388 for details. + BusObject() dbus.BusObject +} + +// ConnectionManager explicitly wraps dependencies on established D-Bus connection. +type connectionManager interface { + Hello() error + Auth(authMethods []dbus.Auth) error + Close() error + + Connection +} + +// Caller describes required functionality from D-Bus object. +type Caller interface { + // TODO: This method should eventually be removed, as it provides no context support. + Call(method string, flags dbus.Flags, args ...interface{}) *dbus.Call } // New establishes a connection to the system bus and authenticates. @@ -47,14 +73,26 @@ func New() (*Conn, error) { return c, nil } +// NewWithConnection creates new login1 client using given D-Bus connection. +func NewWithConnection(connection Connection) (*Conn, error) { + if connection == nil { + return nil, fmt.Errorf("no connection given") + } + + return &Conn{ + conn: connection, + object: connection.Object(dbusDest, dbusPath), + }, nil +} + // Close closes the dbus connection func (c *Conn) Close() { if c == nil { return } - if c.conn != nil { - c.conn.Close() + if c.conn != nil && c.connManager != nil { + c.connManager.Close() } } @@ -65,7 +103,7 @@ func (c *Conn) Connected() bool { func (c *Conn) initConnection() error { var err error - c.conn, err = dbus.SystemBusPrivate() + c.connManager, err = dbus.SystemBusPrivate() if err != nil { return err } @@ -75,18 +113,19 @@ func (c *Conn) initConnection() error { // libc) methods := []dbus.Auth{dbus.AuthExternal(strconv.Itoa(os.Getuid()))} - err = c.conn.Auth(methods) + err = c.connManager.Auth(methods) if err != nil { - c.conn.Close() + c.connManager.Close() return err } - err = c.conn.Hello() + err = c.connManager.Hello() if err != nil { - c.conn.Close() + c.connManager.Close() return err } + c.conn = c.connManager c.object = c.conn.Object("org.freedesktop.login1", dbus.ObjectPath(dbusPath)) return nil diff --git a/login1/dbus_test.go b/login1/dbus_test.go index b570c921..6505fe61 100644 --- a/login1/dbus_test.go +++ b/login1/dbus_test.go @@ -12,26 +12,28 @@ // See the License for the specific language governing permissions and // limitations under the License. -package login1 +package login1_test import ( "fmt" "os/user" "regexp" "testing" + + "github.com/godbus/dbus/v5" + + "github.com/coreos/go-systemd/v22/login1" ) // TestNew ensures that New() works without errors. func TestNew(t *testing.T) { - _, err := New() - - if err != nil { + if _, err := login1.New(); err != nil { t.Fatal(err) } } func TestListSessions(t *testing.T) { - c, err := New() + c, err := login1.New() if err != nil { t.Fatal(err) } @@ -60,7 +62,7 @@ func TestListSessions(t *testing.T) { } func TestListUsers(t *testing.T) { - c, err := New() + c, err := login1.New() if err != nil { t.Fatal(err) } @@ -87,3 +89,96 @@ func TestListUsers(t *testing.T) { } } } + +func Test_Creating_new_connection_with_custom_connection(t *testing.T) { + t.Parallel() + + t.Run("connects_to_global_login1_path_and_interface", func(t *testing.T) { + t.Parallel() + + objectConstructorCalled := false + + connectionWithContextCheck := &mockConnection{ + ObjectF: func(dest string, path dbus.ObjectPath) dbus.BusObject { + objectConstructorCalled = true + + expectedDest := "org.freedesktop.login1" + + if dest != expectedDest { + t.Fatalf("Expected D-Bus destination %q, got %q", expectedDest, dest) + } + + expectedPath := dbus.ObjectPath("/org/freedesktop/login1") + + if path != expectedPath { + t.Fatalf("Expected D-Bus path %q, got %q", expectedPath, path) + } + + return nil + }, + } + + if _, err := login1.NewWithConnection(connectionWithContextCheck); err != nil { + t.Fatalf("Unexpected error creating connection: %v", err) + } + + if !objectConstructorCalled { + t.Fatalf("Expected object constructor to be called") + } + }) + + t.Run("returns_error_when_no_custom_connection_is_given", func(t *testing.T) { + t.Parallel() + + testConn, err := login1.NewWithConnection(nil) + if err == nil { + t.Fatalf("Expected error creating connection with no connector") + } + + if testConn != nil { + t.Fatalf("Expected connection to be nil when New returns error") + } + }) +} + +// mockConnection is a test helper for mocking dbus.Conn. +type mockConnection struct { + ObjectF func(string, dbus.ObjectPath) dbus.BusObject +} + +// Auth ... +func (m *mockConnection) Auth(authMethods []dbus.Auth) error { + return nil +} + +// Hello ... +func (m *mockConnection) Hello() error { + return nil +} + +// Signal ... +func (m *mockConnection) Signal(ch chan<- *dbus.Signal) {} + +// Object ... +func (m *mockConnection) Object(dest string, path dbus.ObjectPath) dbus.BusObject { + if m.ObjectF == nil { + return nil + } + + return m.ObjectF(dest, path) +} + +// Close ... +func (m *mockConnection) Close() error { + return nil +} + +// BusObject ... +func (m *mockConnection) BusObject() dbus.BusObject { + return nil +} + +// Connected ... +func (m *mockConnection) Connected() bool { + return true +}