package main import ( "os" "path/filepath" "strings" "testing" "time" ) const profileDir = testHome + "/.thunderbird/60016jsp.default-default" // links stands in for the kernel's fd links: path → target. func links(t *testing.T, m map[string]string) { t.Helper() old := readLink readLink = func(p string) (string, error) { for k, v := range m { if strings.HasSuffix(p, k) { return v, nil } } return "", os.ErrNotExist } t.Cleanup(func() { readLink = old }) } // betterbird is a running Betterbird with one child, its profile and its prefs. func (f *fake) betterbird() { f.proc(4000, 1000, "betterbird-bin", []string{"betterbird"}, "session-4.scope", "DISPLAY=:1") f.proc(4001, 1000, "betterbird-bin", []string{"/opt/betterbird/betterbird-bin", "-contentproc", "-isForBrowser"}, "session-4.scope") f.write("/proc/4000/fd/40", "") f.write("/proc/4000/fd/41", "") f.write("/proc/4000/fd/1", "") f.write(testHome+"/.thunderbird/profiles.ini", "[Profile0]\nName=default-default\nIsRelative=1\nPath=60016jsp.default-default\n\n"+ "[Profile1]\nName=default\nIsRelative=1\nPath=au2i98w4.default\nDefault=1\n\n[InstallF72D9E66FA3C07ED]\nDefault=60016jsp.default-default\nLocked=1\n") f.write(profileDir+"/prefs.js", "// Mozilla User Preferences\n"+ `user_pref("browser.display.use_system_colors", true);`+"\n"+ `user_pref("extensions.activeThemeID", "thunderbird-compact-dark@mozilla.org");`+"\n"+ `user_pref("calendar.alarms.showmissed", true);`+"\n"+ `user_pref("mail.identity.id1.useremail", "someone@example.org");`+"\n"+ `user_pref("extensions.owl.token", "abc");`+"\n"+ `user_pref("signon.rememberSignons", true);`+"\n") f.write(profileDir+"/extensions.json", `{"addons":[{"id":"owl@beonex.com","active":true,"location":"app-profile"},{"id":"x@mozilla.org","active":true,"location":"app-builtin"}]}`) } func (f *fake) declare(prefs string) { f.write(testHome+"/.config/betterbird/user.js", mark+", novox/hq issue 345). Owned by the mesh.\n//\n"+prefs+"\n") } func TestTheProfileIsTheOneTheRunningBetterbirdHoldsElseTheInstallationsDefault(t *testing.T) { f := newFake(t) f.betterbird() links(t, map[string]string{"/proc/4000/fd/40": profileDir + "/prefs.js", "/proc/4000/fd/41": profileDir + "/places.sqlite"}) p, err := f.profile() if err != nil || p.Dir != "~/.thunderbird/60016jsp.default-default" || !strings.Contains(p.FoundBy, "pid 4000, 2 files") { t.Fatalf("%+v %v", p, err) } links(t, map[string]string{}) p, err = f.profile() if err != nil || p.Dir != "~/.thunderbird/60016jsp.default-default" || !strings.Contains(p.FoundBy, "InstallF72D9E66FA3C07ED") { t.Fatalf("the installation's default, not Profile1's Default=1: %+v %v", p, err) } } func TestPrefsAnswerValuesButNeverACredentialOrAnAccount(t *testing.T) { f := newFake(t) f.betterbird() links(t, nil) f.write(profileDir+"/user.js", mark+")\n"+`user_pref("browser.display.use_system_colors", false);`+"\n") got, err := f.Prefs([]string{"browser.display.", "mail.identity.*", "extensions.owl.token", "signon.rememberSignons", "gfx.webrender.software"}) if err != nil { t.Fatal(err) } ps := got["prefs"].([]Pref) by := map[string]Pref{} for _, p := range ps { by[p.Name] = p } if p := by["browser.display.use_system_colors"]; p.Value != "false" || !strings.HasPrefix(p.From, "user.js") { t.Fatalf("user.js wins: %+v", p) } for _, n := range []string{"mail.identity.id1.useremail", "extensions.owl.token", "signon.rememberSignons"} { if p := by[n]; p.Value != "" || p.Withheld == "" { t.Errorf("%s: %+v", n, p) } } if p := by["gfx.webrender.software"]; p.Value != "" || !strings.Contains(p.From, "default") { t.Fatalf("%+v", p) } if _, err := f.Prefs(nil); err == nil { t.Fatal("names are required") } } func TestTheModulesUserJSIsPlacedOnlyWhereNoOperatorsFileIs(t *testing.T) { f := newFake(t) f.betterbird() links(t, nil) if _, err := f.userJS(true); err == nil || !strings.Contains(err.Error(), "not pushed") { t.Fatalf("nothing declared: %v", err) } f.declare(`user_pref("calendar.alarms.showmissed", true); user_pref("ui.key.x", "a;b");`) u, err := f.userJS(true) if err != nil || !u.Applied || u.Statements != 2 { t.Fatalf("%+v %v", u, err) } placed, _ := os.ReadFile(filepath.Join(f.Root, profileDir, "user.js")) declared, _ := os.ReadFile(filepath.Join(f.Root, testHome, ".config/betterbird/user.js")) if string(placed) != string(declared) { t.Fatal("placed whole, as declared") } if !strings.HasPrefix(u.Effective, "yes") { t.Fatalf("the fake Betterbird started after the file's time: %q", u.Effective) } later := time.Unix(1_900_000_000, 0) os.Chtimes(filepath.Join(f.Root, profileDir, "user.js"), later, later) if u, _ := f.userJS(false); !strings.HasPrefix(u.Effective, "not yet") { t.Fatalf("placed after Betterbird started: %q", u.Effective) } f.write(profileDir+"/user.js", `user_pref("a.b", 1);`+"\n") if _, err := f.userJS(true); err == nil || !strings.Contains(err.Error(), "not the module's") { t.Fatalf("an operator's user.js is never overwritten: %v", err) } if b, _ := os.ReadFile(filepath.Join(f.Root, profileDir, "user.js")); string(b) != `user_pref("a.b", 1);`+"\n" { t.Fatal("and it is left as it was") } } func TestTheUserJSHoldsOnlyPreferencesAndNoCredential(t *testing.T) { for text, ok := range map[string]bool{ mark + ")\n// none\n": true, mark + ")\n" + `user_pref("a.b", true); user_pref("c", -3); user_pref("d", "x\"y;z");`: true, mark + ")\n" + `pref("a.b", true);`: false, mark + ")\n" + `user_pref("a.b", true); Services.foo();`: false, mark + ")\n" + `user_pref("mail.smtpserver.s1.password", "x");`: false, `user_pref("a.b", true);`: false, } { if err := validUserJS(text); (err == nil) != ok { t.Errorf("%q: %v", text, err) } } } func TestStatusSaysWhatRunsTheProfileTheAddOnsAndTheDisplayPrefs(t *testing.T) { f := newFake(t) f.betterbird() links(t, map[string]string{"/proc/4000/fd/1": "pipe:[36802]"}) f.write("/opt/betterbird/application.ini", "[App]\nVendor=Betterbird Project\nName=Betterbird\nVersion=153.1.0\nBuildID=20260820183427\n") f.answer = func(name string, args []string) Output { if name == "pacman" && args[0] == "-Q" { return Output{Stdout: "betterbird-bin 153.1.0esr-1\n"} } if name == "pacman" && args[0] == "-Qmq" { return Output{Stdout: "betterbird-bin\n"} } return Output{} } s, err := f.Status() if err != nil { t.Fatal(err) } if len(s.Running) != 1 || s.Running[0].PID != 4000 || s.Children != 1 || s.Installed != "153.1.0esr-1" || !s.Foreign || s.Version != "153.1.0" || s.Profile == nil || len(s.AddOns) != 1 || s.AddOns[0].ID != "owl@beonex.com" { t.Fatalf("%+v", s) } if !s.Output[0].Dead { t.Fatalf("the session's dead pipe: %+v", s.Output) } found := false for _, p := range s.Display { if p.Name == "extensions.activeThemeID" && p.Value == `"thunderbird-compact-dark@mozilla.org"` { found = true } } if !found { t.Fatalf("%+v", s.Display) } } func TestCheckNamesWhatIsMissingAndWhatToDo(t *testing.T) { f := newFake(t) f.betterbird() links(t, map[string]string{"/proc/4000/fd/1": "pipe:[1]", "/proc/4000/fd/2": "/dev/null"}) f.answer = func(name string, args []string) Output { return Output{Stdout: "betterbird-bin 153.1.0esr-1\n"} } a, err := f.Check() if err != nil || a.OK { t.Fatalf("%+v %v", a, err) } all := "" for _, x := range a.Findings { all += x.What + " | " + x.Do + "\n" } if !strings.Contains(all, "not on this machine") || !strings.Contains(all, "pipe nobody reads") { t.Fatal(all) } } func TestRestartEndsItPlacesTheUserJSAndStartsItUnderTheServiceManager(t *testing.T) { f := newFake(t) f.desktopSession() f.betterbird() links(t, nil) f.declare("// none") f.onStart = func(argv []string) { f.proc(5000, 1000, "betterbird-bin", []string{"betterbird"}, "mesh-betterbird.service") } a, err := f.Restart(false) if err != nil { t.Fatal(err) } if len(a.Ended) != 1 || a.Ended[0] != 4000 || len(a.Running) != 1 || a.Running[0].PID != 5000 || !strings.HasPrefix(a.UserJS, "placed") { t.Fatalf("%+v", a) } if !f.called("systemd-run --user --collect --quiet --unit=mesh-betterbird") { t.Fatalf("%v", f.calls) } } func TestTheReminderTestTouchesNothingOfTheOperatorsProfile(t *testing.T) { f := newFake(t) f.desktopSession() f.betterbird() links(t, nil) f.declare(`user_pref("calendar.alarms.show", true);`) before, _ := os.ReadFile(filepath.Join(f.Root, profileDir, "prefs.js")) a, err := f.ReminderTest(40*time.Second, []string{"extensions.activeThemeID", "mail.identity.id1.useremail", "nope.nope"}, false) if err != nil { t.Fatal(err) } if strings.Join(a.Carried, ",") != "extensions.activeThemeID" || len(a.Withheld) != 2 { t.Fatalf("%+v", a) } dir := filepath.Join(f.Root, "/run/user/1000", testUnit) user, _ := os.ReadFile(filepath.Join(dir, "profile", "user.js")) cal, _ := os.ReadFile(filepath.Join(dir, "test.ics")) if !strings.Contains(string(user), `"calendar.alarms.show", true`) || !strings.Contains(string(user), `"extensions.activeThemeID", "thunderbird-compact-dark@mozilla.org"`) || !strings.Contains(string(user), "file:///run/user/1000/"+testUnit+"/test.ics") || strings.Contains(string(user), "someone@") { t.Fatalf("%s", user) } // The event starts 10 minutes after the reminder, which is due 40 s from now. if !strings.Contains(string(cal), "DTSTART:20270115T081040Z") || !strings.Contains(string(cal), "TRIGGER:-PT10M") || a.Due != "2027-01-15T08:00:40Z" { t.Fatalf("%s %s", cal, a.Due) } if !f.called("systemd-run --user --collect --quiet --unit=" + testUnit + " -p RuntimeMaxSec=600 -p ExecStopPost=/usr/bin/rm -rf /run/user/1000/" + testUnit + " --setenv=MOZ_NO_REMOTE=1") { t.Fatalf("%v", f.calls) } after, _ := os.ReadFile(filepath.Join(f.Root, profileDir, "prefs.js")) if string(before) != string(after) || exists(filepath.Join(f.Root, profileDir, "user.js")) { t.Fatal("the operator's profile is untouched") } if _, err := f.ReminderTest(0, nil, true); err != nil || exists(dir) { t.Fatalf("stop removes the test profile: %v", err) } }