gopkg.in/ubuntu-core/snappy.v0@v0.0.0-20210902073436-25a8614f10a6/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(¬ification.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, ¬ificationtest.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(¬ification.Message{ 129 ExpireTimeout: notification.ServerSelectedExpireTimeout, 130 }) 131 c.Assert(err, IsNil) 132 133 c.Check(s.backend.Get(uint32(id)), DeepEquals, ¬ificationtest.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(¬ification.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(¬ification.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 }