Files
mesh-catalog/modules/betterbird/cmd/betterbird-tools/derive_test.go
T

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, node-b (this one, f) and node-d (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
nodeD *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": "node-b"}, {"name": "node-c"}, {"name": "node-d"}]`)
case key == "seat:mesh-controller.node":
if args["node"] == "node-c" {
return answer(true, "node-c\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@node-d":
if d.asleep {
return nil, errors.New("nats: no responders available for request")
}
here, err := d.nodeD.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 node-b.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 node-d (sources)\n by node-b.node-tools at 2026-10-11 00:05; fingerprint dd0b\n chosen: done\n")
}
return answer(true, "scd34 set mounts on node-d (sources)\n by node-b.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()
// node-d 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, nodeD: 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 "node-b" }
asker = mesh.ask
a, err := f.Derive(nil, false, false, nil, "")
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(a.From, []string{"node-b", "node-d"}) || 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 node-d holds is not: %v", cals)
}
if sh := mustJSON(a.Layers["node-d"]); !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 != "node-d" || mustJSON(d.There) != `["Cheers"]` {
t.Errorf("the answer lists the field node-d 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 node-d'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"] != "node-d" || a.Next != "node-d" ||
!strings.Contains(mesh.proposed[1]["values"].(string), "standup") {
t.Fatalf("then the node's own layer: %v", mesh.proposed)
}
var nodeD map[string]any
if err := json.Unmarshal([]byte(mesh.proposed[1]["values"].(string)), &nodeD); err != nil {
t.Fatal(err)
}
mesh.layers["node-d"] = nodeD
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, nodeD: f2, garbled: true}
oldNode, oldAsk := nodeName, asker
t.Cleanup(func() { nodeName, asker = oldNode, oldAsk })
nodeName = func() string { return "node-b" }
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("node-d"); err != nil || len(got) != 0 {
t.Fatalf("no layer, in the controller's words, is an empty layer: %v %v", got, err)
}
}