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 displayLines(t, m)
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|