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
A restart ended Betterbird with whatever a composer held; now it refuses while a Msgcompose window is open unless discard_unsent says the operator agreed, and answers within the runtime's 30 s. A preference's value could carry an address, a user%40domain@host URI, a host or the home; those are masked, recent-folder, account and cloud-file preferences and every add-on preference but the theme's id are withheld, and a request for everything is refused. A log kept the token after Bearer or Basic. The reminder test's Betterbird counted as the operator's and left its profile behind when it ended on its own; it is now left out everywhere and its unit removes the profile when it stops.
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 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)
|
|
}
|
|
}
|