Files
mesh-catalog/modules/betterbird/cmd/betterbird-tools/betterbird_test.go
T
jschoubben a7953b969d
mesh/merge-gate pass: builds new: modules/betterbird, sent nowhere; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery superseded: a newer head of the same pull request
betterbird: a module for the mail and calendar client, so its windows and preferences can be seen and declared (hq issue 345)
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.
2026-10-09 12:07:29 +02:00

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 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()
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)
}
}