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) } }