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.
315 lines
12 KiB
Go
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)
|
|
}
|
|
}
|