A calendar reminder showed as a transparent, empty window on shanks and no tool could say which window it was or which preferences Betterbird ran with. The module owns one file, the user.js its setting prefs fills, which its tools place whole into the profile Betterbird uses and never over an operator's own; it reads named preferences without credentials or accounts, restarts Betterbird under the user manager so its output reaches the journal, checks what it promises, and shows a reminder on demand from a throw-away profile so a fix can be proved without the operator's calendar. No package: betterbird-bin is the AUR's.
257 lines
10 KiB
Go
257 lines
10 KiB
Go
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 344). 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()
|
|
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 --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)
|
|
}
|
|
}
|