Files
mesh-catalog/modules/messenger/cmd/messenger/whole_test.go
T
jschoubben e009088150 Point messenger, mesh-watcher, restic and mesh-issues away from the controller
The controller is not placed (ADR 0590): messenger no longer treats it as
a condition owner or a walk's subject, mesh-watcher reads only where the bus
runs, restic tells the operator to compose the machine or set config through
the seats that do it now, and mesh-issues' examples name a placed repository.
mesh-issues keeps mesh-controller among its owners: issues located there,
issue 1126 among them, and route-proxy until it moves, still name it
(issue 1126 B11).

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 12:29:56 +01:00

230 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 (
"fmt"
"strings"
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/asks"
)
// novox/hq issue 383: a settings proposal's values are what the operator approves, so where the sender is proven
// they are shown whole; where nothing is proven, the explanation under the whole content rule stands.
// proposalAsk is a proposal as the controller composes it: the explanation with its values masked, the whole
// with them whole, and Details with the fingerprint and where to read it, which the note shows.
func proposalAsk(c *clock, id string) asks.Ask {
a := heldAsk(c, id)
a.Headline = "Set mounts on shanks?"
a.Options = a.Options[:2]
a.Explanation = "+ shares: media=‹path›\n+ sources: recalbox=‹address›@‹path›\nProposed by an agent at 15:11."
a.Whole = "+ shares: media=/storage/media\n+ sources: recalbox=smb://nas.lan/recalbox@/mnt/recalbox\nProposed by an agent at 15:11."
a.Details = "Fingerprint 1234 5678 9abc def0 1234 5678.\nRead it whole: settings proposals " + id + " at the controller's terminal."
return a
}
func TestAnAsksWholeWordsAreShownWhereTheSenderIsProvenAndMaskedElsewhere(t *testing.T) {
h, tg, _, c, store, w := newRouter(t)
link(h, store, c, "42", c.now().Add(-2*time.Hour))
submit(t, h, "mesh-controller", proposalAsk(c, "s-1"))
settle(h, c)
if len(tg.sends) != 1 {
t.Fatalf("telegram got %d messages", len(tg.sends))
}
phone := tg.sends[0]
for _, want := range []string{"+ shares: media=/storage/media", "recalbox=smb://nas.lan/recalbox@/mnt/recalbox", "Proposed by an agent at 15:11."} {
if !strings.Contains(phone.Body, want) {
t.Errorf("the phone, which proves who answers, lacks %q:\n%s", want, phone.Body)
}
}
if strings.Contains(phone.Body, "‹") || strings.Contains(phone.Body, "Fingerprint") || strings.Contains(phone.Body, "settings proposals") {
t.Errorf("the phone shows a masked value, or the Details' words, in the body:\n%s", phone.Body)
}
// The Details are the note's, after what the answers do (issue 639): no answer of their own.
if !strings.Contains(phone.Note, "Fingerprint 1234 5678 9abc def0 1234 5678.") ||
!strings.Contains(phone.Note, "Read it whole: settings proposals s-1 at the controller's terminal.") {
t.Errorf("the phone's note lacks the Details:\n%s", phone.Note)
}
if phone.Title != "Set mounts on shanks?" || ticketFor(phone, "Release") == "" || ticketFor(phone, DetailsLabel) != "" {
t.Errorf("the phone's message: %q offering %s", phone.Title, labelsOf(phone))
}
// The desk proves nothing: the same record composed for it shows the explanation, its values masked.
r := h.open["ask.mesh-controller.s-1"]
if r == nil {
t.Fatal("no record of the ask")
}
desk := h.sayOn(r, "raised", "desktop")
if !strings.Contains(desk.Body, "+ shares: media=‹path›") || strings.Contains(desk.Text(), "/storage/media") || strings.Contains(desk.Text(), "nas.lan") {
t.Errorf("the desk shows a value whole:\n%s", desk.Text())
}
if got := w.of("mesh-controller"); len(got) != 0 || store.asks["mesh-controller.s-1"].State != StateOpen {
t.Errorf("the ask was answered: %+v / %s", got, store.asks["mesh-controller.s-1"].State)
}
// Approved, the message keeps its headline and words, and says below them how it ended (issue 714).
if out, err := h.Chosen("telegram", choiceOf(ticketFor(phone, "Release"), "42", c.now())); err != nil || out != "chosen" {
t.Fatalf("Release: %q %v", out, err)
}
edited, ok := tg.edits["1"]
if !ok {
t.Fatalf("the phone's message was not edited: %+v", tg.edits)
}
if edited.Title != "Set mounts on shanks?" || edited.Body == "" ||
!strings.HasPrefix(edited.Note, "You chose Release at ") || len(edited.Actions) != 0 {
t.Errorf("the edited message: %q\n%s\nactions %+v", edited.Title, edited.Body, edited.Actions)
}
}
// The mutation on the switch: the same ask without whole words is shown on the phone as on the desk, from its
// explanation; the whole words alone make the difference, and only where the sender is proven.
func TestAnAskWithoutWholeWordsIsShownFromItsExplanationEverywhere(t *testing.T) {
h, tg, _, c, store, _ := newRouter(t)
link(h, store, c, "42", c.now().Add(-2*time.Hour))
a := proposalAsk(c, "s-2")
a.Whole, a.Details = "", ""
submit(t, h, "mesh-controller", a)
settle(h, c)
if len(tg.sends) != 1 || !strings.Contains(tg.sends[0].Body, "+ shares: media=‹path›") || strings.Contains(tg.sends[0].Body, "/storage/media") {
t.Fatalf("telegram: %+v", tg.sends)
}
if ticketFor(tg.sends[0], DetailsLabel) != "" || strings.Contains(tg.sends[0].Note, "Fingerprint") {
t.Errorf("an ask that says nothing for Details offers or notes it: %s\n%s", labelsOf(tg.sends[0]), tg.sends[0].Note)
}
// A kind that proves nothing never shows the whole words, whatever the ask says.
a = proposalAsk(c, "s-3")
submit(t, h, "mesh-controller", a)
settle(h, c)
if whole := h.wholeOf(h.open["ask.mesh-controller.s-3"], "desktop"); whole != "" {
t.Errorf("the desk is given the whole words: %q", whole)
}
if whole := h.wholeOf(h.open["ask.mesh-controller.s-3"], "telegram"); whole != a.Whole {
t.Errorf("the phone is not given the whole words: %q", whole)
}
}
// A secret's shape leaves the mesh nowhere: an ask whose whole words carry one is refused as a whole, so the
// phone never sees it, with or without the proven sender.
func TestAnAskWhoseWholeWordsCarryASecretIsRefused(t *testing.T) {
// A path is read as a path, whatever its case and digits; a random run outside one is still a secret.
for text, may := range map[string]bool{
"+ shares: photos=/mnt/Photos_2024/Jochen": true,
"+ sources: games=smb://nas.lan/Recalbox_Games2024": true,
"+ library: /srv/media/Series_Archive/Season01": true,
"+ key: xK9mQ2vL8pR4tW7yB3nF6hJ1dG5sA0zC": false,
} {
if _, ok := CheckSecret(text); ok != may {
t.Errorf("CheckSecret(%q) lets it leave: %v", text, ok)
}
}
// A condition's record never shows an ask's whole words, whichever kind: its words are its own.
{
h, _, _, c, store, _ := newRouter(t)
link(h, store, c, "42", c.now().Add(-2*time.Hour))
k := cond("delivery.hq.held", Warning, "the delivery of hq is held")
k.Raised = c.now()
h.Condition(EventRaised, k)
settle(h, c)
a := proposalAsk(c, "s-5")
a.About = "delivery.hq.held"
submit(t, h, ConditionOwner, a)
settle(h, c)
if r := h.open["delivery.hq.held"]; r == nil || r.Ask != ConditionOwner+".s-5" || h.wholeOf(r, "telegram") != "" {
t.Errorf("a condition's record is given an ask's whole words")
}
}
for _, secret := range []string{
"+ token: ghp_abcdefghijklmnopqrstuvwxyz0123456789",
"+ password=hunter2",
"+ key: 0123456789abcdef0123456789abcdef",
"-----BEGIN PRIVATE KEY-----",
} {
h, tg, _, c, store, w := newRouter(t)
link(h, store, c, "42", c.now().Add(-2*time.Hour))
a := proposalAsk(c, "s-4")
a.Whole = "+ shares: media=/storage/media\n" + secret
submit(t, h, "mesh-controller", a)
settle(h, c)
got := w.of("mesh-controller")
if len(tg.sends) != 0 || len(got) != 1 || got[0].Outcome != asks.OutcomeRefused || !strings.Contains(got[0].Words, "whole words carry secret") {
t.Errorf("%q: sent %d, told %+v", secret, len(tg.sends), got)
}
if _, kept := store.asks["mesh-controller.s-4"]; kept {
t.Errorf("%q: the refused ask was recorded", secret)
}
}
}
// The review of mesh-controller #304: a proposal of five keys, one of them 300 characters long, shows two keys at
// a glance; every key and every value, whole, is in the note on a kind that proves who answers, so the operator can
// read exactly what Approve sets. The desk, which proves nothing, shows them masked.
func TestEveryKeyAndItsWholeValueIsInTheNoteWhereTheSenderIsProven(t *testing.T) {
h, tg, _, c, store, _ := newRouter(t)
link(h, store, c, "42", c.now().Add(-2*time.Hour))
var long strings.Builder
long.WriteString("library=/storage")
for i := 0; long.Len() < 300; i++ {
long.WriteString(fmt.Sprintf("/album-%02d", i))
}
value := long.String()[:300]
keys := map[string]string{"a": "1", "b": "2", "c": "3", "d": "4", "shares": value}
masked := map[string]string{"a": "1", "b": "2", "c": "3", "d": "4", "shares": "library=‹path›"}
lines := func(values map[string]string) string {
var out []string
for _, k := range []string{"a", "b", "c", "d", "shares"} {
out = append(out, "+ "+k+": "+values[k])
}
return strings.Join(out, "\n")
}
a := proposalAsk(c, "s-9")
a.Explanation = "Changes 5 setting(s) of mounts on shanks.\n+ a: 1\n+ b: 2\nand 3 more, each whole below: c, d, shares"
a.Whole = ""
a.Details = "Fingerprint 1234 5678 9abc def0 1234 5678.\n" + lines(masked)
a.WholeDetails = "Fingerprint 1234 5678 9abc def0 1234 5678.\n" + lines(keys)
submit(t, h, "mesh-controller", a)
settle(h, c)
if len(tg.sends) != 1 {
t.Fatalf("telegram got %d messages", len(tg.sends))
}
phone := tg.sends[0]
for k, v := range keys {
if !strings.Contains(phone.Note, "+ "+k+": "+v) {
t.Errorf("the phone's note lacks %s whole:\n%s", k, phone.Note)
}
}
if strings.Contains(phone.Body, "/storage") || strings.Count(phone.Body, "\n") > 3 {
t.Errorf("the body is not the glance:\n%s", phone.Body)
}
desk := h.sayOn(h.open["ask.mesh-controller.s-9"], "raised", "desktop")
if strings.Contains(desk.Text(), "/storage") || !strings.Contains(desk.Note, "+ shares: library=‹path›") {
t.Errorf("the desk shows a value whole, or not every key:\n%s", desk.Note)
}
// A secret's shape in the whole details refuses the ask as a whole.
b := proposalAsk(c, "s-10")
b.WholeDetails = "+ token: ghp_abcdefghijklmnopqrstuvwxyz0123456789"
submit(t, h, "mesh-controller", b)
settle(h, c)
if _, kept := store.asks["mesh-controller.s-10"]; kept || len(tg.sends) != 1 {
t.Errorf("an ask whose whole details carry a secret was taken")
}
}
// A change's line the rule refuses keeps its key's name in the note (the review of mesh-controller #304): the
// operator knows which setting holds a value they are not shown. A line whose key the rule refuses too is said
// withheld whole.
func TestAWithheldChangeLineKeepsItsKey(t *testing.T) {
for line, want := range map[string]string{
"+ password: hunter2": "+ password (a value carrying a value given to a secret's name is withheld here)",
"~ shares: /mnt/media (was: x)": "~ shares (a value carrying a file path is withheld here)",
"Read it at /etc/x": "(a line carrying a file path is withheld here)",
"+ ghp_abcdefghijklmnopqrstuvwxyz0123456789: 1": "(a line carrying ",
} {
refusal, ok := Check(line)
if ok {
t.Fatalf("%q passes the content rule", line)
}
if got := withheldLine(line, refusal, Check); !strings.HasPrefix(got, want) {
t.Errorf("%q: %q, want %q", line, got, want)
}
}
}