Files

229 lines
7.7 KiB
Go

// 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: &notificationID, 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: &notificationID, 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: &notificationID, 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: &notificationID, 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("<b>Mail</b>"),
"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
}