package main import ( "encoding/json" "fmt" "os" "path/filepath" "reflect" "regexp" "strconv" "strings" "testing" ) // betterbird's shape (novox/hq issue 345, ADR 0208, ADR 0316): the X display on its own machine; its directory // in the home with the mergeable files the settings fill, each naming the keys it takes (ADR 0326): prefs, accounts // and configuration (identities, calendars, address-books, filters) trusted, tags not (ADR 0329 §5); the community package declared absent before anything is placed in /opt/betterbird; Betterbird from // its pinned upstream release, Owl pinned by its vendor's digest, the policy and the dictionaries' default // unpacked into the installation; the tools' program for the start's preparation, the start and the launcher; // every new build waiting for the operator's push; and the Go bundle serving exactly the listed tools. type manifest struct { Module string `json:"module"` Version string `json:"version"` Upgrade map[string]string `json:"upgrade"` Requires []string `json:"requires"` Invokes []string `json:"invokes"` Settings map[string]any `json:"settings"` Tools []string `json:"tools"` Resources []map[string]any `json:"resources"` Build struct { Artifacts []map[string]any `json:"artifacts"` } `json:"build"` } func readManifest(t *testing.T) (manifest, string) { t.Helper() raw, err := os.ReadFile(filepath.Join("..", "..", "module.json")) if err != nil { t.Fatal(err) } dec := json.NewDecoder(strings.NewReader(string(raw))) dec.DisallowUnknownFields() var m manifest if err := dec.Decode(&m); err != nil { t.Fatalf("module.json: %v", err) } return m, string(raw) } func resource(t *testing.T, m manifest, id string) map[string]any { t.Helper() for _, r := range m.Resources { if r["id"] == id { return r } } t.Fatalf("module.json has no resource %s", id) return nil } func artifact(t *testing.T, m manifest, name string) map[string]any { t.Helper() for _, a := range m.Build.Artifacts { if a["name"] == name { return a } } t.Fatalf("module.json builds no artifact %s", name) return nil } func TestItPlacesBetterbirdWhereTheCommunityPackageDidAndOnlyAfterItIsGone(t *testing.T) { m, raw := readManifest(t) if m.Module != "betterbird" || !reflect.DeepEqual(m.Requires, []string{"x11-display"}) { t.Fatalf("%+v", m) } // Every new build waits for a person's push: the switch removes a package and replaces what a running // Betterbird has open (ADR 0316 §4). if m.Upgrade["policy"] != "record" || strings.TrimSpace(m.Upgrade["why"]) == "" { t.Fatalf("the module is marked for a person's approval, with why: %v", m.Upgrade) } var ids []string for _, r := range m.Resources { ids = append(ids, r["id"].(string)) } want := "configuration-dir,prefs,accounts,configuration,tags,community-package,installation,owl,policies,spelling,preparation,start,launcher" if strings.Join(ids, ",") != want { t.Fatalf("the resources, in the order the node-engine applies them: %v, want %s", ids, want) } pkg := resource(t, m, "community-package") if pkg["type"] != "package" || pkg["package"] != packageFor || pkg["absent"] != true { t.Fatalf("the community package is declared absent (ADR 0180): %v", pkg) } places := map[string]string{"installation": installDir, "owl": filepath.Dir(owlFile), "policies": filepath.Dir(policyFile), "spelling": installDir + "/defaults/pref", "preparation": filepath.Dir(preparation)} arts := map[string]string{"installation": "archive", "owl": "owl", "policies": "distribution", "spelling": "defaults", "preparation": "tools"} for id, path := range places { r := resource(t, m, id) if r["type"] != "archive" || r["path"] != path || r["artifact"] != arts[id] || r["owner"] != nil { t.Errorf("%s is the %s archive unpacked at %s, root's: %v", id, arts[id], path, r) } } for _, never := range []string{".thunderbird", "prefs.js", "logins.json", "key4.db"} { if regexp.MustCompile(regexp.QuoteMeta(never) + `\b`).MatchString(raw) { t.Errorf("module.json names %q: the profile is Betterbird's", never) } } s := strings.ToLower(raw) for _, never := range []string{"/home/", "jochen", "g14", "shanks", "novox.be", "http://", "mediahuis"} { if strings.Contains(s, never) { t.Errorf("module.json names %q", never) } } } // ADR 0329 §4–§6, ADR 0326, issue 586: every setting reaches the machine as a mergeable file of the account's that // names the keys it takes; four kinds and accounts and prefs are trusted, tags is not; no setting has a default in the // definition, since each is the operator's own value; and each file's default declares nothing. func TestTheSettingsAreMergeableFilesThatNameTheirKeysAndTagsAloneIsUntrusted(t *testing.T) { m, _ := readManifest(t) if len(m.Settings) != 0 { t.Fatalf("no setting has a default in the definition: the operator's setup is the settings' mesh-wide layer (issue 586, ADR 0262): %v", m.Settings) } files := map[string]struct { path, mode string takes []any trusted bool }{ "prefs": {prefsFile, "0644", []any{"prefs"}, true}, "accounts": {accountsFile, "0600", []any{"accounts"}, true}, "configuration": {configurationFile, "0600", []any{"identities", "calendars", "address-books", "filters"}, true}, "tags": {tagsFile, "0644", []any{"tags"}, false}, } merging := 0 for _, r := range m.Resources { if r["merge"] != nil { merging++ } } if merging != len(files) { t.Fatalf("%d mergeable files, want %d", merging, len(files)) } for id, want := range files { r := resource(t, m, id) if r["path"] != "${machine:account-home}/"+want.path || r["merge"] != "json" || r["trusted"] != want.trusted || r["mode"] != want.mode || r["owner"] != "${machine:account}" || !reflect.DeepEqual(r["takes-settings"], want.takes) { t.Errorf("%s: %v", id, r) } var content map[string]any if err := json.Unmarshal([]byte(r["content"].(string)), &content); err != nil || len(content) != len(want.takes) { t.Errorf("%s's default holds each key it takes, empty: %v %v", id, content, err) } } got, err := parseAccounts([]byte(resource(t, m, "accounts")["content"].(string))) if err != nil || len(got) != 0 { t.Fatalf("its default declares no account: %v %v", got, err) } prefs, err := parseDeclaredPrefs([]byte(resource(t, m, "prefs")["content"].(string))) if err != nil || len(prefs) != 0 { t.Fatalf("its default declares no preference: %v %v", prefs, err) } var c Configuration if err := decodeInto([]byte(resource(t, m, "configuration")["content"].(string)), configurationFile, &c); err != nil { t.Fatal(err) } if err := decodeInto([]byte(resource(t, m, "tags")["content"].(string)), tagsFile, &c); err != nil { t.Fatal(err) } } func TestBothPinsAreAURLAVersionAndASha256AndOwlIsNeverInTheRepository(t *testing.T) { m, _ := readManifest(t) sha := regexp.MustCompile(`^[0-9a-f]{64}$`) for _, name := range []string{"archive", "owl"} { a := artifact(t, m, name) u, _ := a["url"].(string) if a["kind"] != "archive" || !strings.HasPrefix(u, "https://") || !sha.MatchString(fmt.Sprint(a["sha256"])) || strings.TrimSpace(fmt.Sprint(a["version"])) == "" || a["from"] != nil { t.Errorf("%s is pinned by URL, version and sha256, and copied from nowhere in the repository: %v", name, a) } } if a := artifact(t, m, "archive"); a["within"] != "betterbird" { t.Errorf("the release unpacks into betterbird/, whose content is the installation: %v", a) } if a := artifact(t, m, "owl"); a["file"] != filepath.Base(owlFile) { t.Errorf("Owl's xpi is kept whole as %s: %v", filepath.Base(owlFile), a) } // Owl's licence forbids copying it to third parties: no xpi anywhere in the module's tree. filepath.Walk(filepath.Join("..", ".."), func(p string, info os.FileInfo, err error) error { if err == nil && strings.HasSuffix(strings.ToLower(p), ".xpi") { t.Errorf("%s is in the repository", p) } return nil }) } // owlSupports is the range of Betterbird versions each pinned Owl declares in its manifest.json // (browser_specific_settings.gecko strict_min_version, strict_max_version), read from the vendor's xpi // when its pin was taken. Moving Owl's pin means reading the new xpi's range into here. var owlSupports = map[string][2]string{ "1.5.6.1": {"115.0", "156.*"}, } // owlAccepts says why a Betterbird version is outside the range an Owl version declares, or nil. func owlAccepts(owl, betterbird string) error { r, known := owlSupports[owl] if !known { return fmt.Errorf("Owl %s's supported range is not recorded in owlSupports: read it from the xpi's manifest.json", owl) } major := func(v string) int { head, _, _ := strings.Cut(v, ".") n, err := strconv.Atoi(head) if err != nil { return -1 } return n } b, lo, hi := major(betterbird), major(r[0]), major(r[1]) if b < 0 || b < lo || b > hi { return fmt.Errorf("Betterbird %s is outside the range Owl %s supports (%s to %s): a Betterbird Owl does not support "+ "waits for Owl's pin to move (novox/hq ADR 0316 §8)", betterbird, owl, r[0], r[1]) } return nil } func TestABetterbirdOwlDoesNotSupportIsRefused(t *testing.T) { m, _ := readManifest(t) owl, bb := fmt.Sprint(artifact(t, m, "owl")["version"]), fmt.Sprint(artifact(t, m, "archive")["version"]) if err := owlAccepts(owl, bb); err != nil { t.Fatal(err) } for _, outside := range []string{"157.0esr-bb1", "102.15.1esr-bb4"} { if err := owlAccepts(owl, outside); err == nil { t.Errorf("Betterbird %s was accepted with Owl %s", outside, owl) } } if err := owlAccepts("9.9", bb); err == nil { t.Error("an Owl whose range nobody read was accepted") } } func TestThePolicyTurnsTheUpdaterOffAndInstallsTheMeshsOwlEverywhere(t *testing.T) { m, _ := readManifest(t) if a := artifact(t, m, "distribution"); a["kind"] != "archive" || a["from"] != "install/distribution" { t.Fatalf("%v", a) } raw, err := os.ReadFile(filepath.Join("..", "..", "install", "distribution", "policies.json")) if err != nil { t.Fatal(err) } if why := policyProblem(raw); why != "" { t.Fatalf("the module's policies.json: %s", why) } entries, _ := os.ReadDir(filepath.Join("..", "..", "install", "distribution")) if len(entries) != 1 { t.Fatalf("the distribution directory holds the policy alone: %v", entries) } if a := artifact(t, m, "defaults"); a["kind"] != "archive" || a["from"] != "install/defaults-pref" { t.Fatalf("%v", a) } prefs, err := os.ReadFile(filepath.Join("..", "..", "install", "defaults-pref", "vendor-prefs.js")) if err != nil || !strings.Contains(string(prefs), `pref("spellchecker.dictionary_path", "/usr/share/hunspell");`) { t.Fatalf("the dictionaries are named as a default, in place of a link: %s %v", prefs, err) } } func TestTheStartPreparesThenStartsBetterbirdAndTheLauncherUsesIt(t *testing.T) { m, _ := readManifest(t) start := resource(t, m, "start") content := start["content"].(string) if start["path"] != "/usr/local/bin/betterbird" || start["mode"] != "0755" || !strings.Contains(content, "\n"+preparation+" prepare ||") || !strings.HasSuffix(content, "\nexec "+startBin+" \"$@\"\n") { t.Fatalf("the start runs the preparation, says when it did not complete, and starts Betterbird either way: %v", start) } launcher := resource(t, m, "launcher") lc := launcher["content"].(string) if launcher["path"] != "/usr/local/share/applications/eu.betterbird.Betterbird.desktop" || !strings.Contains(lc, "\nExec=/usr/local/bin/betterbird %u\n") || !strings.Contains(lc, "\nIcon="+installDir+"/") || !strings.Contains(lc, "\nStartupWMClass=eu.betterbird.Betterbird\n") { t.Fatalf("the launcher starts the module's start and names the icon in the installation: %v", launcher) } if b := artifact(t, m, "tools"); filepath.Base(preparation) != b["binary"] { t.Fatalf("the preparation runs the tools' binary: %v", b) } } func TestTheToolsAgreeWithTheManifest(t *testing.T) { m, _ := readManifest(t) served := map[string]bool{} for _, tool := range tools() { served[tool.Name] = true if !strings.HasPrefix(tool.Name, "betterbird_") || strings.TrimSpace(tool.Description) == "" { t.Errorf("%s: prefixed betterbird_ and described", tool.Name) } } for _, name := range m.Tools { if !served[name] { t.Errorf("module.json lists %s, which the bundle does not serve", name) } delete(served, name) } for name := range served { t.Errorf("the bundle serves %s, which module.json does not list", name) } b := artifact(t, m, "tools") if b["kind"] != "bundle" || b["language"] != "go" || b["from"] != "cmd/betterbird-tools" || b["binary"] != "betterbird-tools" { t.Errorf("the Go tools bundle: %v", b) } // The tools propose through the controller's settings verb (ADR 0317 §9), and betterbird_derive asks the controller // which nodes hold the module and each of them for its profile (ADR 0329 §10). if !reflect.DeepEqual(m.Invokes, []string{"seat:mesh-controller.settings", "seat:mesh-controller.nodes", "seat:mesh-controller.node", "betterbird.betterbird_derive"}) { t.Errorf("the module invokes the controller's settings, nodes and node verbs and its own derive: %v", m.Invokes) } }