mesh/merge-gate head 1835f0efad0f merged into trunk 39e7a06e6293 — pass: builds betterbird, sent nowhere; no bus step; 2 wait(s) for a person; every machin…
mesh/repo-check head 1835f0efad0f merged into trunk 39e7a06e6293 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery superseded: a newer head of the same pull request
The module owned only the accounts' server fields, so identities, signatures, calendars, address books, filters and tags could differ between desktop nodes unseen, and the operator's own preferences were shipped in the catalogue. prefs becomes one entry per preference (a node's layer overrides one key) and the defaults file keeps no value of the operator's; identities, calendars, address-books, filters and tags become settings, adopted like accounts, filters owned whole per account, and what Betterbird rewrites written only while it is closed. New tools list each kind, derive the layers from every desktop node and propose one at a time, and show drift; every answer passes a credential check.
277 lines
11 KiB
Go
277 lines
11 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"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"}]}`)
|
|
}
|
|
|
|
// declare writes the setting prefs as the node-engine does (prefs.json, novox/hq issue 586), from user_pref
|
|
// statements: each one entry of the map.
|
|
func (f *fake) declare(prefs string) {
|
|
f.t.Helper()
|
|
values := map[string]any{}
|
|
for name, lit := range statementsOf([]byte(prefs)) {
|
|
v, ok := prefValue(lit)
|
|
if !ok {
|
|
f.t.Fatalf("%s: %s is not a value the setting holds", name, lit)
|
|
}
|
|
values[name] = v
|
|
}
|
|
body, _ := json.Marshal(map[string]any{"prefs": values})
|
|
f.write(testHome+"/"+prefsFile, string(body))
|
|
}
|
|
|
|
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, _, _ := f.declaredText()
|
|
if string(placed) != declared || !strings.Contains(declared, `user_pref("ui.key.x", "a;b");`) {
|
|
t.Fatalf("placed whole, as declared:\n%s", placed)
|
|
}
|
|
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)
|
|
}
|
|
}
|