diff --git a/go/asks/asks.go b/go/asks/asks.go index 0f6f3aa..8915af8 100644 --- a/go/asks/asks.go +++ b/go/asks/asks.go @@ -174,6 +174,12 @@ func (a Ask) Digest() string { canonical(&b, a.Whole) canonical(&b, a.Details) } + // WholeDetails (novox/hq issue 383, the review of mesh-controller #304) follows only when given, so every + // ask without it digests as it did before it existed. + if a.WholeDetails != "" { + canonical(&b, "whole-details") + canonical(&b, a.WholeDetails) + } sum := sha256.Sum256([]byte(b.String())) return "sha256:" + hex.EncodeToString(sum[:]) } @@ -207,6 +213,12 @@ type Ask struct { // (Message.Note), after what the ask's options do and when it expires — never in its body (novox/hq // issue 639). Details string `json:"details,omitempty"` + // WholeDetails are the Details with their exact values whole, shown in place of Details where Whole is shown + // in place of the explanation (a kind that proves who answers, given the ask's answers): every value an + // approval sets, whole, so the person reads exactly what they approve (novox/hq issue 383). Held, like + // Whole, to the secret rule alone; the router refuses the ask when a secret's shape is left in it. Empty, + // Details are shown everywhere. + WholeDetails string `json:"whole-details,omitempty"` } // What an ask's explanation is held to, so it reads at a glance on a phone (novox/hq issue 639): a summary diff --git a/go/asks/asks_test.go b/go/asks/asks_test.go index c64d49c..3ce66d6 100644 --- a/go/asks/asks_test.go +++ b/go/asks/asks_test.go @@ -253,8 +253,8 @@ func TestDigestsAreCanonicalOverNamedFields(t *testing.T) { t.Error("changing Whole beside Details left the digest the same") } // A field added to Ask or Option without being added to Digest fails here. - if n := reflect.TypeOf(Ask{}).NumField(); n != 11 { - t.Errorf("Ask has %d fields, and Digest covers 11: add the new one to Digest and to this test", n) + if n := reflect.TypeOf(Ask{}).NumField(); n != 12 { + t.Errorf("Ask has %d fields, and Digest covers 12: add the new one to Digest and to this test", n) } if n := reflect.TypeOf(Option{}).NumField(); n != 5 { t.Errorf("Option has %d fields, and Digest covers 5: add the new one to Digest and to this test", n) @@ -317,3 +317,23 @@ func TestAMessageCarriesItsNoteApart(t *testing.T) { t.Fatalf("an empty note is encoded: %s", raw) } } + +// WholeDetails is digested when given, and an ask without it digests as before it existed: a router and an asker +// of different builds still agree on every such ask. +func TestWholeDetailsAreDigestedOnlyWhenGiven(t *testing.T) { + a := Ask{ID: "a", Headline: "h", Explanation: "e", Details: "d"} + before := a.Digest() + a.WholeDetails = "+ shares: media=/storage/media" + if a.Digest() == before { + t.Fatal("the whole details are not in the digest") + } + b := a + b.WholeDetails = "+ shares: media=/storage/other" + if a.Digest() == b.Digest() { + t.Fatal("two whole details digest the same") + } + a.WholeDetails = "" + if a.Digest() != before { + t.Fatal("an ask without whole details digests otherwise than before") + } +}