package main import ( "fmt" "os" "path/filepath" "strings" "sync" "testing" "time" ) // **A notification offers answers, and the chosen one is said as an event** (novox/hq ADR 0253): the id // is answered at once, the answer comes when the operator chooses, and nothing is said for a // notification closed without one. func TestANotificationsChosenAnswerIsSaidAsAnEvent(t *testing.T) { withBus(t) bin := fakeBinaries(t, map[string]string{"notify-send": `for a in "$@"; do printf '[%s]' "$a"; done > "$LOG"; echo 42; sleep 0.2; echo "$CHOSEN"`}) t.Setenv("LOG", filepath.Join(bin, "log")) t.Setenv("MESH_NODE", "g14") var mu sync.Mutex var said []map[string]any before := emit emit = func(event string, body any) error { mu.Lock() defer mu.Unlock() if event != ChosenEvent { t.Errorf("event %q", event) } said = append(said, body.(map[string]any)) return nil } t.Cleanup(func() { emit = before }) n, err := notificationOf(map[string]any{"summary": "Delivery of hq held for 36 hours", "body": "Needs you: release it, or stop it.", "actions": []any{map[string]any{"token": "a1b2", "label": "Release"}, map[string]any{"token": "c3d4", "label": "Stop"}}}) if err != nil { t.Fatal(err) } t.Setenv("CHOSEN", "c3d4") start := time.Now() got, err := Send(n) if err != nil || got.ID != 42 || time.Since(start) > 150*time.Millisecond { t.Fatalf("%+v, %v, after %s: the id is answered before the answer", got, err, time.Since(start)) } asked, _ := os.ReadFile(filepath.Join(bin, "log")) if !strings.Contains(string(asked), "[--expire-time=0][--wait][--action=a1b2=Release][--action=c3d4=Stop][--]") { t.Fatalf("notify-send was asked %s", asked) } deadline := time.Now().Add(3 * time.Second) for { mu.Lock() n := len(said) mu.Unlock() if n > 0 || time.Now().After(deadline) { break } time.Sleep(20 * time.Millisecond) } mu.Lock() if len(said) != 1 || said[0]["token"] != "c3d4" || said[0]["id"] != 42 || said[0]["machine"] != "g14" { t.Fatalf("said %v", said) } said = nil mu.Unlock() // Closed without an answer, or an answer nobody offered: nothing is said. for _, chosen := range []string{"", "zzzz"} { t.Setenv("CHOSEN", chosen) if _, err := Send(n); err != nil { t.Fatal(err) } } time.Sleep(600 * time.Millisecond) mu.Lock() defer mu.Unlock() if len(said) != 0 { t.Fatalf("an answer was invented: %v", said) } } func TestActionsAreRefusedForWhatTheyCannotBe(t *testing.T) { for _, bad := range []any{"x", []any{"x"}, []any{map[string]any{"token": "a b", "label": "x"}}, []any{map[string]any{"token": "a", "label": ""}}, []any{map[string]any{"token": "a", "label": "x=y"}}, []any{map[string]any{"token": "a", "label": "x"}, map[string]any{"token": "a", "label": "y"}}, []any{map[string]any{"token": "default", "label": "x"}, map[string]any{"token": "b", "label": "y"}}, []any{map[string]any{"token": "a", "label": "1"}, map[string]any{"token": "b", "label": "2"}, map[string]any{"token": "c", "label": "3"}, map[string]any{"token": "d", "label": "4"}, map[string]any{"token": "e", "label": "5"}}} { if _, err := notificationOf(map[string]any{"summary": "x", "actions": bad}); err == nil { t.Errorf("accepted %v", bad) } } } // **A bundle that started says so**, so the operator channel shows again the answers lost with the one // before. func TestABundleThatStartedSaysSo(t *testing.T) { t.Setenv("MESH_NODE", "g14") var said []string before := emit emit = func(event string, body any) error { said = append(said, fmt.Sprintf("%s %v", event, body)) return nil } t.Cleanup(func() { emit = before }) sayStarted() if len(said) != 1 || said[0] != "started map[machine:g14]" { t.Fatalf("%v", said) } }