github.com/kubiko/snapd@v0.0.0-20201013125620-d4f3094d9ddf/desktop/notification/fdo_test.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2020 Canonical Ltd
     5   *
     6   * This program is free software: you can redistribute it and/or modify
     7   * it under the terms of the GNU General Public License version 3 as
     8   * published by the Free Software Foundation.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package notification_test
    21  
    22  import (
    23  	"context"
    24  	"fmt"
    25  	"sync"
    26  	"time"
    27  
    28  	"github.com/godbus/dbus"
    29  	. "gopkg.in/check.v1"
    30  
    31  	"github.com/snapcore/snapd/dbusutil"
    32  	"github.com/snapcore/snapd/dbusutil/dbustest"
    33  	"github.com/snapcore/snapd/desktop/notification"
    34  	"github.com/snapcore/snapd/logger"
    35  	"github.com/snapcore/snapd/testutil"
    36  )
    37  
    38  type fdoSuite struct {
    39  	testutil.BaseTest
    40  }
    41  
    42  var _ = Suite(&fdoSuite{})
    43  
    44  func (s *fdoSuite) connectWithHandler(c *C, handler dbustest.DBusHandlerFunc) *notification.Server {
    45  	conn, err := dbustest.Connection(handler)
    46  	c.Assert(err, IsNil)
    47  	restore := dbusutil.MockOnlySessionBusAvailable(conn)
    48  	s.AddCleanup(restore)
    49  	return notification.New(conn)
    50  }
    51  
    52  func (s *fdoSuite) checkGetServerInformationRequest(c *C, msg *dbus.Message) {
    53  	c.Assert(msg.Type, Equals, dbus.TypeMethodCall)
    54  	c.Check(msg.Flags, Equals, dbus.Flags(0))
    55  	c.Check(msg.Headers, DeepEquals, map[dbus.HeaderField]dbus.Variant{
    56  		dbus.FieldDestination: dbus.MakeVariant("org.freedesktop.Notifications"),
    57  		dbus.FieldPath:        dbus.MakeVariant(dbus.ObjectPath("/org/freedesktop/Notifications")),
    58  		dbus.FieldInterface:   dbus.MakeVariant("org.freedesktop.Notifications"),
    59  		dbus.FieldMember:      dbus.MakeVariant("GetServerInformation"),
    60  	})
    61  	c.Check(msg.Body, HasLen, 0)
    62  }
    63  
    64  func (s *fdoSuite) checkGetCapabilitiesRequest(c *C, msg *dbus.Message) {
    65  	c.Assert(msg.Type, Equals, dbus.TypeMethodCall)
    66  	c.Check(msg.Flags, Equals, dbus.Flags(0))
    67  	c.Check(msg.Headers, DeepEquals, map[dbus.HeaderField]dbus.Variant{
    68  		dbus.FieldDestination: dbus.MakeVariant("org.freedesktop.Notifications"),
    69  		dbus.FieldPath:        dbus.MakeVariant(dbus.ObjectPath("/org/freedesktop/Notifications")),
    70  		dbus.FieldInterface:   dbus.MakeVariant("org.freedesktop.Notifications"),
    71  		dbus.FieldMember:      dbus.MakeVariant("GetCapabilities"),
    72  	})
    73  	c.Check(msg.Body, HasLen, 0)
    74  }
    75  
    76  func (s *fdoSuite) checkNotifyRequest(c *C, msg *dbus.Message) {
    77  	c.Assert(msg.Type, Equals, dbus.TypeMethodCall)
    78  	c.Check(msg.Flags, Equals, dbus.Flags(0))
    79  	c.Check(msg.Headers, DeepEquals, map[dbus.HeaderField]dbus.Variant{
    80  		dbus.FieldDestination: dbus.MakeVariant("org.freedesktop.Notifications"),
    81  		dbus.FieldPath:        dbus.MakeVariant(dbus.ObjectPath("/org/freedesktop/Notifications")),
    82  		dbus.FieldInterface:   dbus.MakeVariant("org.freedesktop.Notifications"),
    83  		dbus.FieldMember:      dbus.MakeVariant("Notify"),
    84  		dbus.FieldSignature: dbus.MakeVariant(dbus.SignatureOf(
    85  			"", uint32(0), "", "", "", []string{}, map[string]dbus.Variant{}, int32(0),
    86  		)),
    87  	})
    88  	c.Check(msg.Body, HasLen, 8)
    89  }
    90  
    91  func (s *fdoSuite) checkCloseNotificationRequest(c *C, msg *dbus.Message) {
    92  	c.Assert(msg.Type, Equals, dbus.TypeMethodCall)
    93  	c.Check(msg.Flags, Equals, dbus.Flags(0))
    94  	c.Check(msg.Headers, DeepEquals, map[dbus.HeaderField]dbus.Variant{
    95  		dbus.FieldDestination: dbus.MakeVariant("org.freedesktop.Notifications"),
    96  		dbus.FieldPath:        dbus.MakeVariant(dbus.ObjectPath("/org/freedesktop/Notifications")),
    97  		dbus.FieldInterface:   dbus.MakeVariant("org.freedesktop.Notifications"),
    98  		dbus.FieldMember:      dbus.MakeVariant("CloseNotification"),
    99  		dbus.FieldSignature:   dbus.MakeVariant(dbus.SignatureOf(uint32(0))),
   100  	})
   101  	c.Check(msg.Body, HasLen, 1)
   102  }
   103  
   104  func (s *fdoSuite) nameHasNoOwnerResponse(c *C, msg *dbus.Message) *dbus.Message {
   105  	return &dbus.Message{
   106  		Type: dbus.TypeError,
   107  		Headers: map[dbus.HeaderField]dbus.Variant{
   108  			dbus.FieldReplySerial: dbus.MakeVariant(msg.Serial()),
   109  			dbus.FieldSender:      dbus.MakeVariant(":1"), // This does not matter.
   110  			// dbus.FieldDestination is provided automatically by DBus test helper.
   111  			dbus.FieldErrorName: dbus.MakeVariant("org.freedesktop.DBus.Error.NameHasNoOwner"),
   112  		},
   113  	}
   114  }
   115  
   116  func (s *fdoSuite) TestServerInformationSuccess(c *C) {
   117  	srv := s.connectWithHandler(c, func(msg *dbus.Message, n int) ([]*dbus.Message, error) {
   118  		switch n {
   119  		case 0:
   120  			s.checkGetServerInformationRequest(c, msg)
   121  			responseSig := dbus.SignatureOf("", "", "", "")
   122  			response := &dbus.Message{
   123  				Type: dbus.TypeMethodReply,
   124  				Headers: map[dbus.HeaderField]dbus.Variant{
   125  					dbus.FieldReplySerial: dbus.MakeVariant(msg.Serial()),
   126  					dbus.FieldSender:      dbus.MakeVariant(":1"), // This does not matter.
   127  					// dbus.FieldDestination is provided automatically by DBus test helper.
   128  					dbus.FieldSignature: dbus.MakeVariant(responseSig),
   129  				},
   130  				Body: []interface{}{"name", "vendor", "version", "specVersion"},
   131  			}
   132  			return []*dbus.Message{response}, nil
   133  		}
   134  		return nil, fmt.Errorf("unexpected message #%d: %s", n, msg)
   135  	})
   136  	name, vendor, version, specVersion, err := srv.ServerInformation()
   137  	c.Assert(err, IsNil)
   138  	c.Check(name, Equals, "name")
   139  	c.Check(vendor, Equals, "vendor")
   140  	c.Check(version, Equals, "version")
   141  	c.Check(specVersion, Equals, "specVersion")
   142  }
   143  
   144  func (s *fdoSuite) TestServerInformationError(c *C) {
   145  	srv := s.connectWithHandler(c, func(msg *dbus.Message, n int) ([]*dbus.Message, error) {
   146  		switch n {
   147  		case 0:
   148  			s.checkGetServerInformationRequest(c, msg)
   149  			response := s.nameHasNoOwnerResponse(c, msg)
   150  			return []*dbus.Message{response}, nil
   151  		}
   152  		return nil, fmt.Errorf("unexpected message #%d: %s", n, msg)
   153  	})
   154  	_, _, _, _, err := srv.ServerInformation()
   155  	c.Assert(err, ErrorMatches, "org.freedesktop.DBus.Error.NameHasNoOwner")
   156  }
   157  
   158  func (s *fdoSuite) TestServerCapabilitiesSuccess(c *C) {
   159  	srv := s.connectWithHandler(c, func(msg *dbus.Message, n int) ([]*dbus.Message, error) {
   160  		switch n {
   161  		case 0:
   162  			s.checkGetCapabilitiesRequest(c, msg)
   163  			responseSig := dbus.SignatureOf([]string{})
   164  			response := &dbus.Message{
   165  				Type: dbus.TypeMethodReply,
   166  				Headers: map[dbus.HeaderField]dbus.Variant{
   167  					dbus.FieldReplySerial: dbus.MakeVariant(msg.Serial()),
   168  					dbus.FieldSender:      dbus.MakeVariant(":1"), // This does not matter.
   169  					// dbus.FieldDestination is provided automatically by DBus test helper.
   170  					dbus.FieldSignature: dbus.MakeVariant(responseSig),
   171  				},
   172  				Body: []interface{}{
   173  					[]string{"cap-foo", "cap-bar"},
   174  				},
   175  			}
   176  			return []*dbus.Message{response}, nil
   177  		}
   178  		return nil, fmt.Errorf("unexpected message #%d: %s", n, msg)
   179  	})
   180  	caps, err := srv.ServerCapabilities()
   181  	c.Assert(err, IsNil)
   182  	c.Check(caps, DeepEquals, []notification.ServerCapability{"cap-foo", "cap-bar"})
   183  }
   184  
   185  func (s *fdoSuite) TestServerCapabilitiesError(c *C) {
   186  	srv := s.connectWithHandler(c, func(msg *dbus.Message, n int) ([]*dbus.Message, error) {
   187  		switch n {
   188  		case 0:
   189  			s.checkGetCapabilitiesRequest(c, msg)
   190  			response := s.nameHasNoOwnerResponse(c, msg)
   191  			return []*dbus.Message{response}, nil
   192  		}
   193  		return nil, fmt.Errorf("unexpected message #%d: %s", n, msg)
   194  	})
   195  	_, err := srv.ServerCapabilities()
   196  	c.Assert(err, ErrorMatches, "org.freedesktop.DBus.Error.NameHasNoOwner")
   197  }
   198  
   199  func (s *fdoSuite) TestSendNotificationSuccess(c *C) {
   200  	srv := s.connectWithHandler(c, func(msg *dbus.Message, n int) ([]*dbus.Message, error) {
   201  		switch n {
   202  		case 0:
   203  			s.checkNotifyRequest(c, msg)
   204  			c.Check(msg.Body[0], Equals, "app-name")
   205  			c.Check(msg.Body[1], Equals, uint32(42))
   206  			c.Check(msg.Body[2], Equals, "icon")
   207  			c.Check(msg.Body[3], Equals, "summary")
   208  			c.Check(msg.Body[4], Equals, "body")
   209  			c.Check(msg.Body[5], DeepEquals, []string{"key-1", "text-1", "key-2", "text-2"})
   210  			c.Check(msg.Body[6], DeepEquals, map[string]dbus.Variant{
   211  				"hint-str":  dbus.MakeVariant("str"),
   212  				"hint-bool": dbus.MakeVariant(true),
   213  			})
   214  			c.Check(msg.Body[7], Equals, int32(1000))
   215  			responseSig := dbus.SignatureOf(uint32(0))
   216  			response := &dbus.Message{
   217  				Type: dbus.TypeMethodReply,
   218  				Headers: map[dbus.HeaderField]dbus.Variant{
   219  					dbus.FieldReplySerial: dbus.MakeVariant(msg.Serial()),
   220  					dbus.FieldSender:      dbus.MakeVariant(":1"), // This does not matter.
   221  					// dbus.FieldDestination is provided automatically by DBus test helper.
   222  					dbus.FieldSignature: dbus.MakeVariant(responseSig),
   223  				},
   224  				Body: []interface{}{uint32(7)},
   225  			}
   226  			return []*dbus.Message{response}, nil
   227  		}
   228  		return nil, fmt.Errorf("unexpected message #%d: %s", n, msg)
   229  	})
   230  	id, err := srv.SendNotification(&notification.Message{
   231  		AppName:       "app-name",
   232  		Icon:          "icon",
   233  		Summary:       "summary",
   234  		Body:          "body",
   235  		ExpireTimeout: time.Second * 1,
   236  		ReplacesID:    notification.ID(42),
   237  		Actions: []notification.Action{
   238  			{ActionKey: "key-1", LocalizedText: "text-1"},
   239  			{ActionKey: "key-2", LocalizedText: "text-2"},
   240  		},
   241  		Hints: []notification.Hint{
   242  			{Name: "hint-str", Value: "str"},
   243  			{Name: "hint-bool", Value: true},
   244  		},
   245  	})
   246  	c.Assert(err, IsNil)
   247  	c.Check(id, Equals, notification.ID(7))
   248  }
   249  
   250  func (s *fdoSuite) TestSendNotificationWithServerDecitedExpireTimeout(c *C) {
   251  	srv := s.connectWithHandler(c, func(msg *dbus.Message, n int) ([]*dbus.Message, error) {
   252  		switch n {
   253  		case 0:
   254  			s.checkNotifyRequest(c, msg)
   255  			c.Check(msg.Body[7], Equals, int32(-1))
   256  			responseSig := dbus.SignatureOf(uint32(0))
   257  			response := &dbus.Message{
   258  				Type: dbus.TypeMethodReply,
   259  				Headers: map[dbus.HeaderField]dbus.Variant{
   260  					dbus.FieldReplySerial: dbus.MakeVariant(msg.Serial()),
   261  					dbus.FieldSender:      dbus.MakeVariant(":1"), // This does not matter.
   262  					// dbus.FieldDestination is provided automatically by DBus test helper.
   263  					dbus.FieldSignature: dbus.MakeVariant(responseSig),
   264  				},
   265  				Body: []interface{}{uint32(7)},
   266  			}
   267  			return []*dbus.Message{response}, nil
   268  		}
   269  		return nil, fmt.Errorf("unexpected message #%d: %s", n, msg)
   270  	})
   271  	id, err := srv.SendNotification(&notification.Message{
   272  		ExpireTimeout: notification.ServerSelectedExpireTimeout,
   273  	})
   274  	c.Assert(err, IsNil)
   275  	c.Check(id, Equals, notification.ID(7))
   276  }
   277  
   278  func (s *fdoSuite) TestSendNotificationError(c *C) {
   279  	srv := s.connectWithHandler(c, func(msg *dbus.Message, n int) ([]*dbus.Message, error) {
   280  		switch n {
   281  		case 0:
   282  			s.checkNotifyRequest(c, msg)
   283  			response := s.nameHasNoOwnerResponse(c, msg)
   284  			return []*dbus.Message{response}, nil
   285  		}
   286  		return nil, fmt.Errorf("unexpected message #%d: %s", n, msg)
   287  	})
   288  	_, err := srv.SendNotification(&notification.Message{})
   289  	c.Assert(err, ErrorMatches, "org.freedesktop.DBus.Error.NameHasNoOwner")
   290  }
   291  
   292  func (s *fdoSuite) TestCloseNotificationSuccess(c *C) {
   293  	srv := s.connectWithHandler(c, func(msg *dbus.Message, n int) ([]*dbus.Message, error) {
   294  		switch n {
   295  		case 0:
   296  			s.checkCloseNotificationRequest(c, msg)
   297  			c.Check(msg.Body[0], Equals, uint32(42))
   298  			response := &dbus.Message{
   299  				Type: dbus.TypeMethodReply,
   300  				Headers: map[dbus.HeaderField]dbus.Variant{
   301  					dbus.FieldReplySerial: dbus.MakeVariant(msg.Serial()),
   302  					dbus.FieldSender:      dbus.MakeVariant(":1"), // This does not matter.
   303  					// dbus.FieldDestination is provided automatically by DBus test helper.
   304  				},
   305  			}
   306  			return []*dbus.Message{response}, nil
   307  		}
   308  		return nil, fmt.Errorf("unexpected message #%d: %s", n, msg)
   309  	})
   310  	err := srv.CloseNotification(notification.ID(42))
   311  	c.Assert(err, IsNil)
   312  }
   313  
   314  func (s *fdoSuite) TestCloseNotificationError(c *C) {
   315  	srv := s.connectWithHandler(c, func(msg *dbus.Message, n int) ([]*dbus.Message, error) {
   316  		switch n {
   317  		case 0:
   318  			s.checkCloseNotificationRequest(c, msg)
   319  			response := s.nameHasNoOwnerResponse(c, msg)
   320  			return []*dbus.Message{response}, nil
   321  		}
   322  		return nil, fmt.Errorf("unexpected message #%d: %s", n, msg)
   323  	})
   324  	err := srv.CloseNotification(notification.ID(42))
   325  	c.Assert(err, ErrorMatches, "org.freedesktop.DBus.Error.NameHasNoOwner")
   326  }
   327  
   328  type testObserver struct {
   329  	notificationClosed func(notification.ID, notification.CloseReason) error
   330  	actionInvoked      func(notification.ID, string) error
   331  }
   332  
   333  func (o *testObserver) NotificationClosed(id notification.ID, reason notification.CloseReason) error {
   334  	if o.notificationClosed != nil {
   335  		return o.notificationClosed(id, reason)
   336  	}
   337  	return nil
   338  }
   339  
   340  func (o *testObserver) ActionInvoked(id notification.ID, actionKey string) error {
   341  	if o.actionInvoked != nil {
   342  		return o.actionInvoked(id, actionKey)
   343  	}
   344  	return nil
   345  }
   346  
   347  func (s *fdoSuite) checkAddMatchRequest(c *C, msg *dbus.Message) {
   348  	c.Assert(msg.Type, Equals, dbus.TypeMethodCall)
   349  	c.Check(msg.Flags, Equals, dbus.Flags(0))
   350  	c.Check(msg.Headers, DeepEquals, map[dbus.HeaderField]dbus.Variant{
   351  		dbus.FieldDestination: dbus.MakeVariant("org.freedesktop.DBus"),
   352  		dbus.FieldPath:        dbus.MakeVariant(dbus.ObjectPath("/org/freedesktop/DBus")),
   353  		dbus.FieldInterface:   dbus.MakeVariant("org.freedesktop.DBus"),
   354  		dbus.FieldMember:      dbus.MakeVariant("AddMatch"),
   355  		dbus.FieldSignature:   dbus.MakeVariant(dbus.SignatureOf("")),
   356  	})
   357  }
   358  
   359  func (s *fdoSuite) checkRemoveMatchRequest(c *C, msg *dbus.Message) {
   360  	c.Assert(msg.Type, Equals, dbus.TypeMethodCall)
   361  	c.Check(msg.Flags, Equals, dbus.Flags(0))
   362  	c.Check(msg.Headers, DeepEquals, map[dbus.HeaderField]dbus.Variant{
   363  		dbus.FieldDestination: dbus.MakeVariant("org.freedesktop.DBus"),
   364  		dbus.FieldPath:        dbus.MakeVariant(dbus.ObjectPath("/org/freedesktop/DBus")),
   365  		dbus.FieldInterface:   dbus.MakeVariant("org.freedesktop.DBus"),
   366  		dbus.FieldMember:      dbus.MakeVariant("RemoveMatch"),
   367  		dbus.FieldSignature:   dbus.MakeVariant(dbus.SignatureOf("")),
   368  	})
   369  }
   370  
   371  func (s *fdoSuite) addMatchResponse(c *C, msg *dbus.Message) *dbus.Message {
   372  	return &dbus.Message{
   373  		Type: dbus.TypeMethodReply,
   374  		Headers: map[dbus.HeaderField]dbus.Variant{
   375  			dbus.FieldReplySerial: dbus.MakeVariant(msg.Serial()),
   376  			dbus.FieldSender:      dbus.MakeVariant(":1"), // This does not matter.
   377  			// dbus.FieldDestination is provided automatically by DBus test helper.
   378  		},
   379  	}
   380  }
   381  
   382  func (s *fdoSuite) removeMatchResponse(c *C, msg *dbus.Message) *dbus.Message {
   383  	return &dbus.Message{
   384  		Type: dbus.TypeMethodReply,
   385  		Headers: map[dbus.HeaderField]dbus.Variant{
   386  			dbus.FieldReplySerial: dbus.MakeVariant(msg.Serial()),
   387  			dbus.FieldSender:      dbus.MakeVariant(":1"), // This does not matter.
   388  			// dbus.FieldDestination is provided automatically by DBus test helper.
   389  		},
   390  	}
   391  }
   392  
   393  func (s *fdoSuite) TestObserveNotificationsContextAndSignalWatch(c *C) {
   394  	ctx, cancel := context.WithCancel(context.TODO())
   395  	msgsSeen := 0
   396  	addMatchSeen := make(chan struct{}, 1)
   397  	defer close(addMatchSeen)
   398  	srv := s.connectWithHandler(c, func(msg *dbus.Message, n int) ([]*dbus.Message, error) {
   399  		msgsSeen++
   400  		switch n {
   401  		case 0:
   402  			s.checkAddMatchRequest(c, msg)
   403  			c.Check(msg.Body, HasLen, 1)
   404  			c.Check(msg.Body[0], Equals, "type='signal',sender='org.freedesktop.Notifications',path='/org/freedesktop/Notifications',interface='org.freedesktop.Notifications'")
   405  			response := s.addMatchResponse(c, msg)
   406  			addMatchSeen <- struct{}{}
   407  			return []*dbus.Message{response}, nil
   408  		case 1:
   409  			s.checkRemoveMatchRequest(c, msg)
   410  			c.Check(msg.Body, HasLen, 1)
   411  			c.Check(msg.Body[0], Equals, "type='signal',sender='org.freedesktop.Notifications',path='/org/freedesktop/Notifications',interface='org.freedesktop.Notifications'")
   412  			response := s.removeMatchResponse(c, msg)
   413  			return []*dbus.Message{response}, nil
   414  		default:
   415  			return nil, fmt.Errorf("unexpected message #%d: %s", n, msg)
   416  		}
   417  	})
   418  
   419  	var wg sync.WaitGroup
   420  	wg.Add(1)
   421  	go func() {
   422  		err := srv.ObserveNotifications(ctx, &testObserver{})
   423  		c.Assert(err, ErrorMatches, "context canceled")
   424  		wg.Done()
   425  	}()
   426  	// Wait for the signal that we saw the AddMatch message and then stop.
   427  	<-addMatchSeen
   428  	cancel()
   429  	// Wait for ObserveNotifications to return
   430  	wg.Wait()
   431  	c.Check(msgsSeen, Equals, 2)
   432  }
   433  
   434  func (s *fdoSuite) TestObserveNotificationsAddWatchError(c *C) {
   435  	srv := s.connectWithHandler(c, func(msg *dbus.Message, n int) ([]*dbus.Message, error) {
   436  		switch n {
   437  		case 0:
   438  			s.checkAddMatchRequest(c, msg)
   439  			response := s.nameHasNoOwnerResponse(c, msg)
   440  			return []*dbus.Message{response}, nil
   441  		default:
   442  			return nil, fmt.Errorf("unexpected message #%d: %s", n, msg)
   443  		}
   444  	})
   445  	err := srv.ObserveNotifications(context.TODO(), &testObserver{})
   446  	c.Assert(err, ErrorMatches, "org.freedesktop.DBus.Error.NameHasNoOwner")
   447  }
   448  
   449  func (s *fdoSuite) TestObserveNotificationsRemoveWatchError(c *C) {
   450  	logBuffer, restore := logger.MockLogger()
   451  	defer restore()
   452  
   453  	ctx, cancel := context.WithCancel(context.TODO())
   454  	msgsSeen := 0
   455  	addMatchSeen := make(chan struct{}, 1)
   456  	defer close(addMatchSeen)
   457  	srv := s.connectWithHandler(c, func(msg *dbus.Message, n int) ([]*dbus.Message, error) {
   458  		msgsSeen++
   459  		switch n {
   460  		case 0:
   461  			s.checkAddMatchRequest(c, msg)
   462  			response := s.addMatchResponse(c, msg)
   463  			addMatchSeen <- struct{}{}
   464  			return []*dbus.Message{response}, nil
   465  		case 1:
   466  			s.checkRemoveMatchRequest(c, msg)
   467  			response := s.nameHasNoOwnerResponse(c, msg)
   468  			return []*dbus.Message{response}, nil
   469  		default:
   470  			return nil, fmt.Errorf("unexpected message #%d: %s", n, msg)
   471  		}
   472  	})
   473  
   474  	var wg sync.WaitGroup
   475  	wg.Add(1)
   476  	go func() {
   477  		err := srv.ObserveNotifications(ctx, &testObserver{})
   478  		// The error from RemoveWatch is not clobbering the return value of ObserveNotifications.
   479  		c.Assert(err, ErrorMatches, "context canceled")
   480  		c.Check(logBuffer.String(), testutil.Contains, "Cannot remove D-Bus signal matcher: org.freedesktop.DBus.Error.NameHasNoOwner\n")
   481  		wg.Done()
   482  	}()
   483  	// Wait for the signal that we saw the AddMatch message and then stop.
   484  	<-addMatchSeen
   485  	cancel()
   486  	// Wait for ObserveNotifications to return
   487  	wg.Wait()
   488  	c.Check(msgsSeen, Equals, 2)
   489  }
   490  
   491  func (s *fdoSuite) TestObserveNotificationsProcessingError(c *C) {
   492  	msgsSeen := 0
   493  	srv := s.connectWithHandler(c, func(msg *dbus.Message, n int) ([]*dbus.Message, error) {
   494  		msgsSeen++
   495  		switch n {
   496  		case 0:
   497  			s.checkAddMatchRequest(c, msg)
   498  			response := s.addMatchResponse(c, msg)
   499  			sig := &dbus.Message{
   500  				Type: dbus.TypeSignal,
   501  				Headers: map[dbus.HeaderField]dbus.Variant{
   502  					dbus.FieldPath:      dbus.MakeVariant(dbus.ObjectPath("/org/freedesktop/Notifications")),
   503  					dbus.FieldInterface: dbus.MakeVariant("org.freedesktop.Notifications"),
   504  					dbus.FieldMember:    dbus.MakeVariant("ActionInvoked"),
   505  					dbus.FieldSender:    dbus.MakeVariant("org.freedesktop.Notifications"),
   506  					dbus.FieldSignature: dbus.MakeVariant(dbus.SignatureOf(uint32(0), "")),
   507  				},
   508  				Body: []interface{}{uint32(42), "action-key"},
   509  			}
   510  			// Send the DBus response for the method call and an additional signal.
   511  			return []*dbus.Message{response, sig}, nil
   512  		case 1:
   513  			s.checkRemoveMatchRequest(c, msg)
   514  			response := s.removeMatchResponse(c, msg)
   515  			return []*dbus.Message{response}, nil
   516  		default:
   517  			return nil, fmt.Errorf("unexpected message #%d: %s", n, msg)
   518  		}
   519  	})
   520  	err := srv.ObserveNotifications(context.TODO(), &testObserver{
   521  		actionInvoked: func(id notification.ID, actionKey string) error {
   522  			c.Check(id, Equals, notification.ID(42))
   523  			c.Check(actionKey, Equals, "action-key")
   524  			return fmt.Errorf("boom")
   525  		},
   526  	})
   527  	c.Assert(err, ErrorMatches, "cannot process ActionInvoked signal: boom")
   528  	c.Check(msgsSeen, Equals, 2)
   529  }
   530  
   531  func (s *fdoSuite) TestProcessActionInvokedSignalSuccess(c *C) {
   532  	called := false
   533  	err := notification.ProcessSignal(&dbus.Signal{
   534  		// Sender and Path are not used
   535  		Name: "org.freedesktop.Notifications.ActionInvoked",
   536  		Body: []interface{}{uint32(42), "action-key"},
   537  	}, &testObserver{
   538  		actionInvoked: func(id notification.ID, actionKey string) error {
   539  			called = true
   540  			c.Check(id, Equals, notification.ID(42))
   541  			c.Check(actionKey, Equals, "action-key")
   542  			return nil
   543  		},
   544  	})
   545  	c.Assert(err, IsNil)
   546  	c.Assert(called, Equals, true)
   547  }
   548  
   549  func (s *fdoSuite) TestProcessActionInvokedSignalError(c *C) {
   550  	err := notification.ProcessSignal(&dbus.Signal{
   551  		Name: "org.freedesktop.Notifications.ActionInvoked",
   552  		Body: []interface{}{uint32(42), "action-key"},
   553  	}, &testObserver{
   554  		actionInvoked: func(id notification.ID, actionKey string) error {
   555  			return fmt.Errorf("boom")
   556  		},
   557  	})
   558  	c.Assert(err, ErrorMatches, "cannot process ActionInvoked signal: boom")
   559  }
   560  
   561  func (s *fdoSuite) TestProcessActionInvokedSignalBodyParseErrors(c *C) {
   562  	err := notification.ProcessSignal(&dbus.Signal{
   563  		Name: "org.freedesktop.Notifications.ActionInvoked",
   564  		Body: []interface{}{uint32(42), "action-key", "unexpected"},
   565  	}, &testObserver{})
   566  	c.Assert(err, ErrorMatches, "cannot process ActionInvoked signal: unexpected number of body elements: 3")
   567  
   568  	err = notification.ProcessSignal(&dbus.Signal{
   569  		Name: "org.freedesktop.Notifications.ActionInvoked",
   570  		Body: []interface{}{uint32(42)},
   571  	}, &testObserver{})
   572  	c.Assert(err, ErrorMatches, "cannot process ActionInvoked signal: unexpected number of body elements: 1")
   573  
   574  	err = notification.ProcessSignal(&dbus.Signal{
   575  		Name: "org.freedesktop.Notifications.ActionInvoked",
   576  		Body: []interface{}{uint32(42), true},
   577  	}, &testObserver{})
   578  	c.Assert(err, ErrorMatches, "cannot process ActionInvoked signal: expected second body element to be string, got bool")
   579  
   580  	err = notification.ProcessSignal(&dbus.Signal{
   581  		Name: "org.freedesktop.Notifications.ActionInvoked",
   582  		Body: []interface{}{true, "action-key"},
   583  	}, &testObserver{})
   584  	c.Assert(err, ErrorMatches, "cannot process ActionInvoked signal: expected first body element to be uint32, got bool")
   585  }
   586  
   587  func (s *fdoSuite) TestProcessNotificationClosedSignalSuccess(c *C) {
   588  	called := false
   589  	err := notification.ProcessSignal(&dbus.Signal{
   590  		Name: "org.freedesktop.Notifications.NotificationClosed",
   591  		Body: []interface{}{uint32(42), uint32(2)},
   592  	}, &testObserver{
   593  		notificationClosed: func(id notification.ID, reason notification.CloseReason) error {
   594  			called = true
   595  			c.Check(id, Equals, notification.ID(42))
   596  			c.Check(reason, Equals, notification.CloseReason(2))
   597  			return nil
   598  		},
   599  	})
   600  	c.Assert(err, IsNil)
   601  	c.Assert(called, Equals, true)
   602  }
   603  
   604  func (s *fdoSuite) TestProcessNotificationClosedSignalError(c *C) {
   605  	err := notification.ProcessSignal(&dbus.Signal{
   606  		Name: "org.freedesktop.Notifications.NotificationClosed",
   607  		Body: []interface{}{uint32(42), uint32(2)},
   608  	}, &testObserver{
   609  		notificationClosed: func(id notification.ID, reason notification.CloseReason) error {
   610  			return fmt.Errorf("boom")
   611  		},
   612  	})
   613  	c.Assert(err, ErrorMatches, "cannot process NotificationClosed signal: boom")
   614  }
   615  
   616  func (s *fdoSuite) TestProcessNotificationClosedSignalBodyParseErrors(c *C) {
   617  	err := notification.ProcessSignal(&dbus.Signal{
   618  		Name: "org.freedesktop.Notifications.NotificationClosed",
   619  		Body: []interface{}{uint32(42), uint32(2), "unexpected"},
   620  	}, &testObserver{})
   621  	c.Assert(err, ErrorMatches, "cannot process NotificationClosed signal: unexpected number of body elements: 3")
   622  
   623  	err = notification.ProcessSignal(&dbus.Signal{
   624  		Name: "org.freedesktop.Notifications.NotificationClosed",
   625  		Body: []interface{}{uint32(42)},
   626  	}, &testObserver{})
   627  	c.Assert(err, ErrorMatches, "cannot process NotificationClosed signal: unexpected number of body elements: 1")
   628  
   629  	err = notification.ProcessSignal(&dbus.Signal{
   630  		Name: "org.freedesktop.Notifications.NotificationClosed",
   631  		Body: []interface{}{uint32(42), true},
   632  	}, &testObserver{})
   633  	c.Assert(err, ErrorMatches, "cannot process NotificationClosed signal: expected second body element to be uint32, got bool")
   634  
   635  	err = notification.ProcessSignal(&dbus.Signal{
   636  		Name: "org.freedesktop.Notifications.NotificationClosed",
   637  		Body: []interface{}{true, uint32(2)},
   638  	}, &testObserver{})
   639  	c.Assert(err, ErrorMatches, "cannot process NotificationClosed signal: expected first body element to be uint32, got bool")
   640  }