// Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later package notification import ( "context" "errors" "testing" "github.com/godbus/dbus/v5" ) func TestMakoSyncAnnouncesOnlyNewNotifications(t *testing.T) { presenter := NewPresenter() announcer := &fakeAnnouncer{} monitor := NewMakoMonitor(nil, presenter, announcer) monitor.available = true notifications := []map[string]dbus.Variant{makoNotification(7)} monitor.sync(notifications, true) if len(announcer.entries) != 1 { t.Fatalf("announced %d notifications, want 1", len(announcer.entries)) } if announcer.entries[0].Message != "Notification Mail You have one message" { t.Fatalf("message = %q", announcer.entries[0].Message) } monitor.sync(notifications, true) if len(announcer.entries) != 1 { t.Fatalf("announced %d notifications after refresh, want 1", len(announcer.entries)) } } func TestMakoParseNormalizesMetadata(t *testing.T) { metadata, id := parseMako(makoNotification(9)) if id == nil || *id != 9 { t.Fatalf("id = %v, want 9", id) } if metadata.AppName != "Application" || metadata.Summary != "Mail" || metadata.Body != "You have one message" || metadata.DesktopEntry != "application" || metadata.Urgency != 2 || metadata.Actions["default"] != "View" { t.Fatalf("metadata = %+v", metadata) } } func TestMakoStopMarksLiveHistoryUnavailable(t *testing.T) { presenter := NewPresenter() notificationID := 12 presenter.Save("Notification Mail", SaveOptions{ Source: MakoSource, NotificationID: ¬ificationID, Live: true, }) monitor := NewMakoMonitor(&fakeMakoConnection{}, presenter, &fakeAnnouncer{}) monitor.available = true monitor.running = true monitor.stop = make(chan struct{}) monitor.stopped = make(chan struct{}) close(monitor.stopped) if err := monitor.Stop(context.Background()); err != nil { t.Fatalf("stop mako: %v", err) } entries := presenter.Entries() if len(entries) != 1 || entries[0].Live { t.Fatalf("entries = %+v, want one unavailable entry", entries) } } func TestMakoOwnerLossMarksLiveHistoryUnavailable(t *testing.T) { presenter := NewPresenter() notificationID := 12 presenter.Save("Notification Mail", SaveOptions{ Source: MakoSource, NotificationID: ¬ificationID, Live: true, }) monitor := NewMakoMonitor(&fakeMakoConnection{}, presenter, &fakeAnnouncer{}) monitor.available = true monitor.handleSignal(&dbus.Signal{ Name: "org.freedesktop.DBus.NameOwnerChanged", Path: "/org/freedesktop/DBus", Body: []interface{}{makoBusName, ":1.10", ""}, }) entries := presenter.Entries() if monitor.available || len(entries) != 1 || entries[0].Live { t.Fatalf("available = %v, entries = %+v", monitor.available, entries) } } func TestMakoUnsupportedDaemonMarksHistoryUnavailable(t *testing.T) { presenter := NewPresenter() notificationID := 12 presenter.Save("Notification Mail", SaveOptions{ Source: MakoSource, NotificationID: ¬ificationID, Live: true, }) monitor := NewMakoMonitor(&fakeMakoConnection{ object: &fakeMakoObject{err: errors.New("object does not exist")}, }, presenter, &fakeAnnouncer{}) monitor.available = true if monitor.Refresh(false) { t.Fatal("unsupported notification daemon reported as available") } entries := presenter.Entries() if len(entries) != 1 || entries[0].Live { t.Fatalf("entries = %+v, want one unavailable entry", entries) } if len(presenter.ActionsForEntry(entries[0])) != 0 { t.Fatal("unsupported notification daemon retained live actions") } } func TestPresenterRoutesLiveMakoActions(t *testing.T) { presenter := NewPresenter() controller := &fakeMakoController{} presenter.SetMakoController(controller) notificationID := 6 entry := presenter.Save("Notification Mail New message", SaveOptions{ Source: MakoSource, SourceGeneration: 1, NotificationID: ¬ificationID, Live: true, Actions: map[string]string{"default": "View"}, }) if !presenter.CanControlEntry(entry) { t.Fatal("live mako entry was not controllable") } if actions := presenter.ActionsForEntry(entry); actions["default"] != "View" { t.Fatalf("actions = %q, want View", actions["default"]) } messages, dismissed := presenter.DismissEntry(entry) if !dismissed || messages[0] != MessageDismissed { t.Fatalf("dismiss = %v %q", dismissed, messages) } if len(presenter.Entries()) != 0 || controller.dismissed[0] != 6 { t.Fatalf("entries = %+v dismissed = %v", presenter.Entries(), controller.dismissed) } actionMessages, invoked := presenter.InvokeActionEntry(entry, "default") if !invoked || actionMessages[0] != MessageActionInvoked || controller.invoked[0] != (makoAction{6, "default"}) { t.Fatalf("invoke = %v %q", invoked, actionMessages) } } func makoNotification(id uint32) map[string]dbus.Variant { return map[string]dbus.Variant{ "id": dbus.MakeVariant(id), "app-name": dbus.MakeVariant("Application"), "summary": dbus.MakeVariant("Mail"), "body": dbus.MakeVariant("You have one message"), "desktop-entry": dbus.MakeVariant("application"), "urgency": dbus.MakeVariant(int32(2)), "actions": dbus.MakeVariant(map[string]dbus.Variant{"default": dbus.MakeVariant("View")}), } } type fakeAnnouncer struct{ entries []Entry } func (announcer *fakeAnnouncer) Announce(entry Entry) { announcer.entries = append(announcer.entries, entry) } type fakeMakoConnection struct { object *fakeMakoObject } func (connection *fakeMakoConnection) Signal(chan<- *dbus.Signal) {} func (connection *fakeMakoConnection) RemoveSignal(chan<- *dbus.Signal) {} func (connection *fakeMakoConnection) Object(string, dbus.ObjectPath) dbus.BusObject { return connection.object } func (connection *fakeMakoConnection) AddMatchSignal(...dbus.MatchOption) error { return nil } func (connection *fakeMakoConnection) RemoveMatchSignal(...dbus.MatchOption) error { return nil } func (connection *fakeMakoConnection) Close() error { return nil } type fakeMakoObject struct { err error } func (object *fakeMakoObject) Call(string, dbus.Flags, ...interface{}) *dbus.Call { return &dbus.Call{Err: object.err} } func (object *fakeMakoObject) CallWithContext(context.Context, string, dbus.Flags, ...interface{}) *dbus.Call { return &dbus.Call{Err: object.err} } func (object *fakeMakoObject) Go(string, dbus.Flags, chan *dbus.Call, ...interface{}) *dbus.Call { return &dbus.Call{Err: object.err} } func (object *fakeMakoObject) GoWithContext(context.Context, string, dbus.Flags, chan *dbus.Call, ...interface{}) *dbus.Call { return &dbus.Call{Err: object.err} } func (object *fakeMakoObject) AddMatchSignal(string, string, ...dbus.MatchOption) *dbus.Call { return &dbus.Call{Err: object.err} } func (object *fakeMakoObject) RemoveMatchSignal(string, string, ...dbus.MatchOption) *dbus.Call { return &dbus.Call{Err: object.err} } func (object *fakeMakoObject) GetProperty(string) (dbus.Variant, error) { return dbus.Variant{}, object.err } func (object *fakeMakoObject) StoreProperty(string, interface{}) error { return object.err } func (object *fakeMakoObject) SetProperty(string, interface{}) error { return object.err } func (object *fakeMakoObject) Destination() string { return makoBusName } func (object *fakeMakoObject) Path() dbus.ObjectPath { return makoObjectPath } type makoAction struct { id int key string } type fakeMakoController struct { dismissed []int invoked []makoAction } func (controller *fakeMakoController) IsCurrent(Entry) bool { return true } func (controller *fakeMakoController) Dismiss(id int) bool { controller.dismissed = append(controller.dismissed, id) return true } func (controller *fakeMakoController) InvokeAction(id int, key string) bool { controller.invoked = append(controller.invoked, makoAction{id, key}) return true }