A module that cannot set Betterbird up is of little use (the operator, 2026-10-12). The module now declares the community package absent and unpacks the pinned upstream release where that package had it, so the same profile opens; places the enterprise policy that turns Betterbird's updater off and force-installs Owl from the xpi the mesh pinned by its vendor's digest (never in a repository); and takes the operator's accounts from the trusted setting accounts, without any password or token: an account the profile already holds is adopted under its own keys, a new one is created under the module's mesh_ keys, an ambiguous one is refused with the remedy, and the account lists are only written into. betterbird_accounts says the profile's accounts by count and proposes the setting built from them for the operator's Approve. Every new build waits for the operator's push. The reminder test is visibly a test (MESH TEST band, event name) and its description says it opens a window on the operator's screen.
264 lines
11 KiB
Go
264 lines
11 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.Installation.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)
|
|
}
|
|
// Betterbird's update check stays off, so no About window opens on the operator's screen, and the
|
|
// test's own preferences come last, so nothing carried or declared overrides them.
|
|
if !strings.Contains(string(user), `"betterbird.update.checkIntervalDays", 0`) ||
|
|
strings.Index(string(user), "extensions.activeThemeID") > strings.Index(string(user), "calendar.registry.meshtest.uri") ||
|
|
strings.Index(string(user), `"calendar.alarms.show"`) > strings.Index(string(user), "calendar.registry.meshtest.uri") {
|
|
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)
|
|
}
|
|
}
|