mesh/merge-gate head 73404a3c3f21 merged into trunk 5f491e0c097d — pass: builds betterbird, sent nowhere; no bus step; 2 wait(s) for a person; every machin…
mesh/repo-check head 73404a3c3f21 merged into trunk 5f491e0c097d — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
With calendar.alarms.showmissed on, Betterbird's alarm service fires every reminder of the past month that carries no later acknowledgement at each load of a calendar. Owl never sends a dismissal to Exchange and keeps it only in Betterbird's local cache, so on g14 September's Exchange meetings came back about every ten minutes, Dismiss or not. The policy now locks the pass off whatever the setting or profile says, and betterbird_check says when the running Betterbird still has it on beside an Owl calendar.
308 lines
13 KiB
Go
308 lines
13 KiB
Go
package main
|
|
|
|
import (
|
|
"archive/zip"
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// What the mesh installs, as the tools say it (novox/hq ADR 0316 §16): the version against the pin and the
|
|
// newest published build, the policy, Owl against its pin; and the reminder test, visibly a test.
|
|
|
|
func (f *fake) xpi(path, version string) {
|
|
f.t.Helper()
|
|
full := filepath.Join(f.Root, path)
|
|
if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil {
|
|
f.t.Fatal(err)
|
|
}
|
|
out, err := os.Create(full)
|
|
if err != nil {
|
|
f.t.Fatal(err)
|
|
}
|
|
w := zip.NewWriter(out)
|
|
m, _ := w.Create("manifest.json")
|
|
m.Write([]byte(`{"version": "` + version + `", "browser_specific_settings": {"gecko": {"id": "owl@beonex.com"}}}`))
|
|
w.Close()
|
|
out.Close()
|
|
}
|
|
|
|
// placedByTheMesh is a node after the switch: the release, the policy and Owl's xpi in /opt/betterbird, and
|
|
// the profile running Owl at the pin.
|
|
func (f *fake) placedByTheMesh() {
|
|
f.noPackage()
|
|
f.write(appIni, "[App]\nVendor=Betterbird Project\nVersion=153.4.0\nBuildID=20260929222610\n")
|
|
raw, err := os.ReadFile(filepath.Join("..", "..", "install", "distribution", "policies.json"))
|
|
if err != nil {
|
|
f.t.Fatal(err)
|
|
}
|
|
f.write(policyFile, string(raw))
|
|
f.xpi(owlFile, "1.5.6.1")
|
|
f.write(profileDir+"/extensions.json", `{"addons":[{"id":"owl@beonex.com","version":"1.5.6.1","active":true,"location":"app-profile"}]}`)
|
|
}
|
|
|
|
func TestTheStatusSaysTheInstallationAgainstThePinTheNewestBuildThePolicyAndOwl(t *testing.T) {
|
|
f := newFake(t)
|
|
f.betterbird()
|
|
f.placedByTheMesh()
|
|
links(t, nil)
|
|
publishedBuilds = func() (string, error) {
|
|
return "blocklist,{}\nLinux,153,20261101000000\nLinux,140,20260829111240\n", nil
|
|
}
|
|
t.Cleanup(func() { publishedBuilds = func() (string, error) { return "", os.ErrDeadlineExceeded } })
|
|
s, err := f.Status()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
in := s.Installation
|
|
if in.From != "the mesh" || in.Version != "153.4.0" || in.Newest != "20261101000000" || !in.Behind ||
|
|
!strings.HasPrefix(in.Policy, "placed:") || in.Owl == nil || in.Owl.Pinned != "1.5.6.1" || !in.Owl.AtThePin || !in.Owl.Active {
|
|
t.Fatalf("%+v %+v", in, in.Owl)
|
|
}
|
|
c, err := f.Check()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
found := false
|
|
for _, x := range c.Findings {
|
|
found = found || strings.Contains(x.What, "behind the newest published build 20261101000000")
|
|
}
|
|
if !found {
|
|
t.Fatalf("a release left behind is said: %+v", c.Findings)
|
|
}
|
|
}
|
|
|
|
func TestTheCommunityPackageAPolicyThatIsNotTheModulesAndAnOwlOffItsPinAreSaid(t *testing.T) {
|
|
f := newFake(t)
|
|
f.betterbird()
|
|
f.placedByTheMesh()
|
|
links(t, nil)
|
|
f.write(policyFile, `{"policies": {"ExtensionSettings": {"owl@beonex.com": {"installation_mode": "force_installed",
|
|
"install_url": "https://vendor.example/owl.xpi", "updates_disabled": true}}}}`)
|
|
f.write(profileDir+"/extensions.json", `{"addons":[{"id":"owl@beonex.com","version":"1.5.7","active":true,"location":"app-profile"}]}`)
|
|
in := f.installation(filepath.Join(f.Root, profileDir))
|
|
if !strings.Contains(in.Policy, "DisableAppUpdate") || in.Owl.AtThePin || in.Owl.InProfile != "1.5.7" {
|
|
t.Fatalf("%+v %+v", in, in.Owl)
|
|
}
|
|
// A newer Owl than the pin is a fact with the real remedy, never a finding to remove it.
|
|
if !strings.Contains(in.Owl.Note, "move the module's pin") {
|
|
t.Fatalf("%+v", in.Owl)
|
|
}
|
|
f.write(policyFile, `{"policies": {"DisableAppUpdate": true, "ExtensionSettings": {"owl@beonex.com": {"installation_mode": "force_installed",
|
|
"install_url": "file:///opt/betterbird/owl/owl.xpi", "updates_disabled": true}}}}`)
|
|
c, _ := f.Check()
|
|
for _, x := range c.Findings {
|
|
if strings.Contains(x.What, "Owl") {
|
|
t.Errorf("a newer Owl is not a finding: %+v", x)
|
|
}
|
|
}
|
|
f.write(profileDir+"/extensions.json", `{"addons":[{"id":"owl@beonex.com","version":"1.5.5","active":true,"location":"app-profile"}]}`)
|
|
c, _ = f.Check()
|
|
found := false
|
|
for _, x := range c.Findings {
|
|
found = found || (strings.Contains(x.What, "older than the mesh's pin") && strings.Contains(x.Do, "move the module's Owl pin"))
|
|
}
|
|
if !found {
|
|
t.Fatalf("an older Owl is a finding whose remedy is the pin: %+v", c.Findings)
|
|
}
|
|
f.write(policyFile, `{"policies": {"DisableAppUpdate": true, "ExtensionSettings": {"owl@beonex.com": {"installation_mode": "force_installed",
|
|
"install_url": "https://vendor.example/owl.xpi", "updates_disabled": true}}}}`)
|
|
if in := f.installation(""); !strings.Contains(in.Policy, "not from the mesh's xpi") {
|
|
t.Fatalf("%s", in.Policy)
|
|
}
|
|
f.answer = func(name string, args []string) Output {
|
|
if name == "pacman" && args[0] == "-Q" {
|
|
return Output{Stdout: "betterbird-bin 153.1.0esr-1\n"}
|
|
}
|
|
return Output{}
|
|
}
|
|
in = f.installation("")
|
|
if !strings.HasPrefix(in.From, "the community package betterbird-bin 153.1.0esr-1") || !strings.Contains(in.From, "operator") {
|
|
t.Fatalf("%s", in.From)
|
|
}
|
|
raw, _ := json.Marshal(in)
|
|
if strings.Contains(string(raw), "example.org") {
|
|
t.Fatalf("%s", raw)
|
|
}
|
|
}
|
|
|
|
func TestTheReminderTestIsVisiblyATestAndSaysItOpensAWindow(t *testing.T) {
|
|
f := newFake(t)
|
|
f.desktopSession()
|
|
f.betterbird()
|
|
links(t, nil)
|
|
f.declare("// none")
|
|
f.declareAccounts(personal)
|
|
a, err := f.ReminderTest(40*time.Second, nil, false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
dir := filepath.Join("/run/user/1000", testUnit)
|
|
css := f.read(dir + "/profile/chrome/userChrome.css")
|
|
user := f.read(dir + "/profile/user.js")
|
|
cal := f.read(dir + "/test.ics")
|
|
if !strings.Contains(css, "MESH TEST") || !strings.Contains(css, `:root:not([windowtype="Calendar:AlarmWindow"])`) {
|
|
t.Fatalf("the test's windows but the reminder carry a MESH TEST band: %s", css)
|
|
}
|
|
if !strings.Contains(user, `user_pref("toolkit.legacyUserProfileCustomizations.stylesheets", true);`) ||
|
|
strings.Contains(user, "mesh_personal") || strings.Contains(user, "example.org") {
|
|
t.Fatalf("the test profile reads its userChrome.css and holds no declared account: %s", user)
|
|
}
|
|
if !strings.Contains(cal, "SUMMARY:MESH TEST reminder") || !strings.Contains(a.OnScreen, "operator's screen") {
|
|
t.Fatalf("the event and the answer say it is a test on the operator's screen: %s %+v", cal, a)
|
|
}
|
|
if !f.called("systemd-run --user --collect --quiet --unit="+testUnit) || !strings.Contains(strings.Join(f.calls, "\n"), startBin+" --no-remote") {
|
|
t.Fatalf("the test starts Betterbird itself, never the module's start: %v", f.calls)
|
|
}
|
|
for _, tool := range tools() {
|
|
if tool.Name == "betterbird_reminder_test" && !strings.HasPrefix(tool.Description, "OPENS WINDOWS ON THE OPERATOR'S SCREEN") {
|
|
t.Fatalf("the description says first that a window opens on the operator's screen: %s", tool.Description)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTheUserJSDescriptionSaysAdoption(t *testing.T) {
|
|
for _, tool := range tools() {
|
|
if tool.Name == "betterbird_user_js" && (!strings.Contains(tool.Description, "adopted under its own keys") ||
|
|
strings.Contains(tool.Description, "refuses the apply")) {
|
|
t.Fatalf("%s", tool.Description)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Betterbird never asks at its start to be the default mail client: the module's policy locks
|
|
// mail.shell.checkDefaultClient off, and the check says so when it is on (novox/hq issue 345).
|
|
func TestTheDefaultClientAskIsLockedOffAndTheCheckSaysWhenItIsOn(t *testing.T) {
|
|
f := newFake(t)
|
|
f.betterbird()
|
|
f.placedByTheMesh()
|
|
links(t, nil)
|
|
asks := func() string {
|
|
t.Helper()
|
|
c, err := f.Check()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, x := range c.Findings {
|
|
if strings.Contains(x.What, "asks at its start") {
|
|
return x.What + " | " + x.Do
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
if got := asks(); got != "" {
|
|
t.Fatalf("the module's policy locks the ask off, yet: %s", got)
|
|
}
|
|
unlocked := `{"policies": {"DisableAppUpdate": true, "ExtensionSettings": {"owl@beonex.com": {"installation_mode": "force_installed",
|
|
"install_url": "file:///opt/betterbird/owl/owl.xpi", "updates_disabled": true}}}}`
|
|
if why := policyProblem([]byte(unlocked)); !strings.Contains(why, defaultClientPref) {
|
|
t.Fatalf("a policy that leaves the ask on is said: %q", why)
|
|
}
|
|
f.write(policyFile, unlocked)
|
|
if got := asks(); !strings.Contains(got, "default mail client") || !strings.Contains(got, "push the module") {
|
|
t.Fatalf("the ask on is a finding with its remedy: %q", got)
|
|
}
|
|
f.write(policyFile, `{"policies": {"DisableAppUpdate": true, "Preferences": {"mail.shell.checkDefaultClient": {"Value": false, "Status": "default"}},
|
|
"ExtensionSettings": {"owl@beonex.com": {"installation_mode": "force_installed", "install_url": "file:///opt/betterbird/owl/owl.xpi", "updates_disabled": true}}}}`)
|
|
if why := policyProblem(mustRead(t, f, policyFile)); !strings.Contains(why, "locked") {
|
|
t.Fatalf("a default the profile can override is not a lock: %q", why)
|
|
}
|
|
f.write(profileDir+"/prefs.js", `user_pref("mail.shell.checkDefaultClient", true);`+"\n")
|
|
if got := asks(); got == "" {
|
|
t.Fatal("a profile that turned the ask back on over an unlocked default is a finding")
|
|
}
|
|
f.write(policyFile, unlocked)
|
|
f.write(profileDir+"/prefs.js", `user_pref("mail.shell.checkDefaultClient", false);`+"\n")
|
|
if got := asks(); got != "" {
|
|
t.Fatalf("a profile with the ask off does not ask: %s", got)
|
|
}
|
|
}
|
|
|
|
func mustRead(t *testing.T, f *fake, p string) []byte {
|
|
t.Helper()
|
|
raw, err := os.ReadFile(filepath.Join(f.Root, p))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return raw
|
|
}
|
|
|
|
// owlCalendar is an Owl calendar in the profile's prefs.js, as Owl registers its account's calendar.
|
|
func (f *fake) owlCalendar(extra string) {
|
|
f.write(profileDir+"/prefs.js", "// Mozilla User Preferences\n"+
|
|
`user_pref("calendar.registry.45b5.type", "owl");`+"\n"+
|
|
`user_pref("calendar.registry.45b5.uri", "owl://someone%40example%2Eorg@outlook.example.org/");`+"\n"+
|
|
`user_pref("calendar.registry.45b5.name", "someone@example.org");`+"\n"+extra)
|
|
}
|
|
|
|
// A passed meeting's reminder shows at most once: the policy locks Betterbird's pass over missed reminders off,
|
|
// whatever the setting prefs or the profile say, because Owl keeps a dismissal only in Betterbird's local cache
|
|
// and the pass fires every unacknowledged reminder of the past month again at each reload (novox/hq issue 640).
|
|
func TestAPassedMeetingsReminderIsNotShownAgainAtEachReloadOfAnOwlCalendar(t *testing.T) {
|
|
raw, err := os.ReadFile(filepath.Join("..", "..", "install", "distribution", "policies.json"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if p := policyPrefsOf(raw)[missedPref]; p.Value != "false" || !p.Locked {
|
|
t.Fatalf("the module's policy locks %s off: %+v", missedPref, p)
|
|
}
|
|
before := strings.Replace(string(raw), `"calendar.alarms.showmissed"`, `"calendar.alarms.unrelated"`, 1)
|
|
if why := policyProblem([]byte(before)); !strings.Contains(why, missedPref) {
|
|
t.Fatalf("a policy that leaves the missed reminders on is said: %q", why)
|
|
}
|
|
|
|
f := newFake(t)
|
|
f.betterbird()
|
|
f.placedByTheMesh()
|
|
links(t, nil)
|
|
nag := func() Finding {
|
|
t.Helper()
|
|
c, err := f.Check()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, x := range c.Findings {
|
|
if strings.Contains(x.What, "Owl calendar") {
|
|
return x
|
|
}
|
|
}
|
|
return Finding{}
|
|
}
|
|
// g14 on 2026-10-13: the module's user.js turns the pass on, an Owl calendar is in the profile, and the
|
|
// placed policy does not lock it.
|
|
f.write(policyFile, before)
|
|
f.owlCalendar("")
|
|
f.write(profileDir+"/user.js", mark+")\n"+`user_pref("calendar.alarms.showmissed", true);`+"\n")
|
|
if x := nag(); !strings.Contains(x.What, "1 Owl calendar") || !strings.Contains(x.What, "user.js") ||
|
|
!strings.Contains(x.Do, "push the module") {
|
|
t.Fatalf("the nag is a finding with its remedy: %+v", x)
|
|
}
|
|
// The policy placed after the running Betterbird started: in effect from its next start.
|
|
f.write(policyFile, string(raw))
|
|
later := time.Unix(1_900_000_000, 0)
|
|
os.Chtimes(filepath.Join(f.Root, policyFile), later, later)
|
|
if x := nag(); !strings.Contains(x.Do, "betterbird_restart") || strings.Contains(x.Do, "push") {
|
|
t.Fatalf("a lock placed after the start waits for a restart: %+v", x)
|
|
}
|
|
// The lock in effect: nothing the profile says turns the pass back on.
|
|
f.write(policyFile, string(raw))
|
|
f.owlCalendar(`user_pref("calendar.alarms.showmissed", true);` + "\n")
|
|
if x := nag(); x.What != "" {
|
|
t.Fatalf("the lock holds whatever the profile says: %+v", x)
|
|
}
|
|
// Without the lock, the pass off is no nag, and neither is the pass on without an Owl calendar.
|
|
f.write(policyFile, before)
|
|
f.write(profileDir+"/user.js", mark+")\n"+`user_pref("calendar.alarms.showmissed", false);`+"\n")
|
|
if x := nag(); x.What != "" {
|
|
t.Fatalf("the pass off: %+v", x)
|
|
}
|
|
f.write(profileDir+"/user.js", mark+")\n"+`user_pref("calendar.alarms.showmissed", true);`+"\n")
|
|
f.write(profileDir+"/prefs.js", `user_pref("calendar.registry.c1.type", "caldav");`+"\n"+`user_pref("calendar.registry.c1.uri", "https://dav.example.org/c1/");`+"\n")
|
|
if x := nag(); x.What != "" {
|
|
t.Fatalf("a CalDAV calendar keeps its dismissals on its server: %+v", x)
|
|
}
|
|
}
|