github.com/david-imola/snapd@v0.0.0-20210611180407-2de8ddeece6d/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/desktop/notification"
    32  	"github.com/snapcore/snapd/desktop/notification/notificationtest"
    33  	"github.com/snapcore/snapd/testutil"
    34  )
    35  
    36  type fdoSuite struct {
    37  	testutil.BaseTest
    38  	testutil.DBusTest
    39  
    40  	backend *notificationtest.FdoServer
    41  }
    42  
    43  var _ = Suite(&fdoSuite{})
    44  
    45  func (s *fdoSuite) SetUpTest(c *C) {
    46  	s.BaseTest.SetUpTest(c)
    47  	s.DBusTest.SetUpTest(c)
    48  
    49  	backend, err := notificationtest.NewFdoServer()
    50  	c.Assert(err, IsNil)
    51  	s.AddCleanup(func() { c.Check(backend.Stop(), IsNil) })
    52  	s.backend = backend
    53  }
    54  
    55  func (s *fdoSuite) TearDownTest(c *C) {
    56  	s.DBusTest.TearDownTest(c)
    57  	s.BaseTest.TearDownTest(c)
    58  }
    59  
    60  func (s *fdoSuite) TestServerInformationSuccess(c *C) {
    61  	srv := notification.New(s.SessionBus)
    62  	name, vendor, version, specVersion, err := srv.ServerInformation()
    63  	c.Assert(err, IsNil)
    64  	c.Check(name, Equals, "name")
    65  	c.Check(vendor, Equals, "vendor")
    66  	c.Check(version, Equals, "version")
    67  	c.Check(specVersion, Equals, "specVersion")
    68  }
    69  
    70  func (s *fdoSuite) TestServerInformationError(c *C) {
    71  	s.backend.SetError(&dbus.Error{Name: "org.freedesktop.DBus.Error.Failed"})
    72  	srv := notification.New(s.SessionBus)
    73  	_, _, _, _, err := srv.ServerInformation()
    74  	c.Assert(err, ErrorMatches, "org.freedesktop.DBus.Error.Failed")
    75  }
    76  
    77  func (s *fdoSuite) TestServerCapabilitiesSuccess(c *C) {
    78  	srv := notification.New(s.SessionBus)
    79  	caps, err := srv.ServerCapabilities()
    80  	c.Assert(err, IsNil)
    81  	c.Check(caps, DeepEquals, []notification.ServerCapability{"cap-foo", "cap-bar"})
    82  }
    83  
    84  func (s *fdoSuite) TestServerCapabilitiesError(c *C) {
    85  	s.backend.SetError(&dbus.Error{Name: "org.freedesktop.DBus.Error.Failed"})
    86  	srv := notification.New(s.SessionBus)
    87  	_, err := srv.ServerCapabilities()
    88  	c.Assert(err, ErrorMatches, "org.freedesktop.DBus.Error.Failed")
    89  }
    90  
    91  func (s *fdoSuite) TestSendNotificationSuccess(c *C) {
    92  	srv := notification.New(s.SessionBus)
    93  	id, err := srv.SendNotification(&notification.Message{
    94  		AppName:       "app-name",
    95  		Icon:          "icon",
    96  		Summary:       "summary",
    97  		Body:          "body",
    98  		ExpireTimeout: time.Second * 1,
    99  		ReplacesID:    notification.ID(42),
   100  		Actions: []notification.Action{
   101  			{ActionKey: "key-1", LocalizedText: "text-1"},
   102  			{ActionKey: "key-2", LocalizedText: "text-2"},
   103  		},
   104  		Hints: []notification.Hint{
   105  			{Name: "hint-str", Value: "str"},
   106  			{Name: "hint-bool", Value: true},
   107  		},
   108  	})
   109  	c.Assert(err, IsNil)
   110  
   111  	c.Check(s.backend.Get(uint32(id)), DeepEquals, &notificationtest.FdoNotification{
   112  		ID:      uint32(id),
   113  		AppName: "app-name",
   114  		Icon:    "icon",
   115  		Summary: "summary",
   116  		Body:    "body",
   117  		Actions: []string{"key-1", "text-1", "key-2", "text-2"},
   118  		Hints: map[string]dbus.Variant{
   119  			"hint-str":  dbus.MakeVariant("str"),
   120  			"hint-bool": dbus.MakeVariant(true),
   121  		},
   122  		Expires: 1000,
   123  	})
   124  }
   125  
   126  func (s *fdoSuite) TestSendNotificationWithServerDecidedExpireTimeout(c *C) {
   127  	srv := notification.New(s.SessionBus)
   128  	id, err := srv.SendNotification(&notification.Message{
   129  		ExpireTimeout: notification.ServerSelectedExpireTimeout,
   130  	})
   131  	c.Assert(err, IsNil)
   132  
   133  	c.Check(s.backend.Get(uint32(id)), DeepEquals, &notificationtest.FdoNotification{
   134  		ID:      uint32(id),
   135  		Actions: []string{},
   136  		Hints:   map[string]dbus.Variant{},
   137  		Expires: -1,
   138  	})
   139  }
   140  
   141  func (s *fdoSuite) TestSendNotificationError(c *C) {
   142  	s.backend.SetError(&dbus.Error{Name: "org.freedesktop.DBus.Error.Failed"})
   143  	srv := notification.New(s.SessionBus)
   144  	_, err := srv.SendNotification(&notification.Message{})
   145  	c.Assert(err, ErrorMatches, "org.freedesktop.DBus.Error.Failed")
   146  }
   147  
   148  func (s *fdoSuite) TestCloseNotificationSuccess(c *C) {
   149  	srv := notification.New(s.SessionBus)
   150  	id, err := srv.SendNotification(&notification.Message{})
   151  	c.Assert(err, IsNil)
   152  
   153  	err = srv.CloseNotification(id)
   154  	c.Assert(err, IsNil)
   155  	c.Check(s.backend.Get(uint32(id)), IsNil)
   156  }
   157  
   158  func (s *fdoSuite) TestCloseNotificationError(c *C) {
   159  	s.backend.SetError(&dbus.Error{Name: "org.freedesktop.DBus.Error.Failed"})
   160  	srv := notification.New(s.SessionBus)
   161  	err := srv.CloseNotification(notification.ID(42))
   162  	c.Assert(err, ErrorMatches, "org.freedesktop.DBus.Error.Failed")
   163  }
   164  
   165  type testObserver struct {
   166  	notificationClosed func(notification.ID, notification.CloseReason) error
   167  	actionInvoked      func(notification.ID, string) error
   168  }
   169  
   170  func (o *testObserver) NotificationClosed(id notification.ID, reason notification.CloseReason) error {
   171  	if o.notificationClosed != nil {
   172  		return o.notificationClosed(id, reason)
   173  	}
   174  	return nil
   175  }
   176  
   177  func (o *testObserver) ActionInvoked(id notification.ID, actionKey string) error {
   178  	if o.actionInvoked != nil {
   179  		return o.actionInvoked(id, actionKey)
   180  	}
   181  	return nil
   182  }
   183  
   184  func (s *fdoSuite) TestObserveNotificationsContextAndSignalWatch(c *C) {
   185  	srv := notification.New(s.SessionBus)
   186  
   187  	ctx, cancel := context.WithCancel(context.TODO())
   188  	signalDelivered := make(chan struct{}, 1)
   189  	defer close(signalDelivered)
   190  	var wg sync.WaitGroup
   191  	wg.Add(1)
   192  	go func() {
   193  		err := srv.ObserveNotifications(ctx, &testObserver{
   194  			actionInvoked: func(id notification.ID, actionKey string) error {
   195  				select {
   196  				case signalDelivered <- struct{}{}:
   197  				default:
   198  				}
   199  				return nil
   200  			},
   201  		})
   202  		c.Assert(err, ErrorMatches, "context canceled")
   203  		wg.Done()
   204  	}()
   205  	// Send signals until we've got confirmation that the observer
   206  	// is firing
   207  	for sendSignal := true; sendSignal; {
   208  		c.Check(s.backend.InvokeAction(42, "action-key"), IsNil)
   209  		select {
   210  		case <-signalDelivered:
   211  			sendSignal = false
   212  		default:
   213  		}
   214  	}
   215  	cancel()
   216  	// Wait for ObserveNotifications to return
   217  	wg.Wait()
   218  }
   219  
   220  func (s *fdoSuite) TestObserveNotificationsProcessingError(c *C) {
   221  	srv := notification.New(s.SessionBus)
   222  
   223  	signalDelivered := make(chan struct{}, 1)
   224  	defer close(signalDelivered)
   225  	var wg sync.WaitGroup
   226  	wg.Add(1)
   227  	go func() {
   228  		err := srv.ObserveNotifications(context.TODO(), &testObserver{
   229  			actionInvoked: func(id notification.ID, actionKey string) error {
   230  				signalDelivered <- struct{}{}
   231  				c.Check(id, Equals, notification.ID(42))
   232  				c.Check(actionKey, Equals, "action-key")
   233  				return fmt.Errorf("boom")
   234  			},
   235  		})
   236  		c.Log("End of goroutine")
   237  		c.Check(err, ErrorMatches, "cannot process ActionInvoked signal: boom")
   238  		wg.Done()
   239  	}()
   240  	// We don't know if the other goroutine has set up the signal
   241  	// match yet, so send signals until we get confirmation.
   242  	for sendSignal := true; sendSignal; {
   243  		c.Check(s.backend.InvokeAction(42, "action-key"), IsNil)
   244  		select {
   245  		case <-signalDelivered:
   246  			sendSignal = false
   247  		default:
   248  		}
   249  	}
   250  	// Wait for ObserveNotifications to return
   251  	wg.Wait()
   252  }
   253  
   254  func (s *fdoSuite) TestProcessActionInvokedSignalSuccess(c *C) {
   255  	called := false
   256  	err := notification.ProcessSignal(&dbus.Signal{
   257  		// Sender and Path are not used
   258  		Name: "org.freedesktop.Notifications.ActionInvoked",
   259  		Body: []interface{}{uint32(42), "action-key"},
   260  	}, &testObserver{
   261  		actionInvoked: func(id notification.ID, actionKey string) error {
   262  			called = true
   263  			c.Check(id, Equals, notification.ID(42))
   264  			c.Check(actionKey, Equals, "action-key")
   265  			return nil
   266  		},
   267  	})
   268  	c.Assert(err, IsNil)
   269  	c.Assert(called, Equals, true)
   270  }
   271  
   272  func (s *fdoSuite) TestProcessActionInvokedSignalError(c *C) {
   273  	err := notification.ProcessSignal(&dbus.Signal{
   274  		Name: "org.freedesktop.Notifications.ActionInvoked",
   275  		Body: []interface{}{uint32(42), "action-key"},
   276  	}, &testObserver{
   277  		actionInvoked: func(id notification.ID, actionKey string) error {
   278  			return fmt.Errorf("boom")
   279  		},
   280  	})
   281  	c.Assert(err, ErrorMatches, "cannot process ActionInvoked signal: boom")
   282  }
   283  
   284  func (s *fdoSuite) TestProcessActionInvokedSignalBodyParseErrors(c *C) {
   285  	err := notification.ProcessSignal(&dbus.Signal{
   286  		Name: "org.freedesktop.Notifications.ActionInvoked",
   287  		Body: []interface{}{uint32(42), "action-key", "unexpected"},
   288  	}, &testObserver{})
   289  	c.Assert(err, ErrorMatches, "cannot process ActionInvoked signal: unexpected number of body elements: 3")
   290  
   291  	err = notification.ProcessSignal(&dbus.Signal{
   292  		Name: "org.freedesktop.Notifications.ActionInvoked",
   293  		Body: []interface{}{uint32(42)},
   294  	}, &testObserver{})
   295  	c.Assert(err, ErrorMatches, "cannot process ActionInvoked signal: unexpected number of body elements: 1")
   296  
   297  	err = notification.ProcessSignal(&dbus.Signal{
   298  		Name: "org.freedesktop.Notifications.ActionInvoked",
   299  		Body: []interface{}{uint32(42), true},
   300  	}, &testObserver{})
   301  	c.Assert(err, ErrorMatches, "cannot process ActionInvoked signal: expected second body element to be string, got bool")
   302  
   303  	err = notification.ProcessSignal(&dbus.Signal{
   304  		Name: "org.freedesktop.Notifications.ActionInvoked",
   305  		Body: []interface{}{true, "action-key"},
   306  	}, &testObserver{})
   307  	c.Assert(err, ErrorMatches, "cannot process ActionInvoked signal: expected first body element to be uint32, got bool")
   308  }
   309  
   310  func (s *fdoSuite) TestProcessNotificationClosedSignalSuccess(c *C) {
   311  	called := false
   312  	err := notification.ProcessSignal(&dbus.Signal{
   313  		Name: "org.freedesktop.Notifications.NotificationClosed",
   314  		Body: []interface{}{uint32(42), uint32(2)},
   315  	}, &testObserver{
   316  		notificationClosed: func(id notification.ID, reason notification.CloseReason) error {
   317  			called = true
   318  			c.Check(id, Equals, notification.ID(42))
   319  			c.Check(reason, Equals, notification.CloseReason(2))
   320  			return nil
   321  		},
   322  	})
   323  	c.Assert(err, IsNil)
   324  	c.Assert(called, Equals, true)
   325  }
   326  
   327  func (s *fdoSuite) TestProcessNotificationClosedSignalError(c *C) {
   328  	err := notification.ProcessSignal(&dbus.Signal{
   329  		Name: "org.freedesktop.Notifications.NotificationClosed",
   330  		Body: []interface{}{uint32(42), uint32(2)},
   331  	}, &testObserver{
   332  		notificationClosed: func(id notification.ID, reason notification.CloseReason) error {
   333  			return fmt.Errorf("boom")
   334  		},
   335  	})
   336  	c.Assert(err, ErrorMatches, "cannot process NotificationClosed signal: boom")
   337  }
   338  
   339  func (s *fdoSuite) TestProcessNotificationClosedSignalBodyParseErrors(c *C) {
   340  	err := notification.ProcessSignal(&dbus.Signal{
   341  		Name: "org.freedesktop.Notifications.NotificationClosed",
   342  		Body: []interface{}{uint32(42), uint32(2), "unexpected"},
   343  	}, &testObserver{})
   344  	c.Assert(err, ErrorMatches, "cannot process NotificationClosed signal: unexpected number of body elements: 3")
   345  
   346  	err = notification.ProcessSignal(&dbus.Signal{
   347  		Name: "org.freedesktop.Notifications.NotificationClosed",
   348  		Body: []interface{}{uint32(42)},
   349  	}, &testObserver{})
   350  	c.Assert(err, ErrorMatches, "cannot process NotificationClosed signal: unexpected number of body elements: 1")
   351  
   352  	err = notification.ProcessSignal(&dbus.Signal{
   353  		Name: "org.freedesktop.Notifications.NotificationClosed",
   354  		Body: []interface{}{uint32(42), true},
   355  	}, &testObserver{})
   356  	c.Assert(err, ErrorMatches, "cannot process NotificationClosed signal: expected second body element to be uint32, got bool")
   357  
   358  	err = notification.ProcessSignal(&dbus.Signal{
   359  		Name: "org.freedesktop.Notifications.NotificationClosed",
   360  		Body: []interface{}{true, uint32(2)},
   361  	}, &testObserver{})
   362  	c.Assert(err, ErrorMatches, "cannot process NotificationClosed signal: expected first body element to be uint32, got bool")
   363  }