package main import ( "encoding/json" "errors" "os" "path/filepath" "strings" "testing" ) // Slack's settings as Slack writes them, with keys no answer may carry. const rootState = `{"appTeams":{"selectedTeamId":null,"teamsByIndex":[]}, "settings":{"launchOnStartup":true,"hideOnStartup":true,"runFromTray":true,"notificationMethod":null, "useHwAcceleration":true,"PrefSSBFileDownloadPath":"/home/operator/Downloads","signInMethod":"secret-sso"}}` // A main-process log across a restart: the accounts before it are not counted. const browserLog = `[10/03/26, 09:00:00:001] info: Store: INITIALIZE [10/03/26, 09:00:05:001] info: Store: STORE_USER_WORKSPACES { "userTeamId": "TOLD0000001", "workspaceIds": [ "TOLD0000001", "TOLD0000002" ] } [10/04/26, 16:26:20:100] info: Store: INITIALIZE [10/04/26, 16:26:30:100] info: Store: STORE_USER_WORKSPACES { "userTeamId": "TAAAA000001", "workspaceIds": [ "TAAAA000001" ] } [10/04/26, 16:26:31:100] warn: Request failed with token xoxc-1234-5678-abcdef and cookie d=xoxd-AbCdEfGhIjKlMnOpQrStUv%2Fwx [10/04/26, 16:27:00:100] info: Store: NEW_NOTIFICATION { "title": "A private message", "workspaceName": "Secret Workspace", "teamId": "TAAAA000001" } [10/04/26, 23:41:06:340] info: Store: STORE_USER_WORKSPACES { "userTeamId": "TBBBB000001", "workspaceIds": [ "TBBBB000001", "TAAAA000001" ] } [10/04/26, 23:41:07:000] error: Authorization: Bearer abc.def failed ` // xwininfo -root -tree as the laptop answered it: the chat window under i3's frame, the tray icon in // i3bar, two helper windows at the top. const tree = `xwininfo: Window id: 0x3d7 (the root window) (has no name) Root window id: 0x3d7 (the root window) (has no name) Parent window id: 0x0 (none) 12 children: 0x28000d1 "slack": ("slack" "Slack") 200x200+0+0 +0+0 0x1400003 (has no name): ("i3-frame" "i3-frame") 1440x1774+0+26 +0+26 1 child: 0x2600004 "Slack | general": ("slack" "slack") 1436x1744+2+2 +2+28 0x1002168 (has no name): () 1x1+0+0 +0+0 0x1002169 "i3bar for output eDP-1": ("bar-1" "i3bar") 2880x26+0+0 +0+0 1 child: 0x2800029 "slack": ("slack" "Slack") 22x22+2832+2 +2832+2 0x2800001 "slack": ("slack" "Slack") 10x10+10+10 +10+10 ` // newSlack is a machine with Slack installed from the AUR, running from a session whose output pipe // nobody reads, its icon in i3bar's tray, dunst on the bus. func newSlack(t *testing.T) (*fake, map[string]string) { f := newFake(t) links := map[string]string{} readLink = func(p string) (string, error) { rel := strings.TrimPrefix(p, f.Root) if l, ok := links[rel]; ok { return l, nil } return "", errors.New("no such link") } t.Cleanup(func() { readLink = os.Readlink }) f.desktopSession() f.write(testHome+"/"+stateFile, rootState) f.write(testHome+"/"+localFile, `{"lastElectronVersionLaunched":"43.4.0","electronFeatureOverrides":["x"]}`) f.write(testHome+"/"+logDir+"/browser.log", browserLog) f.write(testHome+"/"+i3Main, "exec --no-startup-id dex --autostart --environment i3\n# slack\n"+contribution(t)) f.proc(3867, 1000, slackComm, []string{"/usr/lib/slack/slack", "--gtk-version=3", "-s"}, "user@1000.service/app.slice/app-slack-3867.scope") f.proc(3945, 1000, slackComm, []string{"/usr/lib/slack/slack", "--type=zygote"}, "session-c1.scope") links["/proc/3867/fd/1"] = "pipe:[36802]" links["/proc/3867/fd/2"] = "/dev/null" f.answer = func(name string, args []string) Output { switch { case name == "pacman" && args[0] == "-Q": return Output{Stdout: "slack-desktop 4.51.191-1\n"} case name == "pacman" && args[0] == "-Qmq": return Output{Stdout: "slack-desktop\n"} case name == "xwininfo": return Output{Stdout: tree} case name == "busctl": return Output{Stdout: ":1.19 3923 dunst operator :1.19 user@1000.service - -\n" + "org.freedesktop.Notifications 3923 dunst operator :1.19 user@1000.service - -\n"} } return Output{} } return f, links } func contribution(t *testing.T) string { m, _ := readManifest(t) return m.Contributions[0].Content } func noSecrets(t *testing.T, v any) string { t.Helper() raw, err := json.Marshal(v) if err != nil { t.Fatal(err) } s := string(raw) for _, never := range []string{"xoxc", "xoxd", "AbCdEf", "abc.def", "private message", "Secret Workspace", "TAAAA", "TBBBB", "secret-sso", "/home/operator"} { if strings.Contains(s, never) { t.Errorf("the answer carries %q: %s", never, s) } } return s } func TestStatusCountsWorkspacesSinceTheLastStartAndNamesNone(t *testing.T) { f, _ := newSlack(t) s, err := f.Status() if err != nil { t.Fatal(err) } noSecrets(t, s) if s.Installed != "4.51.191-1" || !s.Foreign || len(s.Running) != 1 || s.Running[0].PID != 3867 || s.Helpers != 1 { t.Fatalf("%+v", s) } // Two accounts since the start of 10/04 (the earlier start's account is not counted), and the // workspaces they open together: TBBBB's two, one shared with TAAAA. if s.Workspaces.Accounts != 2 || s.Workspaces.Workspaces != 2 || s.Workspaces.AsOf != "10/04/26, 23:41:06:340" { t.Fatalf("%+v", s.Workspaces) } st := s.Settings if !st.Found || !*st.LaunchOnLogin || !*st.HideOnStartup || !*st.RunFromTray || st.NotificationMethod != "Slack's default" || st.Electron != "43.4.0" { t.Fatalf("%+v", st) } if s.Tray == nil || !s.Tray.Present || s.Tray.In != "i3bar" { t.Fatalf("%+v", s.Tray) } if s.Notifier.Owner != "dunst" || s.Notifier.PID != 3923 { t.Fatalf("%+v", s.Notifier) } if len(s.Output) != 2 || !s.Output[0].Dead || s.Output[1].Goes != "discarded (/dev/null)" { t.Fatalf("%+v", s.Output) } if len(s.Starts) != 1 || !strings.Contains(s.Starts[0], "this module's line") { t.Fatalf("%q", s.Starts) } } func TestAPipeWithAReaderIsLiveAndTheJournalIsASocket(t *testing.T) { f, links := newSlack(t) f.proc(500, 1000, "systemd-cat", []string{"systemd-cat", "-t", "x-session"}, "session-c1.scope") links["/proc/500/fd/0"] = "pipe:[36802]" f.write("/proc/500/fdinfo/0", "pos:\t0\nflags:\t02000000\nmnt_id:\t15\n") // The writer's own end, elsewhere, is not a reader. f.proc(501, 1000, "sh", []string{"sh"}, "session-c1.scope") links["/proc/501/fd/1"] = "pipe:[36802]" f.write("/proc/501/fdinfo/1", "pos:\t0\nflags:\t01\n") links["/proc/3867/fd/2"] = "socket:[22289]" for _, fd := range []string{"0", "1"} { _ = os.MkdirAll(filepath.Join(f.Root, "/proc/500/fd"), 0o755) _ = os.WriteFile(filepath.Join(f.Root, "/proc/500/fd", fd), nil, 0o644) _ = os.MkdirAll(filepath.Join(f.Root, "/proc/501/fd"), 0o755) _ = os.WriteFile(filepath.Join(f.Root, "/proc/501/fd", fd), nil, 0o644) } got := f.streams(3867) if got[0].Dead || got[0].Goes != "a pipe read by systemd-cat (pid 500)" || !strings.HasPrefix(got[1].Goes, "a socket") { t.Fatalf("%+v", got) } } func TestTheTrayIsAnIconInsideAnotherProgramsWindow(t *testing.T) { if ok, in := trayIn(tree); !ok || in != "i3bar" { t.Fatal(ok, in) } without := strings.Replace(tree, ` 0x2800029 "slack": ("slack" "Slack") 22x22+2832+2 +2832+2`+"\n", "", 1) if ok, _ := trayIn(without); ok { t.Fatal("the top-level Slack windows and the managed chat window are not a tray icon") } } func TestTheLogIsMasked(t *testing.T) { f, _ := newSlack(t) l, err := f.Log("browser", 2000, false) if err != nil { t.Fatal(err) } all := strings.Join(l.Lines, "\n") for _, never := range []string{"xoxc", "xoxd", "AbCdEf", "abc.def", "private message", "Secret Workspace"} { if strings.Contains(all, never) { t.Errorf("the log carries %q", never) } } if !strings.Contains(all, `"title": ""`) || !strings.Contains(all, "token ") || l.Files[0] != "~/"+logDir+"/browser.log" { t.Fatalf("%+v", l) } p, _ := f.Log("browser", 10, true) if len(p.Lines) != 2 || !strings.Contains(p.Lines[0], "warn:") || !strings.Contains(p.Lines[1], "Authorization: ") { t.Fatalf("%q", p.Lines) } if _, err := f.Log("cookies", 5, false); err == nil { t.Fatal("an unknown source is refused") } if l, err := newFake(t).Log("webapp", 5, false); err != nil || l.Note == "" { t.Fatalf("%+v %v", l, err) } } func TestRestartEndsSlackAndStartsItToTheTray(t *testing.T) { f, _ := newSlack(t) f.onStart = func(argv []string) { f.proc(9000, 1000, slackComm, argv, "user@1000.service/app.slice/"+restartAs+".service") f.proc(9001, 1000, slackComm, []string{"/usr/lib/slack/slack", "--type=zygote"}, "user@1000.service/app.slice/"+restartAs+".service") } a, err := f.Restart() if err != nil { t.Fatal(err) } if len(a.Ended) != 2 || len(a.Running) != 1 || a.Running[0].PID != 9000 || a.Running[0].Command != "/usr/bin/slack --gtk-version=3 -s" { t.Fatalf("%+v", a) } if !f.called("systemd-run --user --collect --quiet --unit=mesh-slack --setenv=DISPLAY=:1") { t.Fatalf("%q", f.calls) } f.onStart = nil if _, err := f.Restart(); err == nil || !strings.Contains(err.Error(), "journalctl --user -u mesh-slack.service") { t.Fatalf("a start that shows no process: %v", err) } none := newFake(t) if _, err := none.Restart(); err == nil || !strings.Contains(err.Error(), "no graphical session") { t.Fatalf("without a desktop: %v", err) } } func TestCheckNamesEveryOtherStartTheOperatorsRulesAndTheDeadPipe(t *testing.T) { f, links := newSlack(t) c, err := f.Check() if err != nil { t.Fatal(err) } // As the desktop machine is: the one finding is the session's dead pipe. if c.OK || len(c.Findings) != 1 || !strings.Contains(c.Findings[0].What, "fd 1 is a pipe nobody reads") || len(c.Starts) != 1 { t.Fatalf("%+v", c) } links["/proc/3867/fd/1"] = "socket:[1]" if c, _ = f.Check(); !c.OK { t.Fatalf("%+v", c) } // As the laptop is before migration: the predecessor's entry, the operator's rules file. f.write(testHome+"/.config/autostart/slack.desktop", "[Desktop Entry]\nExec=/usr/bin/slack --gtk-version=3 -s %U\nHidden=false\n") f.write(testHome+"/.config/i3/config.d/50-slack.conf", "# Slack window rules.\n"+ `for_window [class="(?i)^slack$" window_role="browser-window"] move to workspace $ws3`+"\n") f.write(testHome+"/.config/i3/config.d/60-mine.conf", "exec --no-startup-id slack\n") c, _ = f.Check() all := noSecrets(t, c) for _, want := range []string{"a second start: XDG autostart: ~/.config/autostart/slack.desktop", "(the predecessor's)", "a second start: window manager: ~/.config/i3/config.d/60-mine.conf:1", "~/.config/i3/config.d/50-slack.conf repeats Slack's window rules"} { if !strings.Contains(all, want) { t.Errorf("no finding %q in %s", want, all) } } if len(c.Starts) != 3 { t.Fatalf("%q", c.Starts) } // Nothing installed, nothing placed, two Slacks, no tray, no notifier. f.write(testHome+"/"+i3Main, "exec --no-startup-id dex --autostart --environment i3\n") f.proc(4000, 1000, slackComm, []string{"/usr/lib/slack/slack"}, "s.scope") f.answer = func(name string, args []string) Output { switch name { case "pacman": return Output{Code: 1} case "xwininfo": return Output{Stdout: strings.Replace(tree, `("slack" "Slack") 22x22`, `("other" "Other") 22x22`, 1)} case "busctl": return Output{Stdout: ":1.19 3923 dunst operator :1.19 user@1000.service - -\n"} } return Output{} } c, _ = f.Check() all = noSecrets(t, c) for _, want := range []string{"is not installed", "from the AUR", "not got the module's start", "not got the module's window rules", "2 Slack main processes run", "icon is in no tray", "nobody shows notifications"} { if !strings.Contains(all, want) { t.Errorf("no finding %q in %s", want, all) } } } func TestSlacksOwnLaunchOnLoginLinkIsNamedAsItsSetting(t *testing.T) { f, _ := newSlack(t) // Slack's setting makes ~/.config/autostart/slack.desktop a link to the package's entry. f.write(testHome+"/.config/autostart/slack.desktop", "[Desktop Entry]\nExec=/usr/bin/slack --gtk-version=3 -s %U\n") orig := isLink isLink = func(p string) bool { return strings.HasSuffix(p, "/.config/autostart/slack.desktop") } t.Cleanup(func() { isLink = orig }) c, _ := f.Check() if !strings.Contains(noSecrets(t, c), "untick 'Launch app on login'") { t.Fatalf("%+v", c) } }