Files
mesh-catalog/modules/slack/cmd/slack-tools/slack_test.go
T
jochen 75f25fca21 Add slack and jetbrains-toolbox: one start each, the apps kept as found
Slack comes from the AUR and Toolbox from JetBrains' self-updating tarball,
so neither is declared: the host installs official packages only, and a
pinned archive would fight Toolbox's own updates. Slack's start and the
operator's i3 window rules become one node-display-session contribution,
because Slack's own launch-on-login is a symlink in ~/.config/autostart.
Toolbox keeps its own autostart entry as its one start. The shared
desktop.go header now names all four bundles.
2026-10-05 15:05:55 +02:00

315 lines
12 KiB
Go

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": "<hidden>"`) || !strings.Contains(all, "token <hidden>") || 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: <hidden>") {
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)
}
}