Merge feat/modules-name-no-node into bulk-a
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# betterbird
|
||||
|
||||
Betterbird, the mail and calendar client of g14 and shanks (a Thunderbird fork, with the operator's
|
||||
Betterbird, the mail and calendar client of node-b and node-d (a Thunderbird fork, with the operator's
|
||||
Exchange calendar through the Owl add-on), as a module (novox/hq issue 345, issue 586, ADR 0208, ADR 0316,
|
||||
ADR 0317, ADR 0329, to-be 56). It requires `x11-display`, so it is assigned only where a display server is held
|
||||
on the same machine. It installs Betterbird from a pinned upstream release, ships Owl with it through
|
||||
@@ -58,7 +58,7 @@ reading the new range there (ADR 0316 §8).
|
||||
**The policy** turns Betterbird's own updater off (`DisableAppUpdate`, which Betterbird's update check
|
||||
honours) and installs Owl into every profile from `file:///opt/betterbird/owl/owl.xpi` as
|
||||
`force_installed`, with `updates_disabled`. A profile that already runs another Owl keeps it: the policy
|
||||
installs the pinned one where Owl is missing. A newer Owl in the profile (g14 runs 1.5.7) is said as a
|
||||
installs the pinned one where Owl is missing. A newer Owl in the profile (node-b runs 1.5.7) is said as a
|
||||
fact by `betterbird_status`; Owl installed by policy cannot be removed in Betterbird, so the remedy is
|
||||
moving the pin. An older one is a finding of `betterbird_check`, with the same remedy.
|
||||
|
||||
@@ -323,10 +323,10 @@ question.
|
||||
|
||||
## What changes when it is pushed
|
||||
|
||||
The module is assigned to shanks today (look-only, from issue 345's first part); g14 runs Betterbird
|
||||
The module is assigned to node-d today (look-only, from issue 345's first part); node-b runs Betterbird
|
||||
outside the mesh. With this version, a push to a node (each waits for the operator: `upgrade: record`):
|
||||
|
||||
| | on a node with the community package (g14, shanks) |
|
||||
| | on a node with the community package (node-b, node-d) |
|
||||
|---|---|
|
||||
| the package | `betterbird-bin` removed (quit Betterbird first) |
|
||||
| `/opt/betterbird` | the mesh's 153.4.0esr-bb10, in the same place, so the same profile opens |
|
||||
|
||||
@@ -412,13 +412,13 @@ func TestKeepProposesANodeLayerThroughTheController(t *testing.T) {
|
||||
f.write(profileDir+"/prefs.js", `user_pref("mail.uifontsize", 16);`+"\n"+`user_pref("mail.spam.manualMark", true);`+"\n")
|
||||
oldNode, oldAsk := nodeName, asker
|
||||
t.Cleanup(func() { nodeName, asker = oldNode, oldAsk })
|
||||
nodeName = func() string { return "shanks" }
|
||||
nodeName = func() string { return "node-d" }
|
||||
var asked []map[string]any
|
||||
asker = func(v string, a any) (json.RawMessage, error) {
|
||||
args := a.(map[string]any)
|
||||
asked = append(asked, args)
|
||||
if args["propose"] == nil {
|
||||
return json.RawMessage(`{"ok": true, "output": "{\"prefs\": {\"calendar.alarms.playsound\": false}, \"identities\": {\"mail\": {\"account\": \"mail\"}}}\n set at the terminal\nbetterbird on shanks, every value"}`), nil
|
||||
return json.RawMessage(`{"ok": true, "output": "{\"prefs\": {\"calendar.alarms.playsound\": false}, \"identities\": {\"mail\": {\"account\": \"mail\"}}}\n set at the terminal\nbetterbird on node-d, every value"}`), nil
|
||||
}
|
||||
return json.RawMessage(`{"ok": true, "output": "proposed: p-1"}`), nil
|
||||
}
|
||||
@@ -427,7 +427,7 @@ func TestKeepProposesANodeLayerThroughTheController(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(asked) != 2 || asked[0]["node"] != "shanks" || asked[1]["node"] != "shanks" || asked[1]["propose"] != "true" ||
|
||||
if len(asked) != 2 || asked[0]["node"] != "node-d" || asked[1]["node"] != "node-d" || asked[1]["propose"] != "true" ||
|
||||
asked[1]["module"] != "betterbird" {
|
||||
t.Fatalf("the node's layer read, then proposed: %v", asked)
|
||||
}
|
||||
|
||||
@@ -12,11 +12,11 @@ import (
|
||||
// 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
|
||||
// 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
|
||||
shanks *fake
|
||||
nodeD *fake
|
||||
layers map[string]map[string]any
|
||||
open bool
|
||||
proposed []map[string]any
|
||||
@@ -35,17 +35,17 @@ func (d *derivingMesh) ask(key string, body any) (json.RawMessage, error) {
|
||||
}
|
||||
switch {
|
||||
case key == "seat:mesh-controller.nodes":
|
||||
return answer(true, `[{"name": "g14"}, {"name": "novox"}, {"name": "shanks"}]`)
|
||||
return answer(true, `[{"name": "node-b"}, {"name": "node-c"}, {"name": "node-d"}]`)
|
||||
case key == "seat:mesh-controller.node":
|
||||
if args["node"] == "novox" {
|
||||
return answer(true, "novox\n assigned: postgres, nats\n")
|
||||
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@shanks":
|
||||
case key == "betterbird.betterbird_derive@node-d":
|
||||
if d.asleep {
|
||||
return nil, errors.New("nats: no responders available for request")
|
||||
}
|
||||
here, err := d.shanks.deriveHere(stringsOf(args["kinds"]))
|
||||
here, err := d.nodeD.deriveHere(stringsOf(args["kinds"]))
|
||||
if err != nil {
|
||||
d.t.Fatal(err)
|
||||
}
|
||||
@@ -53,10 +53,10 @@ func (d *derivingMesh) ask(key string, body any) (json.RawMessage, error) {
|
||||
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, "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 shanks (sources)\n by g14.node-tools; 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")
|
||||
@@ -107,23 +107,23 @@ func TestDeriveLaysEachEntryOutPerLayerAndProposesOneLayerAtATime(t *testing.T)
|
||||
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.
|
||||
// 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, shanks: f2, layers: map[string]map[string]any{"": {"accounts": map[string]any{"mail": "as approved"}}}}
|
||||
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 "g14" }
|
||||
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{"g14", "shanks"}) || len(a.NotHeard) != 0 {
|
||||
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)
|
||||
@@ -134,9 +134,9 @@ func TestDeriveLaysEachEntryOutPerLayerAndProposesOneLayerAtATime(t *testing.T)
|
||||
}
|
||||
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)
|
||||
t.Errorf("a calendar both hold is mesh-wide, one only node-d holds is not: %v", cals)
|
||||
}
|
||||
if sh := mustJSON(a.Layers["shanks"]); !strings.Contains(sh, "https://dav.example.org/cal/standup/") {
|
||||
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)
|
||||
@@ -147,8 +147,8 @@ func TestDeriveLaysEachEntryOutPerLayerAndProposesOneLayerAtATime(t *testing.T)
|
||||
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["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)
|
||||
@@ -195,22 +195,22 @@ func TestDeriveLaysEachEntryOutPerLayerAndProposesOneLayerAtATime(t *testing.T)
|
||||
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.
|
||||
// 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"] != "shanks" || a.Next != "shanks" ||
|
||||
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 shanks map[string]any
|
||||
if err := json.Unmarshal([]byte(mesh.proposed[1]["values"].(string)), &shanks); err != nil {
|
||||
var nodeD map[string]any
|
||||
if err := json.Unmarshal([]byte(mesh.proposed[1]["values"].(string)), &nodeD); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
mesh.layers["shanks"] = shanks
|
||||
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)
|
||||
}
|
||||
@@ -240,10 +240,10 @@ func TestALayerThatCannotBeReadRefusesTheProposal(t *testing.T) {
|
||||
f2 := newFake(t)
|
||||
f2.profileWithAccounts()
|
||||
f2.declareAccounts(adoptable)
|
||||
mesh := &derivingMesh{t: t, shanks: f2, garbled: true}
|
||||
mesh := &derivingMesh{t: t, nodeD: f2, garbled: true}
|
||||
oldNode, oldAsk := nodeName, asker
|
||||
t.Cleanup(func() { nodeName, asker = oldNode, oldAsk })
|
||||
nodeName = func() string { return "g14" }
|
||||
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 {
|
||||
@@ -257,7 +257,7 @@ func TestALayerThatCannotBeReadRefusesTheProposal(t *testing.T) {
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ func TestTheAdminProcessAnswersAnAskOverARealBus(t *testing.T) {
|
||||
Retention: jetstream.WorkQueuePolicy, Discard: jetstream.DiscardNew, MaxAge: 24 * time.Hour, MaxBytes: 8 << 20}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
account := process.Account{URL: url, Node: "g14", Module: "brother", User: "g14.brother." + DeviceProcessID, Password: "pw"}
|
||||
account := process.Account{URL: url, Node: "node-b", Module: "brother", User: "node-b.brother." + DeviceProcessID, Password: "pw"}
|
||||
if _, err := js.CreateConsumer(ctx, AsksStream, jetstream.ConsumerConfig{Durable: account.QueueConsumerName(),
|
||||
AckPolicy: jetstream.AckExplicitPolicy, AckWait: 30 * time.Second, MaxDeliver: 3}); err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -234,7 +234,7 @@ func TestAFetchedPDFIsStoredUnderItsHashAndPrinted(t *testing.T) {
|
||||
// A refusal inside the mesh is answered, and nothing is stored nor printed.
|
||||
fake.body = nil
|
||||
before := len(r.objects.keys())
|
||||
if _, err := r.call(t, "printer.print", map[string]any{"url": "http://127.0.0.1:9000/mesh-shanks-brother/x"}); err == nil || !strings.Contains(err.Error(), "loopback") {
|
||||
if _, err := r.call(t, "printer.print", map[string]any{"url": "http://127.0.0.1:9000/mesh-node-d-brother/x"}); err == nil || !strings.Contains(err.Error(), "loopback") {
|
||||
t.Errorf("a loopback address: error = %v", err)
|
||||
}
|
||||
if len(r.objects.keys()) != before || fake.body != nil {
|
||||
|
||||
@@ -56,15 +56,15 @@ func TestPrintTakesExactlyOneSource(t *testing.T) {
|
||||
for name, args := range map[string]map[string]any{
|
||||
"none": {},
|
||||
"only options": {"copies": float64(2)},
|
||||
"object and path": {"object": key, "path": "/home/ops/a.pdf", "node": "g14"},
|
||||
"object and path": {"object": key, "path": "/home/ops/a.pdf", "node": "node-b"},
|
||||
"object and url": {"object": key, "url": "https://example.com/a.pdf"},
|
||||
"path+node and url": {"path": "/home/ops/a.pdf", "node": "g14", "url": "https://example.com/a.pdf"},
|
||||
"all three": {"object": key, "path": "/home/ops/a.pdf", "node": "g14", "url": "https://example.com/a.pdf"},
|
||||
"path+node and url": {"path": "/home/ops/a.pdf", "node": "node-b", "url": "https://example.com/a.pdf"},
|
||||
"all three": {"object": key, "path": "/home/ops/a.pdf", "node": "node-b", "url": "https://example.com/a.pdf"},
|
||||
"path without node": {"path": "/home/ops/a.pdf"},
|
||||
"node without path": {"node": "g14", "object": key},
|
||||
"node without path": {"node": "node-b", "object": key},
|
||||
"the retired upload": {"upload": "u1"},
|
||||
"object and upload": {"object": key, "upload": "u1"},
|
||||
"path and its object": {"path": "/home/ops/a.pdf", "node": "g14", "object": key},
|
||||
"path and its object": {"path": "/home/ops/a.pdf", "node": "node-b", "object": key},
|
||||
} {
|
||||
_, err := r.call(t, "printer.print", args)
|
||||
if err == nil {
|
||||
@@ -82,7 +82,7 @@ func TestPrintTakesExactlyOneSource(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// 1598/AC2: with path and node g14, printer.print asks g14's node-files to upload the path into brother's bucket, and
|
||||
// 1598/AC2: with path and node node-b, printer.print asks node-b's node-files to upload the path into brother's bucket, and
|
||||
// queues a print ask that carries the returned key and the options: under 4 KiB, and no path, node or bytes.
|
||||
func TestPrintOfAFileOfAnotherMachineUploadsThenPrintsTheKey(t *testing.T) {
|
||||
fake := newIPPFake(t, 31, 5)
|
||||
@@ -90,12 +90,12 @@ func TestPrintOfAFileOfAnotherMachineUploadsThenPrintsTheKey(t *testing.T) {
|
||||
key := sha256Hex(doc)
|
||||
up := withUpload(t, key, nil)
|
||||
r := newRig(t, `{"printer":"192.0.2.10"}`, "pw")
|
||||
r.objects.Put(t.Context(), key, "application/pdf", doc) // what g14's upload stored
|
||||
got, err := r.call(t, "printer.print", map[string]any{"path": "/home/ops/letter.pdf", "node": "g14", "copies": float64(2)})
|
||||
r.objects.Put(t.Context(), key, "application/pdf", doc) // what node-b's upload stored
|
||||
got, err := r.call(t, "printer.print", map[string]any{"path": "/home/ops/letter.pdf", "node": "node-b", "copies": float64(2)})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(up.seen()) != 1 || up.seen()[0] != "g14|/home/ops/letter.pdf|brother-test" {
|
||||
if len(up.seen()) != 1 || up.seen()[0] != "node-b|/home/ops/letter.pdf|brother-test" {
|
||||
t.Fatalf("uploads asked: %v", up.seen())
|
||||
}
|
||||
if len(r.bus.published) != 1 {
|
||||
@@ -114,7 +114,7 @@ func TestPrintOfAFileOfAnotherMachineUploadsThenPrintsTheKey(t *testing.T) {
|
||||
}
|
||||
}
|
||||
m := got.(map[string]any)
|
||||
if m["job-id"] != float64(31) || m["node"] != "g14" || m["path"] != "/home/ops/letter.pdf" {
|
||||
if m["job-id"] != float64(31) || m["node"] != "node-b" || m["path"] != "/home/ops/letter.pdf" {
|
||||
t.Errorf("answer = %v", m)
|
||||
}
|
||||
if _, sent := fake.request(t); string(sent) != string(doc) {
|
||||
@@ -127,7 +127,7 @@ func TestARefusedUploadIsAnsweredWithItsReasonAndNothingIsPrinted(t *testing.T)
|
||||
fake := newIPPFake(t, 1, 3)
|
||||
up := withUpload(t, "", errors.New("/home/ops/.ssh/id_ed25519 is refused by name: it is in .ssh, which holds an account's credentials"))
|
||||
r := newRig(t, `{"printer":"192.0.2.10"}`, "pw")
|
||||
_, err := r.call(t, "printer.print", map[string]any{"path": "/home/ops/.ssh/id_ed25519", "node": "g14"})
|
||||
_, err := r.call(t, "printer.print", map[string]any{"path": "/home/ops/.ssh/id_ed25519", "node": "node-b"})
|
||||
if err == nil || !strings.Contains(err.Error(), "is refused by name: it is in .ssh") {
|
||||
t.Fatalf("error = %v", err)
|
||||
}
|
||||
@@ -277,7 +277,7 @@ func TestASlowUploadAndPrintAnswersStartedNeverATimeout(t *testing.T) {
|
||||
t.Cleanup(func() { uploadVia = old })
|
||||
r := newRig(t, `{"printer":"192.0.2.10"}`, "pw")
|
||||
r.objects.Put(t.Context(), key, "application/pdf", doc)
|
||||
got, err := r.call(t, "printer.print", map[string]any{"path": "/home/ops/letter.pdf", "node": "g14"})
|
||||
got, err := r.call(t, "printer.print", map[string]any{"path": "/home/ops/letter.pdf", "node": "node-b"})
|
||||
if err != nil {
|
||||
t.Fatalf("a slow upload failed the call: %v", err)
|
||||
}
|
||||
@@ -292,7 +292,7 @@ func TestASlowUploadAndPrintAnswersStartedNeverATimeout(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if rm := read.(map[string]any); rm["state"] == nil {
|
||||
if rm["job-id"] != float64(72) || rm["path"] != "/home/ops/letter.pdf" || rm["node"] != "g14" {
|
||||
if rm["job-id"] != float64(72) || rm["path"] != "/home/ops/letter.pdf" || rm["node"] != "node-b" {
|
||||
t.Errorf("the outcome = %v", rm)
|
||||
}
|
||||
break
|
||||
@@ -309,7 +309,7 @@ func TestASlowUploadAndPrintAnswersStartedNeverATimeout(t *testing.T) {
|
||||
time.Sleep(150 * time.Millisecond)
|
||||
return "", errors.New("/home/ops/.ssh/id_ed25519 is refused by name")
|
||||
}
|
||||
got, _ = r.call(t, "printer.print", map[string]any{"path": "/home/ops/.ssh/id_ed25519", "node": "g14"})
|
||||
got, _ = r.call(t, "printer.print", map[string]any{"path": "/home/ops/.ssh/id_ed25519", "node": "node-b"})
|
||||
id2, _ := got.(map[string]any)["ask"].(string)
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
if _, err := r.call(t, "printer.print", map[string]any{"ask": id2}); err == nil || !strings.Contains(err.Error(), "refused by name") {
|
||||
|
||||
@@ -139,7 +139,7 @@ func newRig(t *testing.T, settings, password string) *printerRig {
|
||||
return nil
|
||||
},
|
||||
Answers: answers,
|
||||
Node: "shanks",
|
||||
Node: "node-d",
|
||||
Now: func() time.Time { return fixedNow },
|
||||
}
|
||||
bus := &fakeAskBus{worker: w, answers: answers}
|
||||
|
||||
@@ -76,7 +76,7 @@ func TestTheDeviceAnswerCarriesALinkAndNoBytes(t *testing.T) {
|
||||
}
|
||||
|
||||
// The real store: the binding's key signs, for the binding's host and port, in the module's bucket.
|
||||
s3 := NewS3(StoreFacts{Scheme: "http", At: "novox.internal", Port: "20001", Bucket: "mesh-shanks-brother", Region: "eu-west",
|
||||
s3 := NewS3(StoreFacts{Scheme: "http", At: "node-c.internal", Port: "20001", Bucket: "mesh-node-d-brother", Region: "eu-west",
|
||||
Key: "binding-key", Secret: "binding-secret"})
|
||||
s3.Now = func() time.Time { return fixedNow }
|
||||
got, err := s3.Link("GET", "abc.pdf", 10*time.Minute)
|
||||
@@ -84,7 +84,7 @@ func TestTheDeviceAnswerCarriesALinkAndNoBytes(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
gu, _ := url.Parse(got)
|
||||
if gu.Host != "novox.internal:20001" || gu.Path != "/mesh-shanks-brother/abc.pdf" ||
|
||||
if gu.Host != "node-c.internal:20001" || gu.Path != "/mesh-node-d-brother/abc.pdf" ||
|
||||
!strings.HasPrefix(gu.Query().Get("X-Amz-Credential"), "binding-key/") || gu.Query().Get("X-Amz-Expires") != "600" {
|
||||
t.Errorf("link %s", got)
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ func TestScanNowEmitsOneEventAfterTheScanIsKeptUnderTheSha256(t *testing.T) {
|
||||
if kept, err := r.objects.Get(t.Context(), want+".pdf"); err != nil || !bytes.Equal(kept, pdf) {
|
||||
t.Errorf("the bucket does not hold the filed PDF under its sha256 (%v)", err)
|
||||
}
|
||||
if ev.body["bytes"] != len(pdf) || ev.body["node"] != "shanks" || ev.body["cropped"] != true || ev.body["color-mode"] != "color" {
|
||||
if ev.body["bytes"] != len(pdf) || ev.body["node"] != "node-d" || ev.body["cropped"] != true || ev.body["color-mode"] != "color" {
|
||||
t.Errorf("event = %v", ev.body)
|
||||
}
|
||||
if want := fixedNow.UTC().Format(time.RFC3339); ev.body["scanned-at"] != want {
|
||||
|
||||
@@ -52,7 +52,7 @@ type guardWorld struct {
|
||||
|
||||
func newGuardWorld(t *testing.T) *guardWorld {
|
||||
w := &guardWorld{now: time.Date(2026, 10, 5, 23, 55, 0, 0, time.UTC)}
|
||||
w.f = newFakeKeycloak(t, "Novox", "old-password-from-2022")
|
||||
w.f = newFakeKeycloak(t, "NodeC", "old-password-from-2022")
|
||||
w.x = &fakeExec{f: w.f}
|
||||
w.g = &Guard{KC: w.f.client("the-mesh-minted-this"), Exec: w.x,
|
||||
Now: func() time.Time { return w.now },
|
||||
@@ -195,7 +195,7 @@ func TestWhileTheAdminIsRefusedTheProvisionerDoesNotAskKeycloak(t *testing.T) {
|
||||
w := newGuardWorld(t)
|
||||
w.x.fails = true
|
||||
w.g.Ensure(ctx, true, false)
|
||||
a := provisioner{clients: OidcClients{KC: w.g.KC, Realm: "Novox"}, guard: w.g,
|
||||
a := provisioner{clients: OidcClients{KC: w.g.KC, Realm: "NodeC"}, guard: w.g,
|
||||
announce: func(string, map[string]any) {}, log: func(string, ...any) {}}
|
||||
logins := w.f.logins
|
||||
err := a.Create(ctx, grafana("s", nil))
|
||||
|
||||
@@ -91,13 +91,13 @@ func movedWorld(t *testing.T, h *harness.Harness) (f *fakeKeycloak, start func()
|
||||
}
|
||||
}
|
||||
|
||||
// 1332/AC1: a consumer only the grant file gives (oidc-client for novox.mesh-mail-web, not moved) is wanted and
|
||||
// 1332/AC1: a consumer only the grant file gives (oidc-client for node-c.mesh-mail-web, not moved) is wanted and
|
||||
// Adapter.Create is asked for its client, though keycloak runs on the moved path.
|
||||
func TestAGrantFileConsumerNotMovedIsMadeOnTheMovedPath(t *testing.T) {
|
||||
mailWeb := "mesh_novox_mail_web"
|
||||
receives := grantGives(t, map[string]any{"as": mailWeb, "node": "novox", "values": map[string]any{
|
||||
mailWeb := "mesh_node_c_mail_web"
|
||||
receives := grantGives(t, map[string]any{"as": mailWeb, "node": "node-c", "values": map[string]any{
|
||||
"label": "mail-web", "endpoint": "web", "callback": "/login/generic_oauth", "name": "mail.example.org"}})
|
||||
h := &harness.Harness{Resource: ProvisionName, Receives: receives, Node: "novox"}
|
||||
h := &harness.Harness{Resource: ProvisionName, Receives: receives, Node: "node-c"}
|
||||
f, start := movedWorld(t, h)
|
||||
start()
|
||||
waitUntil(t, time.Second, func() bool {
|
||||
@@ -113,13 +113,13 @@ func TestAGrantFileConsumerNotMovedIsMadeOnTheMovedPath(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
// 1332/AC2: a grant-file consumer whose client already exists in the backend (mesh_ace_grafana) counts as asked and
|
||||
// 1332/AC2: a grant-file consumer whose client already exists in the backend (mesh_node_a_grafana) counts as asked and
|
||||
// is not held for retirement on the moved path.
|
||||
func TestAGrantFileConsumerAlreadyInKeycloakIsNotRetiredOnTheMovedPath(t *testing.T) {
|
||||
grafana := "mesh_ace_grafana"
|
||||
receives := grantGives(t, map[string]any{"as": grafana, "node": "ace", "values": map[string]any{
|
||||
grafana := "mesh_node_a_grafana"
|
||||
receives := grantGives(t, map[string]any{"as": grafana, "node": "node-a", "values": map[string]any{
|
||||
"label": "grafana", "endpoint": "web", "callback": "/login/generic_oauth", "name": "grafana.example.org"}})
|
||||
h := &harness.Harness{Resource: ProvisionName, Receives: receives, Node: "ace",
|
||||
h := &harness.Harness{Resource: ProvisionName, Receives: receives, Node: "node-a",
|
||||
StablePasses: 1, StableFor: time.Millisecond}
|
||||
f, start := movedWorld(t, h)
|
||||
// Seed the backend with the client the mesh made earlier, exactly as grafana's already is — before anything
|
||||
|
||||
@@ -8,7 +8,7 @@ import "testing"
|
||||
// 1332/AC3: the moved-path harness, built from the environment, reads MESH_RECEIVES the way the grant path does
|
||||
// (main.go's other branch, line 67), as postgres's moved harness does (postgres-provider/main.go:43).
|
||||
func TestTheMovedPathHarnessReadsMESH_RECEIVESAsTheGrantPathDoes(t *testing.T) {
|
||||
env := map[string]string{"MESH_RECEIVES": "/grants/mesh.json", "MESH_NODE": "novox"}
|
||||
env := map[string]string{"MESH_RECEIVES": "/grants/mesh.json", "MESH_NODE": "node-c"}
|
||||
getenv := func(k string) string { return env[k] }
|
||||
h := movedHarness(getenv, provisioner{})
|
||||
if h.Receives != env["MESH_RECEIVES"] {
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
"git.novox.be/novox/mesh-sdk/go/provider/harness"
|
||||
)
|
||||
|
||||
const testIssuer = "https://id.example.org/realms/Novox"
|
||||
const testIssuer = "https://id.example.org/realms/NodeC"
|
||||
|
||||
// keycloak's own part of the moved path (issue 849): the shared driving of it is proven in the SDK's harness package.
|
||||
|
||||
@@ -143,12 +143,12 @@ func TestBothPathsLeaveTheSameClientInKeycloak(t *testing.T) {
|
||||
})
|
||||
viaFile := made(func(p provisioner) {
|
||||
list, _ := json.Marshal([]provider.ConsumerCredential{{Consumer: "home", Module: "grafana",
|
||||
Credential: provider.PairName("novox", ProvisionName, "home", "grafana", ""), Value: "pw-" + grafana}})
|
||||
Credential: provider.PairName("node-c", ProvisionName, "home", "grafana", ""), Value: "pw-" + grafana}})
|
||||
file := filepath.Join(t.TempDir(), "consumers.json")
|
||||
if err := os.WriteFile(file, list, 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
h := &harness.Harness{Resource: ProvisionName, Adapter: p, Node: "novox",
|
||||
h := &harness.Harness{Resource: ProvisionName, Adapter: p, Node: "node-c",
|
||||
Now: func() time.Time { return now }, Log: func(string, ...any) {}}
|
||||
run, _, _, err := moved(file, h, testIssuer)
|
||||
if err != nil {
|
||||
@@ -156,10 +156,10 @@ func TestBothPathsLeaveTheSameClientInKeycloak(t *testing.T) {
|
||||
}
|
||||
bus := &bucketBus{buckets: map[string]map[string][]byte{}}
|
||||
plan, _ := json.Marshal(map[string]any{"node": "home", "module": "grafana", "provisioning": map[string]any{
|
||||
"provisions": []any{map[string]any{"provision": ProvisionName, "provider": "novox",
|
||||
"provisions": []any{map[string]any{"provision": ProvisionName, "provider": "node-c",
|
||||
"provider-module": "keycloak", "contributes": grafanaContributes, "names": grafanaNames}}}})
|
||||
_ = bus.Put(ctx, provider.AssignmentsBucket, "home.grafana", plan)
|
||||
pr := provider.New(provider.Options{Node: "novox", Module: "keycloak", Bus: bus,
|
||||
pr := provider.New(provider.Options{Node: "node-c", Module: "keycloak", Bus: bus,
|
||||
Vault: provider.ConsumersFile{Paths: []string{file}},
|
||||
Offers: map[string]provider.Offer{ProvisionName: {Credential: provider.PerConsumer,
|
||||
Serves: servedValues(testIssuer)}},
|
||||
|
||||
@@ -64,7 +64,7 @@ func RolesMapper() Rep {
|
||||
|
||||
var realmPath = regexp.MustCompile(`/realms/([^/]+)/?$`)
|
||||
|
||||
// RealmOf is the realm named by an issuer URL — `https://id.example/realms/Novox` is realm `Novox`.
|
||||
// RealmOf is the realm named by an issuer URL — `https://id.example/realms/NodeC` is realm `NodeC`.
|
||||
// The issuer is the one value an assignment sets, so the realm is read out of it rather than set a
|
||||
// second time where the two could disagree.
|
||||
func RealmOf(issuer string) (string, error) {
|
||||
|
||||
@@ -15,8 +15,8 @@ import (
|
||||
)
|
||||
|
||||
func oidcWorld(t *testing.T) (*fakeKeycloak, OidcClients) {
|
||||
f := newFakeKeycloak(t, "Novox", "pw")
|
||||
return f, OidcClients{KC: f.client("pw"), Realm: "Novox"}
|
||||
f := newFakeKeycloak(t, "NodeC", "pw")
|
||||
return f, OidcClients{KC: f.client("pw"), Realm: "NodeC"}
|
||||
}
|
||||
|
||||
// grafana is a dashboard on the home server, as the mesh hands it to the provisioner.
|
||||
@@ -53,8 +53,8 @@ func mustHold(t *testing.T, o OidcClients, p harness.Provision, want bool) {
|
||||
|
||||
func TestTheRealmIsReadOutOfTheIssuer(t *testing.T) {
|
||||
for issuer, want := range map[string]string{
|
||||
"https://id.example.org/realms/Novox": "Novox",
|
||||
"https://id.example.org/realms/Novox/": "Novox",
|
||||
"https://id.example.org/realms/NodeC": "NodeC",
|
||||
"https://id.example.org/realms/NodeC/": "NodeC",
|
||||
"http://127.0.0.1:18500/realms/master": "master",
|
||||
} {
|
||||
if got, err := RealmOf(issuer); err != nil || got != want {
|
||||
|
||||
@@ -14,13 +14,13 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// The environment mailu gives automx on novox when the fixtures were captured (files/mailu.env): the mail domain,
|
||||
// The environment mailu gives automx on node-c when the fixtures were captured (files/mailu.env): the mail domain,
|
||||
// the route name for both servers, the ports.
|
||||
var capturedEnvironment = map[string]string{
|
||||
"AMX_MAIL_DOMAINS": "novox.be",
|
||||
"AMX_IMAP_ADDRESS": "mail.novox.be",
|
||||
"AMX_MAIL_DOMAINS": "node-c.be",
|
||||
"AMX_IMAP_ADDRESS": "mail.node-c.be",
|
||||
"AMX_IMAP_PORT": "143",
|
||||
"AMX_SMTP_ADDRESS": "mail.novox.be",
|
||||
"AMX_SMTP_ADDRESS": "mail.node-c.be",
|
||||
"AMX_SMTP_PORT": "587",
|
||||
"AUTOMX_PORT": "4243",
|
||||
}
|
||||
@@ -176,7 +176,7 @@ func TestTheOnlyCorrectedAnswerIsTheImapServersName(t *testing.T) {
|
||||
}
|
||||
for _, c := range corrected {
|
||||
orig := readFile(t, strings.TrimSuffix(c, ".corrected.body")+".body")
|
||||
want := strings.Replace(orig, "<hostname />", "<hostname>mail.novox.be</hostname>", 1)
|
||||
want := strings.Replace(orig, "<hostname />", "<hostname>mail.node-c.be</hostname>", 1)
|
||||
if strings.Contains(want, "<hostname />") || want == orig {
|
||||
t.Fatalf("%s: the capture has no single empty hostname to correct", c)
|
||||
}
|
||||
@@ -198,12 +198,12 @@ func TestTheImapServerIsNamedFromTheImapAddress(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rec := httptest.NewRecorder()
|
||||
newHandler(cfg).ServeHTTP(rec, httptest.NewRequest("GET", "/mail/config-v1.1.xml?emailaddress=a@novox.be", nil))
|
||||
newHandler(cfg).ServeHTTP(rec, httptest.NewRequest("GET", "/mail/config-v1.1.xml?emailaddress=a@node-c.be", nil))
|
||||
body := rec.Body.String()
|
||||
if !strings.Contains(body, `<incomingServer type="imap"><hostname>imap.example.test</hostname><port>993</port>`) {
|
||||
t.Errorf("incoming server not named from AMX_IMAP_ADDRESS and AMX_IMAP_PORT: %s", body)
|
||||
}
|
||||
if !strings.Contains(body, `<outgoingServer type="smtp"><hostname>mail.novox.be</hostname><port>587</port>`) {
|
||||
if !strings.Contains(body, `<outgoingServer type="smtp"><hostname>mail.node-c.be</hostname><port>587</port>`) {
|
||||
t.Errorf("outgoing server changed: %s", body)
|
||||
}
|
||||
}
|
||||
@@ -213,13 +213,13 @@ func TestEveryDomainOfTheEnvironmentIsServed(t *testing.T) {
|
||||
for k, v := range capturedEnvironment {
|
||||
env[k] = v
|
||||
}
|
||||
env["AMX_MAIL_DOMAINS"] = "novox.be, example.test"
|
||||
env["AMX_MAIL_DOMAINS"] = "node-c.be, example.test"
|
||||
cfg, err := configFrom(getenvOf(env))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
h := newHandler(cfg)
|
||||
for _, d := range []string{"novox.be", "example.test"} {
|
||||
for _, d := range []string{"node-c.be", "example.test"} {
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, httptest.NewRequest("GET", "/mail/config-v1.1.xml?emailaddress=a@"+d, nil))
|
||||
if rec.Code != 200 || !strings.Contains(rec.Body.String(), "<domain>"+d+"</domain>") {
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
<clientConfig version="1.1"><emailProvider id="automx2-1"><identity /><domain>novox.be</domain><displayName>novox.be</displayName><displayShortName>novox.be</displayShortName><incomingServer type="imap"><hostname /><port>143</port><socketType>STARTTLS</socketType><username>%EMAILADDRESS%</username><authentication>password-cleartext</authentication></incomingServer><outgoingServer type="smtp"><hostname>mail.novox.be</hostname><port>587</port><socketType>STARTTLS</socketType><username>%EMAILADDRESS%</username><authentication>password-cleartext</authentication></outgoingServer></emailProvider></clientConfig>
|
||||
<clientConfig version="1.1"><emailProvider id="automx2-1"><identity /><domain>node-c.be</domain><displayName>node-c.be</displayName><displayShortName>node-c.be</displayShortName><incomingServer type="imap"><hostname /><port>143</port><socketType>STARTTLS</socketType><username>%EMAILADDRESS%</username><authentication>password-cleartext</authentication></incomingServer><outgoingServer type="smtp"><hostname>mail.node-c.be</hostname><port>587</port><socketType>STARTTLS</socketType><username>%EMAILADDRESS%</username><authentication>password-cleartext</authentication></outgoingServer></emailProvider></clientConfig>
|
||||
+1
-1
@@ -1 +1 @@
|
||||
<clientConfig version="1.1"><emailProvider id="automx2-1"><identity /><domain>novox.be</domain><displayName>novox.be</displayName><displayShortName>novox.be</displayShortName><incomingServer type="imap"><hostname>mail.novox.be</hostname><port>143</port><socketType>STARTTLS</socketType><username>%EMAILADDRESS%</username><authentication>password-cleartext</authentication></incomingServer><outgoingServer type="smtp"><hostname>mail.novox.be</hostname><port>587</port><socketType>STARTTLS</socketType><username>%EMAILADDRESS%</username><authentication>password-cleartext</authentication></outgoingServer></emailProvider></clientConfig>
|
||||
<clientConfig version="1.1"><emailProvider id="automx2-1"><identity /><domain>node-c.be</domain><displayName>node-c.be</displayName><displayShortName>node-c.be</displayShortName><incomingServer type="imap"><hostname>mail.node-c.be</hostname><port>143</port><socketType>STARTTLS</socketType><username>%EMAILADDRESS%</username><authentication>password-cleartext</authentication></incomingServer><outgoingServer type="smtp"><hostname>mail.node-c.be</hostname><port>587</port><socketType>STARTTLS</socketType><username>%EMAILADDRESS%</username><authentication>password-cleartext</authentication></outgoingServer></emailProvider></clientConfig>
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
GET https://autoconfig.novox.be/mail/config-v1.1.xml?emailaddress=jochen@novox.be
|
||||
GET https://autoconfig.node-c.be/mail/config-v1.1.xml?emailaddress=jochen@node-c.be
|
||||
User-Agent: Thunderbird
|
||||
Accept: */*
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
<clientConfig version="1.1"><emailProvider id="automx2-1"><identity /><domain>novox.be</domain><displayName>novox.be</displayName><displayShortName>novox.be</displayShortName><incomingServer type="imap"><hostname /><port>143</port><socketType>STARTTLS</socketType><username>%EMAILADDRESS%</username><authentication>password-cleartext</authentication></incomingServer><outgoingServer type="smtp"><hostname>mail.novox.be</hostname><port>587</port><socketType>STARTTLS</socketType><username>%EMAILADDRESS%</username><authentication>password-cleartext</authentication></outgoingServer></emailProvider></clientConfig>
|
||||
<clientConfig version="1.1"><emailProvider id="automx2-1"><identity /><domain>node-c.be</domain><displayName>node-c.be</displayName><displayShortName>node-c.be</displayShortName><incomingServer type="imap"><hostname /><port>143</port><socketType>STARTTLS</socketType><username>%EMAILADDRESS%</username><authentication>password-cleartext</authentication></incomingServer><outgoingServer type="smtp"><hostname>mail.node-c.be</hostname><port>587</port><socketType>STARTTLS</socketType><username>%EMAILADDRESS%</username><authentication>password-cleartext</authentication></outgoingServer></emailProvider></clientConfig>
|
||||
+1
-1
@@ -1 +1 @@
|
||||
<clientConfig version="1.1"><emailProvider id="automx2-1"><identity /><domain>novox.be</domain><displayName>novox.be</displayName><displayShortName>novox.be</displayShortName><incomingServer type="imap"><hostname>mail.novox.be</hostname><port>143</port><socketType>STARTTLS</socketType><username>%EMAILADDRESS%</username><authentication>password-cleartext</authentication></incomingServer><outgoingServer type="smtp"><hostname>mail.novox.be</hostname><port>587</port><socketType>STARTTLS</socketType><username>%EMAILADDRESS%</username><authentication>password-cleartext</authentication></outgoingServer></emailProvider></clientConfig>
|
||||
<clientConfig version="1.1"><emailProvider id="automx2-1"><identity /><domain>node-c.be</domain><displayName>node-c.be</displayName><displayShortName>node-c.be</displayShortName><incomingServer type="imap"><hostname>mail.node-c.be</hostname><port>143</port><socketType>STARTTLS</socketType><username>%EMAILADDRESS%</username><authentication>password-cleartext</authentication></incomingServer><outgoingServer type="smtp"><hostname>mail.node-c.be</hostname><port>587</port><socketType>STARTTLS</socketType><username>%EMAILADDRESS%</username><authentication>password-cleartext</authentication></outgoingServer></emailProvider></clientConfig>
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
GET https://automx.novox.be/mail/config-v1.1.xml?emailaddress=jochen@novox.be
|
||||
GET https://automx.node-c.be/mail/config-v1.1.xml?emailaddress=jochen@node-c.be
|
||||
User-Agent: Thunderbird
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
<clientConfig version="1.1"><emailProvider id="automx2-1"><identity /><domain>novox.be</domain><displayName>novox.be</displayName><displayShortName>novox.be</displayShortName><incomingServer type="imap"><hostname /><port>143</port><socketType>STARTTLS</socketType><username>%EMAILADDRESS%</username><authentication>password-cleartext</authentication></incomingServer><outgoingServer type="smtp"><hostname>mail.novox.be</hostname><port>587</port><socketType>STARTTLS</socketType><username>%EMAILADDRESS%</username><authentication>password-cleartext</authentication></outgoingServer></emailProvider></clientConfig>
|
||||
<clientConfig version="1.1"><emailProvider id="automx2-1"><identity /><domain>node-c.be</domain><displayName>node-c.be</displayName><displayShortName>node-c.be</displayShortName><incomingServer type="imap"><hostname /><port>143</port><socketType>STARTTLS</socketType><username>%EMAILADDRESS%</username><authentication>password-cleartext</authentication></incomingServer><outgoingServer type="smtp"><hostname>mail.node-c.be</hostname><port>587</port><socketType>STARTTLS</socketType><username>%EMAILADDRESS%</username><authentication>password-cleartext</authentication></outgoingServer></emailProvider></clientConfig>
|
||||
Vendored
+1
-1
@@ -1 +1 @@
|
||||
<clientConfig version="1.1"><emailProvider id="automx2-1"><identity /><domain>novox.be</domain><displayName>novox.be</displayName><displayShortName>novox.be</displayShortName><incomingServer type="imap"><hostname>mail.novox.be</hostname><port>143</port><socketType>STARTTLS</socketType><username>%EMAILADDRESS%</username><authentication>password-cleartext</authentication></incomingServer><outgoingServer type="smtp"><hostname>mail.novox.be</hostname><port>587</port><socketType>STARTTLS</socketType><username>%EMAILADDRESS%</username><authentication>password-cleartext</authentication></outgoingServer></emailProvider></clientConfig>
|
||||
<clientConfig version="1.1"><emailProvider id="automx2-1"><identity /><domain>node-c.be</domain><displayName>node-c.be</displayName><displayShortName>node-c.be</displayShortName><incomingServer type="imap"><hostname>mail.node-c.be</hostname><port>143</port><socketType>STARTTLS</socketType><username>%EMAILADDRESS%</username><authentication>password-cleartext</authentication></incomingServer><outgoingServer type="smtp"><hostname>mail.node-c.be</hostname><port>587</port><socketType>STARTTLS</socketType><username>%EMAILADDRESS%</username><authentication>password-cleartext</authentication></outgoingServer></emailProvider></clientConfig>
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
GET https://autodiscover.novox.be/mail/config-v1.1.xml?emailaddress=jochen@novox.be
|
||||
GET https://autodiscover.node-c.be/mail/config-v1.1.xml?emailaddress=jochen@node-c.be
|
||||
User-Agent: Thunderbird
|
||||
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
GET https://autoconfig.novox.be/mail/config-v1.1.xml?emailaddress=nobody@unknown.example
|
||||
GET https://autoconfig.node-c.be/mail/config-v1.1.xml?emailaddress=nobody@unknown.example
|
||||
User-Agent: Thunderbird
|
||||
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
GET https://autoconfig.novox.be/mail/config-v1.1.xml
|
||||
GET https://autoconfig.node-c.be/mail/config-v1.1.xml
|
||||
User-Agent: Thunderbird
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
GET https://autoconfig.novox.be/
|
||||
GET https://autoconfig.node-c.be/
|
||||
User-Agent: Mozilla/5.0
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
GET https://autoconfig.novox.be/.well-known/autoconfig/mail/config-v1.1.xml?emailaddress=jochen@novox.be
|
||||
GET https://autoconfig.node-c.be/.well-known/autoconfig/mail/config-v1.1.xml?emailaddress=jochen@node-c.be
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
POST https://autodiscover.novox.be/autodiscover/autodiscover.xml
|
||||
POST https://autodiscover.node-c.be/autodiscover/autodiscover.xml
|
||||
User-Agent: Microsoft Office/16.0 (Windows NT 10.0; Microsoft Outlook 16.0.14326; Pro)
|
||||
Content-Type: text/xml
|
||||
Accept: */*
|
||||
@@ -6,7 +6,7 @@ Accept: */*
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/requestschema/2006">
|
||||
<Request>
|
||||
<EMailAddress>jochen@novox.be</EMailAddress>
|
||||
<EMailAddress>jochen@node-c.be</EMailAddress>
|
||||
<AcceptableResponseSchema>http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a</AcceptableResponseSchema>
|
||||
</Request>
|
||||
</Autodiscover>
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
POST https://autodiscover.novox.be/autodiscover/autodiscover.xml
|
||||
POST https://autodiscover.node-c.be/autodiscover/autodiscover.xml
|
||||
User-Agent: Microsoft Office/16.0
|
||||
Content-Type: text/xml
|
||||
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
POST https://autodiscover.novox.be/autodiscover/autodiscover.xml
|
||||
POST https://autodiscover.node-c.be/autodiscover/autodiscover.xml
|
||||
User-Agent: Microsoft Office/16.0
|
||||
Content-Type: text/xml
|
||||
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
POST https://autodiscover.novox.be/autodiscover/autodiscover.xml
|
||||
POST https://autodiscover.node-c.be/autodiscover/autodiscover.xml
|
||||
User-Agent: Microsoft Office/16.0
|
||||
Content-Type: text/xml
|
||||
|
||||
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
GET https://autodiscover.novox.be/autodiscover/autodiscover.xml
|
||||
GET https://autodiscover.node-c.be/autodiscover/autodiscover.xml
|
||||
User-Agent: Microsoft Office/16.0
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
POST https://autodiscover.novox.be/Autodiscover/Autodiscover.xml
|
||||
POST https://autodiscover.node-c.be/Autodiscover/Autodiscover.xml
|
||||
User-Agent: Microsoft Office/16.0
|
||||
Content-Type: text/xml
|
||||
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/requestschema/2006"><Request><EMailAddress>jochen@novox.be</EMailAddress><AcceptableResponseSchema>http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a</AcceptableResponseSchema></Request></Autodiscover>
|
||||
<Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/requestschema/2006"><Request><EMailAddress>jochen@node-c.be</EMailAddress><AcceptableResponseSchema>http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a</AcceptableResponseSchema></Request></Autodiscover>
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
<html lang=en>
|
||||
<title>Redirecting...</title>
|
||||
<h1>Redirecting...</h1>
|
||||
<p>You should be redirected automatically to the target URL: <a href="https://automx.novox.be/mobileconfig/?emailaddress=jochen@novox.be">https://automx.novox.be/mobileconfig/?emailaddress=jochen@novox.be</a>. If not, click the link.
|
||||
<p>You should be redirected automatically to the target URL: <a href="https://automx.node-c.be/mobileconfig/?emailaddress=jochen@node-c.be">https://automx.node-c.be/mobileconfig/?emailaddress=jochen@node-c.be</a>. If not, click the link.
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
Content-Length: 319
|
||||
Content-Length: 323
|
||||
Content-Type: text/html; charset=utf-8
|
||||
Location: https://automx.novox.be/mobileconfig/?emailaddress=jochen@novox.be
|
||||
Location: https://automx.node-c.be/mobileconfig/?emailaddress=jochen@node-c.be
|
||||
Server: Werkzeug/3.1.9 Python/3.9.5
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
GET https://automx.novox.be/mobileconfig?emailaddress=jochen@novox.be
|
||||
GET https://automx.node-c.be/mobileconfig?emailaddress=jochen@node-c.be
|
||||
User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) Safari
|
||||
Accept: */*
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
GET https://automx.novox.be/mobileconfig/?emailaddress=jochen@novox.be
|
||||
GET https://automx.node-c.be/mobileconfig/?emailaddress=jochen@node-c.be
|
||||
User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) Safari
|
||||
Accept: */*
|
||||
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
GET https://automx.novox.be/mobileconfig/?emailaddress=nobody@unknown.example
|
||||
GET https://automx.node-c.be/mobileconfig/?emailaddress=nobody@unknown.example
|
||||
User-Agent: Mozilla/5.0 (iPhone)
|
||||
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
GET https://automx.novox.be/mobileconfig/
|
||||
GET https://automx.node-c.be/mobileconfig/
|
||||
User-Agent: Mozilla/5.0 (iPhone)
|
||||
|
||||
+5
-5
@@ -1,10 +1,10 @@
|
||||
What mailu's autoconfiguration module (automx2 2021.6, container mailu-automx on novox, Werkzeug/3.1.9 Python/3.9.5)
|
||||
What mailu's autoconfiguration module (automx2 2021.6, container mailu-automx on node-c, Werkzeug/3.1.9 Python/3.9.5)
|
||||
answers to the requests mail clients make (R4d step AMX-1, issue 1630, ADR 0480).
|
||||
|
||||
Captured 2026-10-21 between 13:58:25 and 13:58:52 (CEST; the Date headers said 11:58 GMT) with the mesh tool
|
||||
shanks/netcheck.netcheck_request, called from shanks, through the names clients use (autoconfig.novox.be,
|
||||
autodiscover.novox.be, automx.novox.be: HTTPS, the mesh's route), no ssh. A redirect is reported, never followed.
|
||||
Mail domain served: novox.be. Mailbox used: jochen@novox.be; unknown domain: nobody@unknown.example.
|
||||
node-d/netcheck.netcheck_request, called from node-d, through the names clients use (autoconfig.node-c.be,
|
||||
autodiscover.node-c.be, automx.node-c.be: HTTPS, the mesh's route), no ssh. A redirect is reported, never followed.
|
||||
Mail domain served: node-c.be. Mailbox used: jochen@node-c.be; unknown domain: nobody@unknown.example.
|
||||
|
||||
Each capture is four files with the same stem:
|
||||
<stem>.request method and URL, then the request headers sent, then (after a blank line) the body sent
|
||||
@@ -24,7 +24,7 @@ A fixture is not the bytes on the wire in four ways (issue 1701):
|
||||
What the answers show (read, not guessed):
|
||||
- GET /mail/config-v1.1.xml?emailaddress=... answers 200 application/xml on all three names; same body on each.
|
||||
- The incoming (IMAP) server has an EMPTY hostname (<hostname />), port 143, STARTTLS. The outgoing server is
|
||||
mail.novox.be:587 STARTTLS. Cause (from reading the module, not from the database): automx/files/add-domains:35
|
||||
mail.node-c.be:587 STARTTLS. Cause (from reading the module, not from the database): automx/files/add-domains:35
|
||||
reads AMX_IMAP_SERVER, files/mailu.env:58 sets AMX_IMAP_ADDRESS, so the IMAP name was never written.
|
||||
A client given this answer cannot find its IMAP server.
|
||||
- POST /autodiscover/autodiscover.xml with a valid request for the served domain answers 500 (the Outlook
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ whole list of where the two differ.
|
||||
server because its setup script read AMX_IMAP_SERVER, which mailu's environment does not set (it sets
|
||||
AMX_IMAP_ADDRESS). The program names it from AMX_IMAP_ADDRESS (and the port from AMX_IMAP_PORT).
|
||||
Fixtures 01, 02 and 03 each have a <stem>.corrected.body: the capture with <hostname /> replaced by
|
||||
<hostname>mail.novox.be</hostname> and nothing else (a test checks that). Their Content-Length is that file's
|
||||
<hostname>mail.node-c.be</hostname> and nothing else (a test checks that). Their Content-Length is that file's
|
||||
size, 634, not the captured 612.
|
||||
|
||||
2. The Server header. The capture has "Server: Werkzeug/3.1.9 Python/3.9.5". The program sends no Server header, and
|
||||
|
||||
@@ -18,19 +18,19 @@ const grantFile = `{
|
||||
"contributions": 1,
|
||||
"requirement": "s3-bucket",
|
||||
"given": [
|
||||
{"from": "brother", "node": "shanks", "as": "brother-key",
|
||||
{"from": "brother", "node": "node-d", "as": "brother-key",
|
||||
"values": {"shares": [{"to": "seat:node-files", "methods": ["put"]}, {"to": "print-and-scan", "methods": ["get"]}]},
|
||||
"derived": {"bucket": "mesh-shanks-brother"}},
|
||||
{"from": "nextcloud", "node": "novox", "as": "nextcloud-key", "derived": {"bucket": "mesh-novox-nextcloud"}}
|
||||
"derived": {"bucket": "mesh-node-d-brother"}},
|
||||
{"from": "nextcloud", "node": "node-c", "as": "nextcloud-key", "derived": {"bucket": "mesh-node-c-nextcloud"}}
|
||||
]
|
||||
}`
|
||||
|
||||
// The seats answer is the controller's (mesh-controller.seats read on 2026-10-21): seats[].holders[] of {node, module},
|
||||
// and outside[] of {Claim, Node, Module}.
|
||||
const seatsAnswer = `{"answer": {"seats": [
|
||||
{"seat": "node-files", "scope": "node", "holders": [{"node": "g14", "module": "mesh-files"}, {"node": "shanks", "module": "mesh-files"}]},
|
||||
{"seat": "node-backup", "scope": "node", "holders": [{"node": "g14", "module": "restic"}]}
|
||||
], "outside": [{"Claim": "printer", "Scope": "mesh", "Node": "shanks", "Module": "brother", "Site": "home", "Kind": ""}]}}`
|
||||
{"seat": "node-files", "scope": "node", "holders": [{"node": "node-b", "module": "mesh-files"}, {"node": "node-d", "module": "mesh-files"}]},
|
||||
{"seat": "node-backup", "scope": "node", "holders": [{"node": "node-b", "module": "restic"}]}
|
||||
], "outside": [{"Claim": "printer", "Scope": "mesh", "Node": "node-d", "Module": "brother", "Site": "home", "Kind": ""}]}}`
|
||||
|
||||
type rig struct {
|
||||
s *server
|
||||
@@ -54,10 +54,10 @@ func newRig(t *testing.T) *rig {
|
||||
}
|
||||
return p
|
||||
}
|
||||
// What the mesh renders from files/object-store.json for a store on novox.internal, port 20001.
|
||||
cfg := write("object-store.json", `{"scheme":"http","at":"novox.internal","port":"20001","region":"eu-west"}`)
|
||||
write("links/mesh-shanks-brother.secret", "brother-link-secret\n")
|
||||
write("links/mesh-novox-nextcloud.secret", "nextcloud-link-secret\n")
|
||||
// What the mesh renders from files/object-store.json for a store on node-c.internal, port 20001.
|
||||
cfg := write("object-store.json", `{"scheme":"http","at":"node-c.internal","port":"20001","region":"eu-west"}`)
|
||||
write("links/mesh-node-d-brother.secret", "brother-link-secret\n")
|
||||
write("links/mesh-node-c-nextcloud.secret", "nextcloud-link-secret\n")
|
||||
grants := write("grants/mesh.json", grantFile)
|
||||
s, err := newServer(cfg, filepath.Join(dir, "links"), grants)
|
||||
if err != nil {
|
||||
@@ -96,16 +96,16 @@ func linkOf(t *testing.T, got any) *url.URL {
|
||||
// 1516/AC1: brother's own ask gets a link signed with the bucket's link key, for the store's host and port, 300 s.
|
||||
func TestLinkIsSignedWithTheBucketsLinkKey(t *testing.T) {
|
||||
r := newRig(t)
|
||||
got, err := r.s.link("shanks.brother", ask("mesh-shanks-brother", "get", float64(300)))
|
||||
got, err := r.s.link("node-d.brother", ask("mesh-node-d-brother", "get", float64(300)))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
u := linkOf(t, got)
|
||||
if u.Host != "novox.internal:20001" || u.Scheme != "http" || u.Path != "/mesh-shanks-brother/doc.pdf" {
|
||||
if u.Host != "node-c.internal:20001" || u.Scheme != "http" || u.Path != "/mesh-node-d-brother/doc.pdf" {
|
||||
t.Fatalf("link goes to %s", u)
|
||||
}
|
||||
q := u.Query()
|
||||
if cred := q.Get("X-Amz-Credential"); !strings.HasPrefix(cred, linkKeyID("mesh-shanks-brother")+"/20261021/eu-west/s3/") {
|
||||
if cred := q.Get("X-Amz-Credential"); !strings.HasPrefix(cred, linkKeyID("mesh-node-d-brother")+"/20261021/eu-west/s3/") {
|
||||
t.Fatalf("credential %q is not the bucket's link key", cred)
|
||||
}
|
||||
if strings.Contains(u.String(), "meshroot") {
|
||||
@@ -124,18 +124,18 @@ func TestLinkKeyIDMatchesTheProvisioners(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// 1516/AC2: a share to seat:node-files for put, mesh-files holding node-files on g14: put given, get refused naming the method.
|
||||
// 1516/AC2: a share to seat:node-files for put, mesh-files holding node-files on node-b: put given, get refused naming the method.
|
||||
func TestSeatShareGivesTheHoldersTheSharedMethodOnly(t *testing.T) {
|
||||
r := newRig(t)
|
||||
if _, err := r.s.link("g14.mesh-files", ask("mesh-shanks-brother", "put", nil)); err != nil {
|
||||
if _, err := r.s.link("node-b.mesh-files", ask("mesh-node-d-brother", "put", nil)); err != nil {
|
||||
t.Fatalf("put: %v", err)
|
||||
}
|
||||
_, err := r.s.link("g14.mesh-files", ask("mesh-shanks-brother", "get", nil))
|
||||
_, err := r.s.link("node-b.mesh-files", ask("mesh-node-d-brother", "get", nil))
|
||||
if err == nil || !strings.Contains(err.Error(), "put, not get") {
|
||||
t.Fatalf("get was not refused naming the method: %v", err)
|
||||
}
|
||||
// A module share is judged by name alone.
|
||||
if _, err := r.s.link("novox.print-and-scan", ask("mesh-shanks-brother", "get", nil)); err != nil {
|
||||
if _, err := r.s.link("node-c.print-and-scan", ask("mesh-node-d-brother", "get", nil)); err != nil {
|
||||
t.Fatalf("print-and-scan get: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -148,14 +148,14 @@ func TestRefusals(t *testing.T) {
|
||||
args map[string]any
|
||||
want string
|
||||
}{
|
||||
{"a stranger", "g14.restic", ask("mesh-shanks-brother", "get", nil), "not the bucket's consumer"},
|
||||
{"a seat it does not hold", "g14.restic", ask("mesh-shanks-brother", "put", nil), "not the bucket's consumer"},
|
||||
{"unproven", "unproven", ask("mesh-shanks-brother", "get", nil), "did not prove"},
|
||||
{"no grant", "shanks.brother", ask("no-such-bucket", "get", nil), "no grant names bucket"},
|
||||
{"seconds over 600", "shanks.brother", ask("mesh-shanks-brother", "get", float64(601)), "at most ten minutes"},
|
||||
{"seconds zero", "shanks.brother", ask("mesh-shanks-brother", "get", float64(0)), "outside 1 to 600"},
|
||||
{"method delete", "shanks.brother", ask("mesh-shanks-brother", "delete", nil), "not get or put"},
|
||||
{"another consumer's bucket", "shanks.brother", ask("mesh-novox-nextcloud", "get", nil), "not the bucket's consumer"},
|
||||
{"a stranger", "node-b.restic", ask("mesh-node-d-brother", "get", nil), "not the bucket's consumer"},
|
||||
{"a seat it does not hold", "node-b.restic", ask("mesh-node-d-brother", "put", nil), "not the bucket's consumer"},
|
||||
{"unproven", "unproven", ask("mesh-node-d-brother", "get", nil), "did not prove"},
|
||||
{"no grant", "node-d.brother", ask("no-such-bucket", "get", nil), "no grant names bucket"},
|
||||
{"seconds over 600", "node-d.brother", ask("mesh-node-d-brother", "get", float64(601)), "at most ten minutes"},
|
||||
{"seconds zero", "node-d.brother", ask("mesh-node-d-brother", "get", float64(0)), "outside 1 to 600"},
|
||||
{"method delete", "node-d.brother", ask("mesh-node-d-brother", "delete", nil), "not get or put"},
|
||||
{"another consumer's bucket", "node-d.brother", ask("mesh-node-c-nextcloud", "get", nil), "not the bucket's consumer"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
_, err := r.s.link(c.caller, c.args)
|
||||
@@ -170,14 +170,14 @@ func TestRefusals(t *testing.T) {
|
||||
func TestSeatsUnreadableAndCached(t *testing.T) {
|
||||
r := newRig(t)
|
||||
r.seatsErr = errors.New("the controller did not answer")
|
||||
_, err := r.s.link("g14.mesh-files", ask("mesh-shanks-brother", "put", nil))
|
||||
_, err := r.s.link("node-b.mesh-files", ask("mesh-node-d-brother", "put", nil))
|
||||
if err == nil || !strings.Contains(err.Error(), "the controller did not answer") || !strings.Contains(err.Error(), "node-files") {
|
||||
t.Fatalf("not refused naming why: %v", err)
|
||||
}
|
||||
r.seatsErr = nil
|
||||
put := func() {
|
||||
t.Helper()
|
||||
if _, err := r.s.link("g14.mesh-files", ask("mesh-shanks-brother", "put", nil)); err != nil {
|
||||
if _, err := r.s.link("node-b.mesh-files", ask("mesh-node-d-brother", "put", nil)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -201,17 +201,17 @@ func TestSeatsUnreadableAndCached(t *testing.T) {
|
||||
// 1516/AC5: the grant file is read at each call.
|
||||
func TestGrantFileIsReadAtEachCall(t *testing.T) {
|
||||
r := newRig(t)
|
||||
if _, err := r.s.link("novox.print-and-scan", ask("mesh-shanks-brother", "get", nil)); err != nil {
|
||||
if _, err := r.s.link("node-c.print-and-scan", ask("mesh-node-d-brother", "get", nil)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
changed := strings.Replace(grantFile, `{"to": "print-and-scan", "methods": ["get"]}`, `{"to": "someone-else", "methods": ["get"]}`, 1)
|
||||
if err := os.WriteFile(r.s.receives, []byte(changed), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := r.s.link("novox.print-and-scan", ask("mesh-shanks-brother", "get", nil)); err == nil {
|
||||
if _, err := r.s.link("node-c.print-and-scan", ask("mesh-node-d-brother", "get", nil)); err == nil {
|
||||
t.Fatal("the second call judged by the old shares")
|
||||
}
|
||||
if _, err := r.s.link("novox.someone-else", ask("mesh-shanks-brother", "get", nil)); err != nil {
|
||||
if _, err := r.s.link("node-c.someone-else", ask("mesh-node-d-brother", "get", nil)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user