Lay out the assigner's ask value and name the asking a view answer is for, so a stale answer never fills a later asking of the same key (issue 1297 subtask B1c, for the view lane's B9 and B10)
This commit is contained in:
@@ -4,6 +4,8 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/asks"
|
||||
)
|
||||
|
||||
// The asks the view may answer, and its answers (novox/hq ADR 0528 clause 6, ADR 0429 §3 and §6, ADR 0437 §10). The
|
||||
@@ -143,22 +145,97 @@ func ParseViewAnswerKey(key string) (askKey string, err error) {
|
||||
return askKey, nil
|
||||
}
|
||||
|
||||
// ViewAnswer is the view's answer to an ask: the option the operator chose and their words. The entry's own time
|
||||
// says when.
|
||||
// AssignerAsk is the value of a key in AssignerAsksBucket: the router's ask whole, as the assigner composes it, so the
|
||||
// teller publishes it unchanged. Asking counts the times the assigner wrote the ask at this key, as the gate's Ask
|
||||
// does: 0 or 1 for the first, n for the n-th, and the ask's id is AskingID(key, Asking), so each asking has its own id
|
||||
// and an answer to an earlier asking is never taken for a later one.
|
||||
type AssignerAsk struct {
|
||||
V int `json:"v"`
|
||||
Ask asks.Ask `json:"ask"`
|
||||
Asking int `json:"asking,omitempty"`
|
||||
}
|
||||
|
||||
func (a AssignerAsk) check(key string) error {
|
||||
ask, err := ParseAnyAskKey(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if proof, _ := AskNeedsProof(ask.Kind); proof {
|
||||
return fmt.Errorf("delivery: the assigner asks no %s ask: it needs proof of who answered", ask.Kind)
|
||||
}
|
||||
if a.Asking < 0 {
|
||||
return fmt.Errorf("delivery: the ask's asking %d is negative", a.Asking)
|
||||
}
|
||||
if want := AskingID(key, a.Asking); a.Ask.ID != want {
|
||||
return fmt.Errorf("delivery: the ask's id %q is not %q, the id its key and asking %d give", a.Ask.ID, want, a.Asking)
|
||||
}
|
||||
if len(a.Ask.Options) == 0 {
|
||||
return fmt.Errorf("delivery: the ask has no option")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// EncodeAssignerAsk is a, the ask at key, as JSON at Version; it refuses a key of a kind that needs proof or of no
|
||||
// kind, an id other than the one the key and the asking give, and an ask with no option.
|
||||
func EncodeAssignerAsk(key string, a AssignerAsk) ([]byte, error) {
|
||||
if err := a.check(key); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
a.V = Version
|
||||
return json.Marshal(a)
|
||||
}
|
||||
|
||||
// DecodeAssignerAsk reads the ask at key; it refuses an unknown version (ErrUnknownVersion) and what EncodeAssignerAsk
|
||||
// refuses.
|
||||
func DecodeAssignerAsk(key string, data []byte) (AssignerAsk, error) {
|
||||
var a AssignerAsk
|
||||
if err := decode(data, &a); err != nil {
|
||||
return AssignerAsk{}, err
|
||||
}
|
||||
if err := a.check(key); err != nil {
|
||||
return AssignerAsk{}, err
|
||||
}
|
||||
return a, nil
|
||||
}
|
||||
|
||||
// SameAssignerAsk says whether two asks are the same value.
|
||||
func SameAssignerAsk(a, b AssignerAsk) bool {
|
||||
return a.V == b.V && a.Asking == b.Asking && a.Ask.Digest() == b.Ask.Digest()
|
||||
}
|
||||
|
||||
// ViewAnswer is the view's answer to an ask: the id of the asking it answers (the ask's id, AskingID of its key and
|
||||
// asking), the option the operator chose and their words. The entry's own time says when.
|
||||
type ViewAnswer struct {
|
||||
V int `json:"v"`
|
||||
Asking string `json:"asking"`
|
||||
Option string `json:"option"`
|
||||
Words string `json:"words,omitempty"`
|
||||
}
|
||||
|
||||
func (a ViewAnswer) check() error {
|
||||
if strings.TrimSpace(a.Asking) == "" {
|
||||
return fmt.Errorf("delivery: a view answer names no asking")
|
||||
}
|
||||
if strings.TrimSpace(a.Option) == "" {
|
||||
return fmt.Errorf("delivery: a view answer names no option")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// EncodeViewAnswer is a as JSON at Version; it refuses an answer naming no option.
|
||||
// ViewAnswerFits says whether the view's answer a answers the ask as it stands now: it refuses an answer to another
|
||||
// asking of the ask (a stale answer to an earlier writing of the same key) and an option the ask does not offer,
|
||||
// naming both.
|
||||
func ViewAnswerFits(a ViewAnswer, ask AssignerAsk) error {
|
||||
if a.Asking != ask.Ask.ID {
|
||||
return fmt.Errorf("delivery: the view's answer is to the asking %s, and the ask stands at the asking %s", a.Asking, ask.Ask.ID)
|
||||
}
|
||||
if _, ok := ask.Ask.Option(a.Option); !ok {
|
||||
return fmt.Errorf("delivery: the view's answer chose %q, which the asking %s does not offer", a.Option, ask.Ask.ID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// EncodeViewAnswer is a as JSON at Version; it refuses an answer naming no asking or no option.
|
||||
func EncodeViewAnswer(a ViewAnswer) ([]byte, error) {
|
||||
if err := a.check(); err != nil {
|
||||
return nil, err
|
||||
@@ -168,7 +245,7 @@ func EncodeViewAnswer(a ViewAnswer) ([]byte, error) {
|
||||
}
|
||||
|
||||
// DecodeViewAnswer reads a view answer; it refuses an unknown version (ErrUnknownVersion) and an answer naming no
|
||||
// option.
|
||||
// asking or no option.
|
||||
func DecodeViewAnswer(data []byte) (ViewAnswer, error) {
|
||||
var a ViewAnswer
|
||||
if err := decode(data, &a); err != nil {
|
||||
|
||||
@@ -3,6 +3,9 @@ package delivery
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/asks"
|
||||
)
|
||||
|
||||
// 1297/B1c: the kinds of ask, and which need proof of who answered: the merge does, hold and restore do not, an
|
||||
@@ -73,9 +76,9 @@ func TestViewAnswerKey(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// 1297/B1c: a view answer round-trips; one naming no option is refused.
|
||||
// 1297/B1c: a view answer round-trips; one naming no asking or no option is refused.
|
||||
func TestViewAnswerRoundTrip(t *testing.T) {
|
||||
a := ViewAnswer{Option: "release", Words: "fixed by #12"}
|
||||
a := ViewAnswer{Asking: "hold-mesh-mail-aaaa-0123456789ab", Option: "release", Words: "fixed by #12"}
|
||||
data, err := EncodeViewAnswer(a)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -85,7 +88,74 @@ func TestViewAnswerRoundTrip(t *testing.T) {
|
||||
if err != nil || !SameViewAnswer(got, a) {
|
||||
t.Errorf("view answer came back %+v, %v", got, err)
|
||||
}
|
||||
if _, err := EncodeViewAnswer(ViewAnswer{Option: " "}); err == nil {
|
||||
if _, err := EncodeViewAnswer(ViewAnswer{Asking: "x", Option: " "}); err == nil {
|
||||
t.Errorf("a view answer naming no option was accepted")
|
||||
}
|
||||
if _, err := EncodeViewAnswer(ViewAnswer{Option: "release"}); err == nil {
|
||||
t.Errorf("a view answer naming no asking was accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func holdAsk(t *testing.T, asking int) (string, AssignerAsk) {
|
||||
t.Helper()
|
||||
key, err := HoldAskKey("mesh-mail", fp2of('f'))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return key, AssignerAsk{Asking: asking, Ask: asks.Ask{ID: AskingID(key, asking), Headline: "mesh-mail is held", Explanation: "its health failed",
|
||||
Options: []asks.Option{{ID: "release", Label: "Release"}, {ID: "keep", Label: "Keep held"}}, Who: "operator",
|
||||
Expires: time.Date(2026, 10, 30, 0, 0, 0, 0, time.UTC), OnExpiry: "it stays held"}}
|
||||
}
|
||||
|
||||
// 1297/B1c: the assigner's ask round-trips at its key; an id another key or asking gives, an ask of a kind that needs
|
||||
// proof and one with no option are refused.
|
||||
func TestAssignerAskRoundTrip(t *testing.T) {
|
||||
key, a := holdAsk(t, 2)
|
||||
data, err := EncodeAssignerAsk(key, a)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.HasPrefix(string(data), `{"v":1,"ask":`) {
|
||||
t.Errorf("the ask is written as %s", data)
|
||||
}
|
||||
got, err := DecodeAssignerAsk(key, data)
|
||||
a.V = Version
|
||||
if err != nil || !SameAssignerAsk(got, a) {
|
||||
t.Errorf("the ask came back %+v, %v", got, err)
|
||||
}
|
||||
other, _ := HoldAskKey("mesh-web", fp2of('f'))
|
||||
if _, err := DecodeAssignerAsk(other, data); err == nil {
|
||||
t.Errorf("an ask was read at another key")
|
||||
}
|
||||
stale := a
|
||||
stale.Asking = 3
|
||||
if _, err := EncodeAssignerAsk(key, stale); err == nil {
|
||||
t.Errorf("an ask whose id is another asking's was accepted")
|
||||
}
|
||||
merge, _ := MergeAskKey("g", []Head{{Repository: "novox/hq", Number: 2, Head: strings.Repeat("2", 40)}})
|
||||
if _, err := EncodeAssignerAsk(merge, AssignerAsk{Ask: asks.Ask{ID: AskingID(merge, 0), Options: a.Ask.Options}}); err == nil {
|
||||
t.Errorf("the assigner was let write a merge's ask")
|
||||
}
|
||||
none := a
|
||||
none.Ask.Options = nil
|
||||
if _, err := EncodeAssignerAsk(key, none); err == nil {
|
||||
t.Errorf("an ask with no option was accepted")
|
||||
}
|
||||
}
|
||||
|
||||
// 1297/B1c: a view answer fits only the asking it names: an answer to an earlier asking of the same key is refused,
|
||||
// naming both, and so is an option the ask does not offer.
|
||||
func TestViewAnswerFits(t *testing.T) {
|
||||
key, first := holdAsk(t, 0)
|
||||
_, second := holdAsk(t, 2)
|
||||
answer := ViewAnswer{Asking: AskingID(key, 0), Option: "release"}
|
||||
if err := ViewAnswerFits(answer, first); err != nil {
|
||||
t.Errorf("the answer to the first asking does not fit it: %v", err)
|
||||
}
|
||||
if err := ViewAnswerFits(answer, second); err == nil || !strings.Contains(err.Error(), first.Ask.ID) || !strings.Contains(err.Error(), second.Ask.ID) {
|
||||
t.Errorf("a stale answer fits the later asking, or the refusal names neither: %v", err)
|
||||
}
|
||||
if err := ViewAnswerFits(ViewAnswer{Asking: first.Ask.ID, Option: "restore"}, first); err == nil {
|
||||
t.Errorf("an option the ask does not offer fits")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user