Files
mesh-catalog/modules/betterbird/cmd/betterbird-tools/derive_test.go
T
jochen c9a711c259
mesh/merge-gate head c9a711c259ed merged into trunk 827b89dbf109 — pass: builds betterbird, sent nowhere; no bus step; 2 wait(s) for a person; every machin…
mesh/repo-check head c9a711c259ed merged into trunk 827b89dbf109 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
betterbird: write the account lists only while the profile is closed, and never propose over an unreadable layer (hq issue 586, review 121)
writeAccountLists checked only for a running Betterbird and wrote without a sync;
it now waits for the profile's lock too and writes atomically. readLayer read any
answer that was not JSON as an empty layer, and a proposal replaces the layer
whole, so a changed wording would have dropped accounts; it now refuses. The
README says the rollout order that leaves no node on Betterbird's defaults.
2026-10-13 02:12:06 +02:00

264 lines
11 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"encoding/json"
"errors"
"fmt"
"reflect"
"strings"
"testing"
)
// ADR 0329 §10, §12: derived from every desktop node's profile, laid out per layer, and proposed one layer at a
// time, carrying the layer as it stands.
// derivingMesh is two desktop nodes, g14 (this one, f) and shanks (f2), and a server that holds no Betterbird, as the
// controller answers them; layers are the controller's layers, open the proposals that wait.
type derivingMesh struct {
t *testing.T
shanks *fake
layers map[string]map[string]any
open bool
proposed []map[string]any
asked []string
asleep bool
// garbled answers the layer in words that are not its JSON.
garbled bool
}
func (d *derivingMesh) ask(key string, body any) (json.RawMessage, error) {
d.asked = append(d.asked, key)
args, _ := body.(map[string]any)
answer := func(ok bool, text string) (json.RawMessage, error) {
b, _ := json.Marshal(map[string]any{"ok": ok, "output": text})
return b, nil
}
switch {
case key == "seat:mesh-controller.nodes":
return answer(true, `[{"name": "g14"}, {"name": "novox"}, {"name": "shanks"}]`)
case key == "seat:mesh-controller.node":
if args["node"] == "novox" {
return answer(true, "novox\n assigned: postgres, nats\n")
}
return answer(true, fmt.Sprintf("%s\n last heard from here\n assigned: adwaita, betterbird, dunst\n", args["node"]))
case key == "betterbird.betterbird_derive@shanks":
if d.asleep {
return nil, errors.New("nats: no responders available for request")
}
here, err := d.shanks.deriveHere(stringsOf(args["kinds"]))
if err != nil {
d.t.Fatal(err)
}
text, _ := json.Marshal(credentialChecked(DeriveAnswer{Here: &here}))
return json.Marshal(map[string]any{"content": []any{map[string]any{"type": "text", "text": string(text)}}})
case key == "seat:mesh-controller.settings" && args["proposals"] == "true":
if d.open {
return answer(true, "sab12 set betterbird on the whole mesh (accounts, identities)\n by g14.node-tools at 2026-10-13 01:00; fingerprint ab12\n waiting for the operator until 2026-10-14 01:00\n"+
"scd34 set mounts on shanks (sources)\n by g14.node-tools at 2026-10-11 00:05; fingerprint dd0b\n chosen: done\n")
}
return answer(true, "scd34 set mounts on shanks (sources)\n by g14.node-tools; fingerprint dd0b\n chosen: done\n")
case key == "seat:mesh-controller.settings" && args["propose"] == "true":
d.proposed = append(d.proposed, args)
return answer(true, "proposal sab12: Set the settings of betterbird?\n fingerprint ab12")
case key == "seat:mesh-controller.settings":
node, _ := args["node"].(string)
if d.garbled {
return answer(true, "Settings of betterbird on "+node+":\n accounts = {…}\n")
}
layer := d.layers[node]
if layer == nil {
where := node
if where == "" {
where = "the whole mesh"
}
return answer(true, "betterbird on "+where+": no layer — the module's definition says\nbetterbird on "+where+", every value")
}
b, _ := json.MarshalIndent(layer, "", " ")
return answer(true, string(b)+"\n approved by the operator\nbetterbird on the whole mesh, every value and where it comes from:\n")
}
d.t.Fatalf("asked %s %v", key, body)
return nil, nil
}
func stringsOf(v any) []string {
switch x := v.(type) {
case []string:
return x
case []any:
var out []string
for _, s := range x {
out = append(out, s.(string))
}
return out
}
return nil
}
func TestDeriveLaysEachEntryOutPerLayerAndProposesOneLayerAtATime(t *testing.T) {
f := newFake(t)
f.profileWithAccounts()
f.profileWithConfiguration()
f.write(profileDir+"/prefs.js", f.read(profileDir+"/prefs.js")+
`user_pref("calendar.registry.c6.type", "caldav");`+"\n"+
`user_pref("calendar.registry.c6.uri", "https://other:hunter2@dav.example.org/cal/x/");`+"\n"+
`user_pref("calendar.registry.c6.name", "Old");`+"\n")
f.declareAccounts(adoptable)
links(t, nil)
f2 := newFake(t)
f2.profileWithAccounts()
f2.profileWithConfiguration()
// shanks holds the same accounts and calendars, another signature, another tag colour and a calendar of its own.
f2.write(profileDir+"/prefs.js", strings.NewReplacer(`"Regards"`, `"Cheers"`, `"#FF0000"`, `"#CC0000"`).Replace(f2.read(profileDir+"/prefs.js"))+
`user_pref("calendar.registry.s1.type", "caldav");`+"\n"+
`user_pref("calendar.registry.s1.uri", "https://dav.example.org/cal/standup/");`+"\n"+
`user_pref("calendar.registry.s1.name", "Standup");`+"\n")
f2.declareAccounts(adoptable)
mesh := &derivingMesh{t: t, shanks: f2, layers: map[string]map[string]any{"": {"accounts": map[string]any{"mail": "as approved"}}}}
oldNode, oldAsk := nodeName, asker
t.Cleanup(func() { nodeName, asker = oldNode, oldAsk })
nodeName = func() string { return "g14" }
asker = mesh.ask
a, err := f.Derive(nil, false, false, nil, "")
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(a.From, []string{"g14", "shanks"}) || len(a.NotHeard) != 0 {
t.Fatalf("derived from the nodes holding Betterbird: %v %v", a.From, a.NotHeard)
}
whole := mustJSON(a)
mw := a.Layers[meshWide]
ids, _ := mw[kindIdentities].(map[string]any)
if mail, _ := ids["mail"].(map[string]any); mail == nil || mustJSON(mail["signature"]) != `["Regards"]` || mail["address"] != nil {
t.Errorf("an identity on both nodes is mesh-wide with this node's value, the default without its address: %v", ids)
}
cals, _ := mw[kindCalendars].(map[string]any)
if cals["home"] == nil || cals["standup"] != nil {
t.Errorf("a calendar both hold is mesh-wide, one only shanks holds is not: %v", cals)
}
if sh := mustJSON(a.Layers["shanks"]); !strings.Contains(sh, "https://dav.example.org/cal/standup/") {
t.Errorf("an entry on one node only goes into that node's layer: %s", sh)
}
tags, _ := mw[kindTags].(map[string]any)
if l1, _ := tags["$label1"].(map[string]any); l1 == nil || l1["color"] != "#FF0000" {
t.Errorf("the tag is mesh-wide with this node's colour: %v", tags)
}
diffs := map[string]difference{}
for _, d := range a.Differences {
diffs[d.Kind+"/"+d.Entry+"/"+d.Field] = d
}
if d := diffs["identities/mail/signature"]; d.Node != "shanks" || mustJSON(d.There) != `["Cheers"]` {
t.Errorf("the answer lists the field shanks holds differently: %+v", a.Differences)
}
if d := diffs["tags/$label1/color"]; d.There != "#CC0000" || d.MeshWide != "#FF0000" {
t.Errorf("%+v", a.Differences)
}
for _, never := range []string{"secret-token", "hunter2", "owl://", `"Work"`} {
if strings.Contains(mustJSON(a.Layers), never) {
t.Errorf("no ICS address, credential or Owl calendar is derived: %s in %s", never, mustJSON(a.Layers))
}
}
if !strings.Contains(whole, "Holidays") || !strings.Contains(whole, "‹withheld›") || strings.Contains(mustJSON(credentialChecked(a)), "hunter2") {
t.Errorf("what is left out is named, an ICS calendar by its name and a password as ‹withheld›: %v", a.NotDerived)
}
if len(mesh.proposed) != 0 {
t.Fatal("without propose nothing is proposed")
}
// Propose: the mesh-wide layer first, as it stands with the derived kinds, and the operator's setup of prefs.
prefs := map[string]any{}
for n, v := range shippedDefaultsWant {
prefs[n], _ = prefValue(v)
}
a, err = f.Derive(nil, false, true, prefs, "")
if err != nil {
t.Fatal(err)
}
if len(mesh.proposed) != 1 || mesh.proposed[0]["node"] != nil || mesh.proposed[0]["replace"] != "true" || a.Next != meshWide {
t.Fatalf("the mesh-wide layer is proposed first: %v %s", mesh.proposed, a.Next)
}
var values map[string]any
_ = json.Unmarshal([]byte(mesh.proposed[0]["values"].(string)), &values)
if values["accounts"] == nil || values[kindIdentities] == nil || values[kindTags] == nil || values["prefs"] == nil {
t.Fatalf("the layer as it stands, with every derived kind and prefs: %v", values)
}
body, _ := json.Marshal(map[string]any{"prefs": values["prefs"]})
if got, err := parseDeclaredPrefs(body); err != nil || !reflect.DeepEqual(got, shippedDefaultsWant) {
t.Fatalf("the operator's 21 preferences, one entry each: %v %v", got, err)
}
if !strings.Contains(a.Proposed, "sab12") {
t.Fatalf("%s", a.Proposed)
}
// While it waits nothing more is proposed.
mesh.open = true
if _, err := f.Derive(nil, false, true, nil, ""); err == nil || !strings.Contains(err.Error(), "sab12 (the whole mesh)") || len(mesh.proposed) != 1 {
t.Fatalf("one open proposal of the module's at a time: %v", err)
}
// Approved: the mesh-wide layer holds it, so the next layer is shanks's own.
mesh.open = false
mesh.layers[""] = values
a, err = f.Derive(nil, false, true, prefs, "")
if err != nil {
t.Fatal(err)
}
if len(mesh.proposed) != 2 || mesh.proposed[1]["node"] != "shanks" || a.Next != "shanks" ||
!strings.Contains(mesh.proposed[1]["values"].(string), "standup") {
t.Fatalf("then the node's own layer: %v", mesh.proposed)
}
var shanks map[string]any
if err := json.Unmarshal([]byte(mesh.proposed[1]["values"].(string)), &shanks); err != nil {
t.Fatal(err)
}
mesh.layers["shanks"] = shanks
if a, err = f.Derive(nil, false, true, prefs, ""); err != nil || len(mesh.proposed) != 2 || !strings.HasPrefix(a.Next, "none") {
t.Fatalf("nothing left to propose: %v %s", err, a.Next)
}
// A desktop node that does not answer: derived without it, never proposed.
mesh.asleep = true
a, err = f.Derive(nil, false, true, nil, meshWide)
if err == nil || len(a.NotHeard) != 1 || !strings.Contains(err.Error(), "did not answer") || len(mesh.proposed) != 2 {
t.Fatalf("a layer is never proposed without a desktop node: %v %v", err, a.NotHeard)
}
// A key to adopt is a node's: the mesh-wide layer's proposal refuses one.
if err := proposable(map[string]any{kindCalendars: map[string]any{"c": map[string]any{"name": "C",
"url": "https://dav.example.org/c/", "key": "c3"}}}, true); err == nil || !strings.Contains(err.Error(), "node's own layer") {
t.Fatalf("%v", err)
}
}
// Review 121: a layer the controller answers in any other words than its JSON or "no layer" refuses the proposal,
// because a proposal replaces the layer whole and would drop accounts.
func TestALayerThatCannotBeReadRefusesTheProposal(t *testing.T) {
f := newFake(t)
f.profileWithAccounts()
f.profileWithConfiguration()
f.declareAccounts(adoptable)
links(t, nil)
f2 := newFake(t)
f2.profileWithAccounts()
f2.declareAccounts(adoptable)
mesh := &derivingMesh{t: t, shanks: f2, garbled: true}
oldNode, oldAsk := nodeName, asker
t.Cleanup(func() { nodeName, asker = oldNode, oldAsk })
nodeName = func() string { return "g14" }
asker = mesh.ask
if _, err := f.Derive(nil, false, true, nil, ""); err == nil || !strings.Contains(err.Error(), "cannot be read as the layer") ||
len(mesh.proposed) != 0 {
t.Fatalf("the derive refuses: %v %v", err, mesh.proposed)
}
if _, err := f.ProfileAccounts(true); err == nil || !strings.Contains(err.Error(), "cannot be read as the layer") ||
len(mesh.proposed) != 0 {
t.Fatalf("betterbird_accounts refuses: %v %v", err, mesh.proposed)
}
if _, err := readLayer(""); err == nil {
t.Fatal("an unreadable layer is an error, never an empty layer")
}
mesh.garbled = false
if got, err := readLayer("shanks"); err != nil || len(got) != 0 {
t.Fatalf("no layer, in the controller's words, is an empty layer: %v %v", got, err)
}
}