Owns one dunstrc (the laptop's, in the interface face, its menu on the seat's dmenu command) and the dunstrc.d directory for other modules' rules; D-Bus starts it, so nothing else does. Go tools over the session bus.
161 lines
5.7 KiB
Go
161 lines
5.7 KiB
Go
package main
|
|
|
|
import (
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// withBus gives the fake machine the account's runtime directory and bus socket.
|
|
func withBus(t *testing.T) string {
|
|
t.Helper()
|
|
root := fakeMachine(t)
|
|
runtime := filepath.Join(runUserDir, strconv.Itoa(os.Getuid()))
|
|
if err := os.MkdirAll(runtime, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(runtime, "bus"), nil, 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return root
|
|
}
|
|
|
|
// A history as dunstctl answers it (busctl's JSON), two entries out of order.
|
|
const history = `{"type":"aa{sv}","data":[[
|
|
{"body":{"type":"s","data":"the build is green"},"summary":{"type":"s","data":"CI"},"appname":{"type":"s","data":"mesh"},
|
|
"category":{"type":"s","data":""},"id":{"type":"i","data":7},"timestamp":{"type":"x","data":100000000},"urgency":{"type":"s","data":"NORMAL"}},
|
|
{"body":{"type":"s","data":""},"summary":{"type":"s","data":"Battery low"},"appname":{"type":"s","data":"upower"},
|
|
"category":{"type":"s","data":"device"},"id":{"type":"i","data":9},"timestamp":{"type":"x","data":160000000},"urgency":{"type":"s","data":"CRITICAL"}}
|
|
]]}`
|
|
|
|
func TestTheHistoryIsNewestFirstWithAgesFromDunstsOwnClock(t *testing.T) {
|
|
got, err := parseHistory(history, 200_000_000, 20)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := []Shown{
|
|
{ID: 9, AppName: "upower", Summary: "Battery low", Urgency: "critical", Category: "device", AgeSeconds: 40},
|
|
{ID: 7, AppName: "mesh", Summary: "CI", Body: "the build is green", Urgency: "normal", AgeSeconds: 100},
|
|
}
|
|
if got.Total != 2 || !reflect.DeepEqual(got.Notifications, want) {
|
|
t.Fatalf("%+v", got)
|
|
}
|
|
if got, _ := parseHistory(history, 200_000_000, 1); len(got.Notifications) != 1 || got.Total != 2 {
|
|
t.Fatalf("limited: %+v", got)
|
|
}
|
|
if _, err := parseHistory(`{"type":"as","data":[]}`, 0, 1); err == nil {
|
|
t.Fatal("an answer of another type was accepted")
|
|
}
|
|
if monotonicMicros() <= 0 {
|
|
t.Fatal("the monotonic clock")
|
|
}
|
|
}
|
|
|
|
func TestSendAsksNotifySendOverTheAccountsBusAndAnswersTheId(t *testing.T) {
|
|
withBus(t)
|
|
bin := fakeBinaries(t, map[string]string{"notify-send": `for a in "$@"; do printf '[%s]' "$a"; done > "$LOG"; echo >> "$LOG"; echo "bus=$DBUS_SESSION_BUS_ADDRESS" >> "$LOG"; echo 42`})
|
|
t.Setenv("LOG", filepath.Join(bin, "log"))
|
|
n, err := notificationOf(map[string]any{"summary": "-dash title", "body": "hello", "urgency": "critical", "expire_ms": float64(0)})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, err := Send(n)
|
|
if err != nil || got.ID != 42 {
|
|
t.Fatalf("%+v, %v", got, err)
|
|
}
|
|
asked, _ := os.ReadFile(filepath.Join(bin, "log"))
|
|
want := "[--print-id][--urgency=critical][--app-name=mesh][--expire-time=0][--][-dash title][hello]\nbus=unix:path=" +
|
|
filepath.Join(runUserDir, strconv.Itoa(os.Getuid()), "bus") + "\n"
|
|
if string(asked) != want {
|
|
t.Fatalf("notify-send was asked:\n%s\nwant:\n%s", asked, want)
|
|
}
|
|
}
|
|
|
|
func TestANotificationIsRefusedForWhatItCannotBe(t *testing.T) {
|
|
for _, bad := range []map[string]any{{}, {"summary": " "}, {"summary": "x", "urgency": "urgent"}, {"summary": "x", "expire_ms": float64(-5)}} {
|
|
if _, err := notificationOf(bad); err == nil {
|
|
t.Errorf("accepted %v", bad)
|
|
}
|
|
}
|
|
n, _ := notificationOf(map[string]any{"summary": "x"})
|
|
if n.Urgency != "normal" || n.AppName != "mesh" || n.ExpireMS != -1 {
|
|
t.Fatalf("defaults: %+v", n)
|
|
}
|
|
}
|
|
|
|
func TestWithoutABusTheToolsSaySo(t *testing.T) {
|
|
fakeMachine(t)
|
|
if _, err := Send(Notification{Summary: "x"}); !errors.Is(err, ErrNoBus) {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := Count(); !errors.Is(err, ErrNoBus) {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestCountPauseAndCloseAllAreDunstctlsAnswers(t *testing.T) {
|
|
withBus(t)
|
|
bin := fakeBinaries(t, map[string]string{"dunstctl": `echo "$*" >> "$LOG"
|
|
case "$*" in
|
|
"count displayed") echo 1 ;;
|
|
"count waiting") echo 0 ;;
|
|
"count history") echo 12 ;;
|
|
is-paused) cat "$STATE" 2>/dev/null || echo false ;;
|
|
"set-paused true") echo true > "$STATE" ;;
|
|
"set-paused false") echo false > "$STATE" ;;
|
|
esac`})
|
|
t.Setenv("LOG", filepath.Join(bin, "log"))
|
|
t.Setenv("STATE", filepath.Join(bin, "paused"))
|
|
c, err := Count()
|
|
if err != nil || c != (CountResult{Displayed: 1, History: 12}) {
|
|
t.Fatalf("%+v, %v", c, err)
|
|
}
|
|
p, err := SetPaused(true)
|
|
if err != nil || !p.Paused || !strings.Contains(p.Note, "held back") {
|
|
t.Fatalf("%+v, %v", p, err)
|
|
}
|
|
if c, _ := CloseAll(); !c.Paused {
|
|
t.Fatalf("close-all answers the counts: %+v", c)
|
|
}
|
|
if p, _ := SetPaused(false); p.Paused {
|
|
t.Fatalf("resumed: %+v", p)
|
|
}
|
|
log, _ := os.ReadFile(filepath.Join(bin, "log"))
|
|
if !strings.Contains(string(log), "close-all\n") {
|
|
t.Fatalf("dunstctl was asked:\n%s", log)
|
|
}
|
|
}
|
|
|
|
func TestRulesAreTheRunningNotifiersWithTheFilesTheyComeFrom(t *testing.T) {
|
|
root := t.TempDir()
|
|
for _, f := range []string{"dunstrc", "dunstrc.d/50-slack.conf", "dunstrc.d/10-mail.conf", "dunstrc.d/notes.txt"} {
|
|
if err := os.MkdirAll(filepath.Dir(filepath.Join(root, f)), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(root, f), nil, 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
raw := `{"type":"aa{sv}","data":[[{"enabled":{"type":"b","data":true},"appname":{"type":"s","data":"Slack"},
|
|
"fc":{"type":"s","data":"#6715ebff"},"name":{"type":"s","data":"slack"},"timeout":{"type":"x","data":10000000}}]]}`
|
|
got, err := parseRules(raw, configFiles(root))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(got.Rules) != 1 || got.Rules[0].Name != "slack" || !got.Rules[0].Enabled || got.Rules[0].Sets["appname"] != "Slack" {
|
|
t.Fatalf("%+v", got)
|
|
}
|
|
var names []string
|
|
for _, f := range got.Files {
|
|
rel, _ := filepath.Rel(root, f)
|
|
names = append(names, rel)
|
|
}
|
|
if !reflect.DeepEqual(names, []string{"dunstrc", "dunstrc.d/10-mail.conf", "dunstrc.d/50-slack.conf"}) {
|
|
t.Fatalf("files: %v", names)
|
|
}
|
|
}
|