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, "mesh-controller", a) settle(h, c) if r := h.open["delivery.hq.held"]; r == nil || r.Ask != "mesh-controller.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) } } }